diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..554c663b152 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# http://EditorConfig.org +# https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +tab_width = 2 + +[**.rb] +max_line_length = 80 + +[**.js, **.coffee] +max_line_length = 120 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 179c4166b94..dd394029315 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .idea .loadpath .project +.ruby-version bin/* public/dispatch.cgi public/dispatch.fcgi @@ -37,15 +38,13 @@ spree_test testapp **/spec/dummy tmp -vendor/rails -vendor/extensions/google_base public/google_base.xml public/template_google_base.xml coverage/* var TAGS nbproject -vendor/extensions/theme_default/app/stylesheets/*.css +./vendor tags *.swp rerun.txt @@ -53,3 +52,4 @@ test_app .rvmrc **/coverage */.sass-cache +.localeapp diff --git a/.teatro.yml b/.teatro.yml new file mode 100644 index 00000000000..d7ae4097396 --- /dev/null +++ b/.teatro.yml @@ -0,0 +1,19 @@ +stage: + before: + - export SECRET_KEY_BASE=abc + - bundle exec rake sandbox + - echo 'Rails.logger = Logger.new(STDOUT)' > sandbox/config/initializers/logger_stdout.rb + - cd sandbox + - ln -sf $PWD/public $PWD/../public + + run: + # workdir is sandbox + - bundle exec rails server + + database: + - echo "skip database stage. rake sandbox already did everything" + +config: + database: postgresql + services: + - postgresql diff --git a/.travis.yml b/.travis.yml index d13640646de..8e44052320b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,22 @@ before_script: - - 'sh -e /etc/init.d/xvfb start' - - 'export DISPLAY=:99.0' + - sh -e /etc/init.d/xvfb start + - export DISPLAY=:99.0 bundler_args: --without development production --quiet env: - - GEM='api:sqlite' - - GEM='api:mysql' - - GEM='api:postgres' - - GEM='core:sqlite' - - GEM='core:mysql' - - GEM='core:postgres' - - GEM='dash:sqlite' - - GEM='dash:mysql' - - GEM='dash:postgres' - - GEM='promo:sqlite' - - GEM='promo:mysql' - - GEM='promo:postgres' + - GEM=api DB=mysql + - GEM=api DB=postgres + - GEM=backend DB=mysql + - GEM=backend DB=postgres + - GEM=core DB=mysql + - GEM=core DB=postgres + - GEM=frontend DB=mysql + - GEM=frontend DB=postgres + - GEM=sample DB=mysql + - GEM=sample DB=postgres +before_install: + - cd $GEM; export BUNDLE_GEMFILE="`pwd`/Gemfile" script: - - 'ci/travis.sh' -notifications: - email: - - ryan@spreecommerce.com - irc: - use_notice: true - skip_join: true - channels: - - "irc.freenode.org#spree" + - bundle exec rake test_app + - RSPEC_RETRY_COUNT=2 bundle exec rake spec rvm: - - 1.9.3 + - 2.1.5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 751568c347b..09656b0834c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,30 @@ -Spree is an open source project and we encourage contributions. Please see the [contributors guidelines](http://spreecommerce.com/documentation/contributing_to_spree.html) before contributing. +Spree is an open source project and we encourage contributions. Please see the +[contributors guidelines](http://spreecommerce.com/documentation/contributing_to_spree.html) +before contributing. ## Filing an issue When filing an issue on the Spree project, please provide these details: * A comprehensive list of steps to reproduce the issue. -* The version of Spree *and* the version of Rails. -* A list of all extensions. +* What you're *expecting* to happen compared with what's *actually* happening. +* Your application's complete `Gemfile.lock`, and `Gemfile.lock` as text in a [Gist](https://gist.github.com) (*not as an image*) * Any relevant stack traces ("Full trace" preferred) -In 99% of cases, this information is enough to determine the cause and solution to the problem that is being described. +In 99% of cases, this information is enough to determine the cause and solution +to the problem that is being described. -Any issue that is open for 14 days without actionable information or activity will be marked as "stalled" and then closed. Stalled issues can be re-opened if the information requested is provided. +Please remember to format code using triple backticks (\`) so that it is neatly +formatted when the issue is posted. + +Any issue that is open for 14 days without actionable information or activity +will be marked as "stalled" and then closed. Stalled issues can be re-opened if +the information requested is provided. ## Pull requests -We gladly accept pull requests to fix bugs and, in some circumstances, add new features to Spree. +We gladly accept pull requests to add documentation, fix bugs and, in some circumstances, +add new features to Spree. Here's a quick guide: @@ -24,17 +33,18 @@ Here's a quick guide: 2. Run the tests. We only take pull requests with passing tests, and it's great to know that you have a clean slate: - $ bundle install - $ bundle exec rake test_app - $ bundle exec rake + $ bash build.sh -3. Add a test for your change. Only refactoring and documentation changes -require no new tests. If you are adding functionality or fixing a bug, we need -a test! +3. Create new branch then make changes and add tests for your changes. Only +refactoring and documentation changes require no new tests. If you are adding +functionality or fixing a bug, we need tests! -4. Make the test pass. +4. Push to your fork and submit a pull request. If the changes will apply cleanly +to the latest stable branches and master branch, you will only need to submit one +pull request. -5. Push to your fork and submit a pull request. If the changes will apply cleanly to the latest stable branches and master branch, you will only need to submit one pull request. +5. If a PR does not apply cleanly to one of its targeted branches, then a separate +PR should be created that does. For instance, if a PR applied to master & 2-1-stable but not 2-0-stable, then there should be one PR for master & 2-1-stable and another, separate PR for 2-0-stable. At this point you're waiting on us. We like to at least comment on, if not accept, pull requests within three business days (and, typically, one business @@ -53,9 +63,12 @@ Syntax: * Two spaces, no tabs. * No trailing whitespace. Blank lines should not have any space. * Prefer &&/|| over and/or. -* `MyClass.my_method(my_arg)` not `my_method( my_arg )` or my_method my_arg. +* `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. * `a = b` and not `a=b`. * `a_method { |block| ... }` and not `a_method { | block | ... }` * Follow the conventions you see used in the source already. +* -> symbol over lambda +* Ruby 1.9 hash syntax `{ key: value }` over Ruby 1.8 hash syntax `{ :key => value }` +* Alphabetize the class methods to keep them organized And in case we didn't emphasize it enough: we love tests! diff --git a/README.md b/README.md old mode 100644 new mode 100755 index da2d2f0d9d2..661527290a0 --- a/README.md +++ b/README.md @@ -1,84 +1,153 @@ -**THIS README IS FOR THE MASTER BRANCH OF SPREE AND REFLECTS THE WORK CURRENTLY EXISTING ON THE MASTER BRANCH. IF YOU ARE WISHING TO USE A NON-MASTER BRANCH OF +**THIS README IS FOR THE MASTER BRANCH OF SPREE AND REFLECTS THE WORK CURRENTLY +EXISTING ON THE MASTER BRANCH. IF YOU ARE WISHING TO USE A NON-MASTER BRANCH OF SPREE, PLEASE CONSULT THAT BRANCH'S README AND NOT THIS ONE.** SUMMARY ------- +Spree is a complete open source e-commerce solution built with Ruby on Rails. It +was originally developed by Sean Schofield and is now maintained by a dedicated +[core team](https://github.com/spree/spree/wiki/Core-Team). You can find out more by +visiting the [Spree e-commerce project page](http://spreecommerce.com). + +Spree actually consists of several different gems, each of which are maintained +in a single repository and documented in a single set of +[online documentation](http://spreecommerce.com/documentation). By requiring the +Spree gem you automatically require all of the necessary gem dependencies which are: + +* spree_api (RESTful API) +* spree_frontend (User-facing components) +* spree_backend (Admin area) +* spree_cmd (Command-line tools) +* spree_core (Models & Mailers, the basic components of Spree that it can't run without) +* spree_sample (Sample data) + +All of the gems are designed to work together to provide a fully functional +e-commerce platform. It is also possible, however, to use only the pieces you are +interested in. For example, you could use just the barebones spree\_core gem +and perhaps combine it with your own custom backend admin instead of using +spree_api. + +[![Code Climate](https://codeclimate.com/github/spree/spree.png)](https://codeclimate.com/github/spree/spree) +[![Issue Stats](http://issuestats.com/github/spree/spree/badge/pr)](http://issuestats.com/github/spree/spree) +[![Issue Stats](http://issuestats.com/github/spree/spree/badge/issue)](http://issuestats.com/github/spree/spree) -Spree is a complete open source e-commerce solution built with Ruby on Rails. It was originally developed by Sean Schofield -and is now maintained by a dedicated [core team](http://spreecommerce.com/core-team). You can find out more -by visiting the [Spree e-commerce project page](http://spreecommerce.com). - -Spree actually consists of several different gems, each of which are maintained in a single repository and documented -in a single set of [online documentation](http://spreecommerce.com/documentation). By requiring the Spree gem you -automatically require all of the necessary gem dependencies which are: - -* spree_api -* spree_cmd -* spree_core -* spree_dash -* spree_promo -* spree_sample - -All of the gems are designed to work together to provide a fully functional e-commerce platform. It is also possible, -however, to use only the pieces you are interested in. So for example, you could use just the barebones spree\_core gem -and perhaps combine it with your own custom promotion scheme instead of using spree_promo. - -[![Build Status](https://secure.travis-ci.org/spree/spree.png)](http://travis-ci.org/spree/spree) -[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/spree/spree) Installation ------------ +**THIS README IS FOR THE MASTER BRANCH OF SPREE AND REFLECTS THE WORK CURRENTLY +EXISTING ON THE MASTER BRANCH. IF YOU ARE WISHING TO USE A NON-MASTER BRANCH OF +SPREE, PLEASE CONSULT THAT BRANCH'S README AND NOT THIS ONE.** + The fastest way to get started is by using the spree command line tool available in the spree gem which will add Spree to an existing Rails application. - $ gem install rails -v 3.2.9 - $ gem install spree - $ rails _3.2.9_ new my_store - $ spree install my_store +```shell +gem install rails -v 4.1.11 +gem install spree +rails _4.1.11_ new my_store +spree install my_store +``` -This will add the Spree gem to your Gemfile, create initializers, copy migrations and -optionally generate sample products and orders. +This will add the Spree gem to your Gemfile, create initializers, copy migrations +and optionally generate sample products and orders. -If you get an "Unable to resolve dependencies" error when installing the Spree gem then you can try installing just the spree_cmd gem which should avoid any circular dependency issues. +If you get an "Unable to resolve dependencies" error when installing the Spree gem +then you can try installing just the spree_cmd gem which should avoid any circular +dependency issues. - $ gem install spree_cmd +```shell +gem install spree_cmd +``` To auto accept all prompts while running the install generator, pass -A as an option - $ spree install my_store -A +```shell +spree install my_store -A +``` + +To select a specific branch, pass in the `--branch` option. If there is no branch, you +will be given the latest version of either spree_auth_devise or spree_gateway. -Using the Gem +```shell +spree install my_store --branch "2-4-stable" +``` + +Using stable builds and bleeding edge ------------- -You can manually add Spree to your Rails 3.2.x application. Add Spree to +To use a stable build of Spree, you can manually add Spree to your +Rails application. To use the 2-4-stable branch of Spree, add this line to your Gemfile. ```ruby -gem 'spree', :git => 'git://github.com/spree/spree.git' +gem 'spree', github: 'spree/spree', branch: '2-4-stable' +``` + +Alternatively, if you want to use the bleeding edge version of Spree, use this +line: + +```ruby +gem 'spree', github: 'spree/spree' +``` + +**Note: The master branch is not guaranteed to ever be in a fully functioning +state. It is unwise to use this branch in a production system you care deeply +about.** + +If you wish to have authentication included also, you will need to add the +`spree_auth_devise` gem as well. Either this: + +```ruby +gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-4-stable' ``` -Update your bundle +Or this: - $ bundle install +```ruby +gem 'spree_auth_devise', github: 'spree/spree_auth_devise' +``` -Use the install generator to copy migrations, initializers and generate -sample data. +Once you've done that, then you can install these gems using this command: - $ rails g spree:install +```shell +bundle install +``` -You can avoid running migrations or generating seed and sample data +Use the install generator to set up Spree: - $ rails g spree:install --migrate=false --sample=false --seed=false +```shell +rails g spree:install --sample=false --seed=false +``` -You can always perform the steps later. +At this point, if you are using spree_auth_devise you will need to change this +line in `config/initializers/spree.rb`: - $ bundle exec rake db:migrate - $ bundle exec rake db:seed +```ruby +Spree.user_class = "Spree::LegacyUser" +``` -To manually load sample products, orders, etc., run the following rake task +To this: - $ bundle exec rake spree_sample:load +```ruby +Spree.user_class = "Spree::User" +``` + +You can avoid running migrations or generating seed and sample data by passing +in these flags: + +```shell +rails g spree:install --migrate=false --sample=false --seed=false +``` + +You can always perform the steps later by using these commands. + +```shell +bundle exec rake railties:install:migrations +bundle exec rake db:migrate +bundle exec rake db:seed +bundle exec rake spree_sample:load +``` Browse Store ------------ @@ -90,107 +159,182 @@ Browse Admin Interface http://localhost:nnnn/admin - - Working with the edge source (latest and greatest features) ----------------------------------------------------------- -The source code is essentially a collection of gems. Spree is meant to be run within the context of Rails application. You can easily create a sandbox application inside of your cloned source directory for testing purposes. +The source code is essentially a collection of gems. Spree is meant to be run +within the context of Rails application. You can easily create a sandbox +application inside of your cloned source directory for testing purposes. 1. Clone the Git repo - $ git clone git://github.com/spree/spree.git - $ cd spree +```shell +git clone git://github.com/spree/spree.git +cd spree +``` 2. Install the gem dependencies - $ bundle install +```shell +bundle install +``` -3. Create a sandbox Rails application for testing purposes (and automatically perform all necessary database setup) +3. Create a sandbox Rails application for testing purposes (and automatically +perform all necessary database setup) - $ bundle exec rake sandbox +```shell +bundle exec rake sandbox +``` 4. Start the server - $ cd sandbox - $ rails server +```shell +cd sandbox +rails server +``` Performance ----------- -You may noticed that your Spree store runs slowly in development mode. This is a side-effect of how Rails works in development mode which is to continuous reload your Ruby objects on each request. The introduction of the asset pipeline in Rails 3.1 made default performance in development mode significantly worse. There are, however, a few tricks to speeding up performance in development mode. +You may notice that your Spree store runs slowly in development mode. This is +a side-effect of how Rails works in development mode which is to continuously reload +your Ruby objects on each request. The introduction of the asset pipeline in +Rails 3.1 made default performance in development mode significantly worse. There +are, however, a few tricks to speeding up performance in development mode. -You can recompile your assets as follows: +First, in your `config/development.rb`: - $ bundle exec rake assets:precompile:nondigest +```ruby +config.assets.debug = false +``` -If you want to remove precompiled assets (recommended before you commit to Git and push your changes) use the following rake task: +You can precompile your assets as follows: - $ bundle exec rake assets:clean +```shell +RAILS_ENV=development bundle exec rake assets:precompile +``` + +If you want to remove precompiled assets (recommended before you commit to Git +and push your changes) use the following rake task: + +```shell +RAILS_ENV=development bundle exec rake assets:clean +``` Use Dedicated Spree Devise Authentication ----------------------------------------- Add the following to your Gemfile - $ gem 'spree_auth_devise', :git => 'git://github.com/spree/spree_auth_devise' +```ruby +gem 'spree_auth_devise', github: 'spree/spree_auth_devise' +``` -Then run `bundle install`. Authentication will then work exactly as it did in previous versions of Spree. +Then run `bundle install`. Authentication will then work exactly as it did in +previous versions of Spree. This line is automatically added by the `spree install` command. -If you're installing this in a new Spree 1.2+ application, you'll need to install and run the migrations with +If you're installing this in a new Spree 1.2+ application, you'll need to install +and run the migrations with - $ bundle exec rake spree_auth:install:migrations - $ bundle exec rake db:migrate +```shell +bundle exec rake spree_auth:install:migrations +bundle exec rake db:migrate +``` change the following line in `config/initializers/spree.rb` ```ruby -Spree.user_class = "Spree::LegacyUser" +Spree.user_class = 'Spree::LegacyUser' ``` to ```ruby -Spree.user_class = "Spree::User" +Spree.user_class = 'Spree::User' ``` In order to set up the admin user for the application you should then run: - $ bundle exec rake spree_auth:admin:create - +```shell +bundle exec rake spree_auth:admin:create +``` Running Tests ------------- -Each gem contains its own series of tests, and for each directory, you need to do a quick one-time -creation of a test application and then you can use it to run the tests. For example, to run the -tests for the core project. +[![Team City](http://www.jetbrains.com/img/logos/logo_teamcity_small.gif)](http://www.jetbrains.com/teamcity) - $ cd core - $ bundle exec rake test_app +We use [TeamCity](http://www.jetbrains.com/teamcity/) to run the tests for Spree. -If you're working on multiple facets of Spree, you may want -to run this command at the root of the Spree project to -generate test applications for all the facets: +You can see the build statuses at [http://ci.spree.fm](http://ci.spree.fm/guestLogin.html?guest=1). - $ bundle exec rake test_app +--- -You can run all of the tests inside a facet by also running -this command: +Each gem contains its own series of tests, and for each directory, you need to +do a quick one-time creation of a test application and then you can use it to run +the tests. For example, to run the tests for the core project. +```shell +cd core +bundle exec rake test_app +bundle exec rspec spec +``` - $ cd core - $ bundle exec rake +If you would like to run specs against a particular database you may specify the +dummy apps database, which defaults to sqlite3. +```shell +DB=postgres bundle exec rake test_app +``` If you want to run specs for only a single spec file - - $ bundle exec rspec spec/models/state_spec.rb +```shell +bundle exec rspec spec/models/spree/state_spec.rb +``` If you want to run a particular line of spec +```shell +bundle exec rspec spec/models/spree/state_spec.rb:7 +``` + +You can also enable fail fast in order to stop tests at the first failure +```shell +FAIL_FAST=true bundle exec rspec spec/models/state_spec.rb +``` + +If you want to run the simplecov code coverage report +```shell +COVERAGE=true bundle exec rspec spec +``` + +If you're working on multiple facets of Spree to test, +please ensure that you have a postgres user: - $ bundle exec rspec spec/models/state_spec.rb:7 +```shell +createuser -s -r postgres +``` + +And also ensure that you have [PhantomJS](http://phantomjs.org/) installed as well: -Travis, the continuous integration service, runs the test suite for each gem one at a time, using the same commands as contained within [`build.sh`](https://github.com/spree/spree/tree/master/build.sh). +```shell +brew update && brew install phantomjs +``` + +To execute all the tests, you may want to run this command at the +root of the Spree project to generate test applications and run +specs for all the facets: +```shell +bash build.sh +``` + +Further Documentation +------------ +Spree has a number of really useful guides online at [http://guides.spreecommerce.com](http://guides.spreecommerce.com). + +Roadmap +------------ +Spree roadmap at [https://trello.com/b/PQsUfCL0/spree-roadmap](https://trello.com/b/PQsUfCL0/spree-roadmap). Contributing ------------ -Spree is an open source project and we encourage contributions. Please see the [contributors guidelines](http://spreecommerce.com/documentation/contributing_to_spree.html) before contributing. +Spree is an open source project and we encourage contributions. Please see the +[contributors guidelines](http://guides.spreecommerce.com/developer/contributing.html) +before contributing. diff --git a/Rakefile b/Rakefile index a13993a0db9..ab3c44d20f0 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,10 @@ require 'rake' require 'rubygems/package_task' require 'thor/group' -require File.expand_path('../core/lib/generators/spree/install/install_generator', __FILE__) begin - require 'spree/core/testing_support/common_rake' + require 'spree/testing_support/common_rake' rescue LoadError - raise "Could not find spree/core/testing_support/common_rake. You need to run this command using Bundler." + raise "Could not find spree/testing_support/common_rake. You need to run this command using Bundler." exit end @@ -14,9 +13,22 @@ Gem::PackageTask.new(spec) do |pkg| pkg.gem_spec = spec end +task default: :test + +desc "Runs all tests in all Spree engines" +task :test do + Rake::Task['test_app'].invoke + %w(api backend core frontend sample).each do |gem_name| + Dir.chdir("#{File.dirname(__FILE__)}/#{gem_name}") do + system("RSPEC_RETRY_COUNT=2 rspec") or exit!(1) + end + end +end + desc "Generates a dummy app for testing for every Spree engine" task :test_app do - %w(api core dash promo).each do |engine| + require File.expand_path('../core/lib/generators/spree/install/install_generator', __FILE__) + %w(api backend core frontend sample).each do |engine| ENV['LIB_NAME'] = File.join('spree', engine) ENV['DUMMY_PATH'] = File.expand_path("../#{engine}/spec/dummy", __FILE__) Rake::Task['common:test_app'].execute @@ -30,7 +42,7 @@ task :clean do puts "Deleting pkg directory.." FileUtils.rm_rf("pkg") - %w(api cmd core dash promo).each do |gem_name| + %w(api backend cmd core frontend).each do |gem_name| puts "Cleaning #{gem_name}:" puts " Deleting #{gem_name}/Gemfile" FileUtils.rm_f("#{gem_name}/Gemfile") @@ -46,7 +58,7 @@ end namespace :gem do desc "run rake gem for all gems" task :build do - %w(core api dash promo sample cmd).each do |gem_name| + %w(core api backend frontend sample cmd).each do |gem_name| puts "########################### #{gem_name} #########################" puts "Deleting #{gem_name}/pkg" FileUtils.rm_rf("#{gem_name}/pkg") @@ -63,7 +75,7 @@ namespace :gem do task :install do version = File.read(File.expand_path("../SPREE_VERSION", __FILE__)).strip - %w(core api dash promo sample cmd).each do |gem_name| + %w(core api backend frontend sample cmd).each do |gem_name| puts "########################### #{gem_name} #########################" puts "Deleting #{gem_name}/pkg" FileUtils.rm_rf("#{gem_name}/pkg") @@ -82,7 +94,7 @@ namespace :gem do task :release do version = File.read(File.expand_path("../SPREE_VERSION", __FILE__)).strip - %w(core api dash promo sample cmd).each do |gem_name| + %w(core api backend frontend sample cmd).each do |gem_name| puts "########################### #{gem_name} #########################" cmd = "cd #{gem_name}/pkg && gem push spree_#{gem_name}-#{version}.gem"; puts cmd; system cmd end diff --git a/SPREE_VERSION b/SPREE_VERSION index 8a34a73c889..8972080d1b0 100644 --- a/SPREE_VERSION +++ b/SPREE_VERSION @@ -1 +1 @@ -2.0.0.beta \ No newline at end of file +2.4.11.beta diff --git a/api/.rspec b/api/.rspec deleted file mode 100644 index 53607ea52b7..00000000000 --- a/api/.rspec +++ /dev/null @@ -1 +0,0 @@ ---colour diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md new file mode 100644 index 00000000000..f4a0249631c --- /dev/null +++ b/api/CHANGELOG.md @@ -0,0 +1 @@ +## Spree 2.4.0 (unreleased) ## diff --git a/api/Gemfile b/api/Gemfile index f0986c44cad..af51fbf0c11 100644 --- a/api/Gemfile +++ b/api/Gemfile @@ -1,5 +1,5 @@ eval(File.read(File.dirname(__FILE__) + '/../common_spree_dependencies.rb')) -gem 'spree_core', :path => "../core" +gem 'spree_core', :path => '../core' gemspec diff --git a/api/LICENSE b/api/LICENSE index 4b380802b05..3946ad638a7 100644 --- a/api/LICENSE +++ b/api/LICENSE @@ -1,22 +1,27 @@ -Copyright (c) 2012 Ryan Bigg +Copyright (c) 2007-2014, Spree Commerce, Inc. and other contributors +All rights reserved. -MIT License +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Spree nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/api/Rakefile b/api/Rakefile index 722f98de815..078a9e191f9 100644 --- a/api/Rakefile +++ b/api/Rakefile @@ -4,7 +4,7 @@ require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' require 'rspec/core/rake_task' -require 'spree/core/testing_support/common_rake' +require 'spree/testing_support/common_rake' require 'rails/all' Bundler::GemHelper.install_tasks diff --git a/api/app/controllers/spree/api/addresses_controller.rb b/api/app/controllers/spree/api/addresses_controller.rb index 21a98527348..11edf739326 100644 --- a/api/app/controllers/spree/api/addresses_controller.rb +++ b/api/app/controllers/spree/api/addresses_controller.rb @@ -1,17 +1,43 @@ module Spree module Api class AddressesController < Spree::Api::BaseController + before_action :find_order + def show - @address = Address.find(params[:id]) - authorize! :read, @address + authorize! :read, @order, order_token + find_address + respond_with(@address) end def update - @address = Address.find(params[:id]) - authorize! :read, @address - @address.update_attributes(params[:address]) - render :show, :status => 200 + authorize! :update, @order, order_token + find_address + + if @address.update_attributes(address_params) + respond_with(@address, :default_template => :show) + else + invalid_resource!(@address) + end end + + private + def address_params + params.require(:address).permit(permitted_address_attributes) + end + + def find_order + @order = Spree::Order.find_by!(number: order_id) + end + + def find_address + @address = if @order.bill_address_id == params[:id].to_i + @order.bill_address + elsif @order.ship_address_id == params[:id].to_i + @order.ship_address + else + raise CanCan::AccessDenied + end + end end end end diff --git a/api/app/controllers/spree/api/base_controller.rb b/api/app/controllers/spree/api/base_controller.rb index f0bb985849a..6fcc721cb05 100644 --- a/api/app/controllers/spree/api/base_controller.rb +++ b/api/app/controllers/spree/api/base_controller.rb @@ -1,26 +1,31 @@ +require_dependency 'spree/api/controller_setup' + module Spree module Api - class BaseController < ActionController::Metal + class BaseController < ActionController::Base include Spree::Api::ControllerSetup + include Spree::Core::ControllerHelpers::SSL + include Spree::Core::ControllerHelpers::Store + include Spree::Core::ControllerHelpers::StrongParameters attr_accessor :current_api_user - before_filter :set_content_type - before_filter :check_for_api_key, :if => :requires_authentication? - before_filter :authenticate_user - after_filter :set_jsonp_format + class_attribute :error_notifier + + before_action :set_content_type + before_action :load_user + before_action :authorize_for_order, if: Proc.new { order_token.present? } + before_action :authenticate_user + before_action :load_user_roles - rescue_from CanCan::AccessDenied, :with => :unauthorized - rescue_from ActiveRecord::RecordNotFound, :with => :not_found + rescue_from Exception, with: :error_during_processing + rescue_from ActiveRecord::RecordNotFound, with: :not_found + rescue_from CanCan::AccessDenied, with: :unauthorized + rescue_from Spree::Core::GatewayError, with: :gateway_error helper Spree::Api::ApiHelpers - def set_jsonp_format - if params[:callback] && request.get? - self.response_body = "#{params[:callback]}(#{self.response_body})" - headers["Content-Type"] = 'application/javascript' - end - end + ssl_allowed def map_nested_attributes_keys(klass, attributes) nested_keys = klass.nested_attributes_options.keys @@ -31,35 +36,69 @@ def map_nested_attributes_keys(klass, attributes) end.with_indifferent_access end + # users should be able to set price when importing orders via api + def permitted_line_item_attributes + if @current_user_roles.include?("admin") + super + [:price, :variant_id, :sku] + else + super + end + end + private def set_content_type content_type = case params[:format] when "json" - "application/json" + "application/json; charset=utf-8" when "xml" - "text/xml" + "text/xml; charset=utf-8" end headers["Content-Type"] = content_type end - def check_for_api_key - render "spree/api/errors/must_specify_api_key", :status => 401 and return if api_key.blank? + def load_user + @current_api_user = Spree.user_class.find_by(spree_api_key: api_key.to_s) end def authenticate_user - if requires_authentication? || api_key.present? - unless @current_api_user = Spree.user_class.find_by_spree_api_key(api_key) + unless @current_api_user + if requires_authentication? && api_key.blank? && order_token.blank? + render "spree/api/errors/must_specify_api_key", :status => 401 and return + elsif order_token.blank? && (requires_authentication? || api_key.present?) render "spree/api/errors/invalid_api_key", :status => 401 and return + else + # An anonymous user + @current_api_user = Spree.user_class.new end + end + end + + def load_user_roles + @current_user_roles = if @current_api_user + @current_api_user.spree_roles.pluck(:name) else - # Effectively, an anonymous user - @current_api_user = Spree.user_class.new + [] end end def unauthorized - render "spree/api/errors/unauthorized", :status => 401 and return + render "spree/api/errors/unauthorized", status: 401 and return + end + + def error_during_processing(exception) + Rails.logger.error exception.message + Rails.logger.error exception.backtrace.join("\n") + + error_notifier.call(exception, self) if error_notifier + + render text: { exception: exception.message }.to_json, + status: 422 and return + end + + def gateway_error(exception) + @order.errors.add(:base, exception.message) + invalid_resource!(@order) end def requires_authentication? @@ -67,13 +106,18 @@ def requires_authentication? end def not_found - render "spree/api/errors/not_found", :status => 404 and return + render "spree/api/errors/not_found", status: 404 and return end def current_ability Spree::Ability.new(current_api_user) end + def current_currency + Spree::Config[:currency] + end + helper_method :current_currency + def invalid_resource!(resource) @resource = resource render "spree/api/errors/invalid_resource", :status => 422 @@ -84,28 +128,46 @@ def api_key end helper_method :api_key + def order_token + request.headers["X-Spree-Order-Token"] || params[:order_token] + end + def find_product(id) - begin - product_scope.find_by_permalink!(id.to_s) - rescue ActiveRecord::RecordNotFound - product_scope.find(id) - end + product_scope.friendly.find(id.to_s) + rescue ActiveRecord::RecordNotFound + product_scope.find(id) end def product_scope - if current_api_user.has_spree_role?("admin") - scope = Product + if @current_user_roles.include?("admin") + scope = Product.with_deleted.accessible_by(current_ability, :read).includes(*product_includes) + unless params[:show_deleted] scope = scope.not_deleted end else - scope = Product.active + scope = Product.accessible_by(current_ability, :read).active.includes(*product_includes) end - scope.includes(:master) + scope end + def variants_associations + [{ option_values: :option_type }, :default_price, :images] + end + + def product_includes + [ :option_types, :taxons, product_properties: :property, variants: variants_associations, master: variants_associations ] + end + + def order_id + params[:order_id] || params[:checkout_id] || params[:order_number] + end + + def authorize_for_order + @order = Spree::Order.find_by(number: order_id) + authorize! :read, @order, order_token + end end end end - diff --git a/api/app/controllers/spree/api/checkouts_controller.rb b/api/app/controllers/spree/api/checkouts_controller.rb new file mode 100644 index 00000000000..e716e10abb3 --- /dev/null +++ b/api/app/controllers/spree/api/checkouts_controller.rb @@ -0,0 +1,98 @@ +module Spree + module Api + class CheckoutsController < Spree::Api::BaseController + before_action :associate_user, only: :update + + include Spree::Core::ControllerHelpers::Auth + include Spree::Core::ControllerHelpers::Order + # This before_filter comes from Spree::Core::ControllerHelpers::Order + skip_before_action :set_current_order + + def next + load_order(true) + authorize! :update, @order, order_token + @order.next! + respond_with(@order, default_template: 'spree/api/orders/show', status: 200) + rescue StateMachine::InvalidTransition + respond_with(@order, default_template: 'spree/api/orders/could_not_transition', status: 422) + end + + def advance + load_order(true) + authorize! :update, @order, order_token + while @order.next; end + respond_with(@order, default_template: 'spree/api/orders/show', status: 200) + end + + def update + load_order(true) + authorize! :update, @order, order_token + + if @order.update_from_params(params, permitted_checkout_attributes, request.headers.env) + if current_api_user.has_spree_role?('admin') && user_id.present? + @order.associate_user!(Spree.user_class.find(user_id)) + end + + return if after_update_attributes + + if @order.completed? || @order.next + state_callback(:after) + respond_with(@order, default_template: 'spree/api/orders/show') + else + respond_with(@order, default_template: 'spree/api/orders/could_not_transition', status: 422) + end + else + invalid_resource!(@order) + end + end + + private + def user_id + params[:order][:user_id] if params[:order] + end + + def nested_params + map_nested_attributes_keys Spree::Order, params[:order] || {} + end + + # Should be overriden if you have areas of your checkout that don't match + # up to a step within checkout_steps, such as a registration step + def skip_state_validation? + false + end + + def load_order(lock = false) + @order = Spree::Order.lock(lock).find_by!(number: params[:id]) + raise_insufficient_quantity and return if @order.insufficient_stock_lines.present? + @order.state = params[:state] if params[:state] + state_callback(:before) + end + + def raise_insufficient_quantity + respond_with(@order, default_template: 'spree/api/orders/insufficient_quantity') + end + + def state_callback(before_or_after = :before) + method_name = :"#{before_or_after}_#{@order.state}" + send(method_name) if respond_to?(method_name, true) + end + + def after_update_attributes + if nested_params && nested_params[:coupon_code].present? + handler = Spree::PromotionHandler::Coupon.new(@order).apply + + if handler.error.present? + @coupon_message = handler.error + respond_with(@order, default_template: 'spree/api/orders/could_not_apply_coupon') + return true + end + end + false + end + + def order_id + super || params[:id] + end + end + end +end diff --git a/api/app/controllers/spree/api/classifications_controller.rb b/api/app/controllers/spree/api/classifications_controller.rb new file mode 100644 index 00000000000..64761d103c3 --- /dev/null +++ b/api/app/controllers/spree/api/classifications_controller.rb @@ -0,0 +1,18 @@ +module Spree + module Api + class ClassificationsController < Spree::Api::BaseController + def update + authorize! :update, Product + authorize! :update, Taxon + classification = Spree::Classification.find_by( + :product_id => params[:product_id], + :taxon_id => params[:taxon_id] + ) + # Because position we get back is 0-indexed. + # acts_as_list is 1-indexed. + classification.insert_at(params[:position].to_i + 1) + render :nothing => true + end + end + end +end \ No newline at end of file diff --git a/api/app/controllers/spree/api/config_controller.rb b/api/app/controllers/spree/api/config_controller.rb new file mode 100644 index 00000000000..2d43574e9c1 --- /dev/null +++ b/api/app/controllers/spree/api/config_controller.rb @@ -0,0 +1,6 @@ +module Spree + module Api + class ConfigController < Spree::Api::BaseController + end + end +end \ No newline at end of file diff --git a/api/app/controllers/spree/api/countries_controller.rb b/api/app/controllers/spree/api/countries_controller.rb index d0412a72926..ff8759960f6 100644 --- a/api/app/controllers/spree/api/countries_controller.rb +++ b/api/app/controllers/spree/api/countries_controller.rb @@ -1,13 +1,22 @@ module Spree module Api class CountriesController < Spree::Api::BaseController + skip_before_action :check_for_user_or_api_key + skip_before_action :authenticate_user + def index - @countries = Country.ransack(params[:q]).result.includes(:states).order('name ASC') - .page(params[:page]).per(params[:per_page]) + @countries = Spree::Country.accessible_by(current_ability, :read).ransack(params[:q]).result. + includes(:states).order('name ASC'). + page(params[:page]).per(params[:per_page]) + country = Spree::Country.order("updated_at ASC").last + if stale?(country) + respond_with(@countries) + end end def show - @country = Country.find(params[:id]) + @country = Spree::Country.accessible_by(current_ability, :read).find(params[:id]) + respond_with(@country) end end end diff --git a/api/app/controllers/spree/api/credit_cards_controller.rb b/api/app/controllers/spree/api/credit_cards_controller.rb new file mode 100644 index 00000000000..7b689855c16 --- /dev/null +++ b/api/app/controllers/spree/api/credit_cards_controller.rb @@ -0,0 +1,25 @@ +module Spree + module Api + class CreditCardsController < Spree::Api::BaseController + before_action :user + + def index + @credit_cards = user + .credit_cards + .accessible_by(current_ability, :read) + .with_payment_profile + .ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@credit_cards) + end + + private + + def user + if params[:user_id].present? + @user ||= Spree::user_class.accessible_by(current_ability, :read).find(params[:user_id]) + end + end + + end + end +end diff --git a/api/app/controllers/spree/api/images_controller.rb b/api/app/controllers/spree/api/images_controller.rb index 47077805d88..75ba56fb8a6 100644 --- a/api/app/controllers/spree/api/images_controller.rb +++ b/api/app/controllers/spree/api/images_controller.rb @@ -1,29 +1,47 @@ module Spree module Api class ImagesController < Spree::Api::BaseController + + def index + @images = scope.images.accessible_by(current_ability, :read) + respond_with(@images) + end + def show - @image = Image.find(params[:id]) + @image = Spree::Image.accessible_by(current_ability, :read).find(params[:id]) + respond_with(@image) end def create - authorize! :create, Image - @image = Image.create(params[:image]) - render :show, :status => 201 + authorize! :create, Spree::Image + @image = scope.images.create(image_params) + respond_with(@image, :status => 201, :default_template => :show) end def update - authorize! :update, Image - @image = Image.find(params[:id]) - @image.update_attributes(params[:image]) - render :show, :status => 200 + @image = scope.images.accessible_by(current_ability, :update).find(params[:id]) + @image.update_attributes(image_params) + respond_with(@image, :default_template => :show) end def destroy - authorize! :delete, Image - @image = Image.find(params[:id]) + @image = scope.images.accessible_by(current_ability, :destroy).find(params[:id]) @image.destroy - render :text => nil, :status => 204 + respond_with(@image, :status => 204) end + + private + def image_params + params.require(:image).permit(permitted_image_attributes) + end + + def scope + if params[:product_id] + scope = Spree::Product.friendly.find(params[:product_id]) + elsif params[:variant_id] + scope = Spree::Variant.find(params[:variant_id]) + end + end end end end diff --git a/api/app/controllers/spree/api/inventory_units_controller.rb b/api/app/controllers/spree/api/inventory_units_controller.rb new file mode 100644 index 00000000000..8012efef756 --- /dev/null +++ b/api/app/controllers/spree/api/inventory_units_controller.rb @@ -0,0 +1,52 @@ +module Spree + module Api + class InventoryUnitsController < Spree::Api::BaseController + before_action :prepare_event, only: :update + + def show + @inventory_unit = inventory_unit + respond_with(@inventory_unit) + end + + def update + authorize! :update, inventory_unit.order + + inventory_unit.transaction do + if inventory_unit.update_attributes(inventory_unit_params) + fire + render :show, :status => 200 + else + invalid_resource!(inventory_unit) + end + end + end + + private + + def inventory_unit + @inventory_unit ||= Spree::InventoryUnit.accessible_by(current_ability, :read).find(params[:id]) + end + + def prepare_event + return unless @event = params[:fire] + + can_event = "can_#{@event}?" + + unless inventory_unit.respond_to?(can_event) && + inventory_unit.send(can_event) + render :text => { :exception => "cannot transition to #{@event}" }.to_json, + :status => 200 + false + end + end + + def fire + inventory_unit.send("#{@event}!") if @event + end + + def inventory_unit_params + params.require(:inventory_unit).permit(permitted_inventory_unit_attributes) + end + end + end +end diff --git a/api/app/controllers/spree/api/line_items_controller.rb b/api/app/controllers/spree/api/line_items_controller.rb index 6214a6b5ff9..8b560b883df 100644 --- a/api/app/controllers/spree/api/line_items_controller.rb +++ b/api/app/controllers/spree/api/line_items_controller.rb @@ -1,38 +1,69 @@ module Spree module Api class LineItemsController < Spree::Api::BaseController + class_attribute :line_item_options + + self.line_item_options = [] + def create - authorize! :read, order - @line_item = order.line_items.build(params[:line_item], :as => :api) - if @line_item.save - render :show, :status => 201 + variant = Spree::Variant.find(params[:line_item][:variant_id]) + @line_item = order.contents.add( + variant, + params[:line_item][:quantity] || 1, + line_item_params[:options] || {} + ) + + if @line_item.errors.empty? + respond_with(@line_item, status: 201, default_template: :show) else invalid_resource!(@line_item) end end def update - authorize! :read, order - @line_item = order.line_items.find(params[:id]) - if @line_item.update_attributes(params[:line_item]) - render :show + @line_item = find_line_item + if @order.contents.update_cart(line_items_attributes) + @line_item.reload + respond_with(@line_item, default_template: :show) else invalid_resource!(@line_item) end end def destroy - authorize! :read, order - @line_item = order.line_items.find(params[:id]) - @line_item.destroy - render :text => nil, :status => 204 + @line_item = find_line_item + variant = Spree::Variant.unscoped.find(@line_item.variant_id) + @order.contents.remove(variant, @line_item.quantity) + respond_with(@line_item, status: 204) end private + def order + @order ||= Spree::Order.includes(:line_items).find_by!(number: order_id) + authorize! :update, @order, order_token + end - def order - @order ||= Order.find_by_number!(params[:order_id]) - end + def find_line_item + id = params[:id].to_i + order.line_items.detect { |line_item| line_item.id == id } or + raise ActiveRecord::RecordNotFound + end + + def line_items_attributes + {line_items_attributes: { + id: params[:id], + quantity: params[:line_item][:quantity], + options: line_item_params[:options] || {} + }} + end + + def line_item_params + params.require(:line_item).permit( + :quantity, + :variant_id, + options: line_item_options + ) + end end end end diff --git a/api/app/controllers/spree/api/option_types_controller.rb b/api/app/controllers/spree/api/option_types_controller.rb index fc28b95a6d8..c7168c3fbde 100644 --- a/api/app/controllers/spree/api/option_types_controller.rb +++ b/api/app/controllers/spree/api/option_types_controller.rb @@ -3,21 +3,21 @@ module Api class OptionTypesController < Spree::Api::BaseController def index if params[:ids] - @option_types = Spree::OptionType.where(:id => params[:ids]) + @option_types = Spree::OptionType.includes(:option_values).accessible_by(current_ability, :read).where(id: params[:ids].split(',')) else - @option_types = Spree::OptionType.scoped.ransack(params[:q]).result + @option_types = Spree::OptionType.includes(:option_values).accessible_by(current_ability, :read).load.ransack(params[:q]).result end respond_with(@option_types) end def show - @option_type = Spree::OptionType.find(params[:id]) - respond_with(@option_type) + @option_type = Spree::OptionType.accessible_by(current_ability, :read).find(params[:id]) + respond_with(@option_type) end def create - authorize! :create, Spree::OptionType - @option_type = Spree::OptionType.new(params[:option_type]) + authorize! :create, Spree::OptionType + @option_type = Spree::OptionType.new(option_type_params) if @option_type.save render :show, :status => 201 else @@ -26,9 +26,8 @@ def create end def update - authorize! :update, Spree::OptionType - @option_type = Spree::OptionType.find(params[:id]) - if @option_type.update_attributes(params[:option_type]) + @option_type = Spree::OptionType.accessible_by(current_ability, :update).find(params[:id]) + if @option_type.update_attributes(option_type_params) render :show else invalid_resource!(@option_type) @@ -36,11 +35,15 @@ def update end def destroy - authorize! :destroy, Spree::OptionType - @option_type = Spree::OptionType.find(params[:id]) + @option_type = Spree::OptionType.accessible_by(current_ability, :destroy).find(params[:id]) @option_type.destroy render :text => nil, :status => 204 end + + private + def option_type_params + params.require(:option_type).permit(permitted_option_type_attributes) + end end end end diff --git a/api/app/controllers/spree/api/option_values_controller.rb b/api/app/controllers/spree/api/option_values_controller.rb new file mode 100644 index 00000000000..dd9e2a69b32 --- /dev/null +++ b/api/app/controllers/spree/api/option_values_controller.rb @@ -0,0 +1,58 @@ +module Spree + module Api + class OptionValuesController < Spree::Api::BaseController + def index + if params[:ids] + @option_values = scope.where(:id => params[:ids]) + else + @option_values = scope.ransack(params[:q]).result + end + respond_with(@option_values) + end + + def show + @option_value = scope.find(params[:id]) + respond_with(@option_value) + end + + def create + authorize! :create, Spree::OptionValue + @option_value = scope.new(option_value_params) + if @option_value.save + render :show, :status => 201 + else + invalid_resource!(@option_value) + end + end + + def update + @option_value = scope.accessible_by(current_ability, :update).find(params[:id]) + if @option_value.update_attributes(option_value_params) + render :show + else + invalid_resource!(@option_value) + end + end + + def destroy + @option_value = scope.accessible_by(current_ability, :destroy).find(params[:id]) + @option_value.destroy + render :text => nil, :status => 204 + end + + private + + def scope + if params[:option_type_id] + @scope ||= Spree::OptionType.find(params[:option_type_id]).option_values.accessible_by(current_ability, :read) + else + @scope ||= Spree::OptionValue.accessible_by(current_ability, :read).load + end + end + + def option_value_params + params.require(:option_value).permit(permitted_option_value_attributes) + end + end + end +end diff --git a/api/app/controllers/spree/api/orders_controller.rb b/api/app/controllers/spree/api/orders_controller.rb index 980afcd6447..75258cac5d7 100644 --- a/api/app/controllers/spree/api/orders_controller.rb +++ b/api/app/controllers/spree/api/orders_controller.rb @@ -1,81 +1,138 @@ module Spree module Api class OrdersController < Spree::Api::BaseController - before_filter :authorize_read!, :except => [:index, :search, :create] + skip_before_action :check_for_user_or_api_key, only: :apply_coupon_code + skip_before_action :authenticate_user, only: :apply_coupon_code - def index - # should probably look at turning this into a CanCan step - raise CanCan::AccessDenied unless current_api_user.has_spree_role?("admin") - @orders = Order.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + before_action :find_order, except: [:create, :mine, :current, :index, :update] + + # Dynamically defines our stores checkout steps to ensure we check authorization on each step. + Spree::Order.checkout_steps.keys.each do |step| + define_method step do + find_order + authorize! :update, @order, params[:token] + end end - def show + def cancel + authorize! :update, @order, params[:token] + @order.cancel! + respond_with(@order, :default_template => :show) end def create - @order = Order.build_from_api(current_api_user, nested_params) - next!(:status => 201) - end + authorize! :create, Spree::Order + order_user = if @current_user_roles.include?('admin') && order_params[:user_id] + Spree.user_class.find(order_params[:user_id]) + else + current_api_user + end - def update - authorize! :update, Order - if order.update_attributes(nested_params) - order.update! - render :show + import_params = if @current_user_roles.include?("admin") + params[:order].present? ? params[:order].permit! : {} else - invalid_resource!(order) + order_params end - end - def address - order.build_ship_address(params[:shipping_address]) if params[:shipping_address] - order.build_bill_address(params[:billing_address]) if params[:billing_address] - next! + @order = Spree::Core::Importer::Order.import(order_user, import_params) + respond_with(@order, default_template: :show, status: 201) end - def delivery - begin - ShippingMethod.find(params[:shipping_method_id]) - rescue ActiveRecord::RecordNotFound - render :invalid_shipping_method, :status => 422 - else - order.update_attribute(:shipping_method_id, params[:shipping_method_id]) - next! - end + def empty + authorize! :update, @order, order_token + @order.empty! + render text: nil, status: 204 end - def cancel - order.cancel! - render :show + def index + authorize! :index, Order + @orders = Spree::Order.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@orders) end - def empty - order.line_items.destroy_all - order.update! - render :text => nil, :status => 200 + def show + authorize! :show, @order, order_token + respond_with(@order) end - private + def update + find_order(true) + authorize! :update, @order, order_token - def nested_params - map_nested_attributes_keys Order, params[:order] || {} + result = if request.patch? + # This will update the order without a checkout reset. + @order.update_attributes(order_params) + else + # This will reset checkout back to address and delete all shipments. + @order.contents.update_cart(order_params) + end + + if result + user_id = params[:order][:user_id] + if current_api_user.has_spree_role?('admin') && user_id + @order.associate_user!(Spree.user_class.find(user_id)) + end + respond_with(@order, default_template: :show) + else + invalid_resource!(@order) + end end - def order - @order ||= Order.find_by_number!(params[:id]) + def current + @order = find_current_order + if @order + respond_with(@order, default_template: :show, locals: { root_object: @order }) + else + head :no_content + end end - def next!(options={}) - if @order.valid? && @order.next - render :show, :status => options[:status] || 200 + def mine + if current_api_user.persisted? + @orders = current_api_user.orders.reverse_chronological.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) else - render :could_not_transition, :status => 422 + render "spree/api/errors/unauthorized", status: :unauthorized end end - def authorize_read! - authorize! :read, order + def apply_coupon_code + find_order + authorize! :update, @order, order_token + @order.coupon_code = params[:coupon_code] + @handler = Spree::PromotionHandler::Coupon.new(@order).apply + status = @handler.successful? ? 200 : 422 + render "spree/api/promotions/handler", :status => status end + + private + def order_params + if params[:order] + normalize_params + params.require(:order).permit(permitted_order_attributes) + else + {} + end + end + + def normalize_params + params[:order][:payments_attributes] = params[:order].delete(:payments) if params[:order][:payments] + params[:order][:shipments_attributes] = params[:order].delete(:shipments) if params[:order][:shipments] + params[:order][:line_items_attributes] = params[:order].delete(:line_items) if params[:order][:line_items] + params[:order][:ship_address_attributes] = params[:order].delete(:ship_address) if params[:order][:ship_address] + params[:order][:bill_address_attributes] = params[:order].delete(:bill_address) if params[:order][:bill_address] + end + + def find_order(lock = false) + @order = Spree::Order.lock(lock).find_by!(number: params[:id]) + end + + def find_current_order + current_api_user ? current_api_user.orders.incomplete.order(:created_at).last : nil + end + + def order_id + super || params[:id] + end end end end diff --git a/api/app/controllers/spree/api/payments_controller.rb b/api/app/controllers/spree/api/payments_controller.rb index 5f724ca92cd..75793f5545a 100644 --- a/api/app/controllers/spree/api/payments_controller.rb +++ b/api/app/controllers/spree/api/payments_controller.rb @@ -1,27 +1,42 @@ module Spree module Api class PaymentsController < Spree::Api::BaseController - before_filter :find_order - before_filter :find_payment, :only => [:show, :authorize, :purchase, :capture, :void, :credit] + + before_action :find_order + before_action :find_payment, only: [:update, :show, :authorize, :purchase, :capture, :void] def index @payments = @order.payments.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@payments) end def new - @payment_methods = Spree::PaymentMethod.where(:environment => Rails.env) + @payment_methods = Spree::PaymentMethod.available + respond_with(@payment_method) end def create - @payment = @order.payments.build(params[:payment]) + @payment = @order.payments.build(payment_params) if @payment.save - render :show, :status => 201 + respond_with(@payment, status: 201, default_template: :show) + else + invalid_resource!(@payment) + end + end + + def update + authorize! params[:action], @payment + if !@payment.editable? + render 'update_forbidden', status: 403 + elsif @payment.update_attributes(payment_params) + respond_with(@payment, default_template: :show) else invalid_resource!(@payment) end end def show + respond_with(@payment) end def authorize @@ -40,36 +55,26 @@ def void perform_payment_action(:void_transaction) end - def credit - if params[:amount].to_f > @payment.credit_allowed - render "spree/api/payments/credit_over_limit", :status => 422 - else - perform_payment_action(:credit, params[:amount]) - end - end - private - def find_order - @order = Order.find_by_number(params[:order_id]) - authorize! :read, @order - end - - def find_payment - @payment = @order.payments.find(params[:id]) - end + def find_order + @order = Spree::Order.find_by(number: order_id) + authorize! :read, @order, order_token + end - def perform_payment_action(action, *args) - authorize! action, Payment + def find_payment + @payment = @order.payments.find(params[:id]) + end - begin + def perform_payment_action(action, *args) + authorize! action, Spree::Payment @payment.send("#{action}!", *args) - render :show - rescue Spree::Core::GatewayError => e - @error = e.message - render "spree/api/errors/gateway_error", :status => 422 + respond_with(@payment, default_template: :show) + end + + def payment_params + params.require(:payment).permit(permitted_payment_attributes) end - end end end end diff --git a/api/app/controllers/spree/api/product_properties_controller.rb b/api/app/controllers/spree/api/product_properties_controller.rb index e5ed405d32c..75479f5bf8f 100644 --- a/api/app/controllers/spree/api/product_properties_controller.rb +++ b/api/app/controllers/spree/api/product_properties_controller.rb @@ -1,61 +1,72 @@ module Spree module Api class ProductPropertiesController < Spree::Api::BaseController - before_filter :find_product - before_filter :product_property, :only => [:show, :update, :destroy] + + before_action :find_product + before_action :product_property, only: [:show, :update, :destroy] def index - @product_properties = @product.product_properties.ransack(params[:q]).result - .page(params[:page]).per(params[:per_page]) + @product_properties = @product.product_properties.accessible_by(current_ability, :read). + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + respond_with(@product_properties) end def show + respond_with(@product_property) end def new end def create - authorize! :create, ProductProperty - @product_property = @product.product_properties.new(params[:product_property]) + authorize! :create, Spree::ProductProperty + @product_property = @product.product_properties.new(product_property_params) if @product_property.save - render :show, :status => 201 + respond_with(@product_property, status: 201, default_template: :show) else invalid_resource!(@product_property) end end def update - authorize! :update, ProductProperty - if @product_property && @product_property.update_attributes(params[:product_property]) - render :show, :status => 200 + if @product_property + authorize! :update, @product_property + @product_property.update_attributes(product_property_params) + respond_with(@product_property, status: 200, default_template: :show) else invalid_resource!(@product_property) end end def destroy - authorize! :delete, ProductProperty - if(@product_property) + if @product_property + authorize! :destroy, @product_property @product_property.destroy - render :text => nil, :status => 204 + respond_with(@product_property, status: 204) else invalid_resource!(@product_property) end - end private + def find_product @product = super(params[:product_id]) + authorize! :read, @product end def product_property if @product - @product_property ||= @product.product_properties.find_by_id(params[:id]) - @product_property ||= @product.product_properties.joins(:property).where('spree_properties.name' => params[:id]).readonly(false).first + @product_property ||= @product.product_properties.find_by(id: params[:id]) + @product_property ||= @product.product_properties.includes(:property).where(spree_properties: { name: params[:id] }).first + authorize! :read, @product_property end end + + def product_property_params + params.require(:product_property).permit(permitted_product_properties_attributes) + end end end end diff --git a/api/app/controllers/spree/api/products_controller.rb b/api/app/controllers/spree/api/products_controller.rb index fdf1dd3fa43..80b2b38ed4f 100644 --- a/api/app/controllers/spree/api/products_controller.rb +++ b/api/app/controllers/spree/api/products_controller.rb @@ -1,45 +1,128 @@ module Spree module Api class ProductsController < Spree::Api::BaseController + def index - @products = product_scope.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + if params[:ids] + @products = product_scope.where(:id => params[:ids].split(",")) + else + @products = product_scope.ransack(params[:q]).result + end + + @products = @products.distinct.page(params[:page]).per(params[:per_page]) + expires_in 15.minutes, :public => true + headers['Surrogate-Control'] = "max-age=#{15.minutes}" + respond_with(@products) end def show @product = find_product(params[:id]) + expires_in 15.minutes, :public => true + headers['Surrogate-Control'] = "max-age=#{15.minutes}" + headers['Surrogate-Key'] = "product_id=1" + respond_with(@product) end - def new - end - + # Takes besides the products attributes either an array of variants or + # an array of option types. + # + # By submitting an array of variants the option types will be created + # using the *name* key in options hash. e.g + # + # product: { + # ... + # variants: { + # price: 19.99, + # sku: "hey_you", + # options: [ + # { name: "size", value: "small" }, + # { name: "color", value: "black" } + # ] + # } + # } + # + # Or just pass in the option types hash: + # + # product: { + # ... + # option_types: ['size', 'color'] + # } + # + # By passing the shipping category name you can fetch or create that + # shipping category on the fly. e.g. + # + # product: { + # ... + # shipping_category: "Free Shipping Items" + # } + # def create - authorize! :create, Product + authorize! :create, Spree::Product params[:product][:available_on] ||= Time.now - @product = Product.new(params[:product]) - if @product.save - render :show, :status => 201 + set_up_shipping_category + + options = { variants_attrs: variants_params, options_attrs: option_types_params } + @product = Spree::Core::Importer::Product.new(nil, product_params, options).create + + if @product.persisted? + respond_with(@product, :status => 201, :default_template => :show) else invalid_resource!(@product) end end def update - authorize! :update, Product @product = find_product(params[:id]) - if @product.update_attributes(params[:product]) - render :show, :status => 200 + authorize! :update, @product + + options = { variants_attrs: variants_params, options_attrs: option_types_params } + @product = Spree::Core::Importer::Product.new(@product, product_params, options).update + + if @product.errors.empty? + respond_with(@product.reload, :status => 200, :default_template => :show) else invalid_resource!(@product) end end def destroy - authorize! :delete, Product @product = find_product(params[:id]) - @product.update_attribute(:deleted_at, Time.now) - @product.variants_including_master.update_all(:deleted_at => Time.now) - render :text => nil, :status => 204 + authorize! :destroy, @product + @product.destroy + respond_with(@product, :status => 204) end + + private + def product_params + product_params = params.require(:product).permit(permitted_product_attributes) + if product_params[:taxon_ids].present? + product_params[:taxon_ids] = product_params[:taxon_ids].split(',') + end + product_params + end + + def variants_params + variants_key = if params[:product].has_key? :variants + :variants + else + :variants_attributes + end + + params.require(:product).permit( + variants_key => [permitted_variant_attributes, :id], + ).delete(variants_key) || [] + end + + def option_types_params + params[:product].fetch(:option_types, []) + end + + def set_up_shipping_category + if shipping_category = params[:product].delete(:shipping_category) + id = Spree::ShippingCategory.find_or_create_by(name: shipping_category).id + params[:product][:shipping_category_id] = id + end + end end end end diff --git a/api/app/controllers/spree/api/promotions_controller.rb b/api/app/controllers/spree/api/promotions_controller.rb new file mode 100644 index 00000000000..b09be377050 --- /dev/null +++ b/api/app/controllers/spree/api/promotions_controller.rb @@ -0,0 +1,26 @@ +module Spree + module Api + class PromotionsController < Spree::Api::BaseController + before_filter :requires_admin + before_filter :load_promotion + + def show + if @promotion + respond_with(@promotion, default_template: :show) + else + raise ActiveRecord::RecordNotFound + end + end + + private + def requires_admin + return if @current_user_roles.include?("admin") + unauthorized and return + end + + def load_promotion + @promotion = Spree::Promotion.find_by_id(params[:id]) || Spree::Promotion.with_coupon_code(params[:id]) + end + end + end +end diff --git a/api/app/controllers/spree/api/properties_controller.rb b/api/app/controllers/spree/api/properties_controller.rb new file mode 100644 index 00000000000..7725f2ed021 --- /dev/null +++ b/api/app/controllers/spree/api/properties_controller.rb @@ -0,0 +1,70 @@ +module Spree + module Api + class PropertiesController < Spree::Api::BaseController + + before_action :find_property, only: [:show, :update, :destroy] + + def index + @properties = Spree::Property.accessible_by(current_ability, :read) + + if params[:ids] + @properties = @properties.where(:id => params[:ids].split(",")) + else + @properties = @properties.ransack(params[:q]).result + end + + @properties = @properties.page(params[:page]).per(params[:per_page]) + respond_with(@properties) + end + + def show + respond_with(@property) + end + + def new + end + + def create + authorize! :create, Spree::Property + @property = Spree::Property.new(property_params) + if @property.save + respond_with(@property, status: 201, default_template: :show) + else + invalid_resource!(@property) + end + end + + def update + if @property + authorize! :update, @property + @property.update_attributes(property_params) + respond_with(@property, status: 200, default_template: :show) + else + invalid_resource!(@property) + end + end + + def destroy + if @property + authorize! :destroy, @property + @property.destroy + respond_with(@property, status: 204) + else + invalid_resource!(@property) + end + end + + private + + def find_property + @property = Spree::Property.accessible_by(current_ability, :read).find(params[:id]) + rescue ActiveRecord::RecordNotFound + @property = Spree::Property.accessible_by(current_ability, :read).find_by!(name: params[:id]) + end + + def property_params + params.require(:property).permit(permitted_property_attributes) + end + end + end +end diff --git a/api/app/controllers/spree/api/return_authorizations_controller.rb b/api/app/controllers/spree/api/return_authorizations_controller.rb index 9d46c3cd24a..96cf02ff35d 100644 --- a/api/app/controllers/spree/api/return_authorizations_controller.rb +++ b/api/app/controllers/spree/api/return_authorizations_controller.rb @@ -1,49 +1,68 @@ module Spree module Api class ReturnAuthorizationsController < Spree::Api::BaseController - before_filter :authorize_admin! + + def create + authorize! :create, Spree::ReturnAuthorization + @return_authorization = order.return_authorizations.build(return_authorization_params) + if @return_authorization.save + respond_with(@return_authorization, status: 201, default_template: :show) + else + invalid_resource!(@return_authorization) + end + end + + def destroy + @return_authorization = order.return_authorizations.accessible_by(current_ability, :destroy).find(params[:id]) + @return_authorization.destroy + respond_with(@return_authorization, status: 204) + end def index - @return_authorizations = order.return_authorizations.ransack(params[:q]).result - .page(params[:page]).per(params[:per_page]) + authorize! :admin, Spree::ReturnAuthorization + @return_authorizations = order.return_authorizations.accessible_by(current_ability, :read). + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + respond_with(@return_authorizations) + end + + def new + authorize! :admin, Spree::ReturnAuthorization end def show - @return_authorization = order.return_authorizations.find(params[:id]) + authorize! :admin, Spree::ReturnAuthorization + @return_authorization = order.return_authorizations.accessible_by(current_ability, :read).find(params[:id]) + respond_with(@return_authorization) end - def create - @return_authorization = order.return_authorizations.build(params[:return_authorization], :as => :api) - if @return_authorization.save - render :show, :status => 201 + def update + @return_authorization = order.return_authorizations.accessible_by(current_ability, :update).find(params[:id]) + if @return_authorization.update_attributes(return_authorization_params) + respond_with(@return_authorization, default_template: :show) else invalid_resource!(@return_authorization) end end - def update - @return_authorization = order.return_authorizations.find(params[:id]) - if @return_authorization.update_attributes(params[:return_authorization]) - render :show + def cancel + @return_authorization = order.return_authorizations.accessible_by(current_ability, :update).find(params[:id]) + if @return_authorization.cancel + respond_with @return_authorization, default_template: :show else invalid_resource!(@return_authorization) end end - def destroy - @return_authorization = order.return_authorizations.find(params[:id]) - @return_authorization.destroy - render :text => nil, :status => 204 - end - private def order - @order ||= Order.find_by_number!(params[:order_id]) + @order ||= Spree::Order.find_by!(number: order_id) + authorize! :read, @order end - def authorize_admin! - authorize! :manage, Spree::ReturnAuthorization + def return_authorization_params + params.require(:return_authorization).permit(permitted_return_authorization_attributes) end end end diff --git a/api/app/controllers/spree/api/shipments_controller.rb b/api/app/controllers/spree/api/shipments_controller.rb index b8409ca011e..e52e75791ae 100644 --- a/api/app/controllers/spree/api/shipments_controller.rb +++ b/api/app/controllers/spree/api/shipments_controller.rb @@ -1,41 +1,149 @@ module Spree module Api class ShipmentsController < Spree::Api::BaseController - before_filter :find_order - before_filter :find_and_update_shipment, :only => [:ship, :ready] + + before_action :find_and_update_shipment, only: [:ship, :ready, :add, :remove] + before_action :load_transfer_params, only: [:transfer_to_location, :transfer_to_shipment] + + def mine + if current_api_user.persisted? + @shipments = Spree::Shipment + .reverse_chronological + .joins(:order) + .where(spree_orders: {user_id: current_api_user.id}) + .includes(mine_includes) + .ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + else + render "spree/api/errors/unauthorized", status: :unauthorized + end + end + + def create + @order = Spree::Order.find_by!(number: params.fetch(:shipment).fetch(:order_id)) + authorize! :read, @order + authorize! :create, Shipment + quantity = params[:quantity].to_i + @shipment = @order.shipments.create(stock_location_id: params.fetch(:stock_location_id)) + @order.contents.add(variant, quantity, {shipment: @shipment}) + + @shipment.save! + + respond_with(@shipment.reload, default_template: :show) + end + + def update + @shipment = Spree::Shipment.accessible_by(current_ability, :update).readonly(false).find_by!(number: params[:id]) + @shipment.update_attributes_and_order(shipment_params) + + respond_with(@shipment.reload, default_template: :show) + end def ready - authorize! :read, Shipment unless @shipment.ready? if @shipment.can_ready? @shipment.ready! else - render "spree/api/shipments/cannot_ready_shipment", :status => 422 and return + render 'spree/api/shipments/cannot_ready_shipment', status: 422 and return end end - render :show + respond_with(@shipment, default_template: :show) end def ship - authorize! :read, Shipment unless @shipment.shipped? @shipment.ship! end - render :show + respond_with(@shipment, default_template: :show) + end + + def add + quantity = params[:quantity].to_i + + @shipment.order.contents.add(variant, quantity, {shipment: @shipment}) + + respond_with(@shipment, default_template: :show) + end + + def remove + quantity = params[:quantity].to_i + + @shipment.order.contents.remove(variant, quantity, {shipment: @shipment}) + @shipment.reload if @shipment.persisted? + respond_with(@shipment, default_template: :show) + end + + def transfer_to_location + @stock_location = Spree::StockLocation.find(params[:stock_location_id]) + @original_shipment.transfer_to_location(@variant, @quantity, @stock_location) + render json: {success: true, message: Spree.t(:shipment_transfer_success)}, status: 201 + end + + def transfer_to_shipment + @target_shipment = Spree::Shipment.find_by!(number: params[:target_shipment_number]) + @original_shipment.transfer_to_shipment(@variant, @quantity, @target_shipment) + render json: {success: true, message: Spree.t(:shipment_transfer_success)}, status: 201 end private - def find_order - @order = Spree::Order.find_by_number!(params[:order_id]) - authorize! :read, @order + def load_transfer_params + @original_shipment = Spree::Shipment.where(number: params[:original_shipment_number]).first + @variant = Spree::Variant.find(params[:variant_id]) + @quantity = params[:quantity].to_i + authorize! :read, @original_shipment + authorize! :create, Shipment end def find_and_update_shipment - @shipment = @order.shipments.find_by_number!(params[:id]) - @shipment.update_attributes(params[:shipment]) + @shipment = Spree::Shipment.accessible_by(current_ability, :update).readonly(false).find_by!(number: params[:id]) + @shipment.update_attributes(shipment_params) @shipment.reload end + + def shipment_params + if params[:shipment] && !params[:shipment].empty? + params.require(:shipment).permit(permitted_shipment_attributes) + else + {} + end + end + + def variant + @variant ||= Spree::Variant.unscoped.find(params.fetch(:variant_id)) + end + + def mine_includes + { + order: { + bill_address: { + state: {}, + country: {}, + }, + ship_address: { + state: {}, + country: {}, + }, + adjustments: {}, + payments: { + order: {}, + payment_method: {}, + }, + }, + inventory_units: { + line_item: { + product: {}, + variant: {}, + }, + variant: { + product: {}, + default_price: {}, + option_values: { + option_type: {}, + }, + }, + }, + } + end end end end diff --git a/api/app/controllers/spree/api/states_controller.rb b/api/app/controllers/spree/api/states_controller.rb new file mode 100644 index 00000000000..1846693a383 --- /dev/null +++ b/api/app/controllers/spree/api/states_controller.rb @@ -0,0 +1,38 @@ +module Spree + module Api + class StatesController < Spree::Api::BaseController + skip_before_action :set_expiry + skip_before_action :check_for_user_or_api_key + skip_before_action :authenticate_user + + def index + @states = scope.ransack(params[:q]).result. + includes(:country).order('name ASC') + + if params[:page] || params[:per_page] + @states = @states.page(params[:page]).per(params[:per_page]) + end + + state = @states.last + if stale?(state) + respond_with(@states) + end + end + + def show + @state = scope.find(params[:id]) + respond_with(@state) + end + + private + def scope + if params[:country_id] + @country = Spree::Country.accessible_by(current_ability, :read).find(params[:country_id]) + return @country.states.accessible_by(current_ability, :read) + else + return Spree::State.accessible_by(current_ability, :read) + end + end + end + end +end diff --git a/api/app/controllers/spree/api/stock_items_controller.rb b/api/app/controllers/spree/api/stock_items_controller.rb new file mode 100644 index 00000000000..c341bb2e7ff --- /dev/null +++ b/api/app/controllers/spree/api/stock_items_controller.rb @@ -0,0 +1,75 @@ +module Spree + module Api + class StockItemsController < Spree::Api::BaseController + before_action :stock_location, except: [:update, :destroy] + + def index + @stock_items = scope.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@stock_items) + end + + def show + @stock_item = scope.find(params[:id]) + respond_with(@stock_item) + end + + def create + authorize! :create, Spree::StockItem + + count_on_hand = 0 + if params[:stock_item].has_key?(:count_on_hand) + count_on_hand = params[:stock_item][:count_on_hand].to_i + end + + @stock_item = scope.new(stock_item_params) + if @stock_item.save + @stock_item.adjust_count_on_hand(count_on_hand) + respond_with(@stock_item, status: 201, default_template: :show) + else + invalid_resource!(@stock_item) + end + end + + def update + @stock_item = Spree::StockItem.accessible_by(current_ability, :update).find(params[:id]) + + count_on_hand = 0 + if params[:stock_item].has_key?(:count_on_hand) + count_on_hand = params[:stock_item][:count_on_hand].to_i + params[:stock_item].delete(:count_on_hand) + end + + updated = params[:stock_item][:force] ? @stock_item.set_count_on_hand(count_on_hand) + : @stock_item.adjust_count_on_hand(count_on_hand) + + if updated + respond_with(@stock_item, status: 200, default_template: :show) + else + invalid_resource!(@stock_item) + end + end + + def destroy + @stock_item = Spree::StockItem.accessible_by(current_ability, :destroy).find(params[:id]) + @stock_item.destroy + respond_with(@stock_item, status: 204) + end + + private + + def stock_location + render 'spree/api/shared/stock_location_required', status: 422 and return unless params[:stock_location_id] + @stock_location ||= Spree::StockLocation.accessible_by(current_ability, :read).find(params[:stock_location_id]) + end + + def scope + includes = {:variant => [{ :option_values => :option_type }, :product] } + @stock_location.stock_items.accessible_by(current_ability, :read).includes(includes) + end + + def stock_item_params + params.require(:stock_item).permit(permitted_stock_item_attributes) + end + end + end +end diff --git a/api/app/controllers/spree/api/stock_locations_controller.rb b/api/app/controllers/spree/api/stock_locations_controller.rb new file mode 100644 index 00000000000..87e6ef4c4fb --- /dev/null +++ b/api/app/controllers/spree/api/stock_locations_controller.rb @@ -0,0 +1,50 @@ +module Spree + module Api + class StockLocationsController < Spree::Api::BaseController + def index + authorize! :read, Spree::StockLocation + @stock_locations = Spree::StockLocation.accessible_by(current_ability, :read).order('name ASC').ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@stock_locations) + end + + def show + respond_with(stock_location) + end + + def create + authorize! :create, Spree::StockLocation + @stock_location = Spree::StockLocation.new(stock_location_params) + if @stock_location.save + respond_with(@stock_location, status: 201, default_template: :show) + else + invalid_resource!(@stock_location) + end + end + + def update + authorize! :update, stock_location + if stock_location.update_attributes(stock_location_params) + respond_with(stock_location, status: 200, default_template: :show) + else + invalid_resource!(stock_location) + end + end + + def destroy + authorize! :destroy, stock_location + stock_location.destroy + respond_with(stock_location, :status => 204) + end + + private + + def stock_location + @stock_location ||= Spree::StockLocation.accessible_by(current_ability, :read).find(params[:id]) + end + + def stock_location_params + params.require(:stock_location).permit(permitted_stock_location_attributes) + end + end + end +end diff --git a/api/app/controllers/spree/api/stock_movements_controller.rb b/api/app/controllers/spree/api/stock_movements_controller.rb new file mode 100644 index 00000000000..0d066d80e47 --- /dev/null +++ b/api/app/controllers/spree/api/stock_movements_controller.rb @@ -0,0 +1,43 @@ +module Spree + module Api + class StockMovementsController < Spree::Api::BaseController + before_action :stock_location, except: [:update, :destroy] + + def index + authorize! :read, Spree::StockMovement + @stock_movements = scope.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@stock_movements) + end + + def show + @stock_movement = scope.find(params[:id]) + respond_with(@stock_movement) + end + + def create + authorize! :create, Spree::StockMovement + @stock_movement = scope.new(stock_movement_params) + if @stock_movement.save + respond_with(@stock_movement, status: 201, default_template: :show) + else + invalid_resource!(@stock_movement) + end + end + + private + + def stock_location + render 'spree/api/shared/stock_location_required', status: 422 and return unless params[:stock_location_id] + @stock_location ||= Spree::StockLocation.accessible_by(current_ability, :read).find(params[:stock_location_id]) + end + + def scope + @stock_location.stock_movements.accessible_by(current_ability, :read) + end + + def stock_movement_params + params.require(:stock_movement).permit(permitted_stock_movement_attributes) + end + end + end +end diff --git a/api/app/controllers/spree/api/stores_controller.rb b/api/app/controllers/spree/api/stores_controller.rb new file mode 100644 index 00000000000..f6f57e09a72 --- /dev/null +++ b/api/app/controllers/spree/api/stores_controller.rb @@ -0,0 +1,55 @@ +module Spree + module Api + class StoresController < Spree::Api::BaseController + + before_filter :get_store, except: [:index, :create] + + def index + authorize! :read, Spree::Store + @stores = Spree::Store.accessible_by(current_ability, :read).all + respond_with(@stores) + end + + def create + authorize! :create, Spree::Store + @store = Spree::Store.new(store_params) + @store.code = params[:store][:code] + if @store.save + respond_with(@store, status: 201, default_template: :show) + else + invalid_resource!(@store) + end + end + + def update + authorize! :update, @store + if @store.update_attributes(store_params) + respond_with(@store, status: 200, default_template: :show) + else + invalid_resource!(@store) + end + end + + def show + authorize! :read, @store + respond_with(@store) + end + + def destroy + authorize! :destroy, @store + @store.destroy + respond_with(@store, status: 204) + end + + private + + def get_store + @store = Spree::Store.find(params[:id]) + end + + def store_params + params.require(:store).permit(permitted_store_attributes) + end + end + end +end diff --git a/api/app/controllers/spree/api/taxonomies_controller.rb b/api/app/controllers/spree/api/taxonomies_controller.rb index 78dfaf25785..196b8db9ef3 100644 --- a/api/app/controllers/spree/api/taxonomies_controller.rb +++ b/api/app/controllers/spree/api/taxonomies_controller.rb @@ -1,46 +1,64 @@ module Spree module Api class TaxonomiesController < Spree::Api::BaseController + def index - @taxonomies = Taxonomy.order('name').includes(:root => :children).ransack(params[:q]).result - .page(params[:page]).per(params[:per_page]) + respond_with(taxonomies) end def show - @taxonomy = Taxonomy.find(params[:id]) + respond_with(taxonomy) + end + + # Because JSTree wants parameters in a *slightly* different format + def jstree + show end def create - authorize! :create, Taxonomy - @taxonomy = Taxonomy.new(params[:taxonomy]) + authorize! :create, Spree::Taxonomy + @taxonomy = Spree::Taxonomy.new(taxonomy_params) if @taxonomy.save - render :show, :status => 201 + respond_with(@taxonomy, :status => 201, :default_template => :show) else invalid_resource!(@taxonomy) end end def update - authorize! :update, Taxonomy - if taxonomy.update_attributes(params[:taxonomy]) - render :show, :status => 200 + authorize! :update, taxonomy + if taxonomy.update_attributes(taxonomy_params) + respond_with(taxonomy, :status => 200, :default_template => :show) else invalid_resource!(taxonomy) end end def destroy - authorize! :delete, Taxonomy + authorize! :destroy, taxonomy taxonomy.destroy - render :text => nil, :status => 204 + respond_with(taxonomy, :status => 204) end private + def taxonomies + @taxonomies = Spree::Taxonomy.accessible_by(current_ability, :read).order('name').includes(:root => :children). + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + end + def taxonomy - @taxonomy ||= Taxonomy.find(params[:id]) + @taxonomy ||= Spree::Taxonomy.accessible_by(current_ability, :read).find(params[:id]) end + def taxonomy_params + if params[:taxonomy] && !params[:taxonomy].empty? + params.require(:taxonomy).permit(permitted_taxonomy_attributes) + else + {} + end + end end end end diff --git a/api/app/controllers/spree/api/taxons_controller.rb b/api/app/controllers/spree/api/taxons_controller.rb index bd6b0e4e6f6..d1bc167e11e 100644 --- a/api/app/controllers/spree/api/taxons_controller.rb +++ b/api/app/controllers/spree/api/taxons_controller.rb @@ -2,48 +2,92 @@ module Spree module Api class TaxonsController < Spree::Api::BaseController def index - @taxons = taxonomy.root.children + if taxonomy + @taxons = taxonomy.root.children + else + if params[:ids] + @taxons = Spree::Taxon.accessible_by(current_ability, :read).where(id: params[:ids].split(',')) + else + @taxons = Spree::Taxon.accessible_by(current_ability, :read).order(:taxonomy_id, :lft).ransack(params[:q]).result + end + end + + @taxons = @taxons.page(params[:page]).per(params[:per_page]) + respond_with(@taxons) end def show @taxon = taxon + respond_with(@taxon) + end + + def jstree + show end def create - authorize! :create, Taxon - @taxon = Taxon.new(params[:taxon]) + authorize! :create, Spree::Taxon + @taxon = Spree::Taxon.new(taxon_params) + @taxon.taxonomy_id = params[:taxonomy_id] + taxonomy = Spree::Taxonomy.find_by(id: params[:taxonomy_id]) + + if taxonomy.nil? + @taxon.errors[:taxonomy_id] = I18n.t(:invalid_taxonomy_id, scope: 'spree.api') + invalid_resource!(@taxon) and return + end + + @taxon.parent_id = taxonomy.root.id unless params[:taxon][:parent_id] + if @taxon.save - render :show, :status => 201 + respond_with(@taxon, status: 201, default_template: :show) else invalid_resource!(@taxon) end end def update - authorize! :update, Taxon - if taxon.update_attributes(params[:taxon]) - render :show, :status => 200 + authorize! :update, taxon + if taxon.update_attributes(taxon_params) + respond_with(taxon, status: 200, default_template: :show) else invalid_resource!(taxon) end end def destroy - authorize! :delete, Taxon + authorize! :destroy, taxon taxon.destroy - render :text => nil, :status => 204 + respond_with(taxon, status: 204) + end + + def products + # Returns the products sorted by their position with the classification + # Products#index does not do the sorting. + taxon = Spree::Taxon.find(params[:id]) + @products = taxon.products.ransack(params[:q]).result + @products = @products.page(params[:page]).per(params[:per_page] || 500) + render "spree/api/products/index" end private - def taxonomy - @taxonomy ||= Taxonomy.find(params[:taxonomy_id]) - end + def taxonomy + if params[:taxonomy_id].present? + @taxonomy ||= Spree::Taxonomy.accessible_by(current_ability, :read).find(params[:taxonomy_id]) + end + end - def taxon - @taxon ||= taxonomy.taxons.find(params[:id]) - end + def taxon + @taxon ||= taxonomy.taxons.accessible_by(current_ability, :read).find(params[:id]) + end + def taxon_params + if params[:taxon] && !params[:taxon].empty? + params.require(:taxon).permit(permitted_taxon_attributes) + else + {} + end + end end end end diff --git a/api/app/controllers/spree/api/users_controller.rb b/api/app/controllers/spree/api/users_controller.rb new file mode 100644 index 00000000000..d0cfb7b1f01 --- /dev/null +++ b/api/app/controllers/spree/api/users_controller.rb @@ -0,0 +1,56 @@ +module Spree + module Api + class UsersController < Spree::Api::BaseController + + def index + @users = Spree.user_class.accessible_by(current_ability,:read).ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@users) + end + + def show + respond_with(user) + end + + def new + end + + def create + authorize! :create, Spree.user_class + @user = Spree.user_class.new(user_params) + if @user.save + respond_with(@user, :status => 201, :default_template => :show) + else + invalid_resource!(@user) + end + end + + def update + authorize! :update, user + if user.update_attributes(user_params) + respond_with(user, :status => 200, :default_template => :show) + else + invalid_resource!(user) + end + end + + def destroy + authorize! :destroy, user + user.destroy + respond_with(user, :status => 204) + end + + private + + def user + @user ||= Spree.user_class.accessible_by(current_ability, :read).find(params[:id]) + end + + def user_params + params.require(:user).permit(PermittedAttributes.user_attributes | + [bill_address_attributes: PermittedAttributes.address_attributes, + ship_address_attributes: PermittedAttributes.address_attributes]) + end + + end + end +end diff --git a/api/app/controllers/spree/api/variants_controller.rb b/api/app/controllers/spree/api/variants_controller.rb index 271478ed7de..b723952a0e6 100644 --- a/api/app/controllers/spree/api/variants_controller.rb +++ b/api/app/controllers/spree/api/variants_controller.rb @@ -1,70 +1,72 @@ module Spree module Api class VariantsController < Spree::Api::BaseController - before_filter :product + before_action :product - def index - @variants = scope.includes(:option_values).ransack(params[:q]).result. - page(params[:page]).per(params[:per_page]) + def create + authorize! :create, Spree::Variant + @variant = scope.new(variant_params) + if @variant.save + respond_with(@variant, status: 201, default_template: :show) + else + invalid_resource!(@variant) + end end - def show - @variant = scope.includes(:option_values).find(params[:id]) + def destroy + @variant = scope.accessible_by(current_ability, :destroy).find(params[:id]) + @variant.destroy + respond_with(@variant, status: 204) + end + + # The lazyloaded associations here are pretty much attached to which nodes + # we render on the view so we better update it any time a node is included + # or removed from the views. + def index + @variants = scope.includes({ option_values: :option_type }, :product, :default_price, :images, { stock_items: :stock_location }) + .ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@variants) end def new end - def create - authorize! :create, Variant - @variant = scope.new(params[:product]) - if @variant.save - render :show, :status => 201 - else - invalid_resource!(@variant) - end + def show + @variant = scope.includes({ option_values: :option_type }, :option_values, :product, :default_price, :images, { stock_items: :stock_location }) + .find(params[:id]) + respond_with(@variant) end def update - authorize! :update, Variant - @variant = scope.find(params[:id]) - if @variant.update_attributes(params[:variant]) - render :show, :status => 200 + @variant = scope.accessible_by(current_ability, :update).find(params[:id]) + if @variant.update_attributes(variant_params) + respond_with(@variant, status: 200, default_template: :show) else invalid_resource!(@product) end end - def destroy - authorize! :delete, Variant - @variant = scope.find(params[:id]) - @variant.destroy - render :text => nil, :status => 204 - end - private def product - @product ||= Spree::Product.find_by_permalink(params[:product_id]) if params[:product_id] + @product ||= Spree::Product.accessible_by(current_ability, :read).friendly.find(params[:product_id]) if params[:product_id] end def scope if @product - unless current_api_user.has_spree_role?("admin") || params[:show_deleted] - variants = @product.variants_including_master - else - variants = @product.variants_including_master_and_deleted - end + variants = @product.variants_including_master else - variants = Variant.scoped - if current_api_user.has_spree_role?("admin") - unless params[:show_deleted] - variants = Variant.active - end - else - variants = variants.active - end + variants = Spree::Variant + end + + if current_ability.can?(:manage, Spree::Variant) && params[:show_deleted] + variants = variants.with_deleted end - variants + + variants.accessible_by(current_ability, :read) + end + + def variant_params + params.require(:variant).permit(permitted_variant_attributes) end end end diff --git a/api/app/controllers/spree/api/zones_controller.rb b/api/app/controllers/spree/api/zones_controller.rb index 5a14c6a3b77..4f2b26f900f 100644 --- a/api/app/controllers/spree/api/zones_controller.rb +++ b/api/app/controllers/spree/api/zones_controller.rb @@ -1,42 +1,45 @@ module Spree module Api class ZonesController < Spree::Api::BaseController - def index - @zones = Zone.order('name ASC').ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) - end - - def show - zone - end def create - authorize! :create, Zone - @zone = Zone.new(map_nested_attributes_keys(Spree::Zone, params[:zone])) + authorize! :create, Spree::Zone + @zone = Spree::Zone.new(map_nested_attributes_keys(Spree::Zone, params[:zone])) if @zone.save - render :show, :status => 201 + respond_with(@zone, :status => 201, :default_template => :show) else invalid_resource!(@zone) end end + def destroy + authorize! :destroy, zone + zone.destroy + respond_with(zone, :status => 204) + end + + def index + @zones = Spree::Zone.accessible_by(current_ability, :read).order('name ASC').ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + respond_with(@zones) + end + + def show + respond_with(zone) + end + def update - authorize! :update, Zone + authorize! :update, zone if zone.update_attributes(map_nested_attributes_keys(Spree::Zone, params[:zone])) - render :show, :status => 200 + respond_with(zone, :status => 200, :default_template => :show) else - invalid_resource!(@zone) + invalid_resource!(zone) end end - def destroy - authorize! :delete, Zone - zone.destroy - render :text => nil, :status => 204 - end - private + def zone - @zone ||= Spree::Zone.find(params[:id]) + @zone ||= Spree::Zone.accessible_by(current_ability, :read).find(params[:id]) end end end diff --git a/api/app/helpers/spree/api/api_helpers.rb b/api/app/helpers/spree/api/api_helpers.rb index 8cbf5b5784d..c0b606fcee1 100644 --- a/api/app/helpers/spree/api/api_helpers.rb +++ b/api/app/helpers/spree/api/api_helpers.rb @@ -1,6 +1,39 @@ module Spree module Api module ApiHelpers + ATTRIBUTES = [ + :product_attributes, + :product_property_attributes, + :variant_attributes, + :image_attributes, + :option_value_attributes, + :order_attributes, + :line_item_attributes, + :option_type_attributes, + :payment_attributes, + :payment_method_attributes, + :shipment_attributes, + :taxonomy_attributes, + :taxon_attributes, + :address_attributes, + :country_attributes, + :state_attributes, + :adjustment_attributes, + :inventory_unit_attributes, + :return_authorization_attributes, + :creditcard_attributes, + :payment_source_attributes, + :user_attributes, + :property_attributes, + :stock_location_attributes, + :stock_movement_attributes, + :stock_item_attributes, + :promotion_attributes, + :store_attributes + ] + + mattr_reader *ATTRIBUTES + def required_fields_for(model) required_fields = model._validators.select do |field, validations| validations.any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) } @@ -8,68 +41,131 @@ def required_fields_for(model) # Permalinks presence is validated, but are really automatically generated # Therefore we shouldn't tell API clients that they MUST send one through required_fields.map!(&:to_s).delete("permalink") + # Do not require slugs, either + required_fields.delete("slug") required_fields end - def product_attributes - [:id, :name, :description, :price, :available_on, :permalink, - :count_on_hand, :meta_description, :meta_keywords, :taxon_ids] - end + @@product_attributes = [ + :id, :name, :description, :price, :display_price, :available_on, + :slug, :meta_description, :meta_keywords, :shipping_category_id, + :taxon_ids, :total_on_hand + ] - def product_property_attributes - [:id, :product_id, :property_id, :value, :property_name] - end + @@product_property_attributes = [ + :id, :product_id, :property_id, :value, :property_name + ] - def variant_attributes - [:id, :name, :count_on_hand, :sku, :price, :weight, :height, :width, :depth, :is_master, :cost_price, :permalink] - end + @@variant_attributes = [ + :id, :name, :sku, :price, :weight, :height, :width, :depth, :is_master, + :slug, :description, :track_inventory + ] - def image_attributes - [:id, :position, :attachment_content_type, :attachment_file_name, :type, :attachment_updated_at, :attachment_width, :attachment_height, :alt] - end + @@image_attributes = [ + :id, :position, :attachment_content_type, :attachment_file_name, :type, + :attachment_updated_at, :attachment_width, :attachment_height, :alt + ] - def option_value_attributes - [:id, :name, :presentation, :option_type_name, :option_type_id] - end + @@option_value_attributes = [ + :id, :name, :presentation, :option_type_name, :option_type_id, + :option_type_presentation + ] - def order_attributes - [:id, :number, :item_total, :total, :state, :adjustment_total, :user_id, :created_at, :updated_at, :completed_at, :payment_total, :shipment_state, :payment_state, :email, :special_instructions] - end + @@order_attributes = [ + :id, :number, :item_total, :total, :ship_total, :state, :adjustment_total, + :user_id, :created_at, :updated_at, :completed_at, :payment_total, + :shipment_state, :payment_state, :email, :special_instructions, :channel, + :included_tax_total, :additional_tax_total, :display_included_tax_total, + :display_additional_tax_total, :tax_total, :currency + ] - def line_item_attributes - [:id, :quantity, :price, :variant_id] - end + @@line_item_attributes = [:id, :quantity, :price, :variant_id] - def option_type_attributes - [:id, :name, :presentation, :position] - end + @@option_type_attributes = [:id, :name, :presentation, :position] - def payment_attributes - [:id, :source_type, :source_id, :amount, :payment_method_id, :response_code, :state, :avs_response, :created_at, :updated_at] - end + @@payment_attributes = [ + :id, :source_type, :source_id, :amount, :display_amount, + :payment_method_id, :state, :avs_response, :created_at, + :updated_at + ] - def payment_method_attributes - [:id, :name, :description] - end + @@payment_method_attributes = [:id, :name, :description] - def shipment_attributes - [:id, :tracking, :number, :cost, :shipped_at, :state] - end + @@shipment_attributes = [:id, :tracking, :number, :cost, :shipped_at, :state] - def taxonomy_attributes - [:id, :name] - end + @@taxonomy_attributes = [:id, :name] - def taxon_attributes - [:id, :name, :permalink, :position, :parent_id, :taxonomy_id] - end + @@taxon_attributes = [ + :id, :name, :pretty_name, :permalink, :parent_id, + :taxonomy_id + ] - def return_authorization_attributes - [:id, :number, :state, :amount, :order_id, :reason, :created_at, :updated_at] - end + @@inventory_unit_attributes = [ + :id, :lock_version, :state, :variant_id, :shipment_id, + :return_authorization_id + ] + + @@return_authorization_attributes = [ + :id, :number, :state, :order_id, :memo, :created_at, :updated_at + ] + + @@address_attributes = [ + :id, :firstname, :lastname, :full_name, :address1, :address2, :city, + :zipcode, :phone, :company, :alternative_phone, :country_id, :state_id, + :state_name, :state_text + ] + + @@country_attributes = [:id, :iso_name, :iso, :iso3, :name, :numcode] + + @@state_attributes = [:id, :name, :abbr, :country_id] + + @@adjustment_attributes = [ + :id, :source_type, :source_id, :adjustable_type, :adjustable_id, + :originator_type, :originator_id, :amount, :label, :mandatory, + :locked, :eligible, :created_at, :updated_at + ] + + @@creditcard_attributes = [ + :id, :month, :year, :cc_type, :last_digits, :name, + :gateway_customer_profile_id, :gateway_payment_profile_id + ] + + @@payment_source_attributes = [ + :id, :month, :year, :cc_type, :last_digits, :name + ] - def country_attributes - [:id, :iso_name, :iso, :iso3, :name, :numcode] + @@user_attributes = [:id, :email, :created_at, :updated_at] + + @@property_attributes = [:id, :name, :presentation] + + @@stock_location_attributes = [ + :id, :name, :address1, :address2, :city, :state_id, :state_name, + :country_id, :zipcode, :phone, :active + ] + + @@stock_movement_attributes = [:id, :quantity, :stock_item_id] + + @@stock_item_attributes = [ + :id, :count_on_hand, :backorderable, :lock_version, :stock_location_id, + :variant_id + ] + + @@promotion_attributes = [ + :id, :name, :description, :expires_at, :starts_at, :type, :usage_limit, + :match_policy, :code, :advertise, :path + ] + + @@store_attributes = [ + :id, :name, :url, :meta_description, :meta_keywords, :seo_title, + :mail_from_address, :default_currency, :code, :default + ] + + def variant_attributes + if @current_user_roles && @current_user_roles.include?("admin") + @@variant_attributes + [:cost_price] + else + @@variant_attributes + end end end end diff --git a/api/app/models/spree/line_item_decorator.rb b/api/app/models/spree/line_item_decorator.rb deleted file mode 100644 index d92f90df0e2..00000000000 --- a/api/app/models/spree/line_item_decorator.rb +++ /dev/null @@ -1,3 +0,0 @@ -Spree::LineItem.class_eval do - attr_accessible :quantity, :variant_id, :as => :api -end diff --git a/api/app/models/spree/option_value_decorator.rb b/api/app/models/spree/option_value_decorator.rb index 90a6c76cc94..b211890e810 100644 --- a/api/app/models/spree/option_value_decorator.rb +++ b/api/app/models/spree/option_value_decorator.rb @@ -2,4 +2,8 @@ def option_type_name option_type.name end + + def option_type_presentation + option_type.presentation + end end diff --git a/api/app/models/spree/order_decorator.rb b/api/app/models/spree/order_decorator.rb deleted file mode 100644 index 34e8a2599d1..00000000000 --- a/api/app/models/spree/order_decorator.rb +++ /dev/null @@ -1,13 +0,0 @@ -Spree::Order.class_eval do - def self.build_from_api(user, params) - order = create - params[:line_items_attributes] ||= [] - params[:line_items_attributes].each do |line_item| - order.add_variant(Spree::Variant.find(line_item[:variant_id]), line_item[:quantity]) - end - - order.user = user - order.email = user.email - order - end -end diff --git a/api/app/models/spree/user_decorator.rb b/api/app/models/spree/user_decorator.rb deleted file mode 100644 index 1575d74f84e..00000000000 --- a/api/app/models/spree/user_decorator.rb +++ /dev/null @@ -1,13 +0,0 @@ -if Spree.user_class - Spree.user_class.class_eval do - def generate_spree_api_key! - self.spree_api_key = SecureRandom.hex(24) - save! - end - - def clear_spree_api_key! - self.spree_api_key = nil - save! - end - end -end diff --git a/api/app/overrides/api_admin_user_edit_form.rb b/api/app/overrides/api_admin_user_edit_form.rb deleted file mode 100644 index 71eeed9eef4..00000000000 --- a/api/app/overrides/api_admin_user_edit_form.rb +++ /dev/null @@ -1,6 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/admin/users/edit", - :name => "api_admin_user_edit_form", - :insert_after => "[data-hook='admin_user_edit_general_settings']", - :partial => "spree/admin/users/api_fields", - :disabled => false) - diff --git a/api/app/views/spree/admin/users/_api_fields.html.erb b/api/app/views/spree/admin/users/_api_fields.html.erb deleted file mode 100644 index d5b7bfb523c..00000000000 --- a/api/app/views/spree/admin/users/_api_fields.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -
- <%= t('access', :scope => 'spree.api') %> - - <% if @user.spree_api_key.present? %> -
- <%= label_tag t('key', :scope => 'spree.api') %>: - <%= @user.spree_api_key %> -
-
- <%= form_tag spree.clear_api_key_admin_user_path(@user), :method => :put do %> - <%= button t('clear_key', :scope => 'spree.api'), 'icon-trash' %> - <% end %> - - <%= t(:or)%> - - <%= form_tag spree.generate_api_key_admin_user_path(@user), :method => :put do %> - <%= button t('regenerate_key', :scope => 'spree.api'), 'icon-refresh' %> - <% end %> -
- - <% else %> - -
<%= t('no_key', :scope => 'spree.api') %>
- -
- <%= form_tag spree.generate_api_key_admin_user_path(@user), :method => :put do %> - <%= button t('generate_key', :scope => 'spree.api'), 'icon-key' %> - <% end %> -
- <% end %> - -
diff --git a/api/app/views/spree/api/addresses/show.v1.rabl b/api/app/views/spree/api/addresses/show.v1.rabl index 91f79fa004b..c3416013804 100644 --- a/api/app/views/spree/api/addresses/show.v1.rabl +++ b/api/app/views/spree/api/addresses/show.v1.rabl @@ -1,11 +1,10 @@ object @address -attributes :id, :firstname, :lastname, :address1, :address2, - :city, :zipcode, :phone, - :company, :alternative_phone, :country_id, :state_id, - :state_name +cache [I18n.locale, root_object] +attributes *address_attributes + child(:country) do |address| - attributes :id, :iso_name, :iso, :iso3, :name, :numcode + attributes *country_attributes end child(:state) do |address| - attributes :abbr, :country_id, :id, :name + attributes *state_attributes end diff --git a/api/app/views/spree/api/adjustments/show.v1.rabl b/api/app/views/spree/api/adjustments/show.v1.rabl new file mode 100644 index 00000000000..e88c3d801d7 --- /dev/null +++ b/api/app/views/spree/api/adjustments/show.v1.rabl @@ -0,0 +1,4 @@ +object @adjustment +cache [I18n.locale, root_object] +attributes *adjustment_attributes +node(:display_amount) { |a| a.display_amount.to_s } diff --git a/api/app/views/spree/api/config/money.v1.rabl b/api/app/views/spree/api/config/money.v1.rabl new file mode 100644 index 00000000000..94e162b2cad --- /dev/null +++ b/api/app/views/spree/api/config/money.v1.rabl @@ -0,0 +1,6 @@ +object false +node(:symbol) { ::Money.new(1, Spree::Config[:currency]).symbol } +node(:symbol_position) { Spree::Config[:currency_symbol_position] } +node(:no_cents) { Spree::Config[:hide_cents] } +node(:decimal_mark) { Spree::Config[:currency_decimal_mark] } +node(:thousands_separator) { Spree::Config[:currency_thousands_separator] } \ No newline at end of file diff --git a/api/app/views/spree/api/config/show.v1.rabl b/api/app/views/spree/api/config/show.v1.rabl new file mode 100644 index 00000000000..4e28ef4c345 --- /dev/null +++ b/api/app/views/spree/api/config/show.v1.rabl @@ -0,0 +1,2 @@ +object false +node(:default_country_id) { Spree::Config[:default_country_id] } \ No newline at end of file diff --git a/api/app/views/spree/api/credit_cards/index.v1.rabl b/api/app/views/spree/api/credit_cards/index.v1.rabl new file mode 100644 index 00000000000..04007fdac0f --- /dev/null +++ b/api/app/views/spree/api/credit_cards/index.v1.rabl @@ -0,0 +1,7 @@ +object false +child(@credit_cards => :credit_cards) do + extends "spree/api/credit_cards/show" +end +node(:count) { @credit_cards.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @credit_cards.total_pages } diff --git a/api/app/views/spree/api/credit_cards/show.v1.rabl b/api/app/views/spree/api/credit_cards/show.v1.rabl new file mode 100644 index 00000000000..57d734d6abf --- /dev/null +++ b/api/app/views/spree/api/credit_cards/show.v1.rabl @@ -0,0 +1,3 @@ +object @credit_card +cache [I18n.locale, root_object] +attributes *creditcard_attributes diff --git a/api/app/views/spree/api/errors/gateway_error.v1.rabl b/api/app/views/spree/api/errors/gateway_error.v1.rabl index a4d5f44ec42..cb79f951a59 100644 --- a/api/app/views/spree/api/errors/gateway_error.v1.rabl +++ b/api/app/views/spree/api/errors/gateway_error.v1.rabl @@ -1,2 +1,2 @@ object false -node(:error) { I18n.t(:gateway_error, :scope => "spree.api", :text => @error) } +node(:error) { I18n.t(:gateway_error, scope: "spree.api", text: @error) } diff --git a/api/app/views/spree/api/errors/invalid_resource.v1.rabl b/api/app/views/spree/api/errors/invalid_resource.v1.rabl index 8101214c572..c7a0a8f92ad 100644 --- a/api/app/views/spree/api/errors/invalid_resource.v1.rabl +++ b/api/app/views/spree/api/errors/invalid_resource.v1.rabl @@ -1,3 +1,3 @@ object false node(:error) { I18n.t(:invalid_resource, :scope => "spree.api") } -node(:errors) { @resource.errors } +node(:errors) { @resource.errors.to_hash } diff --git a/api/app/views/spree/api/images/index.v1.rabl b/api/app/views/spree/api/images/index.v1.rabl new file mode 100644 index 00000000000..6fcdd48dbf1 --- /dev/null +++ b/api/app/views/spree/api/images/index.v1.rabl @@ -0,0 +1,4 @@ +object false +child(@images => :images) do + extends "spree/api/images/show" +end diff --git a/api/app/views/spree/api/images/show.v1.rabl b/api/app/views/spree/api/images/show.v1.rabl index cba9cd954a6..d019ed8bddb 100644 --- a/api/app/views/spree/api/images/show.v1.rabl +++ b/api/app/views/spree/api/images/show.v1.rabl @@ -1,3 +1,6 @@ object @image attributes *image_attributes attributes :viewable_type, :viewable_id +Spree::Image.attachment_definitions[:attachment][:styles].each do |k,v| + node("#{k}_url") { |i| i.attachment.url(k) } +end diff --git a/api/app/views/spree/api/inventory_units/show.rabl b/api/app/views/spree/api/inventory_units/show.rabl new file mode 100644 index 00000000000..d9e7960b503 --- /dev/null +++ b/api/app/views/spree/api/inventory_units/show.rabl @@ -0,0 +1,2 @@ +object @inventory_unit +attributes *inventory_unit_attributes diff --git a/api/app/views/spree/api/line_items/show.v1.rabl b/api/app/views/spree/api/line_items/show.v1.rabl index 1918f3462e4..e64a9796435 100644 --- a/api/app/views/spree/api/line_items/show.v1.rabl +++ b/api/app/views/spree/api/line_items/show.v1.rabl @@ -1,5 +1,15 @@ object @line_item +cache [I18n.locale, root_object] attributes *line_item_attributes +node(:single_display_amount) { |li| li.single_display_amount.to_s } +node(:display_amount) { |li| li.display_amount.to_s } +node(:total) { |li| li.total } child :variant do - extends "spree/api/variants/variant" + extends "spree/api/variants/small" + attributes :product_id + child(:images => :images) { extends "spree/api/images/show" } +end + +child :adjustments => :adjustments do + extends "spree/api/adjustments/show" end diff --git a/api/app/views/spree/api/option_values/index.v1.rabl b/api/app/views/spree/api/option_values/index.v1.rabl new file mode 100644 index 00000000000..0bdfd33be3a --- /dev/null +++ b/api/app/views/spree/api/option_values/index.v1.rabl @@ -0,0 +1,3 @@ +collection @option_values + +extends "spree/api/option_values/show" diff --git a/api/app/views/spree/api/option_values/show.v1.rabl b/api/app/views/spree/api/option_values/show.v1.rabl new file mode 100644 index 00000000000..5ed499ef223 --- /dev/null +++ b/api/app/views/spree/api/option_values/show.v1.rabl @@ -0,0 +1,2 @@ +object @option_value +attributes *option_value_attributes \ No newline at end of file diff --git a/api/app/views/spree/api/orders/could_not_apply_coupon.v1.rabl b/api/app/views/spree/api/orders/could_not_apply_coupon.v1.rabl new file mode 100644 index 00000000000..0a457130637 --- /dev/null +++ b/api/app/views/spree/api/orders/could_not_apply_coupon.v1.rabl @@ -0,0 +1,2 @@ +object false +node(:error) { @coupon_message } diff --git a/api/app/views/spree/api/orders/could_not_transition.v1.rabl b/api/app/views/spree/api/orders/could_not_transition.v1.rabl index ba951f343c2..6157703be69 100644 --- a/api/app/views/spree/api/orders/could_not_transition.v1.rabl +++ b/api/app/views/spree/api/orders/could_not_transition.v1.rabl @@ -1,3 +1,3 @@ object false -node(:error) { I18n.t(:could_not_transition, :scope => "spree.api.order") } -node(:errors) { @order.errors } +node(:error) { I18n.t(:could_not_transition, scope: "spree.api.order") } +node(:errors) { @order.errors.to_hash } diff --git a/api/app/views/spree/api/orders/delivery.v1.rabl b/api/app/views/spree/api/orders/delivery.v1.rabl deleted file mode 100644 index ed4d6535c9e..00000000000 --- a/api/app/views/spree/api/orders/delivery.v1.rabl +++ /dev/null @@ -1,3 +0,0 @@ -child(:rate_hash => :shipping_methods) do - attributes :id, :name, :cost -end diff --git a/api/app/views/spree/api/orders/index.v1.rabl b/api/app/views/spree/api/orders/index.v1.rabl index c937d2bffcf..d2cd5ae0517 100644 --- a/api/app/views/spree/api/orders/index.v1.rabl +++ b/api/app/views/spree/api/orders/index.v1.rabl @@ -1,6 +1,6 @@ object false child(@orders => :orders) do - attributes *order_attributes + extends "spree/api/orders/order" end node(:count) { @orders.count } node(:current_page) { params[:page] || 1 } diff --git a/api/app/views/spree/api/orders/mine.v1.rabl b/api/app/views/spree/api/orders/mine.v1.rabl new file mode 100644 index 00000000000..e219aaf2bcc --- /dev/null +++ b/api/app/views/spree/api/orders/mine.v1.rabl @@ -0,0 +1,9 @@ +object false + +child(@orders => :orders) do + extends "spree/api/orders/show" +end + +node(:count) { @orders.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @orders.num_pages } diff --git a/api/app/views/spree/api/orders/order.v1.rabl b/api/app/views/spree/api/orders/order.v1.rabl new file mode 100644 index 00000000000..a511dd34e48 --- /dev/null +++ b/api/app/views/spree/api/orders/order.v1.rabl @@ -0,0 +1,9 @@ +cache [I18n.locale, root_object] +attributes *order_attributes +node(:display_item_total) { |o| o.display_item_total.to_s } +node(:total_quantity) { |o| o.line_items.sum(:quantity) } +node(:display_total) { |o| o.display_total.to_s } +node(:display_ship_total) { |o| o.display_ship_total } +node(:display_tax_total) { |o| o.display_tax_total } +node(:token) { |o| o.guest_token } +node(:checkout_steps) { |o| o.checkout_steps } diff --git a/api/app/views/spree/api/orders/payment.v1.rabl b/api/app/views/spree/api/orders/payment.v1.rabl index b4c866d88ba..ccfe6ca4c8d 100644 --- a/api/app/views/spree/api/orders/payment.v1.rabl +++ b/api/app/views/spree/api/orders/payment.v1.rabl @@ -1,4 +1,3 @@ -attributes :id, :amount, :payment_method_id -child :payment_method => :payment_method do - attributes :id, :name, :environment -end \ No newline at end of file +child :available_payment_methods => :payment_methods do + attributes :id, :name, :environment, :method_type +end diff --git a/api/app/views/spree/api/orders/show.v1.rabl b/api/app/views/spree/api/orders/show.v1.rabl index 36b9b68513f..d65d5f25708 100644 --- a/api/app/views/spree/api/orders/show.v1.rabl +++ b/api/app/views/spree/api/orders/show.v1.rabl @@ -1,8 +1,8 @@ object @order -attributes *order_attributes +extends "spree/api/orders/order" -if lookup_context.find_all("spree/api/orders/#{@order.state}").present? - extends "spree/api/orders/#{@order.state}" +if lookup_context.find_all("spree/api/orders/#{root_object.state}").present? + extends "spree/api/orders/#{root_object.state}" end child :billing_address => :bill_address do @@ -18,12 +18,31 @@ child :line_items => :line_items do end child :payments => :payments do - attributes :id, :amount, :state, :payment_method_id + attributes *payment_attributes + child :payment_method => :payment_method do attributes :id, :name, :environment end + + child :source => :source do + attributes *payment_source_attributes + if @current_user_roles.include?('admin') + attributes *payment_source_attributes.concat([:gateway_customer_profile_id, :gateway_payment_profile_id]) + else + attributes *payment_source_attributes + end + end end child :shipments => :shipments do - extends "spree/api/shipments/show" + extends "spree/api/shipments/small" +end + +child :adjustments => :adjustments do + extends "spree/api/adjustments/show" +end + +# Necessary for backend's order interface +node :permissions do + { can_update: current_ability.can?(:update, root_object) } end diff --git a/api/app/views/spree/api/payments/credit_over_limit.v1.rabl b/api/app/views/spree/api/payments/credit_over_limit.v1.rabl index 9ac8274e38a..3cbf95ad4f1 100644 --- a/api/app/views/spree/api/payments/credit_over_limit.v1.rabl +++ b/api/app/views/spree/api/payments/credit_over_limit.v1.rabl @@ -1,2 +1,2 @@ object false -node(:error) { I18n.t(:credit_over_limit, :limit => @payment.credit_allowed, :scope => 'spree.api') } +node(:error) { I18n.t(:credit_over_limit, :limit => @payment.credit_allowed, :scope => 'spree.api.payment') } diff --git a/api/app/views/spree/api/payments/new.v1.rabl b/api/app/views/spree/api/payments/new.v1.rabl index 0c945623ab0..05d227cf83e 100644 --- a/api/app/views/spree/api/payments/new.v1.rabl +++ b/api/app/views/spree/api/payments/new.v1.rabl @@ -3,4 +3,3 @@ node(:attributes) { [*payment_attributes] } child @payment_methods => :payment_methods do attributes *payment_method_attributes end - diff --git a/api/app/views/spree/api/payments/update_forbidden.v1.rabl b/api/app/views/spree/api/payments/update_forbidden.v1.rabl new file mode 100644 index 00000000000..7f51e9c3a8c --- /dev/null +++ b/api/app/views/spree/api/payments/update_forbidden.v1.rabl @@ -0,0 +1,2 @@ +object false +node(:error) { I18n.t(:update_forbidden, :state => @payment.state, :scope => 'spree.api.payment') } diff --git a/api/app/views/spree/api/products/index.v1.rabl b/api/app/views/spree/api/products/index.v1.rabl index 958b4936c08..5262462e88a 100644 --- a/api/app/views/spree/api/products/index.v1.rabl +++ b/api/app/views/spree/api/products/index.v1.rabl @@ -2,7 +2,8 @@ object false node(:count) { @products.count } node(:total_count) { @products.total_count } node(:current_page) { params[:page] ? params[:page].to_i : 1 } +node(:per_page) { params[:per_page] || Kaminari.config.default_per_page } node(:pages) { @products.num_pages } -child(@products) do +child(@products => :products) do extends "spree/api/products/show" end diff --git a/api/app/views/spree/api/products/show.v1.rabl b/api/app/views/spree/api/products/show.v1.rabl index a04f57c4e36..ddfa558a9a8 100644 --- a/api/app/views/spree/api/products/show.v1.rabl +++ b/api/app/views/spree/api/products/show.v1.rabl @@ -1,25 +1,31 @@ object @product +cache [I18n.locale, @current_user_roles.include?('admin'), current_currency, root_object] + attributes *product_attributes -child :variants_including_master => :variants do - attributes *variant_attributes - child :option_values => :option_values do - attributes *option_value_attributes - end +node(:display_price) { |p| p.display_price.to_s } +node(:has_variants) { |p| p.has_variants? } + +child :master => :master do + extends "spree/api/variants/small" end -child :images => :images do - extends "spree/api/images/show" +child :variants => :variants do + extends "spree/api/variants/small" end child :option_types => :option_types do attributes *option_type_attributes - - child :option_values => :option_values do - attributes *option_value_attributes - end end child :product_properties => :product_properties do attributes *product_property_attributes end + +child :classifications => :classifications do + attributes :taxon_id, :position + + child(:taxon) do + extends "spree/api/taxons/show" + end +end diff --git a/api/app/views/spree/api/promotions/handler.v1.rabl b/api/app/views/spree/api/promotions/handler.v1.rabl new file mode 100644 index 00000000000..e08a89b08ee --- /dev/null +++ b/api/app/views/spree/api/promotions/handler.v1.rabl @@ -0,0 +1,5 @@ +object false +node(:success) { @handler.success } +node(:error) { @handler.error } +node(:successful) { @handler.successful? } +node(:status_code) { @handler.status_code } diff --git a/api/app/views/spree/api/promotions/show.v1.rabl b/api/app/views/spree/api/promotions/show.v1.rabl new file mode 100644 index 00000000000..90a577d22ab --- /dev/null +++ b/api/app/views/spree/api/promotions/show.v1.rabl @@ -0,0 +1,2 @@ +object @promotion +attributes *promotion_attributes diff --git a/api/app/views/spree/api/properties/index.v1.rabl b/api/app/views/spree/api/properties/index.v1.rabl new file mode 100644 index 00000000000..7e83c39c21d --- /dev/null +++ b/api/app/views/spree/api/properties/index.v1.rabl @@ -0,0 +1,7 @@ +object false +child(@properties => :properties) do + attributes *property_attributes +end +node(:count) { @properties.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @properties.num_pages } diff --git a/api/app/views/spree/api/properties/new.v1.rabl b/api/app/views/spree/api/properties/new.v1.rabl new file mode 100644 index 00000000000..76f03444e8e --- /dev/null +++ b/api/app/views/spree/api/properties/new.v1.rabl @@ -0,0 +1,2 @@ +node(:attributes) { [*property_attributes] } +node(:required_attributes) { [] } diff --git a/api/app/views/spree/api/properties/show.v1.rabl b/api/app/views/spree/api/properties/show.v1.rabl new file mode 100644 index 00000000000..08cfbe5e9b6 --- /dev/null +++ b/api/app/views/spree/api/properties/show.v1.rabl @@ -0,0 +1,2 @@ +object @property +attributes *property_attributes diff --git a/api/app/views/spree/api/shared/stock_location_required.v1.rabl b/api/app/views/spree/api/shared/stock_location_required.v1.rabl new file mode 100644 index 00000000000..f7241abc8d6 --- /dev/null +++ b/api/app/views/spree/api/shared/stock_location_required.v1.rabl @@ -0,0 +1,2 @@ +object false +node(:error) { I18n.t(:stock_location_required, scope: "spree.api") } diff --git a/api/app/views/spree/api/shipments/big.v1.rabl b/api/app/views/spree/api/shipments/big.v1.rabl new file mode 100644 index 00000000000..c8842ee0bd5 --- /dev/null +++ b/api/app/views/spree/api/shipments/big.v1.rabl @@ -0,0 +1,48 @@ +object @shipment +cache @shipment +attributes *shipment_attributes + +child selected_shipping_rate: :selected_shipping_rate do + extends "spree/api/shipping_rates/show" +end + +child inventory_units: :inventory_units do + object @inventory_unit + attributes *inventory_unit_attributes + + child :variant do + extends "spree/api/variants/small" + attributes :product_id + child(images: :images) { extends "spree/api/images/show" } + end + + child :line_item do + attributes *line_item_attributes + node(:single_display_amount) { |li| li.single_display_amount.to_s } + node(:display_amount) { |li| li.display_amount.to_s } + node(:total) { |li| li.total } + end +end + +child order: :order do + extends "spree/api/orders/order" + + child billing_address: :bill_address do + extends "spree/api/addresses/show" + end + + child shipping_address: :ship_address do + extends "spree/api/addresses/show" + end + + child adjustments: :adjustments do + extends "spree/api/adjustments/show" + end + + child payments: :payments do + attributes :id, :amount, :display_amount, :state + child payment_method: :payment_method do + attributes :id, :name + end + end +end diff --git a/api/app/views/spree/api/shipments/mine.v1.rabl b/api/app/views/spree/api/shipments/mine.v1.rabl new file mode 100644 index 00000000000..6de1b5a322c --- /dev/null +++ b/api/app/views/spree/api/shipments/mine.v1.rabl @@ -0,0 +1,9 @@ +object false + +node(:count) { @shipments.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @shipments.num_pages } + +child(@shipments => :shipments) do + extends "spree/api/shipments/big" +end diff --git a/api/app/views/spree/api/shipments/show.v1.rabl b/api/app/views/spree/api/shipments/show.v1.rabl index 08129e7789c..03b72bbc9db 100644 --- a/api/app/views/spree/api/shipments/show.v1.rabl +++ b/api/app/views/spree/api/shipments/show.v1.rabl @@ -1,7 +1,32 @@ object @shipment +cache [I18n.locale, root_object] attributes *shipment_attributes node(:order_id) { |shipment| shipment.order.number } -child :shipping_method => :shipping_method do - attributes :name, :zone_id, :shipping_category_id +node(:stock_location_name) { |shipment| shipment.stock_location.name } + +child :shipping_rates => :shipping_rates do + extends "spree/api/shipping_rates/show" +end + +child :selected_shipping_rate => :selected_shipping_rate do + extends "spree/api/shipping_rates/show" end +child :shipping_methods => :shipping_methods do + attributes :id, :name + child :zones => :zones do + attributes :id, :name, :description + end + + child :shipping_categories => :shipping_categories do + attributes :id, :name + end +end + +child :manifest => :manifest do + child :variant => :variant do + extends "spree/api/variants/small" + end + node(:quantity) { |m| m.quantity } + node(:states) { |m| m.states } +end diff --git a/api/app/views/spree/api/shipments/small.v1.rabl b/api/app/views/spree/api/shipments/small.v1.rabl new file mode 100644 index 00000000000..3b8ef491189 --- /dev/null +++ b/api/app/views/spree/api/shipments/small.v1.rabl @@ -0,0 +1,37 @@ +object @shipment +cache [I18n.locale, 'small_shipment', root_object] + +attributes *shipment_attributes +node(:order_id) { |shipment| shipment.order.number } +node(:stock_location_name) { |shipment| shipment.stock_location.name } + +child :shipping_rates => :shipping_rates do + extends "spree/api/shipping_rates/show" +end + +child :selected_shipping_rate => :selected_shipping_rate do + extends "spree/api/shipping_rates/show" +end + +child :shipping_methods => :shipping_methods do + attributes :id, :code, :name + child :zones => :zones do + attributes :id, :name, :description + end + + child :shipping_categories => :shipping_categories do + attributes :id, :name + end +end + +child :manifest => :manifest do + glue(:variant) do + attribute :id => :variant_id + end + node(:quantity) { |m| m.quantity } + node(:states) { |m| m.states } +end + +child :adjustments => :adjustments do + extends "spree/api/adjustments/show" +end diff --git a/api/app/views/spree/api/shipping_rates/show.v1.rabl b/api/app/views/spree/api/shipping_rates/show.v1.rabl new file mode 100644 index 00000000000..64809f39edc --- /dev/null +++ b/api/app/views/spree/api/shipping_rates/show.v1.rabl @@ -0,0 +1,2 @@ +attributes :id, :name, :cost, :selected, :shipping_method_id, :shipping_method_code +node(:display_cost) { |sr| sr.display_cost.to_s } diff --git a/api/app/views/spree/api/states/index.v1.rabl b/api/app/views/spree/api/states/index.v1.rabl new file mode 100644 index 00000000000..e19b05f7dfe --- /dev/null +++ b/api/app/views/spree/api/states/index.v1.rabl @@ -0,0 +1,14 @@ +object false +if @country + node(:states_required) { @country.states_required } +end + +child(@states => :states) do + attributes *state_attributes +end + +if @states.respond_to?(:num_pages) + node(:count) { @states.count } + node(:current_page) { params[:page] || 1 } + node(:pages) { @states.num_pages } +end diff --git a/api/app/views/spree/api/states/show.v1.rabl b/api/app/views/spree/api/states/show.v1.rabl new file mode 100644 index 00000000000..3b7533d511b --- /dev/null +++ b/api/app/views/spree/api/states/show.v1.rabl @@ -0,0 +1,2 @@ +object @state +attributes *state_attributes diff --git a/api/app/views/spree/api/stock_items/index.v1.rabl b/api/app/views/spree/api/stock_items/index.v1.rabl new file mode 100644 index 00000000000..a99ad6f4631 --- /dev/null +++ b/api/app/views/spree/api/stock_items/index.v1.rabl @@ -0,0 +1,7 @@ +object false +child(@stock_items => :stock_items) do + extends 'spree/api/stock_items/show' +end +node(:count) { @stock_items.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @stock_items.num_pages } diff --git a/api/app/views/spree/api/stock_items/show.v1.rabl b/api/app/views/spree/api/stock_items/show.v1.rabl new file mode 100644 index 00000000000..bc96704cdb7 --- /dev/null +++ b/api/app/views/spree/api/stock_items/show.v1.rabl @@ -0,0 +1,5 @@ +object @stock_item +attributes *stock_item_attributes +child(:variant) do + extends "spree/api/variants/small" +end diff --git a/api/app/views/spree/api/stock_locations/index.v1.rabl b/api/app/views/spree/api/stock_locations/index.v1.rabl new file mode 100644 index 00000000000..5d4da1bb629 --- /dev/null +++ b/api/app/views/spree/api/stock_locations/index.v1.rabl @@ -0,0 +1,7 @@ +object false +child(@stock_locations => :stock_locations) do + extends 'spree/api/stock_locations/show' +end +node(:count) { @stock_locations.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @stock_locations.num_pages } diff --git a/api/app/views/spree/api/stock_locations/show.v1.rabl b/api/app/views/spree/api/stock_locations/show.v1.rabl new file mode 100644 index 00000000000..898e12e88cb --- /dev/null +++ b/api/app/views/spree/api/stock_locations/show.v1.rabl @@ -0,0 +1,8 @@ +object @stock_location +attributes *stock_location_attributes +child(:country) do |address| + attributes *country_attributes +end +child(:state) do |address| + attributes *state_attributes +end diff --git a/api/app/views/spree/api/stock_movements/index.v1.rabl b/api/app/views/spree/api/stock_movements/index.v1.rabl new file mode 100644 index 00000000000..f6011b49bcd --- /dev/null +++ b/api/app/views/spree/api/stock_movements/index.v1.rabl @@ -0,0 +1,7 @@ +object false +child(@stock_movements => :stock_movements) do + extends 'spree/api/stock_movements/show' +end +node(:count) { @stock_movements.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @stock_movements.num_pages } diff --git a/api/app/views/spree/api/stock_movements/show.v1.rabl b/api/app/views/spree/api/stock_movements/show.v1.rabl new file mode 100644 index 00000000000..93dff52e7bb --- /dev/null +++ b/api/app/views/spree/api/stock_movements/show.v1.rabl @@ -0,0 +1,5 @@ +object @stock_movement +attributes *stock_movement_attributes +child :stock_item do + extends "spree/api/stock_items/show" +end diff --git a/api/app/views/spree/api/stores/index.v1.rabl b/api/app/views/spree/api/stores/index.v1.rabl new file mode 100644 index 00000000000..ae547356bb7 --- /dev/null +++ b/api/app/views/spree/api/stores/index.v1.rabl @@ -0,0 +1,4 @@ +object false +child(@stores => :stores) do + attributes *store_attributes +end diff --git a/api/app/views/spree/api/stores/show.v1.rabl b/api/app/views/spree/api/stores/show.v1.rabl new file mode 100644 index 00000000000..a0e1941b723 --- /dev/null +++ b/api/app/views/spree/api/stores/show.v1.rabl @@ -0,0 +1,2 @@ +object @store +attributes *store_attributes diff --git a/api/app/views/spree/api/taxonomies/jstree.rabl b/api/app/views/spree/api/taxonomies/jstree.rabl new file mode 100644 index 00000000000..01b25aa4ccd --- /dev/null +++ b/api/app/views/spree/api/taxonomies/jstree.rabl @@ -0,0 +1,8 @@ +object false +node(:data) { @taxonomy.root.name } +node(:attr) do + { :id => @taxonomy.root.id, + :name => @taxonomy.root.name + } +end +node(:state) { "closed" } diff --git a/api/app/views/spree/api/taxonomies/show.v1.rabl b/api/app/views/spree/api/taxonomies/show.v1.rabl index cf614fa0c6b..37d2a1615b3 100644 --- a/api/app/views/spree/api/taxonomies/show.v1.rabl +++ b/api/app/views/spree/api/taxonomies/show.v1.rabl @@ -1,7 +1,7 @@ object @taxonomy -if set = params[:set] - extends "spree/api/taxonomies/#{set}" +if params[:set] == 'nested' + extends "spree/api/taxonomies/nested" else attributes *taxonomy_attributes diff --git a/api/app/views/spree/api/taxons/index.v1.rabl b/api/app/views/spree/api/taxons/index.v1.rabl index d9b61a97a26..785aab6aec2 100644 --- a/api/app/views/spree/api/taxons/index.v1.rabl +++ b/api/app/views/spree/api/taxons/index.v1.rabl @@ -1,4 +1,12 @@ -collection @taxons -attributes *taxon_attributes - -extends "spree/api/taxons/taxons" +object false +node(:count) { @taxons.count } +node(:total_count) { @taxons.total_count } +node(:current_page) { params[:page] ? params[:page].to_i : 1 } +node(:per_page) { params[:per_page] || Kaminari.config.default_per_page } +node(:pages) { @taxons.num_pages } +child @taxons => :taxons do + attributes *taxon_attributes + unless params[:without_children] + extends "spree/api/taxons/taxons" + end +end diff --git a/api/app/views/spree/api/taxons/jstree.rabl b/api/app/views/spree/api/taxons/jstree.rabl new file mode 100644 index 00000000000..aed500e0f36 --- /dev/null +++ b/api/app/views/spree/api/taxons/jstree.rabl @@ -0,0 +1,8 @@ +collection @taxon.children, :object_root => false +node(:data) { |taxon| taxon.name } +node(:attr) do |taxon| + { :id => taxon.id, + :name => taxon.name + } +end +node(:state) { "closed" } diff --git a/api/app/views/spree/api/users/index.v1.rabl b/api/app/views/spree/api/users/index.v1.rabl new file mode 100644 index 00000000000..246512792c2 --- /dev/null +++ b/api/app/views/spree/api/users/index.v1.rabl @@ -0,0 +1,7 @@ +object false +child(@users => :users) do + extends "spree/api/users/show" +end +node(:count) { @users.count } +node(:current_page) { params[:page] || 1 } +node(:pages) { @users.num_pages } diff --git a/api/app/views/spree/api/users/new.v1.rabl b/api/app/views/spree/api/users/new.v1.rabl new file mode 100644 index 00000000000..f3e36dd7d33 --- /dev/null +++ b/api/app/views/spree/api/users/new.v1.rabl @@ -0,0 +1,3 @@ +object false +node(:attributes) { [*user_attributes] } +node(:required_attributes) { required_fields_for(Spree.user_class) } diff --git a/api/app/views/spree/api/users/show.v1.rabl b/api/app/views/spree/api/users/show.v1.rabl new file mode 100644 index 00000000000..4448a3e7ed6 --- /dev/null +++ b/api/app/views/spree/api/users/show.v1.rabl @@ -0,0 +1,10 @@ +object @user + +attributes *user_attributes +child(:bill_address => :bill_address) do + extends "spree/api/addresses/show" +end + +child(:ship_address => :ship_address) do + extends "spree/api/addresses/show" +end \ No newline at end of file diff --git a/api/app/views/spree/api/variants/big.v1.rabl b/api/app/views/spree/api/variants/big.v1.rabl new file mode 100644 index 00000000000..4a685a77195 --- /dev/null +++ b/api/app/views/spree/api/variants/big.v1.rabl @@ -0,0 +1,14 @@ +object @variant + +cache [I18n.locale, @current_user_roles.include?('admin'), 'big_variant', root_object] + +extends "spree/api/variants/small" + +child(:stock_items => :stock_items) do + attributes :id, :count_on_hand, :stock_location_id, :backorderable + attribute :available? => :available + + glue(:stock_location) do + attribute :name => :stock_location_name + end +end diff --git a/api/app/views/spree/api/variants/index.v1.rabl b/api/app/views/spree/api/variants/index.v1.rabl index d37c886a2a0..6c20c5e447e 100644 --- a/api/app/views/spree/api/variants/index.v1.rabl +++ b/api/app/views/spree/api/variants/index.v1.rabl @@ -5,6 +5,5 @@ node(:current_page) { params[:page] ? params[:page].to_i : 1 } node(:pages) { @variants.num_pages } child(@variants => :variants) do - attributes *variant_attributes - child(:option_values => :option_values) { attributes *option_value_attributes } + extends "spree/api/variants/big" end diff --git a/api/app/views/spree/api/variants/show.v1.rabl b/api/app/views/spree/api/variants/show.v1.rabl index f830a0d99ef..b097e25551d 100644 --- a/api/app/views/spree/api/variants/show.v1.rabl +++ b/api/app/views/spree/api/variants/show.v1.rabl @@ -1,3 +1,3 @@ object @variant -extends "spree/api/variants/variant" -child(:option_values => :option_values) { attributes *option_value_attributes } +cache [I18n.locale, @current_user_roles.include?('admin'), 'show', root_object] +extends "spree/api/variants/big" diff --git a/api/app/views/spree/api/variants/small.v1.rabl b/api/app/views/spree/api/variants/small.v1.rabl new file mode 100644 index 00000000000..cae9ffff54c --- /dev/null +++ b/api/app/views/spree/api/variants/small.v1.rabl @@ -0,0 +1,17 @@ +cache [I18n.locale, @current_user_roles.include?('admin'), 'small_variant', root_object] + +attributes *variant_attributes + +node(:display_price) { |p| p.display_price.to_s } +node(:options_text) { |v| v.options_text } +node(:track_inventory) { |v| v.should_track_inventory? } +node(:in_stock) { |v| v.in_stock? } +node(:is_backorderable) { |v| v.is_backorderable? } +node(:total_on_hand) { |v| v.total_on_hand } +node(:is_destroyed) { |v| v.destroyed? } + +child :option_values => :option_values do + attributes *option_value_attributes +end + +child(:images => :images) { extends "spree/api/images/show" } diff --git a/api/app/views/spree/api/variants/variant.v1.rabl b/api/app/views/spree/api/variants/variant.v1.rabl deleted file mode 100644 index 09c28003097..00000000000 --- a/api/app/views/spree/api/variants/variant.v1.rabl +++ /dev/null @@ -1 +0,0 @@ -attributes *variant_attributes diff --git a/api/config/locales/en.yml b/api/config/locales/en.yml index 14e0556b578..62fa7fd7e9e 100644 --- a/api/config/locales/en.yml +++ b/api/config/locales/en.yml @@ -7,7 +7,6 @@ en: invalid_resource: "Invalid resource. Please fix errors and try again." resource_not_found: "The resource you were looking for could not be found." gateway_error: "There was a problem with the payment gateway: %{text}" - credit_over_limit: "This payment can only be credited up to %{limit}. Please specify an amount less than or equal to this number." access: "API Access" key: "Key" clear_key: "Clear key" @@ -19,5 +18,10 @@ en: order: could_not_transition: "The order could not be transitioned. Please fix the errors and try again." invalid_shipping_method: "Invalid shipping method specified." + payment: + credit_over_limit: "This payment can only be credited up to %{limit}. Please specify an amount less than or equal to this number." + update_forbidden: "This payment cannot be updated because it is %{state}." shipment: cannot_ready: "Cannot ready shipment." + stock_location_required: "A stock_location_id parameter must be provided in order to retrieve stock movements." + invalid_taxonomy_id: "Invalid taxonomy id." diff --git a/api/config/routes.rb b/api/config/routes.rb index 86c3e4d2c57..3642e1f20ca 100644 --- a/api/config/routes.rb +++ b/api/config/routes.rb @@ -1,4 +1,4 @@ -Spree::Core::Engine.routes.prepend do +Spree::Core::Engine.add_routes do namespace :admin do resources :users do member do @@ -8,25 +8,20 @@ end end - namespace :api do + namespace :api, defaults: { format: 'json' } do + resources :promotions, only: [:show] + resources :products do + resources :images resources :variants resources :product_properties end - resources :images - resources :variants, :only => [:index] do - end - - resources :option_types - - resources :orders do - resources :return_authorizations + concern :order_routes do member do - put :address - put :delivery put :cancel put :empty + put :apply_coupon_code end resources :line_items @@ -40,19 +35,89 @@ end end - resources :shipments do + resources :addresses, only: [:show, :update] + + resources :return_authorizations do member do - put :ready - put :ship + put :add + put :cancel + put :receive end end end + resources :checkouts, only: [:update], concerns: :order_routes do + member do + put :next + put :advance + end + end + + resources :variants, only: [:index, :show] do + resources :images + end + + resources :option_types do + resources :option_values + end + + get '/orders/mine', to: 'orders#mine', as: 'my_orders' + get "/orders/current", to: "orders#current", as: "current_order" + + resources :orders, concerns: :order_routes + resources :zones - resources :countries, :only => [:index, :show] - resources :addresses, :only => [:show, :update] + resources :countries, only: [:index, :show] do + resources :states, only: [:index, :show] + end + + resources :shipments, only: [:create, :update] do + collection do + post 'transfer_to_location' + post 'transfer_to_shipment' + get :mine + end + + member do + put :ready + put :ship + put :add + put :remove + end + end + resources :states, only: [:index, :show] + resources :taxonomies do - resources :taxons + member do + get :jstree + end + resources :taxons do + member do + get :jstree + end + end end + + resources :taxons, only: [:index] + + resources :inventory_units, only: [:show, :update] + + resources :users do + resources :credit_cards, only: [:index] + end + + resources :properties + resources :stock_locations do + resources :stock_movements + resources :stock_items + end + + resources :stores + + get '/config/money', to: 'config#money' + get '/config', to: 'config#show' + + put '/classifications', to: 'classifications#update', as: :classifications + get '/taxons/products', to: 'taxons#products', as: :taxon_products end end diff --git a/api/db/migrate/20131017162334_add_index_to_user_spree_api_key.rb b/api/db/migrate/20131017162334_add_index_to_user_spree_api_key.rb new file mode 100644 index 00000000000..b7558783d38 --- /dev/null +++ b/api/db/migrate/20131017162334_add_index_to_user_spree_api_key.rb @@ -0,0 +1,7 @@ +class AddIndexToUserSpreeApiKey < ActiveRecord::Migration + def change + unless defined?(User) + add_index :spree_users, :spree_api_key + end + end +end diff --git a/api/lib/spree/api.rb b/api/lib/spree/api.rb index fa4c16257b5..f20221cf112 100644 --- a/api/lib/spree/api.rb +++ b/api/lib/spree/api.rb @@ -1,7 +1,5 @@ require 'spree/core' -require 'spree/api/controller_setup' - require 'rabl' module Spree diff --git a/api/lib/spree/api/controller_setup.rb b/api/lib/spree/api/controller_setup.rb index 0d42b37e480..90a84b1bf98 100644 --- a/api/lib/spree/api/controller_setup.rb +++ b/api/lib/spree/api/controller_setup.rb @@ -1,24 +1,17 @@ +require 'spree/api/responders' + module Spree module Api module ControllerSetup def self.included(klass) klass.class_eval do - include AbstractController::Rendering - include AbstractController::ViewPaths - include AbstractController::Callbacks - include AbstractController::Helpers - - include ActiveSupport::Rescuable - - include ActionController::Rendering - include ActionController::ImplicitRender - include ActionController::Rescue - include ActionController::MimeResponds - include CanCan::ControllerAdditions + include Spree::Core::ControllerHelpers::Auth + prepend_view_path Rails.root + "app/views" append_view_path File.expand_path("../../../app/views", File.dirname(__FILE__)) + self.responder = Spree::Api::Responders::AppResponder respond_to :json end end diff --git a/api/lib/spree/api/engine.rb b/api/lib/spree/api/engine.rb index cf37bafcc21..5e1b142561b 100644 --- a/api/lib/spree/api/engine.rb +++ b/api/lib/spree/api/engine.rb @@ -9,6 +9,11 @@ class Engine < Rails::Engine Rabl.configure do |config| config.include_json_root = false config.include_child_root = false + + # Motivation here it make it call as_json when rendering timestamps + # and therefore display miliseconds. Otherwise it would fall to + # JSON.dump which doesn't display the miliseconds + config.json_engine = ActiveSupport::JSON end config.view_versions = [1] @@ -31,5 +36,3 @@ def self.root end end end - - diff --git a/api/lib/spree/api/responders.rb b/api/lib/spree/api/responders.rb new file mode 100644 index 00000000000..deb18094d16 --- /dev/null +++ b/api/lib/spree/api/responders.rb @@ -0,0 +1,11 @@ +require 'spree/api/responders/rabl_template' + +module Spree + module Api + module Responders + class AppResponder < ActionController::Responder + include RablTemplate + end + end + end +end diff --git a/api/lib/spree/api/responders/rabl_template.rb b/api/lib/spree/api/responders/rabl_template.rb new file mode 100644 index 00000000000..ded026eed54 --- /dev/null +++ b/api/lib/spree/api/responders/rabl_template.rb @@ -0,0 +1,31 @@ +module Spree + module Api + module Responders + module RablTemplate + def to_format + if template + render template, :status => options[:status] || 200 + else + super + end + + rescue ActionView::MissingTemplate => e + api_behavior(e) + end + + def template + options[:default_template] + end + + def api_behavior(error) + if controller.params[:action] == "destroy" + # Render a blank template + super + else + # Do nothing and fallback to the default template + end + end + end + end + end +end diff --git a/api/lib/spree/api/testing_support/caching.rb b/api/lib/spree/api/testing_support/caching.rb new file mode 100644 index 00000000000..7cfa98188f4 --- /dev/null +++ b/api/lib/spree/api/testing_support/caching.rb @@ -0,0 +1,10 @@ +RSpec.configure do |config| + config.before(:each, :caching => true) do + ActionController::Base.perform_caching = true + end + + config.after(:each, :caching => true) do + ActionController::Base.perform_caching = false + Rails.cache.clear + end +end \ No newline at end of file diff --git a/api/lib/spree/api/testing_support/helpers.rb b/api/lib/spree/api/testing_support/helpers.rb index baae3439441..8945f7784db 100644 --- a/api/lib/spree/api/testing_support/helpers.rb +++ b/api/lib/spree/api/testing_support/helpers.rb @@ -3,23 +3,32 @@ module Api module TestingSupport module Helpers def json_response - JSON.parse(response.body) + case body = JSON.parse(response.body) + when Hash + body.with_indifferent_access + when Array + body + end + end + + def assert_not_found! + expect(json_response).to eq({ "error" => "The resource you were looking for could not be found." }) + expect(response.status).to eq 404 end def assert_unauthorized! - json_response.should == { "error" => "You are not authorized to perform that action." } - response.status.should == 401 + expect(json_response).to eq({ "error" => "You are not authorized to perform that action." }) + expect(response.status).to eq 401 end def stub_authentication! - controller.stub :check_for_api_key - Spree::LegacyUser.stub :find_by_spree_api_key => current_api_user + allow(Spree.user_class).to receive(:find_by).with(hash_including(:spree_api_key)) { current_api_user } end # This method can be overriden (with a let block) inside a context # For instance, if you wanted to have an admin user instead. def current_api_user - @current_api_user ||= stub_model(Spree::LegacyUser, :email => "spree@example.com") + @current_api_user ||= stub_model(Spree::LegacyUser, email: "spree@example.com") end def image(filename) @@ -27,7 +36,7 @@ def image(filename) end def upload_image(filename) - fixture_file_upload(image(filename).path) + fixture_file_upload(image(filename).path, 'image/jpg') end end end diff --git a/api/lib/spree/api/testing_support/setup.rb b/api/lib/spree/api/testing_support/setup.rb index f5366a9f3dc..7aa626f83c4 100644 --- a/api/lib/spree/api/testing_support/setup.rb +++ b/api/lib/spree/api/testing_support/setup.rb @@ -5,23 +5,11 @@ module Setup def sign_in_as_admin! let!(:current_api_user) do user = stub_model(Spree::LegacyUser) - user.should_receive(:has_spree_role?).any_number_of_times.with("admin").and_return(true) + allow(user).to receive_message_chain(:spree_roles, :pluck).and_return(["admin"]) + allow(user).to receive(:has_spree_role?).with("admin").and_return(true) user end end - - # Default kaminari's pagination to a certain range - # Means that you don't need to create 25 objects to test pagination - def default_per_page(count) - before do - @current_default_per_page = Kaminari.config.default_per_page - Kaminari.config.default_per_page = 1 - end - - after do - Kaminari.config.default_per_page = @current_default_per_page - end - end end end end diff --git a/api/lib/spree/api/version.rb b/api/lib/spree/api/version.rb deleted file mode 100644 index 88b24326d17..00000000000 --- a/api/lib/spree/api/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Spree - module Api - VERSION = "1.1.0.beta" - end -end diff --git a/api/lib/spree_api.rb b/api/lib/spree_api.rb index d974acf85ac..ab9bf2f954e 100644 --- a/api/lib/spree_api.rb +++ b/api/lib/spree_api.rb @@ -1,2 +1,3 @@ require 'spree/api' +require 'spree/api/responders' require 'versioncake' diff --git a/api/spec/controllers/spree/api/addresses_controller_spec.rb b/api/spec/controllers/spree/api/addresses_controller_spec.rb index 1144401d7b8..4ef547e6678 100644 --- a/api/spec/controllers/spree/api/addresses_controller_spec.rb +++ b/api/spec/controllers/spree/api/addresses_controller_spec.rb @@ -1,43 +1,54 @@ require 'spec_helper' module Spree - describe Api::AddressesController do + describe Api::AddressesController, :type => :controller do render_views before do stub_authentication! @address = create(:address) + @order = create(:order, :bill_address => @address) end context "with their own address" do before do - Address.any_instance.stub :user => current_api_user + allow_any_instance_of(Order).to receive_messages :user => current_api_user end it "gets an address" do - api_get :show, :id => @address.id - json_response['address1'].should eq @address.address1 + api_get :show, :id => @address.id, :order_id => @order.number + expect(json_response['address1']).to eq @address.address1 end it "updates an address" do - api_put :update, :id => @address.id, + api_put :update, :id => @address.id, :order_id => @order.number, :address => { :address1 => "123 Test Lane" } - json_response['address1'].should eq '123 Test Lane' + expect(json_response['address1']).to eq '123 Test Lane' + end + + it "receives the errors object if address is invalid" do + api_put :update, :id => @address.id, :order_id => @order.number, + :address => { :address1 => "" } + + expect(json_response['error']).not_to be_nil + expect(json_response['errors']).not_to be_nil + expect(json_response['errors']['address1'].first).to eq "can't be blank" end end - context "on somebody else's address" do + context "on an address that does not belong to this order" do before do - Address.any_instance.stub :user => stub_model(Spree::LegacyUser) + @order.bill_address_id = nil + @order.ship_address = nil end - it "cannot retreive address information" do - api_get :show, :id => @address.id + it "cannot retrieve address information" do + api_get :show, :id => @address.id, :order_id => @order.number assert_unauthorized! end it "cannot update address information" do - api_get :update, :id => @address.id + api_get :update, :id => @address.id, :order_id => @order.number assert_unauthorized! end end diff --git a/api/spec/controllers/spree/api/base_controller_spec.rb b/api/spec/controllers/spree/api/base_controller_spec.rb index d1e8145d98d..9cd05661bef 100644 --- a/api/spec/controllers/spree/api/base_controller_spec.rb +++ b/api/spec/controllers/spree/api/base_controller_spec.rb @@ -1,42 +1,124 @@ require 'spec_helper' -describe Spree::Api::BaseController do +class FakesController < Spree::Api::BaseController +end + +describe Spree::Api::BaseController, :type => :controller do render_views controller(Spree::Api::BaseController) do def index - render :json => { "products" => [] } + render :text => { "products" => [] }.to_json + end + end + + before do + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw { get 'index', to: 'spree/api/base#index' } + end + end + + context "when validating based on an order token" do + let!(:order) { create :order } + + context "with a correct order token" do + it "succeeds" do + api_get :index, order_token: order.guest_token, order_id: order.number + expect(response.status).to eq(200) + end + + it "succeeds with an order_number parameter" do + api_get :index, order_token: order.guest_token, order_number: order.number + expect(response.status).to eq(200) + end + end + + context "with an incorrect order token" do + it "returns unauthorized" do + api_get :index, order_token: "NOT_A_TOKEN", order_id: order.number + expect(response.status).to eq(401) + end end end context "cannot make a request to the API" do it "without an API key" do api_get :index - json_response.should == { "error" => "You must specify an API key." } - response.status.should == 401 + expect(json_response).to eq({ "error" => "You must specify an API key." }) + expect(response.status).to eq(401) end it "with an invalid API key" do - request.env["X-Spree-Token"] = "fake_key" + request.headers["X-Spree-Token"] = "fake_key" get :index, {} - json_response.should == { "error" => "Invalid API key (fake_key) specified." } - response.status.should == 401 + expect(json_response).to eq({ "error" => "Invalid API key (fake_key) specified." }) + expect(response.status).to eq(401) end it "using an invalid token param" do get :index, :token => "fake_key" - json_response.should == { "error" => "Invalid API key (fake_key) specified." } + expect(json_response).to eq({ "error" => "Invalid API key (fake_key) specified." }) end end - it "maps symantec keys to nested_attributes keys" do - klass = stub(:nested_attributes_options => { :line_items => {}, + it 'handles exceptions' do + expect(subject).to receive(:authenticate_user).and_return(true) + expect(subject).to receive(:load_user_roles).and_return(true) + expect(subject).to receive(:index).and_raise(Exception.new("no joy")) + get :index, :token => "fake_key" + expect(json_response).to eq({ "exception" => "no joy" }) + end + + it "maps semantic keys to nested_attributes keys" do + klass = double(:nested_attributes_options => { :line_items => {}, :bill_address => {} }) attributes = { 'line_items' => { :id => 1 }, 'bill_address' => { :id => 2 }, 'name' => 'test order' } mapped = subject.map_nested_attributes_keys(klass, attributes) - mapped.has_key?('line_items_attributes').should be_true - mapped.has_key?('name').should be_true + expect(mapped.has_key?('line_items_attributes')).to be true + expect(mapped.has_key?('name')).to be true + end + + it "lets a subclass override the product associations that are eager-loaded" do + expect(controller.respond_to?(:product_includes, true)).to be + end + + describe '#error_during_processing' do + controller(FakesController) do + # GET /foo + # Simulates a failed API call. + def foo + raise StandardError + end + end + + # What would be placed in config/initializers/spree.rb + Spree::Api::BaseController.error_notifier = Proc.new do |e, controller| + MockHoneybadger.notify_or_ignore(e, rack_env: controller.request.env) + end + + ## + # Fake HB alert class + class MockHoneybadger + # https://github.com/honeybadger-io/honeybadger-ruby/blob/master/lib/honeybadger.rb#L136 + def self.notify_or_ignore(exception, opts = {}) + end + end + + before do + user = double(email: "spree@example.com") + allow(user).to receive_message_chain :spree_roles, pluck: [] + allow(Spree.user_class).to receive_messages find_by: user + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw { get 'foo' => 'fakes#foo' } + end + end + + it 'should notify notify_error_during_processing' do + expect(MockHoneybadger).to receive(:notify_or_ignore).once.with(kind_of(Exception), rack_env: kind_of(Hash)) + api_get :foo, token: 123 + expect(response.status).to eq(422) + end end end diff --git a/api/spec/controllers/spree/api/checkouts_controller_spec.rb b/api/spec/controllers/spree/api/checkouts_controller_spec.rb new file mode 100644 index 00000000000..f5baafeb568 --- /dev/null +++ b/api/spec/controllers/spree/api/checkouts_controller_spec.rb @@ -0,0 +1,315 @@ +require 'spec_helper' + +module Spree + describe Api::CheckoutsController, type: :controller do + render_views + + before(:each) do + stub_authentication! + Spree::Config[:track_inventory_levels] = false + country_zone = create(:zone, name: 'CountryZone') + @state = create(:state) + @country = @state.country + country_zone.members.create(zoneable: @country) + create(:stock_location) + + @shipping_method = create(:shipping_method, zones: [country_zone]) + @payment_method = create(:credit_card_payment_method) + end + + after do + Spree::Config[:track_inventory_levels] = true + end + + context "PUT 'update'" do + let(:order) do + order = create(:order_with_line_items) + # Order should be in a pristine state + # Without doing this, the order may transition from 'cart' straight to 'delivery' + order.shipments.delete_all + order + end + + before(:each) do + allow_any_instance_of(Order).to receive_messages(confirmation_required?: true) + allow_any_instance_of(Order).to receive_messages(payment_required?: true) + end + + it "should transition a recently created order from cart to address" do + expect(order.state).to eq "cart" + expect(order.email).not_to be_nil + api_put :update, id: order.to_param, order_token: order.guest_token + expect(order.reload.state).to eq "address" + end + + it "should transition a recently created order from cart to address with order token in header" do + expect(order.state).to eq "cart" + expect(order.email).not_to be_nil + request.headers["X-Spree-Order-Token"] = order.guest_token + api_put :update, :id => order.to_param + expect(order.reload.state).to eq "address" + end + + it "can take line_items_attributes as a parameter" do + line_item = order.line_items.first + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { line_items_attributes: { 0 => { id: line_item.id, quantity: 1 } } } + expect(response.status).to eq(200) + expect(order.reload.state).to eq "address" + end + + it "can take line_items as a parameter" do + line_item = order.line_items.first + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { line_items: { 0 => { id: line_item.id, quantity: 1 } } } + expect(response.status).to eq(200) + expect(order.reload.state).to eq "address" + end + + it "will return an error if the order cannot transition" do + skip "not sure if this test is valid" + order.bill_address = nil + order.save + order.update_column(:state, "address") + api_put :update, id: order.to_param, order_token: order.guest_token + # Order has not transitioned + expect(response.status).to eq(422) + end + + context "transitioning to delivery" do + before do + order.update_column(:state, "address") + end + + let(:address) do + { + firstname: 'John', + lastname: 'Doe', + address1: '7735 Old Georgetown Road', + city: 'Bethesda', + phone: '3014445002', + zipcode: '20814', + state_id: @state.id, + country_id: @country.id + } + end + + it "can update addresses and transition from address to delivery" do + api_put :update, + id: order.to_param, order_token: order.guest_token, + order: { + bill_address_attributes: address, + ship_address_attributes: address + } + expect(json_response['state']).to eq('delivery') + expect(json_response['bill_address']['firstname']).to eq('John') + expect(json_response['ship_address']['firstname']).to eq('John') + expect(response.status).to eq(200) + end + + # Regression Spec for #5389 & #5880 + it "can update addresses but not transition to delivery w/o shipping setup" do + Spree::ShippingMethod.destroy_all + api_put :update, + id: order.to_param, order_token: order.guest_token, + order: { + bill_address_attributes: address, + ship_address_attributes: address + } + expect(json_response['error']).to eq(I18n.t(:could_not_transition, scope: "spree.api.order")) + expect(response.status).to eq(422) + end + + # Regression test for #4498 + it "does not contain duplicate variant data in delivery return" do + api_put :update, + id: order.to_param, order_token: order.guest_token, + order: { + bill_address_attributes: address, + ship_address_attributes: address + } + # Shipments manifests should not return the ENTIRE variant + # This information is already present within the order's line items + expect(json_response['shipments'].first['manifest'].first['variant']).to be_nil + expect(json_response['shipments'].first['manifest'].first['variant_id']).to_not be_nil + end + end + + it "can update shipping method and transition from delivery to payment" do + order.update_column(:state, "delivery") + shipment = create(:shipment, order: order) + shipment.refresh_rates + shipping_rate = shipment.shipping_rates.where(selected: false).first + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { shipments_attributes: { "0" => { selected_shipping_rate_id: shipping_rate.id, id: shipment.id } } } + expect(response.status).to eq(200) + # Find the correct shipment... + json_shipment = json_response['shipments'].detect { |s| s["id"] == shipment.id } + # Find the correct shipping rate for that shipment... + json_shipping_rate = json_shipment['shipping_rates'].detect { |sr| sr["id"] == shipping_rate.id } + # ... And finally ensure that it's selected + expect(json_shipping_rate['selected']).to be true + # Order should automatically transfer to payment because all criteria are met + expect(json_response['state']).to eq('payment') + end + + it "can update payment method and transition from payment to confirm" do + order.update_column(:state, "payment") + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { payments_attributes: [{ payment_method_id: @payment_method.id }] } + expect(json_response['state']).to eq('confirm') + expect(json_response['payments'][0]['payment_method']['name']).to eq(@payment_method.name) + expect(json_response['payments'][0]['amount']).to eq(order.total.to_s) + expect(response.status).to eq(200) + end + + it "can update payment method with source and transition from payment to confirm" do + order.update_column(:state, "payment") + source_attributes = { + number: "4111111111111111", + month: 1.month.from_now.month, + year: 1.month.from_now.year, + verification_value: "123", + name: "Spree Commerce" + } + + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { payments_attributes: [{ payment_method_id: @payment_method.id.to_s }], + payment_source: { @payment_method.id.to_s => source_attributes } } + expect(json_response['payments'][0]['payment_method']['name']).to eq(@payment_method.name) + expect(json_response['payments'][0]['amount']).to eq(order.total.to_s) + expect(response.status).to eq(200) + end + + it "returns errors when source is missing attributes" do + order.update_column(:state, "payment") + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { + payments_attributes: [{ payment_method_id: @payment_method.id }] + }, + payment_source: { + @payment_method.id.to_s => { name: "Spree" } + } + + expect(response.status).to eq(422) + cc_errors = json_response['errors']['payments.Credit Card'] + expect(cc_errors).to include("Number can't be blank") + expect(cc_errors).to include("Month is not a number") + expect(cc_errors).to include("Year is not a number") + expect(cc_errors).to include("Verification Value can't be blank") + end + + it "allow users to reuse a credit card" do + order.update_column(:state, "payment") + credit_card = create(:credit_card, user_id: order.user_id, payment_method_id: @payment_method.id) + + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { existing_card: credit_card.id } + + expect(response.status).to eq 200 + expect(order.credit_cards).to match_array [credit_card] + end + + it "can transition from confirm to complete" do + order.update_columns(completed_at: Time.now, state: 'complete') + allow_any_instance_of(Spree::Order).to receive_messages(payment_required?: false) + api_put :update, id: order.to_param, order_token: order.guest_token + expect(json_response['state']).to eq('complete') + expect(response.status).to eq(200) + end + + it "returns the order if the order is already complete" do + order.update_columns(completed_at: Time.now, state: 'complete') + api_put :update, id: order.to_param, order_token: order.guest_token + expect(json_response['number']).to eq(order.number) + expect(response.status).to eq(200) + end + + # Regression test for #3784 + it "can update the special instructions for an order" do + instructions = "Don't drop it. (Please)" + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { special_instructions: instructions } + expect(json_response['special_instructions']).to eql(instructions) + end + + context "as an admin" do + sign_in_as_admin! + it "can assign a user to the order" do + user = create(:user) + # Need to pass email as well so that validations succeed + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { user_id: user.id, email: "guest@spreecommerce.com" } + expect(response.status).to eq(200) + expect(json_response['user_id']).to eq(user.id) + end + end + + it "can assign an email to the order" do + api_put :update, id: order.to_param, order_token: order.guest_token, + order: { email: "guest@spreecommerce.com" } + expect(json_response['email']).to eq("guest@spreecommerce.com") + expect(response.status).to eq(200) + end + + it "can apply a coupon code to an order" do + skip "ensure that the order totals are properly updated, see frontend orders_controller or checkout_controller as example" + + order.update_column(:state, "payment") + expect(PromotionHandler::Coupon).to receive(:new).with(order).and_call_original + expect_any_instance_of(PromotionHandler::Coupon).to receive(:apply).and_return({ coupon_applied?: true }) + api_put :update, :id => order.to_param, order_token: order.guest_token, order: { coupon_code: "foobar" } + end + end + + context "PUT 'next'" do + let!(:order) { create(:order_with_line_items) } + it "cannot transition to address without a line item" do + order.line_items.delete_all + order.update_column(:email, "spree@example.com") + api_put :next, id: order.to_param, order_token: order.guest_token + expect(response.status).to eq(422) + expect(json_response["errors"]["base"]).to include(Spree.t(:there_are_no_items_for_this_order)) + end + + it "can transition an order to the next state" do + order.update_column(:email, "spree@example.com") + + api_put :next, id: order.to_param, order_token: order.guest_token + expect(response.status).to eq(200) + expect(json_response['state']).to eq('address') + end + + it "cannot transition if order email is blank" do + order.update_columns( + state: 'address', + email: nil + ) + + api_put :next, :id => order.to_param, :order_token => order.guest_token + expect(response.status).to eq(422) + expect(json_response['error']).to match(/could not be transitioned/) + end + + it "doesnt advance payment state if order has no payment" do + order.update_column(:state, "payment") + api_put :next, id: order.to_param, order_token: order.guest_token, order: {} + expect(json_response["errors"]["base"]).to include(Spree.t(:no_payment_found)) + end + end + + context "PUT 'advance'" do + let!(:order) { create(:order_with_line_items) } + + it 'continues to advance advances an order while it can move forward' do + expect_any_instance_of(Spree::Order).to receive(:next).exactly(3).times.and_return(true, true, false) + api_put :advance, id: order.to_param, order_token: order.guest_token + end + + it 'returns the order' do + api_put :advance, id: order.to_param, order_token: order.guest_token + expect(json_response['id']).to eq(order.id) + end + end + end +end diff --git a/api/spec/controllers/spree/api/classifications_controller_spec.rb b/api/spec/controllers/spree/api/classifications_controller_spec.rb new file mode 100644 index 00000000000..318efd4fc9d --- /dev/null +++ b/api/spec/controllers/spree/api/classifications_controller_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +module Spree + describe Api::ClassificationsController, type: :controller do + let(:taxon) do + taxon = create(:taxon) + + 3.times do + product = create(:product) + product.taxons << taxon + end + taxon + end + + before do + stub_authentication! + end + + context "as a user" do + it "cannot change the order of a product" do + api_put :update, taxon_id: taxon, product_id: taxon.products.first, position: 1 + expect(response.status).to eq(401) + end + end + + context "as an admin" do + sign_in_as_admin! + + let(:last_product) { taxon.products.last } + + it "can change the order a product" do + classification = taxon.classifications.find_by(product_id: last_product.id) + expect(classification.position).to eq(3) + api_put :update, taxon_id: taxon, product_id: last_product, position: 0 + expect(response.status).to eq(200) + expect(classification.reload.position).to eq(1) + end + + it "should touch the taxon" do + taxon.update_attributes(updated_at: Time.now - 10.seconds) + taxon_last_updated_at = taxon.updated_at + api_put :update, taxon_id: taxon, product_id: last_product, position: 0 + taxon.reload + expect(taxon_last_updated_at.to_i).to_not eq(taxon.updated_at.to_i) + end + end + end +end diff --git a/api/spec/controllers/spree/api/config_controller_spec.rb b/api/spec/controllers/spree/api/config_controller_spec.rb new file mode 100644 index 00000000000..eac33ffe1cf --- /dev/null +++ b/api/spec/controllers/spree/api/config_controller_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +module Spree + describe Api::ConfigController, :type => :controller do + render_views + + before do + stub_authentication! + end + + it "returns Spree::Money settings" do + api_get :money + expect(response).to be_success + expect(json_response["symbol"]).to eq("$") + expect(json_response["symbol_position"]).to eq("before") + expect(json_response["no_cents"]).to eq(false) + expect(json_response["decimal_mark"]).to eq(".") + expect(json_response["thousands_separator"]).to eq(",") + end + + it "returns some configuration settings" do + api_get :show + expect(response).to be_success + expect(json_response["default_country_id"]).to eq(Spree::Config[:default_country_id]) + end + end +end \ No newline at end of file diff --git a/api/spec/controllers/spree/api/countries_controller_spec.rb b/api/spec/controllers/spree/api/countries_controller_spec.rb index be925416ce5..976938036f4 100644 --- a/api/spec/controllers/spree/api/countries_controller_spec.rb +++ b/api/spec/controllers/spree/api/countries_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Spree - describe Api::CountriesController do + describe Api::CountriesController, :type => :controller do render_views before do @@ -12,7 +12,7 @@ module Spree it "gets all countries" do api_get :index - json_response['countries'].first['iso3'].should eq @country.iso3 + expect(json_response['countries'].first['iso3']).to eq @country.iso3 end context "with two countries" do @@ -20,29 +20,29 @@ module Spree it "can view all countries" do api_get :index - json_response['count'].should == 2 - json_response['current_page'].should == 1 - json_response['pages'].should == 1 + expect(json_response['count']).to eq(2) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(1) end it 'can query the results through a paramter' do api_get :index, :q => { :name_cont => 'zam' } - json_response['count'].should == 1 - json_response['countries'].first['name'].should eq @zambia.name + expect(json_response['count']).to eq(1) + expect(json_response['countries'].first['name']).to eq @zambia.name end it 'can control the page size through a parameter' do api_get :index, :per_page => 1 - json_response['count'].should == 1 - json_response['current_page'].should == 1 - json_response['pages'].should == 2 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) end end it "includes states" do api_get :show, :id => @country.id states = json_response['states'] - states.first['name'].should eq @state.name + expect(states.first['name']).to eq @state.name end end end diff --git a/api/spec/controllers/spree/api/credit_cards_controller_spec.rb b/api/spec/controllers/spree/api/credit_cards_controller_spec.rb new file mode 100644 index 00000000000..7b52621ac57 --- /dev/null +++ b/api/spec/controllers/spree/api/credit_cards_controller_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +module Spree + describe Api::CreditCardsController, :type => :controller do + render_views + + let!(:admin_user) do + user = Spree.user_class.new(:email => "spree@example.com", :id => 1) + user.generate_spree_api_key! + allow(user).to receive(:has_spree_role?).with('admin').and_return(true) + user + end + + let!(:normal_user) do + user = Spree.user_class.new(:email => "spree2@example.com", :id => 2) + user.generate_spree_api_key! + user + end + + let!(:card) { create(:credit_card, :user_id => admin_user.id, gateway_customer_profile_id: "random") } + + before do + stub_authentication! + end + + it "the user id doesn't exist" do + api_get :index, user_id: 1000 + expect(response.status).to eq(404) + end + + context "calling user is in admin role" do + let(:current_api_user) do + user = admin_user + user + end + + it "no credit cards exist for user" do + api_get :index, user_id: normal_user.id + + expect(response.status).to eq(200) + expect(json_response["pages"]).to eq(0) + end + + it "can view all credit cards for user" do + api_get :index, user_id: current_api_user.id + + expect(response.status).to eq(200) + expect(json_response["pages"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["credit_cards"].length).to eq(1) + expect(json_response["credit_cards"].first["id"]).to eq(card.id) + end + end + + context "calling user is not in admin role" do + let(:current_api_user) do + user = normal_user + user + end + + let!(:card) { create(:credit_card, :user_id => normal_user.id, gateway_customer_profile_id: "random") } + + it "can not view user" do + api_get :index, user_id: admin_user.id + + expect(response.status).to eq(404) + end + + it "can view own credit cards" do + api_get :index, user_id: normal_user.id + + expect(response.status).to eq(200) + expect(json_response["pages"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["credit_cards"].length).to eq(1) + expect(json_response["credit_cards"].first["id"]).to eq(card.id) + end + end + end +end diff --git a/api/spec/controllers/spree/api/images_controller_spec.rb b/api/spec/controllers/spree/api/images_controller_spec.rb index 5e08d4ea1f4..fc2b754676b 100644 --- a/api/spec/controllers/spree/api/images_controller_spec.rb +++ b/api/spec/controllers/spree/api/images_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Spree - describe Spree::Api::ImagesController do + describe Spree::Api::ImagesController, :type => :controller do render_views let!(:product) { create(:product) } @@ -17,49 +17,76 @@ module Spree sign_in_as_admin! it "can upload a new image for a variant" do - lambda do + expect do api_post :create, :image => { :attachment => upload_image('thinking-cat.jpg'), :viewable_type => 'Spree::Variant', - :viewable_id => product.master.to_param } - response.status.should == 201 - json_response.should have_attributes(attributes) - end.should change(Image, :count).by(1) + :viewable_id => product.master.to_param }, + :product_id => product.id + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + end.to change(Image, :count).by(1) end context "working with an existing image" do let!(:product_image) { product.master.images.create!(:attachment => image('thinking-cat.jpg')) } + it "can get a single product image" do + api_get :show, :id => product_image.id, :product_id => product.id + expect(response.status).to eq(200) + expect(json_response).to have_attributes(attributes) + end + + it "can get a single variant image" do + api_get :show, :id => product_image.id, :variant_id => product.master.id + expect(response.status).to eq(200) + expect(json_response).to have_attributes(attributes) + end + + it "can get a list of product images" do + api_get :index, :product_id => product.id + expect(response.status).to eq(200) + expect(json_response).to have_key("images") + expect(json_response["images"].first).to have_attributes(attributes) + end + + it "can get a list of variant images" do + api_get :index, :variant_id => product.master.id + expect(response.status).to eq(200) + expect(json_response).to have_key("images") + expect(json_response["images"].first).to have_attributes(attributes) + end + it "can update image data" do - product_image.position.should == 1 - api_post :update, :image => { :position => 2 }, :id => product_image.id - response.status.should == 200 - json_response.should have_attributes(attributes) - product_image.reload.position.should == 2 + expect(product_image.position).to eq(1) + api_post :update, :image => { :position => 2 }, :id => product_image.id, :product_id => product.id + expect(response.status).to eq(200) + expect(json_response).to have_attributes(attributes) + expect(product_image.reload.position).to eq(2) end it "can delete an image" do - api_delete :destroy, :id => product_image.id - response.status.should == 204 - lambda { product_image.reload }.should raise_error(ActiveRecord::RecordNotFound) + api_delete :destroy, :id => product_image.id, :product_id => product.id + expect(response.status).to eq(204) + expect { product_image.reload }.to raise_error(ActiveRecord::RecordNotFound) end end end context "as a non-admin" do it "cannot create an image" do - api_post :create + api_post :create, :product_id => product.id assert_unauthorized! end it "cannot update an image" do - api_put :update, :id => 1 - assert_unauthorized! + api_put :update, :id => 1, :product_id => product.id + assert_not_found! end it "cannot delete an image" do - api_delete :destroy, :id => 1 - assert_unauthorized! + api_delete :destroy, :id => 1, :product_id => product.id + assert_not_found! end end end diff --git a/api/spec/controllers/spree/api/inventory_units_controller_spec.rb b/api/spec/controllers/spree/api/inventory_units_controller_spec.rb new file mode 100644 index 00000000000..c7b2953a292 --- /dev/null +++ b/api/spec/controllers/spree/api/inventory_units_controller_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +module Spree + describe Api::InventoryUnitsController, :type => :controller do + render_views + + before do + stub_authentication! + @inventory_unit = create(:inventory_unit) + end + + context "as an admin" do + sign_in_as_admin! + + it "gets an inventory unit" do + api_get :show, :id => @inventory_unit.id + expect(json_response['state']).to eq @inventory_unit.state + end + + it "updates an inventory unit (only shipment is accessable by default)" do + api_put :update, :id => @inventory_unit.id, + :inventory_unit => { :shipment => nil } + expect(json_response['shipment_id']).to be_nil + end + + context 'fires state event' do + it 'if supplied with :fire param' do + api_put :update, :id => @inventory_unit.id, + :fire => 'ship', + :inventory_unit => { :shipment => nil } + + expect(json_response['state']).to eq 'shipped' + end + + it 'and returns exception if cannot fire' do + api_put :update, :id => @inventory_unit.id, + :fire => 'return' + expect(json_response['exception']).to match /cannot transition to return/ + end + + it 'and returns exception bad state' do + api_put :update, :id => @inventory_unit.id, + :fire => 'bad' + expect(json_response['exception']).to match /cannot transition to bad/ + end + end + end + end +end diff --git a/api/spec/controllers/spree/api/line_items_controller_spec.rb b/api/spec/controllers/spree/api/line_items_controller_spec.rb index 06fc5e39d9b..9421ea12738 100644 --- a/api/spec/controllers/spree/api/line_items_controller_spec.rb +++ b/api/spec/controllers/spree/api/line_items_controller_spec.rb @@ -1,17 +1,24 @@ require 'spec_helper' module Spree - describe Api::LineItemsController do + PermittedAttributes.module_eval do + mattr_writer :line_item_attributes + end + + unless PermittedAttributes.line_item_attributes.include? :some_option + PermittedAttributes.line_item_attributes += [:some_option] + end + + # This should go in an initializer + Spree::Api::LineItemsController.line_item_options += [:some_option] + + describe Api::LineItemsController, :type => :controller do render_views - let!(:order) do - order = create(:order) - order.line_items << create(:line_item) - order - end + let!(:order) { create(:order_with_line_items, line_items_count: 1) } let(:product) { create(:product) } - let(:attributes) { [:id, :quantity, :price, :variant] } + let(:attributes) { [:id, :quantity, :price, :variant, :total, :display_amount, :single_display_amount] } let(:resource_scoping) { { :order_id => order.to_param } } before do @@ -20,39 +27,140 @@ module Spree it "can learn how to create a new line item" do api_get :new - json_response["attributes"].should == ["quantity", "price", "variant_id"] + expect(json_response["attributes"]).to eq(["quantity", "price", "variant_id"]) required_attributes = json_response["required_attributes"] - required_attributes.should include("quantity", "variant_id") + expect(required_attributes).to include("quantity", "variant_id") + end + + context "authenticating with a token" do + it "can add a new line item to an existing order" do + api_post :create, :line_item => { :variant_id => product.master.to_param, :quantity => 1 }, :order_token => order.guest_token + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + expect(json_response["variant"]["name"]).not_to be_blank + end + + it "can add a new line item to an existing order with token in header" do + request.headers["X-Spree-Order-Token"] = order.guest_token + api_post :create, :line_item => { :variant_id => product.master.to_param, :quantity => 1 } + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + expect(json_response["variant"]["name"]).not_to be_blank + end end context "as the order owner" do before do - Order.any_instance.stub :user => current_api_user + allow_any_instance_of(Order).to receive_messages :user => current_api_user end it "can add a new line item to an existing order" do api_post :create, :line_item => { :variant_id => product.master.to_param, :quantity => 1 } - response.status.should == 201 - json_response.should have_attributes(attributes) - json_response["variant"]["name"].should_not be_blank + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + expect(json_response["variant"]["name"]).not_to be_blank + end + + it "can add a new line item to an existing order with options" do + expect_any_instance_of(LineItem).to receive(:some_option=).with(4) + api_post :create, + line_item: { + variant_id: product.master.to_param, + quantity: 1, + options: { some_option: 4 } + } + expect(response.status).to eq(201) + end + + it "default quantity to 1 if none is given" do + api_post :create, :line_item => { :variant_id => product.master.to_param } + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + expect(json_response[:quantity]).to eq 1 + end + + it "increases a line item's quantity if it exists already" do + order.line_items.create(:variant_id => product.master.id, :quantity => 10) + api_post :create, :line_item => { :variant_id => product.master.to_param, :quantity => 1 } + expect(response.status).to eq(201) + order.reload + expect(order.line_items.count).to eq(2) # 1 original due to factory, + 1 in this test + expect(json_response).to have_attributes(attributes) + expect(json_response["quantity"]).to eq(11) end it "can update a line item on the order" do line_item = order.line_items.first - api_put :update, :id => line_item.id, :line_item => { :quantity => 1000 } - response.status.should == 200 - json_response.should have_attributes(attributes) + api_put :update, :id => line_item.id, :line_item => { :quantity => 101 } + expect(response.status).to eq(200) + order.reload + expect(order.total).to eq(1010) # 10 original due to factory, + 1000 in this test + expect(json_response).to have_attributes(attributes) + expect(json_response["quantity"]).to eq(101) + end + + it "can update a line item's options on the order" do + expect_any_instance_of(LineItem).to receive(:some_option=).with(12) + line_item = order.line_items.first + api_put :update, + id: line_item.id, + line_item: { quantity: 1, options: { some_option: 12 } } + expect(response.status).to eq(200) end it "can delete a line item on the order" do line_item = order.line_items.first api_delete :destroy, :id => line_item.id - response.status.should == 204 - lambda { line_item.reload }.should raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(204) + order.reload + expect(order.line_items.count).to eq(0) # 1 original due to factory, - 1 in this test + expect { line_item.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context "order contents changed after shipments were created" do + let!(:order) { Order.create } + let!(:line_item) { order.contents.add(product.master) } + + before { order.create_proposed_shipments } + + it "clear out shipments on create" do + expect(order.reload.shipments).not_to be_empty + api_post :create, :line_item => { :variant_id => product.master.to_param, :quantity => 1 } + expect(order.reload.shipments).to be_empty + end + + it "clear out shipments on update" do + expect(order.reload.shipments).not_to be_empty + api_put :update, :id => line_item.id, :line_item => { :quantity => 1000 } + expect(order.reload.shipments).to be_empty + end + + it "clear out shipments on delete" do + expect(order.reload.shipments).not_to be_empty + api_delete :destroy, :id => line_item.id + expect(order.reload.shipments).to be_empty + end + + context "order is completed" do + before do + allow(order).to receive_messages completed?: true + allow(Order).to receive_message_chain :includes, find_by!: order + end + + it "doesn't destroy shipments or restart checkout flow" do + expect(order.reload.shipments).not_to be_empty + api_post :create, :line_item => { :variant_id => product.master.to_param, :quantity => 1 } + expect(order.reload.shipments).not_to be_empty + end + end end end context "as just another user" do + before do + user = create(:user) + end + it "cannot add a new line item to the order" do api_post :create, :line_item => { :variant_id => product.master.to_param, :quantity => 1 } assert_unauthorized! @@ -62,16 +170,15 @@ module Spree line_item = order.line_items.first api_put :update, :id => line_item.id, :line_item => { :quantity => 1000 } assert_unauthorized! - line_item.reload.quantity.should_not == 1000 + expect(line_item.reload.quantity).not_to eq(1000) end it "cannot delete a line item on the order" do line_item = order.line_items.first api_delete :destroy, :id => line_item.id assert_unauthorized! - lambda { line_item.reload }.should_not raise_error(ActiveRecord::RecordNotFound) + expect { line_item.reload }.not_to raise_error end end - end end diff --git a/api/spec/controllers/spree/api/option_types_controller_spec.rb b/api/spec/controllers/spree/api/option_types_controller_spec.rb index 3cd19e7777b..1bcc87c1bca 100644 --- a/api/spec/controllers/spree/api/option_types_controller_spec.rb +++ b/api/spec/controllers/spree/api/option_types_controller_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' module Spree - describe Api::OptionTypesController do + describe Api::OptionTypesController, :type => :controller do render_views let(:attributes) { [:id, :name, :position, :presentation] } - let!(:option_value) { Factory(:option_value) } + let!(:option_value) { create(:option_value) } let!(:option_type) { option_value.option_type } before do @@ -13,41 +13,43 @@ module Spree end def check_option_values(option_values) - option_values.count.should == 1 - option_values.first.should have_attributes([:id, :name, :presentation, + expect(option_values.count).to eq(1) + expect(option_values.first).to have_attributes([:id, :name, :presentation, :option_type_name, :option_type_id]) end it "can list all option types" do api_get :index - json_response.count.should == 1 - json_response.first.should have_attributes(attributes) + expect(json_response.count).to eq(1) + expect(json_response.first).to have_attributes(attributes) check_option_values(json_response.first["option_values"]) end it "can search for an option type" do - Factory(:option_type, :name => "buzz") + create(:option_type, :name => "buzz") api_get :index, :q => { :name_cont => option_type.name } - json_response.count.should == 1 - json_response.first.should have_attributes(attributes) + expect(json_response.count).to eq(1) + expect(json_response.first).to have_attributes(attributes) end - it "can retreive a list of option types" do - option_type_1 = Factory(:option_type) - option_type_2 = Factory(:option_type) - api_get :index, :ids => [option_type, option_type_1] - json_response.count.should == 2 + it "can retrieve a list of specific option types" do + option_type_1 = create(:option_type) + option_type_2 = create(:option_type) + api_get :index, :ids => "#{option_type.id},#{option_type_1.id}" + expect(json_response.count).to eq(2) + + check_option_values(json_response.first["option_values"]) end it "can list a single option type" do api_get :show, :id => option_type.id - json_response.should have_attributes(attributes) + expect(json_response).to have_attributes(attributes) check_option_values(json_response["option_values"]) end it "cannot create a new option type" do - api_post :create, :option_type => { + api_post :create, :option_type => { :name => "Option Type", :presentation => "Option Type" } @@ -60,31 +62,31 @@ def check_option_values(option_values) :option_type => { :name => "Option Type" } - assert_unauthorized! - option_type.reload.name.should == original_name + assert_not_found! + expect(option_type.reload.name).to eq(original_name) end it "cannot delete an option type" do api_delete :destroy, :id => option_type.id - assert_unauthorized! - lambda { option_type.reload }.should_not raise_error(ActiveRecord::RecordNotFound) + assert_not_found! + expect { option_type.reload }.not_to raise_error end context "as an admin" do sign_in_as_admin! it "can create an option type" do - api_post :create, :option_type => { + api_post :create, :option_type => { :name => "Option Type", :presentation => "Option Type" } - json_response.should have_attributes(attributes) - response.status.should == 201 + expect(json_response).to have_attributes(attributes) + expect(response.status).to eq(201) end it "cannot create an option type with invalid attributes" do api_post :create, :option_type => {} - response.status.should == 422 + expect(response.status).to eq(422) end it "can update an option type" do @@ -92,23 +94,23 @@ def check_option_values(option_values) api_put :update, :id => option_type.id, :option_type => { :name => "Option Type", } - response.status.should == 200 + expect(response.status).to eq(200) option_type.reload - option_type.name.should == "Option Type" + expect(option_type.name).to eq("Option Type") end it "cannot update an option type with invalid attributes" do api_put :update, :id => option_type.id, :option_type => { :name => "" } - response.status.should == 422 + expect(response.status).to eq(422) end it "can delete an option type" do api_delete :destroy, :id => option_type.id - response.status.should == 204 + expect(response.status).to eq(204) end - end + end end end diff --git a/api/spec/controllers/spree/api/option_values_controller_spec.rb b/api/spec/controllers/spree/api/option_values_controller_spec.rb new file mode 100644 index 00000000000..8ee3f11f731 --- /dev/null +++ b/api/spec/controllers/spree/api/option_values_controller_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +module Spree + describe Api::OptionValuesController, :type => :controller do + render_views + + let(:attributes) { [:id, :name, :presentation, :option_type_name, :option_type_name] } + let!(:option_value) { create(:option_value) } + let!(:option_type) { option_value.option_type } + + before do + stub_authentication! + end + + def check_option_values(option_values) + expect(option_values.count).to eq(1) + expect(option_values.first).to have_attributes([:id, :name, :presentation, + :option_type_name, :option_type_id]) + end + + context "without any option type scoping" do + before do + # Create another option value with a brand new option type + create(:option_value, :option_type => create(:option_type)) + end + + it "can retrieve a list of all option values" do + api_get :index + expect(json_response.count).to eq(2) + expect(json_response.first).to have_attributes(attributes) + end + end + + context "for a particular option type" do + let(:resource_scoping) { { :option_type_id => option_type.id } } + + it "can list all option values" do + api_get :index + expect(json_response.count).to eq(1) + expect(json_response.first).to have_attributes(attributes) + end + + it "can search for an option type" do + create(:option_value, :name => "buzz") + api_get :index, :q => { :name_cont => option_value.name } + expect(json_response.count).to eq(1) + expect(json_response.first).to have_attributes(attributes) + end + + it "can retrieve a list of option types" do + option_value_1 = create(:option_value, :option_type => option_type) + option_value_2 = create(:option_value, :option_type => option_type) + api_get :index, :ids => [option_value.id, option_value_1.id] + expect(json_response.count).to eq(2) + end + + it "can list a single option value" do + api_get :show, :id => option_value.id + expect(json_response).to have_attributes(attributes) + end + + it "cannot create a new option value" do + api_post :create, :option_value => { + :name => "Option Value", + :presentation => "Option Value" + } + assert_unauthorized! + end + + it "cannot alter an option value" do + original_name = option_type.name + api_put :update, :id => option_type.id, + :option_value => { + :name => "Option Value" + } + assert_not_found! + expect(option_type.reload.name).to eq(original_name) + end + + it "cannot delete an option value" do + api_delete :destroy, :id => option_type.id + assert_not_found! + expect { option_type.reload }.not_to raise_error + end + + context "as an admin" do + sign_in_as_admin! + + it "can create an option value" do + api_post :create, :option_value => { + :name => "Option Value", + :presentation => "Option Value" + } + expect(json_response).to have_attributes(attributes) + expect(response.status).to eq(201) + end + + it "cannot create an option type with invalid attributes" do + api_post :create, :option_value => {} + expect(response.status).to eq(422) + end + + it "can update an option value" do + original_name = option_value.name + api_put :update, :id => option_value.id, :option_value => { + :name => "Option Value", + } + expect(response.status).to eq(200) + + option_value.reload + expect(option_value.name).to eq("Option Value") + end + + it "permits the correct attributes" do + expect(controller).to receive(:permitted_option_value_attributes) + api_put :update, :id => option_value.id, :option_value => { + :name => "" + } + end + + it "cannot update an option value with invalid attributes" do + api_put :update, :id => option_value.id, :option_value => { + :name => "" + } + expect(response.status).to eq(422) + end + + it "can delete an option value" do + api_delete :destroy, :id => option_value.id + expect(response.status).to eq(204) + end + end + end + end +end diff --git a/api/spec/controllers/spree/api/orders_controller_spec.rb b/api/spec/controllers/spree/api/orders_controller_spec.rb old mode 100644 new mode 100755 index e2f6a8071aa..8947a77665a --- a/api/spec/controllers/spree/api/orders_controller_spec.rb +++ b/api/spec/controllers/spree/api/orders_controller_spec.rb @@ -1,46 +1,249 @@ require 'spec_helper' +require 'spree/testing_support/bar_ability' module Spree - describe Api::OrdersController do + describe Api::OrdersController, :type => :controller do render_views let!(:order) { create(:order) } - let(:attributes) { [:number, :item_total, :total, + let(:variant) { create(:variant) } + let(:line_item) { create(:line_item) } + + let(:attributes) { [:number, :item_total, :display_total, :total, :state, :adjustment_total, :user_id, :created_at, :updated_at, :completed_at, :payment_total, :shipment_state, - :payment_state, :email, :special_instructions] } + :payment_state, :email, :special_instructions, + :total_quantity, :display_item_total, :currency] } + + let(:address_params) { { :country_id => Country.first.id, :state_id => State.first.id } } + let(:current_api_user) do + user = Spree.user_class.new(:email => "spree@example.com") + user.generate_spree_api_key! + user + end before do stub_authentication! end + describe 'PATCH #update' do + subject { api_patch :update, id: order.to_param, order: { email: "foo@bar.com" } } + + before do + allow_any_instance_of(Spree::Order).to receive_messages :user => current_api_user + end + + it 'should be ok' do + expect(subject).to be_ok + end + + it 'should not invoke OrderContents#update_cart' do + expect_any_instance_of(Spree::OrderContents).to_not receive(:update_cart) + subject + end + + it 'should update the email' do + subject + expect(order.reload.email).to eq('foo@bar.com') + end + end + it "cannot view all orders" do api_get :index assert_unauthorized! end + context "the current api user is not persisted" do + let(:current_api_user) { Spree.user_class.new } + + it "returns a 401" do + api_get :mine + expect(response.status).to eq(401) + end + end + + context "the current api user is authenticated" do + let(:current_api_user) { order.user } + let(:order) { create(:order, line_items: [line_item]) } + + it "can view all of their own orders" do + api_get :mine + + expect(response.status).to eq(200) + expect(json_response["pages"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["orders"].length).to eq(1) + expect(json_response["orders"].first["number"]).to eq(order.number) + expect(json_response["orders"].first["line_items"].length).to eq(1) + expect(json_response["orders"].first["line_items"].first["id"]).to eq(line_item.id) + end + + it "can filter the returned results" do + api_get :mine, q: {completed_at_not_null: 1} + + expect(response.status).to eq(200) + expect(json_response["orders"].length).to eq(0) + end + + it "returns orders in reverse chronological order by completed_at" do + order.update_columns completed_at: Time.now + + order2 = Order.create user: order.user, completed_at: Time.now - 1.day + expect(order2.created_at).to be > order.created_at + order3 = Order.create user: order.user, completed_at: nil + expect(order3.created_at).to be > order2.created_at + order4 = Order.create user: order.user, completed_at: nil + expect(order4.created_at).to be > order3.created_at + + api_get :mine + expect(response.status).to eq(200) + expect(json_response["pages"]).to eq(1) + expect(json_response["orders"].length).to eq(4) + expect(json_response["orders"][0]["number"]).to eq(order.number) + expect(json_response["orders"][1]["number"]).to eq(order2.number) + expect(json_response["orders"][2]["number"]).to eq(order4.number) + expect(json_response["orders"][3]["number"]).to eq(order3.number) + end + end + + describe 'current' do + let(:current_api_user) { order.user } + let!(:order) { create(:order, line_items: [line_item]) } + + subject do + api_get :current, format: 'json' + end + + context "an incomplete order exists" do + it "returns that order" do + expect(JSON.parse(subject.body)['id']).to eq order.id + expect(subject).to be_success + end + end + + context "multiple incomplete orders exist" do + it "returns the latest incomplete order" do + new_order = Spree::Order.create! user: order.user + expect(new_order.created_at).to be > order.created_at + expect(JSON.parse(subject.body)['id']).to eq new_order.id + end + end + + context "an incomplete order does not exist" do + + before do + order.update_attribute(:state, order_state) + order.update_attribute(:completed_at, 5.minutes.ago) + end + + ["complete", "returned", "awaiting_return"].each do |order_state| + context "order is in the #{order_state} state" do + let(:order_state) { order_state } + + it "returns no content" do + expect(subject.status).to eq 204 + expect(subject.body).to be_blank + end + end + end + end + end + it "can view their own order" do - Order.any_instance.stub :user => current_api_user + allow_any_instance_of(Order).to receive_messages :user => current_api_user + api_get :show, :id => order.to_param + expect(response.status).to eq(200) + expect(json_response).to have_attributes(attributes) + expect(json_response["adjustments"]).to be_empty + end + + describe 'GET #show' do + let(:order) { create :order_with_line_items } + let(:adjustment) { FactoryGirl.create(:adjustment, order: order) } + + subject { api_get :show, id: order.to_param } + + before do + allow_any_instance_of(Order).to receive_messages :user => current_api_user + end + + context 'when inventory information is present' do + it 'contains stock information on variant' do + subject + variant = json_response['line_items'][0]['variant'] + expect(variant).to_not be_nil + expect(variant['in_stock']).to eq(false) + expect(variant['total_on_hand']).to eq(0) + expect(variant['is_backorderable']).to eq(true) + expect(variant['is_destroyed']).to eq(false) + end + end + + context 'when shipment adjustments are present' do + before do + order.shipments.first.adjustments << adjustment + end + + it 'contains adjustments on shipment' do + subject + + # Test to insure shipment has adjustments + shipment = json_response['shipments'][0] + expect(shipment).to_not be_nil + expect(shipment['adjustments'][0]).not_to be_empty + expect(shipment['adjustments'][0]['label']).to eq(adjustment.label) + end + end + end + + it "orders contain the basic checkout steps" do + allow_any_instance_of(Order).to receive_messages :user => current_api_user api_get :show, :id => order.to_param - response.status.should == 200 - json_response.should have_attributes(attributes) + expect(response.status).to eq(200) + expect(json_response["checkout_steps"]).to eq(["address", "delivery", "complete"]) end # Regression test for #1992 it "can view an order not in a standard state" do - Order.any_instance.stub :user => current_api_user + allow_any_instance_of(Order).to receive_messages :user => current_api_user order.update_column(:state, 'shipped') api_get :show, :id => order.to_param end it "can not view someone else's order" do - Order.any_instance.stub :user => stub_model(Spree::LegacyUser) + allow_any_instance_of(Order).to receive_messages :user => stub_model(Spree::LegacyUser) api_get :show, :id => order.to_param assert_unauthorized! end + it "can view an order if the token is known" do + api_get :show, :id => order.to_param, :order_token => order.guest_token + expect(response.status).to eq(200) + end + + it "can view an order if the token is passed in header" do + request.headers["X-Spree-Order-Token"] = order.guest_token + api_get :show, :id => order.to_param + expect(response.status).to eq(200) + end + + context "with BarAbility registered" do + before { Spree::Ability.register_ability(::BarAbility) } + after { Spree::Ability.remove_ability(::BarAbility) } + + it "can view an order" do + user = mock_model(Spree::LegacyUser) + allow(user).to receive_message_chain(:spree_roles, :pluck).and_return(["bar"]) + allow(user).to receive(:has_spree_role?).with('bar').and_return(true) + allow(user).to receive(:has_spree_role?).with('admin').and_return(false) + allow(Spree.user_class).to receive_messages find_by: user + api_get :show, :id => order.to_param + expect(response.status).to eq(200) + end + end + it "cannot cancel an order that doesn't belong to them" do order.update_attribute(:completed_at, Time.now) order.update_attribute(:shipment_state, "ready") @@ -53,36 +256,117 @@ module Spree assert_unauthorized! end - it "cannot change delivery information on an order that doesn't belong to them" do - api_put :delivery, :id => order.to_param - assert_unauthorized! - end - it "can create an order" do - variant = create(:variant) - api_post :create, :order => { :line_items => [{ :variant_id => variant.to_param, :quantity => 5 }] } - response.status.should == 201 + api_post :create, :order => { :line_items => { "0" => { :variant_id => variant.to_param, :quantity => 5 } } } + expect(response.status).to eq(201) + order = Order.last - order.line_items.count.should == 1 - order.line_items.first.variant.should == variant - order.line_items.first.quantity.should == 5 - json_response["state"].should == "address" + expect(order.line_items.count).to eq(1) + expect(order.line_items.first.variant).to eq(variant) + expect(order.line_items.first.quantity).to eq(5) + + expect(json_response['number']).to be_present + expect(json_response["token"]).not_to be_blank + expect(json_response["state"]).to eq("cart") + expect(order.user).to eq(current_api_user) + expect(order.email).to eq(current_api_user.email) + expect(json_response["user_id"]).to eq(current_api_user.id) + end + + it "assigns email when creating a new order" do + api_post :create, :order => { :email => "guest@spreecommerce.com" } + expect(json_response['email']).not_to eq controller.current_api_user + expect(json_response['email']).to eq "guest@spreecommerce.com" + end + + # Regression test for #3404 + it "can specify additional parameters for a line item" do + expect(Order).to receive(:create!).and_return(order = Spree::Order.new) + allow(order).to receive(:associate_user!) + allow(order).to receive_message_chain(:contents, :add).and_return(line_item = double('LineItem')) + expect(line_item).to receive(:update_attributes!).with("special" => true) + + allow(controller).to receive_messages(permitted_line_item_attributes: [:id, :variant_id, :quantity, :special]) + api_post :create, :order => { + :line_items => { + "0" => { + :variant_id => variant.to_param, :quantity => 5, :special => true + } + } + } + expect(response.status).to eq(201) + end + + it "cannot arbitrarily set the line items price" do + api_post :create, :order => { + :line_items => { + "0" => { + :price => 33.0, :variant_id => variant.to_param, :quantity => 5 + } + } + } + + expect(response.status).to eq 201 + expect(Order.last.line_items.first.price.to_f).to eq(variant.price) + end + + context "admin user imports order" do + before do + allow(current_api_user).to receive_messages has_spree_role?: true + allow(current_api_user).to receive_message_chain :spree_roles, pluck: ["admin"] + end + + it "is able to set any default unpermitted attribute" do + api_post :create, :order => { number: "WOW" } + expect(response.status).to eq 201 + expect(json_response['number']).to eq "WOW" + end + end + + # Regression test for #3404 + it "does not update line item needlessly" do + expect(Order).to receive(:create!).and_return(order = Spree::Order.new) + allow(order).to receive(:associate_user!) + allow(order).to receive_message_chain(:contents, :add).and_return(line_item = double('LineItem')) + expect(line_item).not_to receive(:update_attributes) + api_post :create, :order => { + :line_items => { + "0" => { + :variant_id => variant.to_param, :quantity => 5 + } + } + } end it "can create an order without any parameters" do - lambda { api_post :create }.should_not raise_error(NoMethodError) - response.status.should == 201 + expect { api_post :create }.not_to raise_error + expect(response.status).to eq(201) order = Order.last - json_response["state"].should == "address" + expect(json_response["state"]).to eq("cart") end context "working with an order" do + + let(:variant) { create(:variant) } + let!(:line_item) { order.contents.add(variant, 1) } + let!(:payment_method) { create(:check_payment_method) } + + let(:address_params) { { :country_id => country.id } } + let(:billing_address) { { :firstname => "Tiago", :lastname => "Motta", :address1 => "Av Paulista", + :city => "Sao Paulo", :zipcode => "01310-300", :phone => "12345678", + :country_id => country.id} } + let(:shipping_address) { { :firstname => "Tiago", :lastname => "Motta", :address1 => "Av Paulista", + :city => "Sao Paulo", :zipcode => "01310-300", :phone => "12345678", + :country_id => country.id} } + let(:country) { create(:country, {name: "Brazil", iso_name: "BRAZIL", iso: "BR", iso3: "BRA", numcode: 76 })} + before do - Order.any_instance.stub :user => current_api_user - create(:payment_method) + allow_any_instance_of(Order).to receive_messages user: current_api_user order.next # Switch from cart to address - order.ship_address.should be_nil - order.state.should == "address" + order.bill_address = nil + order.ship_address = nil + order.save + expect(order.state).to eq("address") end def clean_address(address) @@ -91,96 +375,232 @@ def clean_address(address) address end - let(:address_params) { { :country_id => Country.first.id, :state_id => State.first.id } } - let(:shipping_address) { clean_address(attributes_for(:address).merge!(address_params)) } - let(:billing_address) { clean_address(attributes_for(:address).merge!(address_params)) } - let!(:shipping_method) { create(:shipping_method) } - let!(:payment_method) { create(:payment_method) } + context "line_items hash not present in request" do + it "responds successfully" do + api_put :update, :id => order.to_param, :order => { + :email => "hublock@spreecommerce.com" + } + + expect(response).to be_success + end + end - it "can add address information to an order" do - api_put :address, :id => order.to_param, :shipping_address => shipping_address, :billing_address => billing_address + it "updates quantities of existing line items" do + api_put :update, :id => order.to_param, :order => { + :line_items => { + "0" => { :id => line_item.id, :quantity => 10 } + } + } - response.status.should == 200 - order.reload - order.shipping_address.reload - order.billing_address.reload - # We can assume the rest of the parameters are set if these two are - order.shipping_address.firstname.should == shipping_address[:firstname] - order.billing_address.firstname.should == billing_address[:firstname] - order.state.should == "delivery" - json_response["shipping_methods"].should_not be_empty + expect(response.status).to eq(200) + expect(json_response['line_items'].count).to eq(1) + expect(json_response['line_items'].first['quantity']).to eq(10) end - it "can add just shipping address information to an order" do - api_put :address, :id => order.to_param, :shipping_address => shipping_address - response.status.should == 200 - order.reload - order.shipping_address.reload - order.shipping_address.firstname.should == shipping_address[:firstname] - order.bill_address.should be_nil + it "adds an extra line item" do + variant2 = create(:variant) + api_put :update, :id => order.to_param, :order => { + :line_items => { + "0" => { :id => line_item.id, :quantity => 10 }, + "1" => { :variant_id => variant2.id, :quantity => 1} + } + } + + expect(response.status).to eq(200) + expect(json_response['line_items'].count).to eq(2) + expect(json_response['line_items'][0]['quantity']).to eq(10) + expect(json_response['line_items'][1]['variant_id']).to eq(variant2.id) + expect(json_response['line_items'][1]['quantity']).to eq(1) end - it "cannot use an address that has no valid shipping methods" do - shipping_method.destroy - api_put :address, :id => order.to_param, :shipping_address => shipping_address, :billing_address => billing_address - response.status.should == 422 - json_response["errors"]["base"].should == ["No shipping methods available for selected location, please change your address and try again."] + it "cannot change the price of an existing line item" do + api_put :update, :id => order.to_param, :order => { + :line_items => { + 0 => { :id => line_item.id, :price => 0 } + } + } + + expect(response.status).to eq(200) + expect(json_response['line_items'].count).to eq(1) + expect(json_response['line_items'].first['price'].to_f).to_not eq(0) + expect(json_response['line_items'].first['price'].to_f).to eq(line_item.variant.price) end - it "can not add invalid ship address information to an order" do - shipping_address[:firstname] = "" - api_put :address, :id => order.to_param, :shipping_address => shipping_address, :billing_address => billing_address + it "can add billing address" do + api_put :update, :id => order.to_param, :order => { :bill_address_attributes => billing_address } - response.status.should == 422 - json_response["errors"]["ship_address.firstname"].should_not be_blank + expect(order.reload.bill_address).to_not be_nil end - it "can not add invalid ship address information to an order" do + it "receives error message if trying to add billing address with errors" do billing_address[:firstname] = "" - api_put :address, :id => order.to_param, :shipping_address => shipping_address, :billing_address => billing_address - response.status.should == 422 - json_response["errors"]["bill_address.firstname"].should_not be_blank + api_put :update, :id => order.to_param, :order => { :bill_address_attributes => billing_address } + + expect(json_response['error']).not_to be_nil + expect(json_response['errors']).not_to be_nil + expect(json_response['errors']['bill_address.firstname'].first).to eq "can't be blank" + end + + it "can add shipping address" do + expect(order.ship_address).to be_nil + + api_put :update, :id => order.to_param, :order => { :ship_address_attributes => shipping_address } + + expect(order.reload.ship_address).not_to be_nil + end + + it "receives error message if trying to add shipping address with errors" do + expect(order.ship_address).to be_nil + shipping_address[:firstname] = "" + + api_put :update, :id => order.to_param, :order => { :ship_address_attributes => shipping_address } + + expect(json_response['error']).not_to be_nil + expect(json_response['errors']).not_to be_nil + expect(json_response['errors']['ship_address.firstname'].first).to eq "can't be blank" end - it "can add line items" do - api_put :update, :id => order.to_param, :order => { :line_items => [{:variant_id => create(:variant).id, :quantity => 2}] } + it "cannot set the user_id for the order" do + user = Spree.user_class.create + original_id = order.user_id + api_post :update, :id => order.to_param, :order => { user_id: user.id } + expect(response.status).to eq 200 + expect(json_response["user_id"]).to eq(original_id) + end - response.status.should == 200 - json_response['item_total'].to_f.should_not == order.item_total.to_f + context "order has shipments" do + before { order.create_proposed_shipments } + + it "clears out all existing shipments on line item udpate" do + previous_shipments = order.shipments + api_put :update, :id => order.to_param, :order => { + :line_items => { + 0 => { :id => line_item.id, :quantity => 10 } + } + } + expect(order.reload.shipments).to be_empty + end end context "with a line item" do - before do - order.line_items << create(:line_item) + let(:order_with_line_items) do + order = create(:order_with_line_items) + create(:adjustment, order: order, adjustable: order) + order end - context "for delivery" do - before do - order.update_attribute(:state, "delivery") + it "can empty an order" do + expect(order_with_line_items.adjustments.count).to eq(1) + api_put :empty, :id => order_with_line_items.to_param + expect(response.status).to eq(204) + order_with_line_items.reload + expect(order_with_line_items.line_items).to be_empty + expect(order_with_line_items.adjustments).to be_empty + end + + it "can list its line items with images" do + order.line_items.first.variant.images.create!(:attachment => image("thinking-cat.jpg")) + + api_get :show, :id => order.to_param + + expect(json_response['line_items'].first['variant']).to have_attributes([:images]) + end + + it "lists variants product id" do + api_get :show, :id => order.to_param + + expect(json_response['line_items'].first['variant']).to have_attributes([:product_id]) + end + + it "includes the tax_total in the response" do + api_get :show, :id => order.to_param + + expect(json_response['included_tax_total']).to eq('0.0') + expect(json_response['additional_tax_total']).to eq('0.0') + expect(json_response['display_included_tax_total']).to eq('$0.00') + expect(json_response['display_additional_tax_total']).to eq('$0.00') + end + + it "lists line item adjustments" do + adjustment = create(:adjustment, + :label => "10% off!", + :order => order, + :adjustable => order.line_items.first) + adjustment.update_column(:amount, 5) + api_get :show, :id => order.to_param + + adjustment = json_response['line_items'].first['adjustments'].first + expect(adjustment['label']).to eq("10% off!") + expect(adjustment['amount']).to eq("5.0") + end + + it "lists payments source without gateway info" do + order.payments.push payment = create(:payment) + api_get :show, :id => order.to_param + + source = json_response[:payments].first[:source] + expect(source[:name]).to eq payment.source.name + expect(source[:cc_type]).to eq payment.source.cc_type + expect(source[:last_digits]).to eq payment.source.last_digits + expect(source[:month].to_i).to eq payment.source.month + expect(source[:year].to_i).to eq payment.source.year + expect(source.has_key?(:gateway_customer_profile_id)).to be false + expect(source.has_key?(:gateway_payment_profile_id)).to be false + end + + context "when in delivery" do + let!(:shipping_method) do + FactoryGirl.create(:shipping_method).tap do |shipping_method| + shipping_method.calculator.preferred_amount = 10 + shipping_method.calculator.save + end end - it "can select a shipping method for an order" do - order.shipping_method.should be_nil - api_put :delivery, :id => order.to_param, :shipping_method_id => shipping_method.id - response.status.should == 200 - order.reload - order.state.should == "payment" - order.shipping_method.should == shipping_method + before do + order.bill_address = FactoryGirl.create(:address) + order.ship_address = FactoryGirl.create(:address) + order.next! + order.save end - it "cannot select an invalid shipping method for an order" do - order.shipping_method.should be_nil - api_put :delivery, :id => order.to_param, :shipping_method_id => '1234567890' - response.status.should == 422 - json_response["errors"].should include("Invalid shipping method specified.") + it "includes the ship_total in the response" do + api_get :show, id: order.to_param + + expect(json_response['ship_total']).to eq '10.0' + expect(json_response['display_ship_total']).to eq '$10.00' end - end - it "can empty an order" do - api_put :empty, :id => order.to_param - response.status.should == 200 - order.reload.line_items.should be_empty + it "returns available shipments for an order" do + api_get :show, :id => order.to_param + expect(response.status).to eq(200) + expect(json_response["shipments"]).not_to be_empty + shipment = json_response["shipments"][0] + # Test for correct shipping method attributes + # Regression test for #3206 + expect(shipment["shipping_methods"]).not_to be_nil + json_shipping_method = shipment["shipping_methods"][0] + expect(json_shipping_method["id"]).to eq(shipping_method.id) + expect(json_shipping_method["name"]).to eq(shipping_method.name) + expect(json_shipping_method["code"]).to eq(shipping_method.code) + expect(json_shipping_method["zones"]).not_to be_empty + expect(json_shipping_method["shipping_categories"]).not_to be_empty + + # Test for correct shipping rates attributes + # Regression test for #3206 + expect(shipment["shipping_rates"]).not_to be_nil + shipping_rate = shipment["shipping_rates"][0] + expect(shipping_rate["name"]).to eq(json_shipping_method["name"]) + expect(shipping_rate["cost"]).to eq("10.0") + expect(shipping_rate["selected"]).to be true + expect(shipping_rate["display_cost"]).to eq("$10.00") + expect(shipping_rate["shipping_method_code"]).to eq(json_shipping_method["code"]) + + expect(shipment["stock_location_name"]).not_to be_blank + manifest_item = shipment["manifest"][0] + expect(manifest_item["quantity"]).to eq(1) + expect(manifest_item["variant_id"]).to eq(order.line_items.first.variant_id) + end end end end @@ -192,8 +612,52 @@ def clean_address(address) before { Spree::Order.delete_all } it "still returns a root :orders key" do api_get :index - json_response["orders"].should == [] + expect(json_response["orders"]).to eq([]) + end + end + + it "responds with orders updated_at with miliseconds precision" do + if ActiveRecord::Base.connection.adapter_name == "Mysql2" + skip "MySQL does not support millisecond timestamps." + else + skip "Probable need to make it call as_json. See https://github.com/rails/rails/commit/0f33d70e89991711ff8b3dde134a61f4a5a0ec06" + end + + api_get :index + milisecond = order.updated_at.strftime("%L") + updated_at = json_response["orders"].first["updated_at"] + expect(updated_at.split("T").last).to have_content(milisecond) + end + + context "caching enabled" do + before do + ActionController::Base.perform_caching = true + 3.times { Order.create } + end + + it "returns unique orders" do + api_get :index + + orders = json_response[:orders] + expect(orders.count).to be >= 3 + expect(orders.map { |o| o[:id] }).to match_array Order.pluck(:id) end + + after { ActionController::Base.perform_caching = false } + end + + it "lists payments source with gateway info" do + order.payments.push payment = create(:payment) + api_get :show, :id => order.to_param + + source = json_response[:payments].first[:source] + expect(source[:name]).to eq payment.source.name + expect(source[:cc_type]).to eq payment.source.cc_type + expect(source[:last_digits]).to eq payment.source.last_digits + expect(source[:month].to_i).to eq payment.source.month + expect(source[:year].to_i).to eq payment.source.year + expect(source[:gateway_customer_profile_id]).to eq payment.source.gateway_customer_profile_id + expect(source[:gateway_payment_profile_id]).to eq payment.source.gateway_payment_profile_id end context "with two orders" do @@ -201,20 +665,20 @@ def clean_address(address) it "can view all orders" do api_get :index - json_response["orders"].first.should have_attributes(attributes) - json_response["count"].should == 2 - json_response["current_page"].should == 1 - json_response["pages"].should == 1 + expect(json_response["orders"].first).to have_attributes(attributes) + expect(json_response["count"]).to eq(2) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) end # Test for #1763 it "can control the page size through a parameter" do api_get :index, :per_page => 1 - json_response["orders"].count.should == 1 - json_response["orders"].first.should have_attributes(attributes) - json_response["count"].should == 1 - json_response["current_page"].should == 1 - json_response["pages"].should == 2 + expect(json_response["orders"].count).to eq(1) + expect(json_response["orders"].first).to have_attributes(attributes) + expect(json_response["count"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(2) end end @@ -228,17 +692,56 @@ def clean_address(address) it "can query the results through a parameter" do api_get :index, :q => { :email_cont => 'spree' } - json_response["orders"].count.should == 1 - json_response["orders"].first.should have_attributes(attributes) - json_response["orders"].first["email"].should == expected_result.email - json_response["count"].should == 1 - json_response["current_page"].should == 1 - json_response["pages"].should == 1 + expect(json_response["orders"].count).to eq(1) + expect(json_response["orders"].first).to have_attributes(attributes) + expect(json_response["orders"].first["email"]).to eq(expected_result.email) + expect(json_response["count"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) + end + end + + context "creation" do + it "can create an order without any parameters" do + expect { api_post :create }.not_to raise_error + expect(response.status).to eq(201) + order = Order.last + expect(json_response["state"]).to eq("cart") + end + + it "can arbitrarily set the line items price" do + api_post :create, :order => { + :line_items => { + "0" => { + :price => 33.0, :variant_id => variant.to_param, :quantity => 5 + } + } + } + expect(response.status).to eq 201 + expect(Order.last.line_items.first.price.to_f).to eq(33.0) + end + + it "can set the user_id for the order" do + user = Spree.user_class.create + api_post :create, :order => { user_id: user.id } + expect(response.status).to eq 201 + expect(json_response["user_id"]).to eq(user.id) + end + end + + context "updating" do + it "can set the user_id for the order" do + user = Spree.user_class.create + api_post :update, :id => order.number, :order => { user_id: user.id } + expect(response.status).to eq 200 + expect(json_response["user_id"]).to eq(user.id) end end context "can cancel an order" do before do + Spree::Config[:mails_from] = "spree@example.com" + order.completed_at = Time.now order.state = 'complete' order.shipment_state = 'ready' @@ -247,7 +750,7 @@ def clean_address(address) specify do api_put :cancel, :id => order.to_param - json_response["state"].should == "canceled" + expect(json_response["state"]).to eq("canceled") end end end diff --git a/api/spec/controllers/spree/api/payments_controller_spec.rb b/api/spec/controllers/spree/api/payments_controller_spec.rb index 931f87a34cd..416ceb1082f 100644 --- a/api/spec/controllers/spree/api/payments_controller_spec.rb +++ b/api/spec/controllers/spree/api/payments_controller_spec.rb @@ -1,15 +1,16 @@ require 'spec_helper' module Spree - describe Spree::Api::PaymentsController do + describe Spree::Api::PaymentsController, :type => :controller do render_views let!(:order) { create(:order) } let!(:payment) { create(:payment, :order => order) } - let!(:attributes) { [:id, :source_type, :source_id, :amount, - :payment_method_id, :response_code, :state, :avs_response, + let!(:attributes) { [:id, :source_type, :source_id, :amount, :display_amount, + :payment_method_id, :state, :avs_response, :created_at, :updated_at] } let(:resource_scoping) { { :order_id => order.to_param } } + before do stub_authentication! end @@ -17,30 +18,35 @@ module Spree context "as a user" do context "when the order belongs to the user" do before do - Order.any_instance.stub :user => current_api_user + allow_any_instance_of(Order).to receive_messages :user => current_api_user end it "can view the payments for their order" do api_get :index - json_response["payments"].first.should have_attributes(attributes) + expect(json_response["payments"].first).to have_attributes(attributes) end it "can learn how to create a new payment" do api_get :new - json_response["attributes"].should == attributes.map(&:to_s) - json_response["payment_methods"].should_not be_empty - json_response["payment_methods"].first.should have_attributes([:id, :name, :description]) + expect(json_response["attributes"]).to eq(attributes.map(&:to_s)) + expect(json_response["payment_methods"]).not_to be_empty + expect(json_response["payment_methods"].first).to have_attributes([:id, :name, :description]) end it "can create a new payment" do api_post :create, :payment => { :payment_method_id => PaymentMethod.first.id, :amount => 50 } - response.status.should == 201 - json_response.should have_attributes(attributes) + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) end it "can view a pre-existing payment's details" do api_get :show, :id => payment.to_param - json_response.should have_attributes(attributes) + expect(json_response).to have_attributes(attributes) + end + + it "cannot update a payment" do + api_put :update, :id => payment.to_param, :payment => { :amount => 2.01 } + assert_unauthorized! end it "cannot authorize a payment" do @@ -51,13 +57,18 @@ module Spree context "when the order does not belong to the user" do before do - Order.any_instance.stub :user => stub_model(LegacyUser) + allow_any_instance_of(Order).to receive_messages :user => stub_model(LegacyUser) end it "cannot view payments for somebody else's order" do api_get :index, :order_id => order.to_param assert_unauthorized! end + + it "can view the payments for an order given the order token" do + api_get :index, :order_id => order.to_param, :order_token => order.guest_token + expect(json_response["payments"].first).to have_attributes(attributes) + end end end @@ -66,138 +77,158 @@ module Spree it "can view the payments on any order" do api_get :index - response.status.should == 200 - json_response["payments"].first.should have_attributes(attributes) + expect(response.status).to eq(200) + expect(json_response["payments"].first).to have_attributes(attributes) end context "multiple payments" do - before { @payment = create(:payment, :order => order, :response_code => '99999') } + before { @payment = create(:payment, :order => order) } it "can view all payments on an order" do api_get :index - json_response["count"].should == 2 + expect(json_response["count"]).to eq(2) end it 'can control the page size through a parameter' do api_get :index, :per_page => 1 - json_response['count'].should == 1 - json_response['current_page'].should == 1 - json_response['pages'].should == 2 - end - - it 'can query the results through a paramter' do - api_get :index, :q => { :response_code_cont => '999' } - json_response['count'].should == 1 - json_response['payments'].first['response_code'].should eq @payment.response_code + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) end end context "for a given payment" do + context "updating" do + context "when the state is checkout" do + it "can update" do + payment.update_attributes(state: 'checkout') + api_put(:update, id: payment.to_param, payment: { amount: 2.01 }) + expect(response.status).to be(200) + expect(payment.reload.amount).to eq(2.01) + end + end - it "can authorize" do - api_put :authorize, :id => payment.to_param - response.status.should == 200 - payment.reload - payment.state.should == "pending" - end - - it "returns a 422 status when authorization fails" do - fake_response = stub(:success? => false, :to_s => "Could not authorize card") - Spree::Gateway::Bogus.any_instance.should_receive(:authorize).and_return(fake_response) - api_put :authorize, :id => payment.to_param - response.status.should == 422 - json_response["error"].should == "There was a problem with the payment gateway: Could not authorize card" - payment.reload - payment.state.should == "failed" - end - - it "can capture" do - api_put :capture, :id => payment.to_param - response.status.should == 200 - payment.reload - payment.state.should == "completed" - end - - it "returns a 422 status when purchasing fails" do - fake_response = stub(:success? => false, :to_s => "Insufficient funds") - Spree::Gateway::Bogus.any_instance.should_receive(:capture).and_return(fake_response) - api_put :capture, :id => payment.to_param - response.status.should == 422 - json_response["error"].should == "There was a problem with the payment gateway: Insufficient funds" - - payment.reload - payment.state.should == "failed" - end + context "when the state is pending" do + it "can update" do + payment.update_attributes(state: 'pending') + api_put(:update, id: payment.to_param, payment: { amount: 2.01 }) + expect(response.status).to be(200) + expect(payment.reload.amount).to eq(2.01) + end + end - it "can purchase" do - api_put :purchase, :id => payment.to_param - response.status.should == 200 - payment.reload - payment.state.should == "completed" + context "update fails" do + it "returns a 422 status when the amount is invalid" do + payment.update_attributes(state: 'pending') + api_put(:update, id: payment.to_param, payment: { amount: 'invalid' }) + expect(response.status).to be(422) + expect(json_response['error']).to eql('Invalid resource. Please fix errors and try again.') + end + + it "returns a 403 status when the payment is not pending" do + payment.update_attributes(state: 'completed') + api_put(:update, id: payment.to_param, payment: { amount: 2.01 }) + expect(response.status).to be(403) + expect(json_response['error']).to eql('This payment cannot be updated because it is completed.') + end + end end - it "returns a 422 status when purchasing fails" do - fake_response = stub(:success? => false, :to_s => "Insufficient funds") - Spree::Gateway::Bogus.any_instance.should_receive(:purchase).and_return(fake_response) - api_put :purchase, :id => payment.to_param - response.status.should == 422 - json_response["error"].should == "There was a problem with the payment gateway: Insufficient funds" - - payment.reload - payment.state.should == "failed" - end + context "authorizing" do + it "can authorize" do + api_put :authorize, :id => payment.to_param + expect(response.status).to eq(200) + expect(payment.reload.state).to eq("pending") + end - it "can void" do - api_put :void, :id => payment.to_param - response.status.should == 200 - payment.reload - payment.state.should == "void" + context "authorization fails" do + before do + fake_response = double(:success? => false, :to_s => "Could not authorize card") + expect_any_instance_of(Spree::Gateway::Bogus).to receive(:authorize).and_return(fake_response) + api_put :authorize, :id => payment.to_param + end + + it "returns a 422 status" do + expect(response.status).to eq(422) + expect(json_response["error"]).to eq "Invalid resource. Please fix errors and try again." + expect(json_response["errors"]["base"][0]).to eq "Could not authorize card" + end + + it "does not raise a stack level error" do + skip "Investigate why a payment.reload after the request raises 'stack level too deep'" + expect(payment.reload.state).to eq("failed") + end + end end - it "returns a 422 status when voiding fails" do - fake_response = stub(:success? => false, :to_s => "NO REFUNDS") - Spree::Gateway::Bogus.any_instance.should_receive(:void).and_return(fake_response) - api_put :void, :id => payment.to_param - response.status.should == 422 - json_response["error"].should == "There was a problem with the payment gateway: NO REFUNDS" + context "capturing" do + it "can capture" do + api_put :capture, :id => payment.to_param + expect(response.status).to eq(200) + expect(payment.reload.state).to eq("completed") + end - payment.reload - payment.state.should == "pending" + context "capturing fails" do + before do + fake_response = double(:success? => false, :to_s => "Insufficient funds") + expect_any_instance_of(Spree::Gateway::Bogus).to receive(:capture).and_return(fake_response) + end + + it "returns a 422 status" do + api_put :capture, :id => payment.to_param + expect(response.status).to eq(422) + expect(json_response["error"]).to eq "Invalid resource. Please fix errors and try again." + expect(json_response["errors"]["base"][0]).to eq "Insufficient funds" + end + end end - context "crediting" do - before do - payment.purchase! + context "purchasing" do + it "can purchase" do + api_put :purchase, :id => payment.to_param + expect(response.status).to eq(200) + expect(payment.reload.state).to eq("completed") end - it "can credit" do - api_put :credit, :id => payment.to_param - response.status.should == 200 - payment.reload - payment.state.should == "completed" - - # Ensur that a credit payment was created, and it has correct credit amount - credit_payment = Payment.where(:source_type => 'Spree::Payment', :source_id => payment.id).last - credit_payment.amount.to_f.should == -45.75 + context "purchasing fails" do + before do + fake_response = double(:success? => false, :to_s => "Insufficient funds") + expect_any_instance_of(Spree::Gateway::Bogus).to receive(:purchase).and_return(fake_response) + end + + it "returns a 422 status" do + api_put :purchase, :id => payment.to_param + expect(response.status).to eq(422) + expect(json_response["error"]).to eq "Invalid resource. Please fix errors and try again." + expect(json_response["errors"]["base"][0]).to eq "Insufficient funds" + end end + end - it "returns a 422 status when crediting fails" do - fake_response = stub(:success? => false, :to_s => "NO CREDIT FOR YOU") - Spree::Gateway::Bogus.any_instance.should_receive(:credit).and_return(fake_response) - api_put :credit, :id => payment.to_param - response.status.should == 422 - json_response["error"].should == "There was a problem with the payment gateway: NO CREDIT FOR YOU" + context "voiding" do + it "can void" do + api_put :void, id: payment.to_param + expect(response.status).to eq 200 + expect(payment.reload.state).to eq "void" end - it "cannot credit over credit_allowed limit" do - api_put :credit, :id => payment.to_param, :amount => 1000000 - response.status.should == 422 - json_response["error"].should == "This payment can only be credited up to 45.75. Please specify an amount less than or equal to this number." + context "voiding fails" do + before do + fake_response = double(success?: false, to_s: "NO REFUNDS") + expect_any_instance_of(Spree::Gateway::Bogus).to receive(:void).and_return(fake_response) + end + + it "returns a 422 status" do + api_put :void, id: payment.to_param + expect(response.status).to eq 422 + expect(json_response["error"]).to eq "Invalid resource. Please fix errors and try again." + expect(json_response["errors"]["base"][0]).to eq "NO REFUNDS" + expect(payment.reload.state).to eq "checkout" + end end end - end + end end - end end diff --git a/api/spec/controllers/spree/api/product_properties_controller_spec.rb b/api/spec/controllers/spree/api/product_properties_controller_spec.rb index 3da79c9968b..10c04ab79c6 100644 --- a/api/spec/controllers/spree/api/product_properties_controller_spec.rb +++ b/api/spec/controllers/spree/api/product_properties_controller_spec.rb @@ -2,12 +2,12 @@ require 'shared_examples/protect_product_actions' module Spree - describe Spree::Api::ProductPropertiesController do + describe Spree::Api::ProductPropertiesController, :type => :controller do render_views let!(:product) { create(:product) } - let!(:property_1) {product.product_properties.create(:property_name => "My Property 1", :value => "my value 1")} - let!(:property_2) {product.product_properties.create(:property_name => "My Property 2", :value => "my value 2")} + let!(:property_1) {product.product_properties.create(:property_name => "My Property 1", :value => "my value 1", :position => 0)} + let!(:property_2) {product.product_properties.create(:property_name => "My Property 2", :value => "my value 2", :position => 1)} let(:attributes) { [:id, :product_id, :property_id, :value, :property_name] } let(:resource_scoping) { { :product_id => product.to_param } } @@ -18,45 +18,45 @@ module Spree context "if product is deleted" do before do - product.update_column(:deleted_at, Time.now) + product.update_column(:deleted_at, 1.day.ago) end it "can not see a list of product properties" do api_get :index - response.status.should == 404 + expect(response.status).to eq(404) end end it "can see a list of all product properties" do api_get :index - json_response["product_properties"].count.should eq 2 - json_response["product_properties"].first.should have_attributes(attributes) + expect(json_response["product_properties"].count).to eq 2 + expect(json_response["product_properties"].first).to have_attributes(attributes) end it "can control the page size through a parameter" do api_get :index, :per_page => 1 - json_response['product_properties'].count.should == 1 - json_response['current_page'].should == 1 - json_response['pages'].should == 2 + expect(json_response['product_properties'].count).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) end - it 'can query the results through a paramter' do + it 'can query the results through a parameter' do Spree::ProductProperty.last.update_attribute(:value, 'loose') property = Spree::ProductProperty.last api_get :index, :q => { :value_cont => 'loose' } - json_response['count'].should == 1 - json_response['product_properties'].first['value'].should eq property.value + expect(json_response['count']).to eq(1) + expect(json_response['product_properties'].first['value']).to eq property.value end it "can see a single product_property" do api_get :show, :id => property_1.property_name - json_response.should have_attributes(attributes) + expect(json_response).to have_attributes(attributes) end it "can learn how to create a new product property" do api_get :new - json_response["attributes"].should == attributes.map(&:to_s) - json_response["required_attributes"].should be_empty + expect(json_response["attributes"]).to eq(attributes.map(&:to_s)) + expect(json_response["required_attributes"]).to be_empty end it "cannot create a new product property if not an admin" do @@ -72,7 +72,7 @@ module Spree it "cannot delete a product property" do api_delete :destroy, :property_name => property_1.property_name assert_unauthorized! - lambda { property_1.reload }.should_not raise_error + expect { property_1.reload }.not_to raise_error end context "as an admin" do @@ -82,19 +82,19 @@ module Spree expect do api_post :create, :product_property => { :property_name => "My Property 3", :value => "my value 3" } end.to change(product.product_properties, :count).by(1) - json_response.should have_attributes(attributes) - response.status.should == 201 + expect(json_response).to have_attributes(attributes) + expect(response.status).to eq(201) end it "can update a product property" do api_put :update, :id => property_1.property_name, :product_property => { :value => "my value 456" } - response.status.should == 200 + expect(response.status).to eq(200) end - it "can delete a variant" do + it "can delete a product property" do api_delete :destroy, :id => property_1.property_name - response.status.should == 204 - lambda { property_1.reload }.should raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(204) + expect { property_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -102,13 +102,13 @@ module Spree let(:resource_scoping) { { :product_id => product.id } } it "can see a list of all product properties" do api_get :index - json_response["product_properties"].count.should eq 2 - json_response["product_properties"].first.should have_attributes(attributes) + expect(json_response["product_properties"].count).to eq 2 + expect(json_response["product_properties"].first).to have_attributes(attributes) end it "can see a single product_property by id" do api_get :show, :id => property_1.id - json_response.should have_attributes(attributes) + expect(json_response).to have_attributes(attributes) end end diff --git a/api/spec/controllers/spree/api/products_controller_spec.rb b/api/spec/controllers/spree/api/products_controller_spec.rb index dcf92f1d412..98e4d182647 100644 --- a/api/spec/controllers/spree/api/products_controller_spec.rb +++ b/api/spec/controllers/spree/api/products_controller_spec.rb @@ -2,209 +2,421 @@ require 'shared_examples/protect_product_actions' module Spree - describe Spree::Api::ProductsController do + describe Spree::Api::ProductsController, :type => :controller do render_views let!(:product) { create(:product) } - let!(:inactive_product) { create(:product, :available_on => Time.now.tomorrow, :name => "inactive") } - let(:attributes) { [:id, :name, :description, :price, :available_on, :permalink, :count_on_hand, :meta_description, :meta_keywords, :taxon_ids] } + let!(:inactive_product) { create(:product, available_on: Time.now.tomorrow, name: "inactive") } + let(:base_attributes) { Api::ApiHelpers.product_attributes } + let(:show_attributes) { base_attributes.dup.push(:has_variants) } + let(:new_attributes) { base_attributes } + + let(:product_data) do + { name: "The Other Product", + price: 19.99, + shipping_category_id: create(:shipping_category).id } + end + let(:attributes_for_variant) do + h = attributes_for(:variant).except(:option_values, :product) + h.merge({ + options: [ + { name: "size", value: "small" }, + { name: "color", value: "black" } + ] + }) + end before do stub_authentication! end context "as a normal user" do + context "with caching enabled" do + let!(:product_2) { create(:product) } + + before do + ActionController::Base.perform_caching = true + end + + it "returns unique products" do + api_get :index + product_ids = json_response["products"].map { |p| p["id"] } + expect(product_ids.uniq.count).to eq(product_ids.count) + end + + after do + ActionController::Base.perform_caching = false + end + end + it "retrieves a list of products" do api_get :index - json_response["products"].first.should have_attributes(attributes) - json_response["count"].should == 1 - json_response["current_page"].should == 1 - json_response["pages"].should == 1 + expect(json_response["products"].first).to have_attributes(show_attributes) + expect(json_response["total_count"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) + expect(json_response["per_page"]).to eq(Kaminari.config.default_per_page) + end + + it "retrieves a list of products by id" do + api_get :index, :ids => [product.id] + expect(json_response["products"].first).to have_attributes(show_attributes) + expect(json_response["total_count"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) + expect(json_response["per_page"]).to eq(Kaminari.config.default_per_page) + end + + context "product has more than one price" do + before { product.master.prices.create currency: "EUR", amount: 22 } + + it "returns distinct products only" do + api_get :index + expect(assigns(:products).map(&:id).uniq).to eq assigns(:products).map(&:id) + end + end + + it "retrieves a list of products by ids string" do + second_product = create(:product) + api_get :index, :ids => [product.id, second_product.id].join(",") + expect(json_response["products"].first).to have_attributes(show_attributes) + expect(json_response["products"][1]).to have_attributes(show_attributes) + expect(json_response["total_count"]).to eq(2) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) + expect(json_response["per_page"]).to eq(Kaminari.config.default_per_page) + end + + it "does not return inactive products when queried by ids" do + api_get :index, :ids => [inactive_product.id] + expect(json_response["count"]).to eq(0) end it "does not list unavailable products" do api_get :index - json_response["products"].first["name"].should_not eq("inactive") + expect(json_response["products"].first["name"]).not_to eq("inactive") end context "pagination" do - default_per_page(1) - it "can select the next page of products" do second_product = create(:product) - api_get :index, :page => 2 - json_response["products"].first.should have_attributes(attributes) - json_response["total_count"].should == 2 - json_response["current_page"].should == 2 - json_response["pages"].should == 2 + api_get :index, :page => 2, :per_page => 1 + expect(json_response["products"].first).to have_attributes(show_attributes) + expect(json_response["total_count"]).to eq(2) + expect(json_response["current_page"]).to eq(2) + expect(json_response["pages"]).to eq(2) end it 'can control the page size through a parameter' do create(:product) api_get :index, :per_page => 1 - json_response['count'].should == 1 - json_response['total_count'].should == 2 - json_response['current_page'].should == 1 - json_response['pages'].should == 2 - end - end - - context "jsonp" do - it "retrieves a list of products of jsonp" do - api_get :index, {:callback => 'callback'} - response.body.should =~ /^callback\(.*\)$/ - response.header['Content-Type'].should include('application/javascript') + expect(json_response['count']).to eq(1) + expect(json_response['total_count']).to eq(2) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) end end it "can search for products" do create(:product, :name => "The best product in the world") api_get :index, :q => { :name_cont => "best" } - json_response["products"].first.should have_attributes(attributes) - json_response["count"].should == 1 + expect(json_response["products"].first).to have_attributes(show_attributes) + expect(json_response["count"]).to eq(1) end it "gets a single product" do product.master.images.create!(:attachment => image("thinking-cat.jpg")) + product.variants.create! + product.variants.first.images.create!(:attachment => image("thinking-cat.jpg")) product.set_property("spree", "rocks") - api_get :show, :id => product.to_param - json_response.should have_attributes(attributes) - json_response['variants'].first.should have_attributes([:name, - :is_master, - :count_on_hand, - :price]) + product.taxons << create(:taxon) - json_response["images"].first.should have_attributes([:attachment_file_name, - :attachment_width, - :attachment_height, - :attachment_content_type]) + api_get :show, :id => product.to_param - json_response["product_properties"].first.should have_attributes([:value, + expect(json_response).to have_attributes(show_attributes) + expect(json_response['variants'].first).to have_attributes([:name, + :is_master, + :price, + :images, + :in_stock]) + + expect(json_response['variants'].first['images'].first).to have_attributes([:attachment_file_name, + :attachment_width, + :attachment_height, + :attachment_content_type, + :mini_url, + :small_url, + :product_url, + :large_url]) + + expect(json_response["product_properties"].first).to have_attributes([:value, :product_id, :property_name]) + + expect(json_response["classifications"].first).to have_attributes([:taxon_id, :position, :taxon]) + expect(json_response["classifications"].first['taxon']).to have_attributes([:id, :name, :pretty_name, :permalink, :taxonomy_id, :parent_id]) end + context "tracking is disabled" do + before { Config.track_inventory_levels = false } + + it "still displays valid json with total_on_hand Float::INFINITY" do + api_get :show, :id => product.to_param + expect(response).to be_ok + expect(json_response[:total_on_hand]).to eq nil + end + + after { Config.track_inventory_levels = true } + end - context "finds a product by permalink first then by id" do - let!(:other_product) { create(:product, :permalink => "these-are-not-the-droids-you-are-looking-for") } + context "finds a product by slug first then by id" do + let!(:other_product) { create(:product, :slug => "these-are-not-the-droids-you-are-looking-for") } before do - product.update_attribute(:permalink, "#{other_product.id}-and-1-ways") + product.update_column(:slug, "#{other_product.id}-and-1-ways") end specify do api_get :show, :id => product.to_param - json_response["permalink"].should =~ /and-1-ways/ + expect(json_response["slug"]).to match(/and-1-ways/) product.destroy api_get :show, :id => other_product.id - json_response["permalink"].should =~ /droids/ + expect(json_response["slug"]).to match(/droids/) end end it "cannot see inactive products" do api_get :show, :id => inactive_product.to_param - json_response["error"].should == "The resource you were looking for could not be found." - response.status.should == 404 + assert_not_found! end it "returns a 404 error when it cannot find a product" do api_get :show, :id => "non-existant" - json_response["error"].should == "The resource you were looking for could not be found." - response.status.should == 404 + assert_not_found! end it "can learn how to create a new product" do api_get :new - json_response["attributes"].should == attributes.map(&:to_s) + expect(json_response["attributes"]).to eq(new_attributes.map(&:to_s)) required_attributes = json_response["required_attributes"] - required_attributes.should include("name") - required_attributes.should include("price") + expect(required_attributes).to include("name") + expect(required_attributes).to include("price") + expect(required_attributes).to include("shipping_category_id") end it_behaves_like "modifying product actions are restricted" end context "as an admin" do + let(:taxon_1) { create(:taxon) } + let(:taxon_2) { create(:taxon) } + sign_in_as_admin! it "can see all products" do api_get :index - json_response["products"].count.should == 2 - json_response["count"].should == 2 - json_response["current_page"].should == 1 - json_response["pages"].should == 1 + expect(json_response["products"].count).to eq(2) + expect(json_response["count"]).to eq(2) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) end # Regression test for #1626 context "deleted products" do before do - create(:product, :deleted_at => Time.now) + create(:product, :deleted_at => 1.day.ago) end it "does not include deleted products" do api_get :index - json_response["products"].count.should == 2 + expect(json_response["products"].count).to eq(2) end it "can include deleted products" do api_get :index, :show_deleted => 1 - json_response["products"].count.should == 3 + expect(json_response["products"].count).to eq(3) end end - it "can create a new product" do - api_post :create, :product => { :name => "The Other Product", - :price => 19.99 } - json_response.should have_attributes(attributes) - response.status.should == 201 - end + describe "creating a product" do + it "can create a new product" do + api_post :create, :product => { :name => "The Other Product", + :price => 19.99, + :shipping_category_id => create(:shipping_category).id } + expect(json_response).to have_attributes(base_attributes) + expect(response.status).to eq(201) + end - # Regression test for #2140 - context "with authentication_required set to false" do - before do - Spree::Api::Config.requires_authentication = false + it "creates with embedded variants" do + product_data.merge!({ + variants: [attributes_for_variant, attributes_for_variant] + }) + + api_post :create, :product => product_data + expect(response.status).to eq 201 + + variants = json_response['variants'] + expect(variants.count).to eq(2) + expect(variants.last['option_values'][0]['name']).to eq('small') + expect(variants.last['option_values'][0]['option_type_name']).to eq('size') + + expect(json_response['option_types'].count).to eq(2) # size, color end - after do - Spree::Api::Config.requires_authentication = true + it "can create a new product with embedded product_properties" do + product_data.merge!({ + product_properties_attributes: [{ + property_name: "fabric", + value: "cotton" + }] + }) + + api_post :create, :product => product_data + + expect(json_response['product_properties'][0]['property_name']).to eq('fabric') + expect(json_response['product_properties'][0]['value']).to eq('cotton') end - it "can still create a product" do - api_post :create, :product => { :name => "The Other Product", - :price => 19.99 }, - :token => "fake" - json_response.should have_attributes(attributes) - response.status.should == 201 + it "can create a new product with option_types" do + product_data.merge!({ + option_types: ['size', 'color'] + }) + + api_post :create, :product => product_data + expect(json_response['option_types'].count).to eq(2) end - end - it "cannot create a new product with invalid attributes" do - api_post :create, :product => {} - response.status.should == 422 - json_response["error"].should == "Invalid resource. Please fix errors and try again." - errors = json_response["errors"] - errors.delete("permalink") # Don't care about this one. - errors.keys.should =~ ["name", "price"] - end + it "creates product with option_types ids" do + option_type = create(:option_type) + product_data.merge!(option_type_ids: [option_type.id]) + api_post :create, product: product_data + expect(json_response['option_types'].first['id']).to eq option_type.id + end + + it "creates with shipping categories" do + hash = { :name => "The Other Product", + :price => 19.99, + :shipping_category => "Free Ships" } + + api_post :create, :product => hash + expect(response.status).to eq 201 + + shipping_id = ShippingCategory.find_by_name("Free Ships").id + expect(json_response['shipping_category_id']).to eq shipping_id + end + + it "puts the created product in the given taxon" do + product_data[:taxon_ids] = taxon_1.id.to_s + api_post :create, :product => product_data + expect(json_response["taxon_ids"]).to eq([taxon_1.id,]) + end - it "can update a product" do - api_put :update, :id => product.to_param, :product => { :name => "New and Improved Product!" } - response.status.should == 200 + # Regression test for #4123 + it "puts the created product in the given taxons" do + product_data[:taxon_ids] = [taxon_1.id, taxon_2.id].join(',') + api_post :create, :product => product_data + expect(json_response["taxon_ids"]).to eq([taxon_1.id, taxon_2.id]) + end + + # Regression test for #2140 + context "with authentication_required set to false" do + before do + Spree::Api::Config.requires_authentication = false + end + + after do + Spree::Api::Config.requires_authentication = true + end + + it "can still create a product" do + api_post :create, :product => product_data, :token => "fake" + expect(json_response).to have_attributes(show_attributes) + expect(response.status).to eq(201) + end + end + + it "cannot create a new product with invalid attributes" do + api_post :create, product: {} + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") + errors = json_response["errors"] + errors.delete("slug") # Don't care about this one. + expect(errors.keys).to match_array(["name", "price", "shipping_category_id"]) + end end - it "cannot update a product with an invalid attribute" do - api_put :update, :id => product.to_param, :product => { :name => "" } - response.status.should == 422 - json_response["error"].should == "Invalid resource. Please fix errors and try again." - json_response["errors"]["name"].should == ["can't be blank"] + context 'updating a product' do + it "can update a product" do + api_put :update, :id => product.to_param, :product => { :name => "New and Improved Product!" } + expect(response.status).to eq(200) + end + + it "can create new option types on a product" do + api_put :update, :id => product.to_param, :product => { :option_types => ['shape', 'color'] } + expect(json_response['option_types'].count).to eq(2) + end + + it "can create new variants on a product" do + api_put :update, :id => product.to_param, :product => { :variants => [attributes_for_variant, attributes_for_variant.merge(sku: "ABC-#{Kernel.rand(9999)}")] } + expect(response.status).to eq 200 + expect(json_response['variants'].count).to eq(2) # 2 variants + + variants = json_response['variants'].select { |v| !v['is_master'] } + expect(variants.last['option_values'][0]['name']).to eq('small') + expect(variants.last['option_values'][0]['option_type_name']).to eq('size') + + expect(json_response['option_types'].count).to eq(2) # size, color + end + + it "can update an existing variant on a product" do + variant_hash = { + :sku => '123', :price => 19.99, :options => [{:name => "size", :value => "small"}] + } + variant_id = product.variants.create!({ product: product }.merge(variant_hash)).id + + api_put :update, :id => product.to_param, :product => { + :variants => [ + variant_hash.merge( + :id => variant_id.to_s, + :sku => '456', + :options => [{:name => "size", :value => "large" }] + ) + ] + } + + expect(json_response['variants'].count).to eq(1) + variants = json_response['variants'].select { |v| !v['is_master'] } + expect(variants.last['option_values'][0]['name']).to eq('large') + expect(variants.last['sku']).to eq('456') + expect(variants.count).to eq(1) + end + + it "cannot update a product with an invalid attribute" do + api_put :update, :id => product.to_param, :product => { :name => "" } + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") + expect(json_response["errors"]["name"]).to eq(["can't be blank"]) + end + + # Regression test for #4123 + it "puts the created product in the given taxon" do + api_put :update, :id => product.to_param, :product => {:taxon_ids => taxon_1.id.to_s} + expect(json_response["taxon_ids"]).to eq([taxon_1.id,]) + end + + # Regression test for #4123 + it "puts the created product in the given taxons" do + api_put :update, :id => product.to_param, :product => {:taxon_ids => [taxon_1.id, taxon_2.id].join(',')} + expect(json_response["taxon_ids"]).to eq([taxon_1.id, taxon_2.id]) + end end it "can delete a product" do - product.deleted_at.should be_nil + expect(product.deleted_at).to be_nil api_delete :destroy, :id => product.to_param - response.status.should == 204 - product.reload.deleted_at.should_not be_nil + expect(response.status).to eq(204) + expect(product.reload.deleted_at).not_to be_nil end end end diff --git a/api/spec/controllers/spree/api/promotion_application_spec.rb b/api/spec/controllers/spree/api/promotion_application_spec.rb new file mode 100644 index 00000000000..7fe269ba527 --- /dev/null +++ b/api/spec/controllers/spree/api/promotion_application_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +module Spree::Api + describe OrdersController, :type => :controller do + render_views + + before do + stub_authentication! + end + + context "with an available promotion" do + let!(:order) { create(:order_with_line_items, :line_items_count => 1) } + let!(:promotion) do + promotion = Spree::Promotion.create(name: "10% off", code: "10off") + calculator = Spree::Calculator::FlatPercentItemTotal.create(preferred_flat_percent: "10") + action = Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator) + promotion.actions << action + promotion + end + + it "can apply a coupon code to the order" do + expect(order.total).to eq(110.00) + api_put :apply_coupon_code, :id => order.to_param, :coupon_code => "10off", :order_token => order.guest_token + expect(response.status).to eq(200) + expect(order.reload.total).to eq(109.00) + expect(json_response["success"]).to eq("The coupon code was successfully applied to your order.") + expect(json_response["error"]).to be_blank + expect(json_response["successful"]).to be true + expect(json_response["status_code"]).to eq("coupon_code_applied") + end + + context "with an expired promotion" do + before do + promotion.starts_at = 2.weeks.ago + promotion.expires_at = 1.week.ago + promotion.save + end + + it "fails to apply" do + api_put :apply_coupon_code, :id => order.to_param, :coupon_code => "10off", :order_token => order.guest_token + expect(response.status).to eq(422) + expect(json_response["success"]).to be_blank + expect(json_response["error"]).to eq("The coupon code is expired") + expect(json_response["successful"]).to be false + expect(json_response["status_code"]).to eq("coupon_code_expired") + end + end + end + end +end diff --git a/api/spec/controllers/spree/api/promotions_controller_spec.rb b/api/spec/controllers/spree/api/promotions_controller_spec.rb new file mode 100644 index 00000000000..0423f44ebdd --- /dev/null +++ b/api/spec/controllers/spree/api/promotions_controller_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +module Spree + describe Spree::Api::PromotionsController, :type => :controller do + render_views + + shared_examples "a JSON response" do + it 'should be ok' do + expect(subject).to be_ok + end + + it 'should return JSON' do + payload = HashWithIndifferentAccess.new(JSON.parse(subject.body)) + expect(payload).to_not be_nil + Spree::Api::ApiHelpers.promotion_attributes.each do |attribute| + expect(payload.has_key?(attribute)).to be true + end + end + end + + before do + stub_authentication! + end + + let(:promotion) { create :promotion, code: '10off' } + + describe 'GET #show' do + subject { api_get :show, id: id } + + context 'when admin' do + sign_in_as_admin! + + context 'when finding by id' do + let(:id) { promotion.id } + + it_behaves_like "a JSON response" + end + + context 'when finding by code' do + let(:id) { promotion.code } + + it_behaves_like "a JSON response" + end + + context 'when id does not exist' do + let(:id) { 'argh' } + + it 'should be 404' do + expect(subject.status).to eq(404) + end + end + end + + context 'when non admin' do + let(:id) { promotion.id } + + it 'should be unauthorized' do + subject + assert_unauthorized! + end + end + end + end +end diff --git a/api/spec/controllers/spree/api/properties_controller_spec.rb b/api/spec/controllers/spree/api/properties_controller_spec.rb new file mode 100644 index 00000000000..04ad01ec0ce --- /dev/null +++ b/api/spec/controllers/spree/api/properties_controller_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' +module Spree + describe Spree::Api::PropertiesController, :type => :controller do + render_views + + let!(:property_1) { Property.create!(:name => "foo", :presentation => "Foo") } + let!(:property_2) { Property.create!(:name => "bar", :presentation => "Bar") } + + let(:attributes) { [:id, :name, :presentation] } + + before do + stub_authentication! + end + + it "can see a list of all properties" do + api_get :index + expect(json_response["properties"].count).to eq(2) + expect(json_response["properties"].first).to have_attributes(attributes) + end + + it "can control the page size through a parameter" do + api_get :index, :per_page => 1 + expect(json_response['properties'].count).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) + end + + it 'can query the results through a parameter' do + api_get :index, :q => { :name_cont => 'ba' } + expect(json_response['count']).to eq(1) + expect(json_response['properties'].first['presentation']).to eq property_2.presentation + end + + it "retrieves a list of properties by id" do + api_get :index, :ids => [property_1.id] + expect(json_response["properties"].first).to have_attributes(attributes) + expect(json_response["count"]).to eq(1) + end + + it "retrieves a list of properties by ids string" do + api_get :index, :ids => [property_1.id, property_2.id].join(",") + expect(json_response["properties"].first).to have_attributes(attributes) + expect(json_response["properties"][1]).to have_attributes(attributes) + expect(json_response["count"]).to eq(2) + end + + it "can see a single property" do + api_get :show, :id => property_1.id + expect(json_response).to have_attributes(attributes) + end + + it "can see a property by name" do + api_get :show, :id => property_1.name + expect(json_response).to have_attributes(attributes) + end + + it "can learn how to create a new property" do + api_get :new + expect(json_response["attributes"]).to eq(attributes.map(&:to_s)) + expect(json_response["required_attributes"]).to be_empty + end + + it "cannot create a new property if not an admin" do + api_post :create, :property => { :name => "My Property 3" } + assert_unauthorized! + end + + it "cannot update a property" do + api_put :update, :id => property_1.name, :property => { :presentation => "my value 456" } + assert_unauthorized! + end + + it "cannot delete a property" do + api_delete :destroy, :id => property_1.id + assert_unauthorized! + expect { property_1.reload }.not_to raise_error + end + + context "as an admin" do + sign_in_as_admin! + + it "can create a new property" do + expect(Spree::Property.count).to eq(2) + api_post :create, :property => { :name => "My Property 3", :presentation => "my value 3" } + expect(json_response).to have_attributes(attributes) + expect(response.status).to eq(201) + expect(Spree::Property.count).to eq(3) + end + + it "can update a property" do + api_put :update, :id => property_1.name, :property => { :presentation => "my value 456" } + expect(response.status).to eq(200) + end + + it "can delete a property" do + api_delete :destroy, :id => property_1.name + expect(response.status).to eq(204) + expect { property_1.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/api/spec/controllers/spree/api/return_authorizations_controller_spec.rb b/api/spec/controllers/spree/api/return_authorizations_controller_spec.rb index c74e0eb0cfc..bf425fe84e4 100644 --- a/api/spec/controllers/spree/api/return_authorizations_controller_spec.rb +++ b/api/spec/controllers/spree/api/return_authorizations_controller_spec.rb @@ -1,21 +1,13 @@ require 'spec_helper' module Spree - describe Api::ReturnAuthorizationsController do + describe Api::ReturnAuthorizationsController, :type => :controller do render_views - let!(:order) do - order = create(:order) - order.line_items << create(:line_item) - order.shipments << create(:shipment, :state => 'shipped') - order.finalize! - order.shipments.update_all(:state => 'shipped') - order.inventory_units.update_all(:state => 'shipped') - order - end + let!(:order) { create(:shipped_order) } let(:product) { create(:product) } - let(:attributes) { [:id, :reason, :amount, :state] } + let(:attributes) { [:id, :memo, :state] } let(:resource_scoping) { { :order_id => order.to_param } } before do @@ -24,7 +16,7 @@ module Spree context "as the order owner" do before do - Order.any_instance.stub :user => current_api_user + allow_any_instance_of(Order).to receive_messages :user => current_api_user end it "cannot see any return authorizations" do @@ -49,12 +41,12 @@ module Spree it "cannot update a return authorization" do api_put :update - assert_unauthorized! + assert_not_found! end it "cannot delete a return authorization" do api_delete :destroy - assert_unauthorized! + assert_not_found! end end @@ -65,82 +57,96 @@ module Spree FactoryGirl.create(:return_authorization, :order => order) return_authorization = order.return_authorizations.first api_get :show, :order_id => order.number, :id => return_authorization.id - response.status.should == 200 - json_response.should have_attributes(attributes) - json_response["state"].should_not be_blank + expect(response.status).to eq(200) + expect(json_response).to have_attributes(attributes) + expect(json_response["state"]).not_to be_blank end it "can get a list of return authorizations" do FactoryGirl.create(:return_authorization, :order => order) FactoryGirl.create(:return_authorization, :order => order) api_get :index, { :order_id => order.number } - response.status.should == 200 + expect(response.status).to eq(200) return_authorizations = json_response["return_authorizations"] - return_authorizations.first.should have_attributes(attributes) - return_authorizations.first.should_not == return_authorizations.last + expect(return_authorizations.first).to have_attributes(attributes) + expect(return_authorizations.first).not_to eq(return_authorizations.last) end it 'can control the page size through a parameter' do FactoryGirl.create(:return_authorization, :order => order) FactoryGirl.create(:return_authorization, :order => order) api_get :index, :order_id => order.number, :per_page => 1 - json_response['count'].should == 1 - json_response['current_page'].should == 1 - json_response['pages'].should == 2 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) end it 'can query the results through a paramter' do FactoryGirl.create(:return_authorization, :order => order) - expected_result = create(:return_authorization, :reason => 'damaged') + expected_result = create(:return_authorization, :memo => 'damaged') order.return_authorizations << expected_result - api_get :index, :q => { :reason_cont => 'damage' } - json_response['count'].should == 1 - json_response['return_authorizations'].first['reason'].should eq expected_result.reason + api_get :index, :q => { :memo_cont => 'damaged' } + expect(json_response['count']).to eq(1) + expect(json_response['return_authorizations'].first['memo']).to eq expected_result.memo end it "can learn how to create a new return authorization" do api_get :new - json_response["attributes"].should == ["id", "number", "state", "amount", "order_id", "reason", "created_at", "updated_at"] + expect(json_response["attributes"]).to eq(["id", "number", "state", "order_id", "memo", "created_at", "updated_at"]) required_attributes = json_response["required_attributes"] - required_attributes.should include("order") + expect(required_attributes).to include("order") end it "can update a return authorization on the order" do FactoryGirl.create(:return_authorization, :order => order) return_authorization = order.return_authorizations.first - api_put :update, :id => return_authorization.id, :return_authorization => { :amount => 19.99 } - response.status.should == 200 - json_response.should have_attributes(attributes) + api_put :update, :id => return_authorization.id, :return_authorization => { :memo => "ABC" } + expect(response.status).to eq(200) + expect(json_response).to have_attributes(attributes) + end + + it "can cancel a return authorization on the order" do + FactoryGirl.create(:new_return_authorization, :order => order) + return_authorization = order.return_authorizations.first + expect(return_authorization.state).to eq("authorized") + api_delete :cancel, :id => return_authorization.id + expect(response.status).to eq(200) + expect(return_authorization.reload.state).to eq("canceled") end it "can delete a return authorization on the order" do FactoryGirl.create(:return_authorization, :order => order) return_authorization = order.return_authorizations.first api_delete :destroy, :id => return_authorization.id - response.status.should == 204 - lambda { return_authorization.reload }.should raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(204) + expect { return_authorization.reload }.to raise_error(ActiveRecord::RecordNotFound) end it "can add a new return authorization to an existing order" do - api_post :create, :return_autorization => { :order_id => order.number, :amount => 14.22, :reason => "Defective" } - response.status.should == 201 - json_response.should have_attributes(attributes) - json_response["state"].should_not be_blank + stock_location = FactoryGirl.create(:stock_location) + reason = FactoryGirl.create(:return_authorization_reason) + rma_params = { :stock_location_id => stock_location.id, + :return_authorization_reason_id => reason.id, + :memo => "Defective" } + api_post :create, :order_id => order.number, :return_authorization => rma_params + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + expect(json_response["state"]).not_to be_blank end end context "as just another user" do it "cannot add a return authorization to the order" do - api_post :create, :return_autorization => { :order_id => order.number, :amount => 14.22, :reason => "Defective" } + api_post :create, :return_autorization => { :order_id => order.number, :memo => "Defective" } assert_unauthorized! end it "cannot update a return authorization on the order" do FactoryGirl.create(:return_authorization, :order => order) return_authorization = order.return_authorizations.first - api_put :update, :id => return_authorization.id, :return_authorization => { :amount => 19.99 } + api_put :update, :id => return_authorization.id, :return_authorization => { :memo => "ABC" } assert_unauthorized! - return_authorization.reload.amount.should_not == 19.99 + expect(return_authorization.reload.memo).not_to eq("ABC") end it "cannot delete a return authorization on the order" do @@ -148,7 +154,7 @@ module Spree return_authorization = order.return_authorizations.first api_delete :destroy, :id => return_authorization.id assert_unauthorized! - lambda { return_authorization.reload }.should_not raise_error(ActiveRecord::RecordNotFound) + expect { return_authorization.reload }.not_to raise_error end end end diff --git a/api/spec/controllers/spree/api/shipments_controller_spec.rb b/api/spec/controllers/spree/api/shipments_controller_spec.rb index bbd3bcaa39e..960bad709ee 100644 --- a/api/spec/controllers/spree/api/shipments_controller_spec.rb +++ b/api/spec/controllers/spree/api/shipments_controller_spec.rb @@ -1,59 +1,190 @@ require 'spec_helper' -describe Spree::Api::ShipmentsController do +describe Spree::Api::ShipmentsController, :type => :controller do render_views let!(:shipment) { create(:shipment) } - let!(:attributes) { [:id, :tracking, :number, :cost, :shipped_at] } + let!(:attributes) { [:id, :tracking, :number, :cost, :shipped_at, :stock_location_name, :order_id, :shipping_rates, :shipping_methods] } before do stub_authentication! end - let!(:resource_scoping) { { :order_id => shipment.order.to_param, :id => shipment.to_param } } + let!(:resource_scoping) { { id: shipment.to_param, shipment: { order_id: shipment.order.to_param } } } context "as a non-admin" do it "cannot make a shipment ready" do api_put :ready - assert_unauthorized! + assert_not_found! end it "cannot make a shipment shipped" do api_put :ship - assert_unauthorized! + assert_not_found! end end context "as an admin" do + let!(:order) { shipment.order } + let!(:stock_location) { create(:stock_location_with_items) } + let!(:variant) { create(:variant) } + sign_in_as_admin! + # Start writing this spec a bit differently than before.... + describe 'POST #create' do + let(:params) do + { + variant_id: stock_location.stock_items.first.variant.to_param, + shipment: { order_id: order.number }, + stock_location_id: stock_location.to_param + } + end + + subject do + api_post :create, params + end + + [:variant_id, :stock_location_id].each do |field| + context "when #{field} is missing" do + before do + params.delete(field) + end + + it 'should return proper error' do + subject + expect(response.status).to eq(422) + expect(json_response['exception']).to eq("param is missing or the value is empty: #{field.to_s}") + end + end + end + + it 'should create a new shipment' do + expect(subject).to be_ok + expect(json_response).to have_attributes(attributes) + end + end + + it 'can update a shipment' do + params = { + shipment: { + stock_location_id: stock_location.to_param + } + } + + api_put :update, params + expect(response.status).to eq(200) + expect(json_response['stock_location_name']).to eq(stock_location.name) + end + it "can make a shipment ready" do - Spree::Order.any_instance.stub(:paid? => true, :complete? => true) + allow_any_instance_of(Spree::Order).to receive_messages(:paid? => true, :complete? => true) api_put :ready - json_response.should have_attributes(attributes) - json_response["state"].should == "ready" - shipment.reload.state.should == "ready" + expect(json_response).to have_attributes(attributes) + expect(json_response["state"]).to eq("ready") + expect(shipment.reload.state).to eq("ready") end it "cannot make a shipment ready if the order is unpaid" do - Spree::Order.any_instance.stub(:paid? => false) + allow_any_instance_of(Spree::Order).to receive_messages(:paid? => false) api_put :ready - json_response["error"].should == "Cannot ready shipment." - response.status.should == 422 + expect(json_response["error"]).to eq("Cannot ready shipment.") + expect(response.status).to eq(422) + end + + context 'for completed shipments' do + let(:order) { create :completed_order_with_totals } + let!(:resource_scoping) { { id: order.shipments.first.to_param, shipment: { order_id: order.to_param } } } + + it 'adds a variant to a shipment' do + api_put :add, { variant_id: variant.to_param, quantity: 2 } + expect(response.status).to eq(200) + expect(json_response['manifest'].detect { |h| h['variant']['id'] == variant.id }["quantity"]).to eq(2) + end + + it 'removes a variant from a shipment' do + order.contents.add(variant, 2) + + api_put :remove, { variant_id: variant.to_param, quantity: 1 } + expect(response.status).to eq(200) + expect(json_response['manifest'].detect { |h| h['variant']['id'] == variant.id }["quantity"]).to eq(1) + end + + it 'removes a destroyed variant from a shipment' do + order.contents.add(variant, 2) + variant.destroy + + api_put :remove, { variant_id: variant.to_param, quantity: 1 } + expect(response.status).to eq(200) + expect(json_response['manifest'].detect { |h| h['variant']['id'] == variant.id }["quantity"]).to eq(1) + end end context "can transition a shipment from ready to ship" do before do - Spree::Order.any_instance.stub(:paid? => true, :complete? => true) + allow_any_instance_of(Spree::Order).to receive_messages(:paid? => true, :complete? => true) + # For the shipment notification email + Spree::Config[:mails_from] = "spree@example.com" + shipment.update!(shipment.order) - shipment.state.should == "ready" + expect(shipment.state).to eq("ready") + allow_any_instance_of(Spree::ShippingRate).to receive_messages(:cost => 5) end it "can transition a shipment from ready to ship" do shipment.reload - api_put :ship, :order_id => shipment.order.to_param, :id => shipment.to_param, :shipment => { :tracking => "123123" } - json_response.should have_attributes(attributes) - json_response["state"].should == "shipped" + api_put :ship, id: shipment.to_param, shipment: { tracking: "123123", order_id: shipment.order.to_param } + expect(json_response).to have_attributes(attributes) + expect(json_response["state"]).to eq("shipped") end + end + + describe '#mine' do + subject do + api_get :mine, format: 'json', params: params + end + + let(:params) { {} } + + before { subject } + + context "the current api user is authenticated and has orders" do + let(:current_api_user) { shipped_order.user } + let(:shipped_order) { create(:shipped_order) } + + it 'succeeds' do + expect(response.status).to eq 200 + end + + describe 'json output' do + render_views + + let(:rendered_shipment_ids) { json_response['shipments'].map { |s| s['id'] } } + + it 'contains the shipments' do + expect(rendered_shipment_ids).to match_array current_api_user.orders.flat_map(&:shipments).map(&:id) + end + end + + context 'with filtering' do + let(:params) { {q: {order_completed_at_not_null: 1}} } + + let!(:incomplete_order) { create(:order, user: current_api_user) } + + it 'filters' do + expect(assigns(:shipments).map(&:id)).to match_array current_api_user.orders.complete.flat_map(&:shipments).map(&:id) + end + end + end + + context "the current api user is not persisted" do + let(:current_api_user) { Spree.user_class.new } + + it "returns a 401" do + expect(response.status).to eq(401) + end + end + end + end end diff --git a/api/spec/controllers/spree/api/states_controller_spec.rb b/api/spec/controllers/spree/api/states_controller_spec.rb new file mode 100644 index 00000000000..b05dc41580e --- /dev/null +++ b/api/spec/controllers/spree/api/states_controller_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +module Spree + describe Api::StatesController, :type => :controller do + render_views + + let!(:state) { create(:state, :name => "Victoria") } + let(:attributes) { [:id, :name, :abbr, :country_id] } + + before do + stub_authentication! + end + + it "gets all states" do + api_get :index + expect(json_response["states"].first).to have_attributes(attributes) + expect(json_response['states'].first['name']).to eq(state.name) + end + + it "gets all the states for a particular country" do + api_get :index, :country_id => state.country.id + expect(json_response["states"].first).to have_attributes(attributes) + expect(json_response['states'].first['name']).to eq(state.name) + end + + context "pagination" do + before do + expect(State).to receive(:accessible_by).and_return(@scope = double) + allow(@scope).to receive_message_chain(:ransack, :result, :includes, :order).and_return(@scope) + end + + it "does not paginate states results when asked not to do so" do + expect(@scope).not_to receive(:page) + expect(@scope).not_to receive(:per) + api_get :index + end + + it "paginates when page parameter is passed through" do + expect(@scope).to receive(:page).with(1).and_return(@scope) + expect(@scope).to receive(:per).with(nil) + api_get :index, :page => 1 + end + + it "paginates when per_page parameter is passed through" do + expect(@scope).to receive(:page).with(nil).and_return(@scope) + expect(@scope).to receive(:per).with(25) + api_get :index, :per_page => 25 + end + end + + + context "with two states" do + before { create(:state, :name => "New South Wales") } + + it "gets all states for a country" do + country = create(:country, :states_required => true) + state.country = country + state.save + + api_get :index, :country_id => country.id + expect(json_response["states"].first).to have_attributes(attributes) + expect(json_response["states"].count).to eq(1) + json_response["states_required"] = true + end + + it "can view all states" do + api_get :index + expect(json_response["states"].first).to have_attributes(attributes) + end + + it 'can query the results through a paramter' do + api_get :index, :q => { :name_cont => 'Vic' } + expect(json_response['states'].first['name']).to eq("Victoria") + end + end + + it "can view a state" do + api_get :show, :id => state.id + expect(json_response).to have_attributes(attributes) + end + end +end diff --git a/api/spec/controllers/spree/api/stock_items_controller_spec.rb b/api/spec/controllers/spree/api/stock_items_controller_spec.rb new file mode 100644 index 00000000000..cb027a6ca3c --- /dev/null +++ b/api/spec/controllers/spree/api/stock_items_controller_spec.rb @@ -0,0 +1,141 @@ +require 'spec_helper' + +module Spree + describe Api::StockItemsController, :type => :controller do + render_views + + let!(:stock_location) { create(:stock_location_with_items) } + let!(:stock_item) { stock_location.stock_items.order(:id).first } + let!(:attributes) { [:id, :count_on_hand, :backorderable, + :stock_location_id, :variant_id] } + + before do + stub_authentication! + end + + context "as a normal user" do + it "cannot list stock items for a stock location" do + api_get :index, stock_location_id: stock_location.to_param + expect(response.status).to eq(404) + end + + it "cannot see a stock item" do + api_get :show, stock_location_id: stock_location.to_param, id: stock_item.to_param + expect(response.status).to eq(404) + end + + it "cannot create a stock item" do + variant = create(:variant) + params = { + stock_location_id: stock_location.to_param, + stock_item: { + variant_id: variant.id, + count_on_hand: '20' + } + } + + api_post :create, params + expect(response.status).to eq(404) + end + + it "cannot update a stock item" do + api_put :update, stock_location_id: stock_location.to_param, stock_item_id: stock_item.to_param + expect(response.status).to eq(404) + end + + it "cannot destroy a stock item" do + api_delete :destroy, stock_location_id: stock_location.to_param, stock_item_id: stock_item.to_param + expect(response.status).to eq(404) + end + end + + context "as an admin" do + sign_in_as_admin! + + it 'cannot list of stock items' do + api_get :index, stock_location_id: stock_location.to_param + expect(json_response['stock_items'].first).to have_attributes(attributes) + expect(json_response['stock_items'].first['variant']['sku']).to include 'SKU' + end + + it 'requires a stock_location_id to be passed as a parameter' do + api_get :index + expect(json_response['error']).to match(/stock_location_id parameter must be provided/) + expect(response.status).to eq(422) + end + + it 'can control the page size through a parameter' do + api_get :index, stock_location_id: stock_location.to_param, per_page: 1 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + end + + it 'can query the results through a paramter' do + stock_item.update_column(:count_on_hand, 30) + api_get :index, stock_location_id: stock_location.to_param, q: { count_on_hand_eq: '30' } + expect(json_response['count']).to eq(1) + expect(json_response['stock_items'].first['count_on_hand']).to eq 30 + end + + it 'gets a stock item' do + api_get :show, stock_location_id: stock_location.to_param, id: stock_item.to_param + expect(json_response).to have_attributes(attributes) + expect(json_response['count_on_hand']).to eq stock_item.count_on_hand + end + + it 'can create a new stock item' do + variant = create(:variant) + # Creating a variant also creates stock items. + # We don't want any to exist (as they would conflict with what we're about to create) + StockItem.delete_all + params = { + stock_location_id: stock_location.to_param, + stock_item: { + variant_id: variant.id, + count_on_hand: '20' + } + } + + api_post :create, params + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + end + + it 'can update a stock item to add new inventory' do + expect(stock_item.count_on_hand).to eq(10) + params = { + id: stock_item.to_param, + stock_item: { + count_on_hand: 40, + } + } + + api_put :update, params + expect(response.status).to eq(200) + expect(json_response['count_on_hand']).to eq 50 + end + + it 'can set a stock item to modify the current inventory' do + expect(stock_item.count_on_hand).to eq(10) + + params = { + id: stock_item.to_param, + stock_item: { + count_on_hand: 40, + force: true, + } + } + + api_put :update, params + expect(response.status).to eq(200) + expect(json_response['count_on_hand']).to eq 40 + end + + it 'can delete a stock item' do + api_delete :destroy, id: stock_item.to_param + expect(response.status).to eq(204) + expect { Spree::StockItem.find(stock_item.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/api/spec/controllers/spree/api/stock_locations_controller_spec.rb b/api/spec/controllers/spree/api/stock_locations_controller_spec.rb new file mode 100644 index 00000000000..92ba1245413 --- /dev/null +++ b/api/spec/controllers/spree/api/stock_locations_controller_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +module Spree + describe Api::StockLocationsController, :type => :controller do + render_views + + let!(:stock_location) { create(:stock_location) } + let!(:attributes) { [:id, :name, :address1, :address2, :city, :state_id, :state_name, :country_id, :zipcode, :phone, :active] } + + before do + stub_authentication! + end + + context "as a user" do + it "cannot see stock locations" do + api_get :index + expect(response.status).to eq(401) + end + + it "cannot see a single stock location" do + api_get :show, :id => stock_location.id + expect(response.status).to eq(404) + end + + it "cannot create a new stock location" do + params = { + stock_location: { + name: "North Pole", + active: true + } + } + + api_post :create, params + expect(response.status).to eq(401) + end + + it "cannot update a stock location" do + api_put :update, :stock_location => { :name => "South Pole" }, :id => stock_location.to_param + expect(response.status).to eq(404) + end + + it "cannot delete a stock location" do + api_put :destroy, :id => stock_location.to_param + expect(response.status).to eq(404) + end + end + + + context "as an admin" do + sign_in_as_admin! + + it "gets list of stock locations" do + api_get :index + expect(json_response['stock_locations'].first).to have_attributes(attributes) + expect(json_response['stock_locations'].first['country']).not_to be_nil + expect(json_response['stock_locations'].first['state']).not_to be_nil + end + + it 'can control the page size through a parameter' do + create(:stock_location) + api_get :index, per_page: 1 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) + end + + it 'can query the results through a paramter' do + expected_result = create(:stock_location, name: 'South America') + api_get :index, q: { name_cont: 'south' } + expect(json_response['count']).to eq(1) + expect(json_response['stock_locations'].first['name']).to eq expected_result.name + end + + it "gets a stock location" do + api_get :show, id: stock_location.to_param + expect(json_response).to have_attributes(attributes) + expect(json_response['name']).to eq stock_location.name + end + + it "can create a new stock location" do + params = { + stock_location: { + name: "North Pole", + active: true + } + } + + api_post :create, params + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + end + + it "can update a stock location" do + params = { + id: stock_location.to_param, + stock_location: { + name: "South Pole" + } + } + + api_put :update, params + expect(response.status).to eq(200) + expect(json_response['name']).to eq 'South Pole' + end + + it "can delete a stock location" do + api_delete :destroy, id: stock_location.to_param + expect(response.status).to eq(204) + expect { stock_location.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/api/spec/controllers/spree/api/stock_movements_controller_spec.rb b/api/spec/controllers/spree/api/stock_movements_controller_spec.rb new file mode 100644 index 00000000000..a03b8727cbd --- /dev/null +++ b/api/spec/controllers/spree/api/stock_movements_controller_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +module Spree + describe Api::StockMovementsController, :type => :controller do + render_views + + let!(:stock_location) { create(:stock_location_with_items) } + let!(:stock_item) { stock_location.stock_items.order(:id).first } + let!(:stock_movement) { create(:stock_movement, stock_item: stock_item) } + let!(:attributes) { [:id, :quantity, :stock_item_id] } + + before do + stub_authentication! + end + + context 'as a user' do + it 'cannot see a list of stock movements' do + api_get :index, stock_location_id: stock_location.to_param + expect(response.status).to eq(404) + end + + it 'cannot see a stock movement' do + api_get :show, stock_location_id: stock_location.to_param, id: stock_movement.id + expect(response.status).to eq(404) + end + + it 'cannot create a stock movement' do + params = { + stock_location_id: stock_location.to_param, + stock_movement: { + stock_item_id: stock_item.to_param + } + } + + api_post :create, params + expect(response.status).to eq(404) + end + end + + context 'as an admin' do + sign_in_as_admin! + + it 'gets list of stock movements' do + api_get :index, stock_location_id: stock_location.to_param + expect(json_response['stock_movements'].first).to have_attributes(attributes) + expect(json_response['stock_movements'].first['stock_item']['count_on_hand']).to eq 11 + end + + it 'requires a stock_location_id to be passed as a parameter' do + api_get :index + expect(json_response['error']).to match(/stock_location_id parameter must be provided/) + expect(response.status).to eq(422) + end + + it 'can control the page size through a parameter' do + create(:stock_movement, stock_item: stock_item) + api_get :index, stock_location_id: stock_location.to_param, per_page: 1 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) + end + + it 'can query the results through a paramter' do + expected_result = create(:stock_movement, :received, quantity: 10, stock_item: stock_item) + api_get :index, stock_location_id: stock_location.to_param, q: { quantity_eq: '10' } + expect(json_response['count']).to eq(1) + end + + it 'gets a stock movement' do + api_get :show, stock_location_id: stock_location.to_param, id: stock_movement.to_param + expect(json_response).to have_attributes(attributes) + expect(json_response['stock_item_id']).to eq stock_movement.stock_item_id + end + + it 'can create a new stock movement' do + params = { + stock_location_id: stock_location.to_param, + stock_movement: { + stock_item_id: stock_item.to_param + } + } + + api_post :create, params + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + end + end + end +end + diff --git a/api/spec/controllers/spree/api/stores_controller_spec.rb b/api/spec/controllers/spree/api/stores_controller_spec.rb new file mode 100644 index 00000000000..05d5db02c37 --- /dev/null +++ b/api/spec/controllers/spree/api/stores_controller_spec.rb @@ -0,0 +1,133 @@ +require "spec_helper" + +module Spree + describe Api::StoresController, type: :controller do + render_views + + let!(:store) do + create(:store, name: "My Spree Store", url: "spreestore.example.com") + end + + before do + stub_authentication! + end + + context "as an admin" do + sign_in_as_admin! + + let!(:non_default_store) do + create(:store, + name: "Extra Store", + url: "spreestore-5.example.com", + default: false + ) + end + + it "I can list the available stores" do + api_get :index + expect(json_response["stores"]).to eq([ + { + "id" => store.id, + "name" => "My Spree Store", + "url" => "spreestore.example.com", + "meta_description" => nil, + "meta_keywords" => nil, + "seo_title" => nil, + "mail_from_address" => "spree@example.org", + "default_currency" => nil, + "code" => store.code, + "default" => true + }, + { + "id" => non_default_store.id, + "name" => "Extra Store", + "url" => "spreestore-5.example.com", + "meta_description" => nil, + "meta_keywords" => nil, + "seo_title" => nil, + "mail_from_address" => "spree@example.org", + "default_currency" => nil, + "code" => non_default_store.code, + "default" => false + } + ]) + end + + it "I can get the store details" do + api_get :show, id: store.id + expect(json_response).to eq( + "id" => store.id, + "name" => "My Spree Store", + "url" => "spreestore.example.com", + "meta_description" => nil, + "meta_keywords" => nil, + "seo_title" => nil, + "mail_from_address" => "spree@example.org", + "default_currency" => nil, + "code" => store.code, + "default" => true + ) + end + + it "I can create a new store" do + store_hash = { + code: "spree123", + name: "Hack0rz", + url: "spree123.example.com", + mail_from_address: "me@example.com" + } + api_post :create, store: store_hash + expect(response.status).to eq(201) + end + + it "I can update an existing store" do + store_hash = { + url: "spree123.example.com", + mail_from_address: "me@example.com" + } + api_put :update, id: store.id, store: store_hash + expect(response.status).to eq(200) + expect(store.reload.url).to eql "spree123.example.com" + expect(store.reload.mail_from_address).to eql "me@example.com" + end + + context "deleting a store" do + it "will fail if it's the default Store" do + api_delete :destroy, id: store.id + expect(response.status).to eq(422) + expect(json_response["errors"]["base"]).to eql( + ["Cannot destroy the default Store."] + ) + end + + it "will destroy the store" do + api_delete :destroy, id: non_default_store.id + expect(response.status).to eq(204) + end + end + end + + context "as an user" do + + it "I cannot list all the stores" do + api_get :index + expect(response.status).to eq(401) + end + + it "I cannot get the store details" do + api_get :show, id: store.id + expect(response.status).to eq(401) + end + + it "I cannot create a new store" do + api_post :create, store: {} + expect(response.status).to eq(401) + end + + it "I cannot update an existing store" do + api_put :update, id: store.id, store: {} + expect(response.status).to eq(401) + end + end + end +end diff --git a/api/spec/controllers/spree/api/taxonomies_controller_spec.rb b/api/spec/controllers/spree/api/taxonomies_controller_spec.rb index 77795e091e7..19f75cf1ebf 100644 --- a/api/spec/controllers/spree/api/taxonomies_controller_spec.rb +++ b/api/spec/controllers/spree/api/taxonomies_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Spree - describe Api::TaxonomiesController do + describe Api::TaxonomiesController, :type => :controller do render_views let(:taxonomy) { create(:taxonomy) } @@ -20,50 +20,57 @@ module Spree it "gets all taxonomies" do api_get :index - json_response["taxonomies"].first['name'].should eq taxonomy.name - json_response["taxonomies"].first['root']['taxons'].count.should eq 1 + expect(json_response["taxonomies"].first['name']).to eq taxonomy.name + expect(json_response["taxonomies"].first['root']['taxons'].count).to eq 1 end it 'can control the page size through a parameter' do create(:taxonomy) api_get :index, :per_page => 1 - json_response['count'].should == 1 - json_response['current_page'].should == 1 - json_response['pages'].should == 2 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) end it 'can query the results through a paramter' do expected_result = create(:taxonomy, :name => 'Style') api_get :index, :q => { :name_cont => 'style' } - json_response['count'].should == 1 - json_response['taxonomies'].first['name'].should eq expected_result.name + expect(json_response['count']).to eq(1) + expect(json_response['taxonomies'].first['name']).to eq expected_result.name end it "gets a single taxonomy" do api_get :show, :id => taxonomy.id - json_response['name'].should eq taxonomy.name + expect(json_response['name']).to eq taxonomy.name children = json_response['root']['taxons'] - children.count.should eq 1 - children.first['name'].should eq taxon.name - children.first.key?('taxons').should be_false + expect(children.count).to eq 1 + expect(children.first['name']).to eq taxon.name + expect(children.first.key?('taxons')).to be false end it "gets a single taxonomy with set=nested" do api_get :show, :id => taxonomy.id, :set => 'nested' - json_response['name'].should eq taxonomy.name + expect(json_response['name']).to eq taxonomy.name children = json_response['root']['taxons'] - children.first.key?('taxons').should be_true + expect(children.first.key?('taxons')).to be true + end + + it "gets the jstree-friendly version of a taxonomy" do + api_get :jstree, :id => taxonomy.id + expect(json_response["data"]).to eq(taxonomy.root.name) + expect(json_response["attr"]).to eq({ "id" => taxonomy.root.id, "name" => taxonomy.root.name}) + expect(json_response["state"]).to eq("closed") end it "can learn how to create a new taxonomy" do api_get :new - json_response["attributes"].should == attributes.map(&:to_s) + expect(json_response["attributes"]).to eq(attributes.map(&:to_s)) required_attributes = json_response["required_attributes"] - required_attributes.should include("name") + expect(required_attributes).to include("name") end it "cannot create a new taxonomy if not an admin" do @@ -87,20 +94,20 @@ module Spree it "can create" do api_post :create, :taxonomy => { :name => "Colors"} - json_response.should have_attributes(attributes) - response.status.should == 201 + expect(json_response).to have_attributes(attributes) + expect(response.status).to eq(201) end it "cannot create a new taxonomy with invalid attributes" do api_post :create, :taxonomy => {} - response.status.should == 422 - json_response["error"].should == "Invalid resource. Please fix errors and try again." + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") errors = json_response["errors"] end it "can destroy" do api_delete :destroy, :id => taxonomy.id - response.status.should == 204 + expect(response.status).to eq(204) end end end diff --git a/api/spec/controllers/spree/api/taxons_controller_spec.rb b/api/spec/controllers/spree/api/taxons_controller_spec.rb index e471aee9f65..7b2a498ee0f 100644 --- a/api/spec/controllers/spree/api/taxons_controller_spec.rb +++ b/api/spec/controllers/spree/api/taxons_controller_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' module Spree - describe Api::TaxonsController do + describe Api::TaxonsController, :type => :controller do render_views let(:taxonomy) { create(:taxonomy) } let(:taxon) { create(:taxon, :name => "Ruby", :taxonomy => taxonomy) } let(:taxon2) { create(:taxon, :name => "Rails", :taxonomy => taxonomy) } - let(:attributes) { ["id", "name", "permalink", "position", "parent_id", "taxonomy_id"] } + let(:attributes) { ["id", "name", "pretty_name", "permalink", "parent_id", "taxonomy_id"] } before do stub_authentication! @@ -17,28 +17,94 @@ module Spree end context "as a normal user" do - it "gets all taxons" do + it "gets all taxons for a taxonomy" do api_get :index, :taxonomy_id => taxonomy.id - json_response.first['name'].should eq taxon.name - children = json_response.first['taxons'] - children.count.should eq 1 - children.first['name'].should eq taxon2.name - children.first['taxons'].count.should eq 1 + expect(json_response['taxons'].first['name']).to eq taxon.name + children = json_response['taxons'].first['taxons'] + expect(children.count).to eq 1 + expect(children.first['name']).to eq taxon2.name + expect(children.first['taxons'].count).to eq 1 + end + + # Regression test for #4112 + it "does not include children when asked not to" do + api_get :index, :taxonomy_id => taxonomy.id, :without_children => 1 + + expect(json_response['taxons'].first['name']).to eq(taxon.name) + expect(json_response['taxons'].first['taxons']).to be_nil + end + + it "paginates through taxons" do + new_taxon = create(:taxon, :name => "Go", :taxonomy => taxonomy) + taxonomy.root.children << new_taxon + expect(taxonomy.root.children.count).to eql(2) + api_get :index, :taxonomy_id => taxonomy.id, :page => 1, :per_page => 1 + expect(json_response["count"]).to eql(1) + expect(json_response["total_count"]).to eql(2) + expect(json_response["current_page"]).to eql(1) + expect(json_response["per_page"]).to eql(1) + expect(json_response["pages"]).to eql(2) + end + + describe 'searching' do + context 'with a name' do + before do + api_get :index, :q => { :name_cont => name } + end + + context 'with one result' do + let(:name) { "Ruby" } + + it "returns an array including the matching taxon" do + expect(json_response['taxons'].count).to eq(1) + expect(json_response['taxons'].first['name']).to eq "Ruby" + end + end + + context 'with no results' do + let(:name) { "Imaginary" } + + it 'returns an empty array of taxons' do + expect(json_response.keys).to include('taxons') + expect(json_response['taxons'].count).to eq(0) + end + end + end + + context 'with no filters' do + it "gets all taxons" do + api_get :index + + expect(json_response['taxons'].first['name']).to eq taxonomy.root.name + children = json_response['taxons'].first['taxons'] + expect(children.count).to eq 1 + expect(children.first['name']).to eq taxon.name + expect(children.first['taxons'].count).to eq 1 + end + end end it "gets a single taxon" do api_get :show, :id => taxon.id, :taxonomy_id => taxonomy.id - json_response['name'].should eq taxon.name - json_response['taxons'].count.should eq 1 + expect(json_response['name']).to eq taxon.name + expect(json_response['taxons'].count).to eq 1 + end + + it "gets all taxons in JSTree form" do + api_get :jstree, :taxonomy_id => taxonomy.id, :id => taxon.id + response = json_response.first + expect(response["data"]).to eq(taxon2.name) + expect(response["attr"]).to eq({ "name" => taxon2.name, "id" => taxon2.id}) + expect(response["state"]).to eq("closed") end it "can learn how to create a new taxon" do api_get :new, :taxonomy_id => taxonomy.id - json_response["attributes"].should == attributes.map(&:to_s) + expect(json_response["attributes"]).to eq(attributes.map(&:to_s)) required_attributes = json_response["required_attributes"] - required_attributes.should include("name") + expect(required_attributes).to include("name") end it "cannot create a new taxon if not an admin" do @@ -61,25 +127,49 @@ module Spree sign_in_as_admin! it "can create" do - api_post :create, :taxonomy_id => taxonomy.id, :taxon => { :name => "Colors", :parent_id => taxon.id} - json_response.should have_attributes(attributes) - response.status.should == 201 + api_post :create, :taxonomy_id => taxonomy.id, :taxon => { :name => "Colors" } + expect(json_response).to have_attributes(attributes) + expect(response.status).to eq(201) - taxon.reload.children.count.should eq 2 + expect(taxonomy.reload.root.children.count).to eq 2 + taxon = Spree::Taxon.where(:name => 'Colors').first + + expect(taxon.parent_id).to eq taxonomy.root.id + expect(taxon.taxonomy_id).to eq taxonomy.id + end + + it "can update the position in the list" do + taxonomy.root.children << taxon2 + api_put :update, :taxonomy_id => taxonomy.id, :id => taxon.id, :taxon => {:parent_id => taxon.parent_id, :child_index => 2 } + expect(response.status).to eq(200) + expect(taxonomy.reload.root.children[0]).to eql taxon2 + expect(taxonomy.reload.root.children[1]).to eql taxon end it "cannot create a new taxon with invalid attributes" do api_post :create, :taxonomy_id => taxonomy.id, :taxon => {} - response.status.should == 422 - json_response["error"].should == "Invalid resource. Please fix errors and try again." + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") + errors = json_response["errors"] + + expect(taxonomy.reload.root.children.count).to eq 1 + end + + it "cannot create a new taxon with invalid taxonomy_id" do + api_post :create, :taxonomy_id => 1000, :taxon => { :name => "Colors" } + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") + errors = json_response["errors"] + expect(errors["taxonomy_id"]).not_to be_nil + expect(errors["taxonomy_id"].first).to eq "Invalid taxonomy id." - taxon.reload.children.count.should eq 1 + expect(taxonomy.reload.root.children.count).to eq 1 end it "can destroy" do api_delete :destroy, :taxonomy_id => taxonomy.id, :id => taxon.id - response.status.should == 204 + expect(response.status).to eq(204) end end diff --git a/api/spec/controllers/spree/api/unauthenticated_products_controller_spec.rb b/api/spec/controllers/spree/api/unauthenticated_products_controller_spec.rb index 330adc45fab..6c28618e7d9 100644 --- a/api/spec/controllers/spree/api/unauthenticated_products_controller_spec.rb +++ b/api/spec/controllers/spree/api/unauthenticated_products_controller_spec.rb @@ -2,21 +2,21 @@ require 'spec_helper' module Spree - describe Spree::Api::ProductsController do + describe Spree::Api::ProductsController, :type => :controller do render_views let!(:product) { create(:product) } - let(:attributes) { [:id, :name, :description, :price, :available_on, :permalink, :count_on_hand, :meta_description, :meta_keywords, :taxon_ids] } + let(:attributes) { [:id, :name, :description, :price, :available_on, :slug, :meta_description, :meta_keywords, :taxon_ids] } context "without authentication" do before { Spree::Api::Config[:requires_authentication] = false } - it "retreives a list of products" do + it "retrieves a list of products" do api_get :index - json_response["products"].first.should have_attributes(attributes) - json_response["count"].should == 1 - json_response["current_page"].should == 1 - json_response["pages"].should == 1 + expect(json_response["products"].first).to have_attributes(attributes) + expect(json_response["count"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) end it_behaves_like "modifying product actions are restricted" diff --git a/api/spec/controllers/spree/api/users_controller_spec.rb b/api/spec/controllers/spree/api/users_controller_spec.rb new file mode 100644 index 00000000000..e5b5725a89e --- /dev/null +++ b/api/spec/controllers/spree/api/users_controller_spec.rb @@ -0,0 +1,153 @@ +require 'spec_helper' + +module Spree + describe Api::UsersController, :type => :controller do + render_views + + let(:user) { create(:user, spree_api_key: rand.to_s) } + let(:stranger) { create(:user, :email => 'stranger@example.com') } + let(:attributes) { [:id, :email, :created_at, :updated_at] } + + context "as a normal user" do + it "can get own details" do + api_get :show, id: user.id, token: user.spree_api_key + + expect(json_response['email']).to eq user.email + end + + it "cannot get other users details" do + api_get :show, id: stranger.id, token: user.spree_api_key + + assert_not_found! + end + + it "can learn how to create a new user" do + api_get :new, token: user.spree_api_key + expect(json_response["attributes"]).to eq(attributes.map(&:to_s)) + end + + it "can create a new user" do + user_params = { + :email => 'new@example.com', :password => 'spree123', :password_confirmation => 'spree123' + } + + api_post :create, :user => user_params, token: user.spree_api_key + expect(json_response['email']).to eq 'new@example.com' + end + + # there's no validations on LegacyUser? + xit "cannot create a new user with invalid attributes" do + api_post :create, :user => {}, token: user.spree_api_key + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") + errors = json_response["errors"] + end + + it "can update own details" do + country = create(:country) + api_put :update, id: user.id, token: user.spree_api_key, user: { + email: "mine@example.com", + bill_address_attributes: { + first_name: 'First', + last_name: 'Last', + address1: '1 Test Rd', + city: 'City', + country_id: country.id, + state_id: 1, + zipcode: '55555', + phone: '5555555555' + }, + ship_address_attributes: { + first_name: 'First', + last_name: 'Last', + address1: '1 Test Rd', + city: 'City', + country_id: country.id, + state_id: 1, + zipcode: '55555', + phone: '5555555555' + } + } + expect(json_response['email']).to eq 'mine@example.com' + expect(json_response['bill_address']).to_not be_nil + expect(json_response['ship_address']).to_not be_nil + end + + it "cannot update other users details" do + api_put :update, id: stranger.id, token: user.spree_api_key, user: { :email => "mine@example.com" } + assert_not_found! + end + + it "can delete itself" do + api_delete :destroy, id: user.id, token: user.spree_api_key + expect(response.status).to eq(204) + end + + it "cannot delete other user" do + api_delete :destroy, id: stranger.id, token: user.spree_api_key + assert_not_found! + end + + it "should only get own details on index" do + 2.times { create(:user) } + api_get :index, token: user.spree_api_key + + expect(Spree.user_class.count).to eq 3 + expect(json_response['count']).to eq 1 + expect(json_response['users'].size).to eq 1 + end + end + + context "as an admin" do + before { stub_authentication! } + + sign_in_as_admin! + + it "gets all users" do + allow(Spree::LegacyUser).to receive(:find_by).with(hash_including(:spree_api_key)) { current_api_user } + + 2.times { create(:user) } + + api_get :index + expect(Spree.user_class.count).to eq 2 + expect(json_response['count']).to eq 2 + expect(json_response['users'].size).to eq 2 + end + + it 'can control the page size through a parameter' do + 2.times { create(:user) } + api_get :index, :per_page => 1 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) + end + + it 'can query the results through a paramter' do + expected_result = create(:user, :email => 'brian@spreecommerce.com') + api_get :index, :q => { :email_cont => 'brian' } + expect(json_response['count']).to eq(1) + expect(json_response['users'].first['email']).to eq expected_result.email + end + + it "can create" do + api_post :create, :user => { :email => "new@example.com", :password => 'spree123', :password_confirmation => 'spree123' } + expect(json_response).to have_attributes(attributes) + expect(response.status).to eq(201) + end + + it "can destroy user without orders" do + user.orders.destroy_all + api_delete :destroy, :id => user.id + expect(response.status).to eq(204) + end + + it "cannot destroy user with orders" do + create(:completed_order_with_totals, :user => user) + api_delete :destroy, :id => user.id + expect(json_response["exception"]).to eq "Spree::Core::DestroyWithOrdersError" + expect(response.status).to eq(422) + end + + end + end +end diff --git a/api/spec/controllers/spree/api/variants_controller_spec.rb b/api/spec/controllers/spree/api/variants_controller_spec.rb index d20b21d7f4d..7aa55a86535 100644 --- a/api/spec/controllers/spree/api/variants_controller_spec.rb +++ b/api/spec/controllers/spree/api/variants_controller_spec.rb @@ -1,20 +1,20 @@ require 'spec_helper' module Spree - describe Api::VariantsController do + describe Api::VariantsController, :type => :controller do render_views - + let(:option_value) { create(:option_value) } let!(:product) { create(:product) } let!(:variant) do variant = product.master - variant.option_values << create(:option_value) + variant.option_values << option_value variant end - let!(:attributes) { [:id, :name, :count_on_hand, - :sku, :price, :weight, :height, - :width, :depth, :is_master, :cost_price, - :permalink] } + + let!(:base_attributes) { Api::ApiHelpers.variant_attributes } + let!(:show_attributes) { base_attributes.dup.push(:in_stock, :display_price) } + let!(:new_attributes) { base_attributes } before do stub_authentication! @@ -22,36 +22,60 @@ module Spree it "can see a paginated list of variants" do api_get :index - json_response["variants"].first.should have_attributes(attributes) - json_response["count"].should == 1 - json_response["current_page"].should == 1 - json_response["pages"].should == 1 + first_variant = json_response["variants"].first + expect(first_variant).to have_attributes(show_attributes) + expect(first_variant["stock_items"]).to be_present + expect(json_response["count"]).to eq(1) + expect(json_response["current_page"]).to eq(1) + expect(json_response["pages"]).to eq(1) end it 'can control the page size through a parameter' do create(:variant) api_get :index, :per_page => 1 - json_response['count'].should == 1 - json_response['current_page'].should == 1 - json_response['pages'].should == 3 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(3) end - it 'can query the results through a paramter' do + it 'can query the results through a parameter' do expected_result = create(:variant, :sku => 'FOOBAR') api_get :index, :q => { :sku_cont => 'FOO' } - json_response['count'].should == 1 - json_response['variants'].first['sku'].should eq expected_result.sku + expect(json_response['count']).to eq(1) + expect(json_response['variants'].first['sku']).to eq expected_result.sku end it "variants returned contain option values data" do api_get :index option_values = json_response["variants"].last["option_values"] - option_values.first.should have_attributes([:name, + expect(option_values.first).to have_attributes([:name, :presentation, :option_type_name, :option_type_id]) end + it "variants returned contain images data" do + variant.images.create!(:attachment => image("thinking-cat.jpg")) + + api_get :index + + expect(json_response["variants"].last).to have_attributes([:images]) + expect(json_response['variants'].first['images'].first).to have_attributes([:attachment_file_name, + :attachment_width, + :attachment_height, + :attachment_content_type, + :mini_url, + :small_url, + :product_url, + :large_url]) + + end + + it 'variants returned do not contain cost price data' do + api_get :index + expect(json_response["variants"].first.has_key?(:cost_price)).to eq false + end + # Regression test for #2141 context "a deleted variant" do before do @@ -60,33 +84,45 @@ module Spree it "is not returned in the results" do api_get :index - json_response["variants"].count.should == 0 + expect(json_response["variants"].count).to eq(0) end it "is not returned even when show_deleted is passed" do api_get :index, :show_deleted => true - json_response["variants"].count.should == 0 + expect(json_response["variants"].count).to eq(0) end end context "pagination" do - default_per_page(1) - it "can select the next page of variants" do second_variant = create(:variant) - api_get :index, :page => 2 - json_response["variants"].first.should have_attributes(attributes) - json_response["total_count"].should == 3 - json_response["current_page"].should == 2 - json_response["pages"].should == 3 + api_get :index, :page => 2, :per_page => 1 + expect(json_response["variants"].first).to have_attributes(show_attributes) + expect(json_response["total_count"]).to eq(3) + expect(json_response["current_page"]).to eq(2) + expect(json_response["pages"]).to eq(3) end end it "can see a single variant" do api_get :show, :id => variant.to_param - json_response.should have_attributes(attributes) + expect(json_response).to have_attributes(show_attributes) + expect(json_response["stock_items"]).to be_present option_values = json_response["option_values"] - option_values.first.should have_attributes([:name, + expect(option_values.first).to have_attributes([:name, + :presentation, + :option_type_name, + :option_type_id]) + end + + it "can see a single variant with images" do + variant.images.create!(:attachment => image("thinking-cat.jpg")) + + api_get :show, :id => variant.to_param + + expect(json_response).to have_attributes(show_attributes + [:images]) + option_values = json_response["option_values"] + expect(option_values.first).to have_attributes([:name, :presentation, :option_type_name, :option_type_id]) @@ -94,8 +130,8 @@ module Spree it "can learn how to create a new variant" do api_get :new - json_response["attributes"].should == attributes.map(&:to_s) - json_response["required_attributes"].should be_empty + expect(json_response["attributes"]).to eq(new_attributes.map(&:to_s)) + expect(json_response["required_attributes"]).to be_empty end it "cannot create a new variant if not an admin" do @@ -105,13 +141,13 @@ module Spree it "cannot update a variant" do api_put :update, :id => variant.to_param, :variant => { :sku => "12345" } - assert_unauthorized! + assert_not_found! end it "cannot delete a variant" do api_delete :destroy, :id => variant.to_param - assert_unauthorized! - lambda { variant.reload }.should_not raise_error + assert_not_found! + expect { variant.reload }.not_to raise_error end context "as an admin" do @@ -126,30 +162,36 @@ module Spree it "are visible by admin" do api_get :index, :show_deleted => 1 - json_response["variants"].count.should == 1 + expect(json_response["variants"].count).to eq(1) end end it "can create a new variant" do - api_post :create, :variant => { :sku => "12345" } - json_response.should have_attributes(attributes) - response.status.should == 201 + api_post :create, variant: { sku: "12345", option_value_ids: [option_value.id] } + + expect(json_response).to have_attributes(new_attributes) + expect(response.status).to eq(201) + expect(json_response["sku"]).to eq("12345") + expect(json_response["option_values"].first["id"]).to eq option_value.id - variant.product.variants.count.should == 1 + expect(variant.product.variants.count).to eq(1) end it "can update a variant" do api_put :update, :id => variant.to_param, :variant => { :sku => "12345" } - response.status.should == 200 + expect(response.status).to eq(200) end it "can delete a variant" do api_delete :destroy, :id => variant.to_param - response.status.should == 204 - lambda { variant.reload }.should raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(204) + expect { Spree::Variant.find(variant.id) }.to raise_error(ActiveRecord::RecordNotFound) end - end - + it 'variants returned contain cost price data' do + api_get :index + expect(json_response["variants"].first.has_key?(:cost_price)).to eq true + end + end end end diff --git a/api/spec/controllers/spree/api/zones_controller_spec.rb b/api/spec/controllers/spree/api/zones_controller_spec.rb index e0ed9c935f8..66f0e41748a 100644 --- a/api/spec/controllers/spree/api/zones_controller_spec.rb +++ b/api/spec/controllers/spree/api/zones_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Spree - describe Api::ZonesController do + describe Api::ZonesController, :type => :controller do render_views let!(:attributes) { [:id, :name, :zone_members] } @@ -13,29 +13,29 @@ module Spree it "gets list of zones" do api_get :index - json_response['zones'].first.should have_attributes(attributes) + expect(json_response['zones'].first).to have_attributes(attributes) end it 'can control the page size through a parameter' do create(:zone) api_get :index, :per_page => 1 - json_response['count'].should == 1 - json_response['current_page'].should == 1 - json_response['pages'].should == 2 + expect(json_response['count']).to eq(1) + expect(json_response['current_page']).to eq(1) + expect(json_response['pages']).to eq(2) end it 'can query the results through a paramter' do expected_result = create(:zone, :name => 'South America') api_get :index, :q => { :name_cont => 'south' } - json_response['count'].should == 1 - json_response['zones'].first['name'].should eq expected_result.name + expect(json_response['count']).to eq(1) + expect(json_response['zones'].first['name']).to eq expected_result.name end it "gets a zone" do api_get :show, :id => @zone.id - json_response.should have_attributes(attributes) - json_response['name'].should eq @zone.name - json_response['zone_members'].size.should eq @zone.zone_members.count + expect(json_response).to have_attributes(attributes) + expect(json_response['name']).to eq @zone.name + expect(json_response['zone_members'].size).to eq @zone.zone_members.count end context "as an admin" do @@ -55,9 +55,9 @@ module Spree } api_post :create, params - response.status.should == 201 - json_response.should have_attributes(attributes) - json_response["zone_members"].should_not be_empty + expect(response.status).to eq(201) + expect(json_response).to have_attributes(attributes) + expect(json_response["zone_members"]).not_to be_empty end it "updates a zone" do @@ -74,15 +74,15 @@ module Spree } api_put :update, params - response.status.should == 200 - json_response['name'].should eq 'North Pole' - json_response['zone_members'].should_not be_blank + expect(response.status).to eq(200) + expect(json_response['name']).to eq 'North Pole' + expect(json_response['zone_members']).not_to be_blank end it "can delete a zone" do api_delete :destroy, :id => @zone.id - response.status.should == 204 - lambda { @zone.reload }.should raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(204) + expect { @zone.reload }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/api/spec/fixtures/thinking-cat.jpg b/api/spec/fixtures/thinking-cat.jpg index b0b19936ee6..7e8524d367b 100644 Binary files a/api/spec/fixtures/thinking-cat.jpg and b/api/spec/fixtures/thinking-cat.jpg differ diff --git a/api/spec/models/spree/legacy_user_spec.rb b/api/spec/models/spree/legacy_user_spec.rb index e406f427f4f..9084ca7ba78 100644 --- a/api/spec/models/spree/legacy_user_spec.rb +++ b/api/spec/models/spree/legacy_user_spec.rb @@ -1,19 +1,19 @@ require 'spec_helper' module Spree - describe LegacyUser do + describe LegacyUser, :type => :model do let(:user) { LegacyUser.new } it "can generate an API key" do - user.should_receive(:save!) + expect(user).to receive(:save!) user.generate_spree_api_key! - user.spree_api_key.should_not be_blank + expect(user.spree_api_key).not_to be_blank end it "can clear an API key" do - user.should_receive(:save!) + expect(user).to receive(:save!) user.clear_spree_api_key! - user.spree_api_key.should be_blank + expect(user.spree_api_key).to be_blank end end end diff --git a/api/spec/models/spree/order_spec.rb b/api/spec/models/spree/order_spec.rb deleted file mode 100644 index 0abab0daea1..00000000000 --- a/api/spec/models/spree/order_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' - -module Spree - describe Order do - let(:user) { stub_model(LegacyUser) } - - it 'can build an order from API parameters' do - product = Spree::Product.create!(:name => 'Test', :sku => 'TEST-1', :price => 33.22) - variant_id = product.master.id - order = Order.build_from_api(user, { :line_items_attributes => [{ :variant_id => variant_id, :quantity => 5 }]}) - - order.user.should == user - line_item = order.line_items.first - line_item.quantity.should == 5 - line_item.variant_id.should == variant_id - end - end -end diff --git a/api/spec/requests/rabl_cache_spec.rb b/api/spec/requests/rabl_cache_spec.rb new file mode 100644 index 00000000000..b1a7d0b666d --- /dev/null +++ b/api/spec/requests/rabl_cache_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe "Rabl Cache", :type => :request, :caching => true do + let!(:user) { create(:admin_user) } + + before do + create(:variant) + user.generate_spree_api_key! + expect(Spree::Product.count).to eq(1) + end + + it "doesn't create a cache key collision for models with different rabl templates" do + get "/api/variants", :token => user.spree_api_key + expect(response.status).to eq(200) + + # Make sure we get a non master variant + variant_a = JSON.parse(response.body)['variants'].select do |v| + !v['is_master'] + end.first + + expect(variant_a['is_master']).to be false + expect(variant_a['stock_items']).not_to be_nil + + get "/api/products/#{Spree::Product.first.id}", :token => user.spree_api_key + expect(response.status).to eq(200) + variant_b = JSON.parse(response.body)['variants'].last + expect(variant_b['is_master']).to be false + + expect(variant_a['id']).to eq(variant_b['id']) + expect(variant_b['stock_items']).to be_nil + end +end diff --git a/api/spec/requests/ransackable_attributes_spec.rb b/api/spec/requests/ransackable_attributes_spec.rb new file mode 100644 index 00000000000..05a3487cae4 --- /dev/null +++ b/api/spec/requests/ransackable_attributes_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe "Ransackable Attributes" do + let(:user) { create(:user).tap(&:generate_spree_api_key!) } + let(:order) { create(:order_with_line_items, user: user) } + context "filtering by attributes one association away" do + it "does not allow the filtering of variants by order attributes" do + 2.times { create(:variant) } + + get "/api/variants?q[orders_email_start]=#{order.email}", token: user.spree_api_key + + variants_response = JSON.parse(response.body) + expect(variants_response['total_count']).to eq(Spree::Variant.count) + end + end + + context "filtering by attributes two associations away" do + it "does not allow the filtering of variants by user attributes" do + 2.times { create(:variant) } + + get "/api/variants?q[orders_user_email_start]=#{order.user.email}", token: user.spree_api_key + + variants_response = JSON.parse(response.body) + expect(variants_response['total_count']).to eq(Spree::Variant.count) + end + end + + context "it maintains desired association behavior" do + it "allows filtering of variants product name" do + product = create(:product, name: "Fritos") + variant = create(:variant, product: product) + other_variant = create(:variant) + + get "/api/variants?q[product_name_or_sku_cont]=fritos", token: user.spree_api_key + + skus = JSON.parse(response.body)['variants'].map { |variant| variant['sku'] } + expect(skus).to include variant.sku + expect(skus).not_to include other_variant.sku + end + end + + context "filtering by attributes" do + it "most attributes are not filterable by default" do + product = create(:product, meta_title: "special product") + other_product = create(:product) + + get "/api/products?q[meta_title_cont]=special", token: user.spree_api_key + + products_response = JSON.parse(response.body) + expect(products_response['total_count']).to eq(Spree::Product.count) + end + + it "id is filterable by default" do + product = create(:product) + other_product = create(:product) + + get "/api/products?q[id_eq]=#{product.id}", token: user.spree_api_key + + product_names = JSON.parse(response.body)['products'].map { |product| product['name'] } + expect(product_names).to include product.name + expect(product_names).not_to include other_product.name + end + end + + context "filtering by whitelisted attributes" do + it "filtering is supported for whitelisted attributes" do + product = create(:product, name: "Fritos") + other_product = create(:product) + + get "/api/products?q[name_cont]=fritos", token: user.spree_api_key + + product_names = JSON.parse(response.body)['products'].map { |product| product['name'] } + expect(product_names).to include product.name + expect(product_names).not_to include other_product.name + end + end + + +end diff --git a/api/spec/spec_helper.rb b/api/spec/spec_helper.rb index b2f1b6903ae..ab24b39fdaa 100644 --- a/api/spec/spec_helper.rb +++ b/api/spec/spec_helper.rb @@ -1,27 +1,55 @@ +if ENV["COVERAGE"] + # Run Coverage report + require 'simplecov' + SimpleCov.start do + add_group 'Controllers', 'app/controllers' + add_group 'Helpers', 'app/helpers' + add_group 'Mailers', 'app/mailers' + add_group 'Models', 'app/models' + add_group 'Views', 'app/views' + add_group 'Libraries', 'lib' + end +end + # This file is copied to spec/ when you run 'rails generate rspec:install' ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../dummy/config/environment", __FILE__) + +begin + require File.expand_path("../dummy/config/environment", __FILE__) +rescue LoadError + puts "Could not load dummy application. Please ensure you have run `bundle exec rake test_app`" + exit +end + require 'rspec/rails' -require 'rspec/autorun' +require 'ffaker' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f} -require 'spree/core/testing_support/factories' +require 'spree/testing_support/factories' +require 'spree/testing_support/preferences' +require 'spree/api/testing_support/caching' require 'spree/api/testing_support/helpers' require 'spree/api/testing_support/setup' RSpec.configure do |config| - config.backtrace_clean_patterns = [/gems\/activesupport/, /gems\/actionpack/, /gems\/rspec/] + config.backtrace_exclusion_patterns = [/gems\/activesupport/, /gems\/actionpack/, /gems\/rspec/] + config.color = true + config.infer_spec_type_from_file_location! config.include FactoryGirl::Syntax::Methods config.include Spree::Api::TestingSupport::Helpers, :type => :controller config.extend Spree::Api::TestingSupport::Setup, :type => :controller - config.include Spree::Core::TestingSupport::Preferences, :type => :controller + config.include Spree::TestingSupport::Preferences, :type => :controller + + config.fail_fast = ENV['FAIL_FAST'] || false config.before do Spree::Api::Config[:requires_authentication] = true end + + config.use_transactional_fixtures = true end diff --git a/api/spec/support/controller_hacks.rb b/api/spec/support/controller_hacks.rb index 9a50dee124c..b80b3bc0313 100644 --- a/api/spec/support/controller_hacks.rb +++ b/api/spec/support/controller_hacks.rb @@ -12,16 +12,20 @@ def api_put(action, params={}, session=nil, flash=nil) api_process(action, params, session, flash, "PUT") end + def api_patch(action, params={}, session=nil, flash=nil) + api_process(action, params, session, flash, "PATCH") + end + def api_delete(action, params={}, session=nil, flash=nil) api_process(action, params, session, flash, "DELETE") end def api_process(action, params={}, session=nil, flash=nil, method="get") scoping = respond_to?(:resource_scoping) ? resource_scoping : {} - process(action, params.merge(scoping).reverse_merge!(:use_route => :spree, :format => :json), session, flash, method) + process(action, method, params.merge(scoping).reverse_merge!(:use_route => :spree, :format => :json), session, flash) end end RSpec.configure do |config| - config.include ControllerHacks, :type => :controller + config.include ControllerHacks, type: :controller end diff --git a/api/spree_api.gemspec b/api/spree_api.gemspec index aced7589cfc..8d51c571ace 100644 --- a/api/spree_api.gemspec +++ b/api/spree_api.gemspec @@ -16,8 +16,6 @@ Gem::Specification.new do |gem| gem.version = version gem.add_dependency 'spree_core', version - gem.add_dependency 'versioncake', '0.4.0' - - gem.add_development_dependency 'rspec-rails', '2.9.0' - gem.add_development_dependency 'database_cleaner' + gem.add_dependency 'rabl', '~> 0.9.4.pre1' + gem.add_dependency 'versioncake', '~> 2.3.1' end diff --git a/backend/CHANGELOG.md b/backend/CHANGELOG.md new file mode 100644 index 00000000000..f4a0249631c --- /dev/null +++ b/backend/CHANGELOG.md @@ -0,0 +1 @@ +## Spree 2.4.0 (unreleased) ## diff --git a/backend/Gemfile b/backend/Gemfile new file mode 100644 index 00000000000..49bfb5b7790 --- /dev/null +++ b/backend/Gemfile @@ -0,0 +1,6 @@ +eval(File.read(File.dirname(__FILE__) + '/../common_spree_dependencies.rb')) + +gem 'spree_core', :path => '../core' +gem 'spree_api', :path => '../api' + +gemspec diff --git a/backend/Rakefile b/backend/Rakefile new file mode 100644 index 00000000000..a7518215f96 --- /dev/null +++ b/backend/Rakefile @@ -0,0 +1,29 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/packagetask' +require 'rubygems/package_task' +require 'rspec/core/rake_task' +require 'spree/testing_support/common_rake' + +Bundler::GemHelper.install_tasks +RSpec::Core::RakeTask.new + +spec = eval(File.read('spree_backend.gemspec')) +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end + +desc "Release to gemcutter" +task :release do + version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip + cmd = "cd pkg && gem push spree_backend-#{version}.gem"; puts cmd; system cmd +end + +task :default => :spec + +desc "Generates a dummy app for testing" +task :test_app do + ENV['LIB_NAME'] = 'spree/backend' + Rake::Task['common:test_app'].invoke +end diff --git a/backend/app/assets/images/admin/payment_banner.png b/backend/app/assets/images/admin/payment_banner.png new file mode 100644 index 00000000000..87573a5626b Binary files /dev/null and b/backend/app/assets/images/admin/payment_banner.png differ diff --git a/backend/app/assets/images/admin/progress.gif b/backend/app/assets/images/admin/progress.gif new file mode 100644 index 00000000000..17cfb29986f Binary files /dev/null and b/backend/app/assets/images/admin/progress.gif differ diff --git a/backend/app/assets/images/credit_cards/credit_card.gif b/backend/app/assets/images/credit_cards/credit_card.gif new file mode 100644 index 00000000000..2e61a23c310 Binary files /dev/null and b/backend/app/assets/images/credit_cards/credit_card.gif differ diff --git a/backend/app/assets/javascripts/spree/backend.js b/backend/app/assets/javascripts/spree/backend.js new file mode 100644 index 00000000000..24b4e0c8f17 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend.js @@ -0,0 +1,44 @@ +//= require jquery +//= require jquery_ujs +//= require jquery-migrate-1.0.0 +//= require jquery-ui/datepicker +//= require jquery-ui/sortable +//= require jquery-ui/autocomplete +//= require modernizr +//= require jquery.cookie +//= require jquery.delayedobserver +//= require jquery.jstree/jquery.jstree +//= require jquery.alerts/jquery.alerts +//= require jquery.powertip +//= require jquery.vAlign +//= require css_browser_selector_dev +//= require spin +//= require trunk8 +//= require jquery.adaptivemenu +//= require equalize +//= require responsive-tables +//= require spree +//= require spree/backend/spree-select2 +//= require_tree . + +Spree.routes.checkouts_api = Spree.pathFor('api/checkouts') +Spree.routes.classifications_api = Spree.pathFor('api/classifications') +Spree.routes.clear_cache = Spree.pathFor('admin/general_settings/clear_cache') +Spree.routes.option_type_search = Spree.pathFor('api/option_types') +Spree.routes.orders_api = Spree.pathFor('api/orders') +Spree.routes.product_search = Spree.pathFor('api/products') +Spree.routes.shipments_api = Spree.pathFor('api/shipments') +Spree.routes.checkouts_api = Spree.pathFor('api/checkouts') +Spree.routes.stock_locations_api = Spree.pathFor('api/stock_locations') +Spree.routes.taxon_products_api = Spree.pathFor('api/taxons/products') +Spree.routes.taxons_search = Spree.pathFor('api/taxons') +Spree.routes.user_search = Spree.pathFor('admin/search/users') +Spree.routes.variants_api = Spree.pathFor('api/variants') + +Spree.routes.payments_api = function(order_id) { + return Spree.pathFor('api/orders/' + order_id + '/payments') +} + +Spree.routes.stock_items_api = function(stock_location_id) { + return Spree.pathFor('api/stock_locations/' + stock_location_id + '/stock_items') +} diff --git a/backend/app/assets/javascripts/spree/backend/address_states.js b/backend/app/assets/javascripts/spree/backend/address_states.js new file mode 100644 index 00000000000..64dfe363a54 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/address_states.js @@ -0,0 +1,33 @@ +var update_state = function (region, done) { + 'use strict'; + + var country = $('span#' + region + 'country .select2').select2('val'); + var state_select = $('span#' + region + 'state select.select2'); + var state_input = $('span#' + region + 'state input.state_name'); + + $.get(Spree.routes.states_search + '?country_id=' + country, function (data) { + var states = data.states; + if (states.length > 0) { + state_select.html(''); + var states_with_blank = [{ + name: '', + id: '' + }].concat(states); + $.each(states_with_blank, function (pos, state) { + var opt = $(document.createElement('option')) + .prop('value', state.id) + .html(state.name); + state_select.append(opt); + }); + state_select.prop('disabled', false).show(); + state_select.select2(); + state_input.hide().prop('disabled', true); + + } else { + state_input.prop('disabled', false).show(); + state_select.select2('destroy').hide(); + } + + if(done) done(); + }); +}; diff --git a/backend/app/assets/javascripts/spree/backend/adjustments.js.coffee b/backend/app/assets/javascripts/spree/backend/adjustments.js.coffee new file mode 100644 index 00000000000..0187c4f0da9 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/adjustments.js.coffee @@ -0,0 +1,17 @@ +$(@).ready( -> + $('[data-hook=adjustments_new_coupon_code] #add_coupon_code').click -> + return if $("#coupon_code").val().length == 0 + $.ajax + type: 'PUT' + url: Spree.url(Spree.routes.apply_coupon_code(order_number)) + data: + coupon_code: $("#coupon_code").val() + token: Spree.api_key + success: -> + window.location.reload(); + error: (msg) -> + if msg.responseJSON["error"] + show_flash 'error', msg.responseJSON["error"] + else + show_flash 'error', "There was a problem adding this coupon code." +) diff --git a/backend/app/assets/javascripts/spree/backend/admin.js.erb b/backend/app/assets/javascripts/spree/backend/admin.js.erb new file mode 100644 index 00000000000..81162d9a7f8 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/admin.js.erb @@ -0,0 +1,287 @@ +//= require_self +//= require spree/backend/handlebar_extensions +//= require spree/backend/variant_autocomplete +//= require spree/backend/taxon_autocomplete +//= require spree/backend/option_type_autocomplete +//= require spree/backend/user_picker +//= require spree/backend/product_picker +//= require spree/backend/taxons + +/** +This is a collection of javascript functions and whatnot +under the spree namespace that do stuff we find helpful. +Hopefully, this will evolve into a propper class. +**/ + +jQuery(function($) { + + // Vertical align of checkbox fields + $('.field.checkbox label').vAlign() + + <% # Re-adjusting admin menu during test causes tests to fail, + # like states_spec and shipping_methods_spec. Let's not do this. %> + <% unless Rails.env.test? %> + $('.main-menu-wrapper ul').AdaptiveMenu({ + text: " " + Spree.translations.more + "", + klass: "dropdown" + }); + <% end %> + + // Add some tips + $('.with-tip').powerTip({ + smartPlacement: true, + fadeInTime: 50, + fadeOutTime: 50, + }); + + $('body') + .on('powerTipPreRender', '.with-tip', function() { + $('#powerTip').addClass($(this).data('action')); + $('#powerTip').addClass($(this).data('tip-color')); + }) + .on('powerTipClose', '.with-tip', function() { + $('#powerTip').removeClass($(this).data('action')); + }) + + // Make flash messages dissapear + setTimeout('$(".flash").fadeOut()', 5000); + + // Highlight hovered table column + $('table tbody tr td.actions').find('a, button').hover(function(){ + var tr = $(this).closest('tr'); + var klass = 'highlight action-' + $(this).data('action') + tr.addClass(klass) + tr.prev().addClass('before-' + klass); + }, function(){ + var tr = $(this).closest('tr'); + var klass = 'highlight action-' + $(this).data('action') + tr.removeClass(klass) + tr.prev().removeClass('before-' + klass); + }); + + // Trunkate text in page_title that didn't fit + var truncate_elements = $('.truncate'); + + truncate_elements.each(function(){ + $(this).trunk8(); + }); + $(window).resize(function (event) { + truncate_elements.each(function(){ + $(this).trunk8(); + }) + }); + + // Make height of dt/dd elements the same + $("dl").equalize('outerHeight'); + +}); + + +$.fn.visible = function(cond) { this[cond ? 'show' : 'hide' ]() }; + +show_flash = function(type, message) { + var flash_div = $('.flash.' + type); + if (flash_div.length == 0) { + flash_div = $('
'); + $('#wrapper').prepend(flash_div); + } + flash_div.html(message).show().delay(5000).fadeOut(500); +} + + +// Apply to individual radio button that makes another element visible when checked +$.fn.radioControlsVisibilityOfElement = function(dependentElementSelector){ + if(!this.get(0)){ return } + showValue = this.get(0).value; + radioGroup = $("input[name='" + this.get(0).name + "']"); + radioGroup.each(function(){ + $(this).click(function(){ + $(dependentElementSelector).visible(this.checked && this.value == showValue) + }); + if(this.checked){ this.click() } + }); +} + +handle_date_picker_fields = function(){ + $('.datepicker').datepicker({ + dateFormat: Spree.translations.date_picker, + dayNames: Spree.translations.abbr_day_names, + dayNamesMin: Spree.translations.abbr_day_names, + firstDay: Spree.translations.first_day, + monthNames: Spree.translations.month_names, + prevText: Spree.translations.previous, + nextText: Spree.translations.next, + showOn: "focus" + }); + + // Correctly display range dates + $('.date-range-filter .datepicker-from').datepicker('option', 'onSelect', function(selectedDate) { + $(".date-range-filter .datepicker-to" ).datepicker( "option", "minDate", selectedDate ); + }); + $('.date-range-filter .datepicker-to').datepicker('option', 'onSelect', function(selectedDate) { + $(".date-range-filter .datepicker-from" ).datepicker( "option", "maxDate", selectedDate ); + }); +} + +$(document).ready(function(){ + handle_date_picker_fields(); + $(".observe_field").on('change', function() { + target = $(this).data("update"); + $(target).hide(); + $.ajax({ dataType: 'html', + url: $(this).data("base-url")+encodeURIComponent($(this).val()), + type: 'get', + success: function(data){ + $(target).html(data); + $(target).show(); + } + }); + }); + var uniqueId = 1; + $('.spree_add_fields').click(function() { + var target = $(this).data("target"); + var new_table_row = $(target + ' tr:visible:last').clone(); + var new_id = new Date().getTime() + (uniqueId++); + new_table_row.find("input, select").each(function () { + var el = $(this); + el.val(""); + el.prop("id", el.prop("id").replace(/\d+/, new_id)) + el.prop("name", el.prop("name").replace(/\d+/, new_id)) + }) + // When cloning a new row, set the href of all icons to be an empty "#" + // This is so that clicking on them does not perform the actions for the + // duplicated row + new_table_row.find("a").each(function () { + var el = $(this); + el.prop('href', '#'); + }) + $(target).prepend(new_table_row); + }) + + $('body').on('click', '.delete-resource', function() { + var el = $(this); + if (confirm(el.data("confirm"))) { + $.ajax({ + type: 'POST', + url: $(this).prop("href"), + data: { + _method: 'delete', + authenticity_token: AUTH_TOKEN + }, + dataType: 'script', + success: function(response) { + el.parents("tr").fadeOut('hide', function() { + $(this).remove(); + }); + }, + error: function(response, textStatus, errorThrown) { + show_flash('error', response.responseText); + } + }); + } + return false; + }); + + $('body').on('click', 'a.spree_remove_fields', function() { + el = $(this); + el.prev("input[type=hidden]").val("1"); + el.closest(".fields").hide(); + if (el.prop("href").substr(-1) == '#') { + el.parents("tr").fadeOut('hide'); + }else if (el.prop("href")) { + $.ajax({ + type: 'POST', + url: el.prop("href"), + data: { + _method: 'delete', + authenticity_token: AUTH_TOKEN + }, + success: function(response) { + el.parents("tr").fadeOut('hide'); + }, + error: function(response, textStatus, errorThrown) { + show_flash('error', response.responseText); + } + + }) + } + return false; + }); + + $('body').on('click', '.select_properties_from_prototype', function(){ + $("#busy_indicator").show(); + var clicked_link = $(this); + $.ajax({ dataType: 'script', url: clicked_link.prop("href"), type: 'get', + success: function(data){ + clicked_link.parent("td").parent("tr").hide(); + $("#busy_indicator").hide(); + } + }); + return false; + }); + + // Fix sortable helper + var fixHelper = function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + }; + + $('table.sortable').ready(function(){ + var td_count = $(this).find('tbody tr:first-child td').length + $('table.sortable tbody').sortable( + { + handle: '.handle', + helper: fixHelper, + placeholder: 'ui-sortable-placeholder', + update: function(event, ui) { + $("#progress").show(); + positions = {}; + $.each($('table.sortable tbody tr'), function(position, obj){ + reg = /spree_(\w+_?)+_(\d+)/; + parts = reg.exec($(obj).prop('id')); + if (parts) { + positions['positions['+parts[2]+']'] = position; + } + }); + $.ajax({ + type: 'POST', + dataType: 'script', + url: $(ui.item).closest("table.sortable").data("sortable-link"), + data: positions, + success: function(data){ $("#progress").hide(); } + }); + }, + start: function (event, ui) { + // Set correct height for placehoder (from dragged tr) + ui.placeholder.height(ui.item.height()) + // Fix placeholder content to make it correct width + ui.placeholder.html("") + }, + stop: function (event, ui) { + // Fix odd/even classes after reorder + $("table.sortable tr:even").removeClass("odd even").addClass("even"); + $("table.sortable tr:odd").removeClass("odd even").addClass("odd"); + } + + }); + }); + + $('a.dismiss').click(function() { + $(this).parent().fadeOut(); + }); + + window.Spree.advanceOrder = function() { + $.ajax({ + type: "PUT", + async: false, + data: { + token: Spree.api_key + }, + url: Spree.url(Spree.routes.checkouts_api + "/" + order_number + "/advance") + }).done(function() { + window.location.reload(); + }); + } +}); diff --git a/backend/app/assets/javascripts/spree/backend/calculator.js b/backend/app/assets/javascripts/spree/backend/calculator.js new file mode 100644 index 00000000000..fd39f36cda6 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/calculator.js @@ -0,0 +1,16 @@ +$(function() { + var calculator_select = $('select#calc_type') + var original_calc_type = calculator_select.prop('value'); + $('.calculator-settings-warning').hide(); + calculator_select.change(function() { + if (calculator_select.prop('value') == original_calc_type) { + $('div.calculator-settings').show(); + $('.calculator-settings-warning').hide(); + $('.calculator-settings').find('input,textarea').prop("disabled", false); + } else { + $('div.calculator-settings').hide(); + $('.calculator-settings-warning').show(); + $('.calculator-settings').find('input,texttarea').prop("disabled", true); + } + }); +}) diff --git a/backend/app/assets/javascripts/spree/backend/checkouts/edit.js b/backend/app/assets/javascripts/spree/backend/checkouts/edit.js new file mode 100644 index 00000000000..f606325bf71 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/checkouts/edit.js @@ -0,0 +1,89 @@ +//= require_self +$(document).ready(function() { + if ($('#customer_autocomplete_template').length > 0) { + window.customerTemplate = Handlebars.compile($('#customer_autocomplete_template').text()); + } + + formatCustomerResult = function(customer) { + return customerTemplate({ + customer: customer, + bill_address: customer.bill_address, + ship_address: customer.ship_address + }) + } + + if ($("#customer_search").length > 0) { + $("#customer_search").select2({ + placeholder: Spree.translations.choose_a_customer, + ajax: { + url: Spree.routes.user_search, + datatype: 'json', + data: function(term, page) { + return { + q: term, + token: Spree.api_key + } + }, + results: function(data, page) { + return { results: data } + } + }, + dropdownCssClass: 'customer_search', + formatResult: formatCustomerResult, + formatSelection: function (customer) { + $('#order_email').val(customer.email); + $('#user_id').val(customer.id); + $('#guest_checkout_true').prop("checked", false); + $('#guest_checkout_false').prop("checked", true); + $('#guest_checkout_false').prop("disabled", false); + + var billAddress = customer.bill_address; + if(billAddress) { + $('#order_bill_address_attributes_firstname').val(billAddress.firstname); + $('#order_bill_address_attributes_lastname').val(billAddress.lastname); + $('#order_bill_address_attributes_address1').val(billAddress.address1); + $('#order_bill_address_attributes_address2').val(billAddress.address2); + $('#order_bill_address_attributes_city').val(billAddress.city); + $('#order_bill_address_attributes_zipcode').val(billAddress.zipcode); + $('#order_bill_address_attributes_phone').val(billAddress.phone); + + $('#order_bill_address_attributes_country_id').select2("val", billAddress.country_id).promise().done(function () { + update_state('b', function () { + $('#order_bill_address_attributes_state_id').select2("val", billAddress.state_id); + }); + }); + } + return customer.email; + } + }) + } + + var order_use_billing_input = $('input#order_use_billing'); + + var order_use_billing = function () { + if (!order_use_billing_input.is(':checked')) { + $('#shipping').show(); + } else { + $('#shipping').hide(); + } + }; + + order_use_billing_input.click(function() { + order_use_billing(); + }); + + order_use_billing(); + + $('#guest_checkout_true').change(function() { + $('#customer_search').val(""); + $('#user_id').val(""); + $('#checkout_email').val(""); + + var fields = ["firstname", "lastname", "company", "address1", "address2", + "city", "zipcode", "state_id", "country_id", "phone"] + $.each(fields, function(i, field) { + $('#order_bill_address_attributes' + field).val(""); + $('#order_ship_address_attributes' + field).val(""); + }) + }); +}); diff --git a/backend/app/assets/javascripts/spree/backend/gateway.js b/backend/app/assets/javascripts/spree/backend/gateway.js new file mode 100644 index 00000000000..58aa425cfb0 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/gateway.js @@ -0,0 +1,13 @@ +$(function() { + var original_gtwy_type = $('#gtwy-type').prop('value'); + $('div#gateway-settings-warning').hide(); + $('#gtwy-type').change(function() { + if ($('#gtwy-type').prop('value') == original_gtwy_type) { + $('div.gateway-settings').show(); + $('div#gateway-settings-warning').hide(); + } else { + $('div.gateway-settings').hide(); + $('div#gateway-settings-warning').show(); + } + }); +}) diff --git a/backend/app/assets/javascripts/spree/backend/general_settings.js.coffee b/backend/app/assets/javascripts/spree/backend/general_settings.js.coffee new file mode 100644 index 00000000000..fbbec7e49e9 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/general_settings.js.coffee @@ -0,0 +1,13 @@ +$(@).ready( -> + $('[data-hook=general_settings_clear_cache] #clear_cache').click -> + $.ajax + type: 'POST' + url: Spree.routes.clear_cache + success: -> + show_flash 'success', "Cache was flushed." + error: (msg) -> + if msg.responseJSON["error"] + show_flash 'error', msg.responseJSON["error"] + else + show_flash 'error', "There was a problem while flushing cache." +) diff --git a/backend/app/assets/javascripts/spree/backend/handlebar_extensions.js b/backend/app/assets/javascripts/spree/backend/handlebar_extensions.js new file mode 100644 index 00000000000..02f372bb005 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/handlebar_extensions.js @@ -0,0 +1,9 @@ +//= require handlebars +Handlebars.registerHelper("t", function(key) { + if (Spree.translations[key]) { + return Spree.translations[key] + } else { + console.error("No translation found for " + key + ". Does it exist within spree/admin/shared/_translations.html.erb?") + } +}); + diff --git a/core/app/assets/javascripts/admin/images/index.js.coffee b/backend/app/assets/javascripts/spree/backend/images/index.js.coffee similarity index 89% rename from core/app/assets/javascripts/admin/images/index.js.coffee rename to backend/app/assets/javascripts/spree/backend/images/index.js.coffee index a7fab5d7350..38823e48a90 100644 --- a/core/app/assets/javascripts/admin/images/index.js.coffee +++ b/backend/app/assets/javascripts/spree/backend/images/index.js.coffee @@ -13,3 +13,4 @@ $ -> ) success: (r) -> ($ '#images').html r + ($ '.select2').select2() diff --git a/core/app/assets/javascripts/admin/images/new.js.coffee b/backend/app/assets/javascripts/spree/backend/images/new.js.coffee similarity index 100% rename from core/app/assets/javascripts/admin/images/new.js.coffee rename to backend/app/assets/javascripts/spree/backend/images/new.js.coffee diff --git a/backend/app/assets/javascripts/spree/backend/line_items.js.coffee b/backend/app/assets/javascripts/spree/backend/line_items.js.coffee new file mode 100644 index 00000000000..52728fcb948 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/line_items.js.coffee @@ -0,0 +1,64 @@ +$(document).ready -> + #handle edit click + $('a.edit-line-item').click toggleLineItemEdit + + #handle cancel click + $('a.cancel-line-item').click toggleLineItemEdit + + #handle save click + $('a.save-line-item').click -> + save = $ this + line_item_id = save.data('line-item-id') + quantity = parseInt(save.parents('tr').find('input.line_item_quantity').val()) + + toggleItemEdit() + adjustLineItem(line_item_id, quantity) + false + + # handle delete click + $('a.delete-line-item').click -> + if confirm(Spree.translations.are_you_sure_delete) + del = $(this); + line_item_id = del.data('line-item-id'); + + toggleItemEdit() + deleteLineItem(line_item_id) + +toggleLineItemEdit = -> + link = $(this); + link.parent().find('a.edit-line-item').toggle(); + link.parent().find('a.cancel-line-item').toggle(); + link.parent().find('a.save-line-item').toggle(); + link.parent().find('a.delete-line-item').toggle(); + link.parents('tr').find('td.line-item-qty-show').toggle(); + link.parents('tr').find('td.line-item-qty-edit').toggle(); + + false + +lineItemURL = (line_item_id) -> + url = Spree.routes.orders_api + "/" + order_number + "/line_items/" + line_item_id + ".json" + +adjustLineItem = (line_item_id, quantity) -> + url = lineItemURL(line_item_id) + $.ajax( + type: "PUT", + url: Spree.url(url), + data: + line_item: + quantity: quantity + token: Spree.api_key + ).done (msg) -> + window.Spree.advanceOrder() + +deleteLineItem = (line_item_id) -> + url = lineItemURL(line_item_id) + $.ajax( + type: "DELETE" + url: Spree.url(url) + data: + token: Spree.api_key + ).done (msg) -> + $('#line-item-' + line_item_id).remove() + if $('.line-items tr.line-item').length == 0 + $('.line-items').remove() + window.Spree.advanceOrder() diff --git a/backend/app/assets/javascripts/spree/backend/line_items_on_order_edit.js.erb b/backend/app/assets/javascripts/spree/backend/line_items_on_order_edit.js.erb new file mode 100644 index 00000000000..d04b7d85868 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/line_items_on_order_edit.js.erb @@ -0,0 +1,56 @@ +// This file contains the code for interacting with line items in the manual cart +$(document).ready(function () { + 'use strict'; + + // handle variant selection, show stock level. + $('#add_line_item_variant_id').change(function(){ + var variant_id = $(this).val(); + + var variant = _.find(window.variants, function(variant){ + return variant.id == variant_id + }) + $('#stock_details').html(variantLineItemTemplate({variant: variant})); + $('#stock_details').show(); + + $('button.add_variant').click(addVariant); + + // Add some tips + $('.with-tip').powerTip({ + smartPlacement: true, + fadeInTime: 50, + fadeOutTime: 50, + intentPollInterval: 300 + }); + + }); +}); + +addVariant = function() { + $('#stock_details').hide(); + + var variant_id = $('input.variant_autocomplete').val(); + var quantity = $("input.quantity[data-variant-id='" + variant_id + "']").val(); + + adjustLineItems(order_number, variant_id, quantity); + return 1 +} + +adjustLineItems = function(order_number, variant_id, quantity){ + var url = Spree.routes.orders_api + "/" + order_number + '/line_items'; + + $.ajax({ + type: "POST", + url: Spree.url(url), + data: { + line_item: { + variant_id: variant_id, + quantity: quantity + }, + token: Spree.api_key + } + }).done(function( msg ) { + window.Spree.advanceOrder(); + window.location.reload(); + }); + +} diff --git a/backend/app/assets/javascripts/spree/backend/nested-attribute.js b/backend/app/assets/javascripts/spree/backend/nested-attribute.js new file mode 100644 index 00000000000..e095c00d68e --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/nested-attribute.js @@ -0,0 +1,27 @@ +// On page load +var replace_ids = function (s) { + var new_id = new Date().getTime(); + return s.replace(/NEW_RECORD/g, new_id); +}; + +$(function () { + 'use strict'; + + $('a[id*=nested]').on('click', function () { + var template = $(this).prop('href').replace(/.*#/, ''); + var html = replace_ids(eval(template)); + $('#ul-' + $(this).prop('id')).append(html); + update_remove_links(); + }); + update_remove_links(); +}); + +var update_remove_links = function () { + 'use strict'; + + $('.remove').on('click', function () { + $(this).prevAll(':first').val(1); + $(this).parent().hide(); + return false; + }); +}; \ No newline at end of file diff --git a/backend/app/assets/javascripts/spree/backend/option_type_autocomplete.js.erb b/backend/app/assets/javascripts/spree/backend/option_type_autocomplete.js.erb new file mode 100644 index 00000000000..72037222cae --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/option_type_autocomplete.js.erb @@ -0,0 +1,43 @@ +$(document).ready(function () { + 'use strict'; + + if ($('#product_option_type_ids').length > 0) { + $('#product_option_type_ids').select2({ + placeholder: Spree.translations.option_type_placeholder, + multiple: true, + initSelection: function (element, callback) { + var url = Spree.url(Spree.routes.option_type_search, { + ids: element.val(), + token: Spree.api_key + }); + return $.getJSON(url, null, function (data) { + return callback(data); + }); + }, + ajax: { + url: Spree.routes.option_type_search, + quietMillis: 200, + datatype: 'json', + data: function (term) { + return { + q: { + name_cont: term + }, + token: Spree.api_key + }; + }, + results: function (data) { + return { + results: data + }; + } + }, + formatResult: function (option_type) { + return option_type.presentation + ' (' + option_type.name + ')'; + }, + formatSelection: function (option_type) { + return option_type.presentation + ' (' + option_type.name + ')'; + } + }); + } +}); diff --git a/backend/app/assets/javascripts/spree/backend/orders/edit.js b/backend/app/assets/javascripts/spree/backend/orders/edit.js new file mode 100644 index 00000000000..26d38c7ea0d --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/orders/edit.js @@ -0,0 +1,5 @@ +$(document).ready(function () { + 'use strict'; + + $('[data-hook="add_product_name"]').find('.variant_autocomplete').variantAutocomplete(); +}); diff --git a/backend/app/assets/javascripts/spree/backend/orders/edit_form.js b/backend/app/assets/javascripts/spree/backend/orders/edit_form.js new file mode 100644 index 00000000000..9f5f74fa937 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/orders/edit_form.js @@ -0,0 +1,21 @@ +$(document).ready(function () { + 'use strict'; + + $.each($('td.qty input'), function (i, input) { + + $(input).on('change', function () { + + var id = '#' + $(this).prop('id').replace('_quantity', '_id'); + + $.post('/admin/orders/' + $('input#order_number').val() + '/line_items/' + $(id).val(), { + _method: 'put', + 'line_item[quantity]': $(this).val(), + token: Spree.api_key + }, + + function (resp) { + $('#order-form-wrapper').html(resp.responseText); + }); + }); + }); +}); diff --git a/backend/app/assets/javascripts/spree/backend/payments/edit.js.coffee b/backend/app/assets/javascripts/spree/backend/payments/edit.js.coffee new file mode 100644 index 00000000000..02e962321ac --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/payments/edit.js.coffee @@ -0,0 +1,149 @@ +jQuery ($) -> + # Payment model + order_id = $('#payments').data('order-id') + class Payment + constructor: (id) -> + @url = Spree.url("#{Spree.routes.payments_api(order_id)}/#{id}.json" + '?token=' + Spree.api_key) + @json = $.getJSON @url.toString(), (data) => + @data = data + + if_editable: (callback) -> + @json.done (data) -> + callback() if data.state in ['checkout', 'pending'] + + update: (attributes, success) -> + jqXHR = $.ajax + type: 'PUT' + url: @url.toString() + data: { payment: attributes } + jqXHR.done (data) => + @data = data + jqXHR.fail -> + response = $.parseJSON(jqXHR.responseText) + show_flash('error', response.error) + + amount: -> @data.amount + display_amount: -> @data.display_amount + + # Payment base view + class PaymentView + constructor: (@$el, @payment) -> + @render() + + render: -> + @add_action_button() + + show: -> + @remove_buttons() + new ShowPaymentView(@$el, @payment) + + edit: -> + @remove_buttons() + new EditPaymentView(@$el, @payment) + + add_action_button: -> + @$actions().prepend @$new_button(@action) + + remove_buttons: -> + @$buttons().remove() + + $new_button: (action) -> + $('') + .attr + class: "fa fa-#{action} icon_link no-text with-tip" + title: Spree.translations[action] + .data + action: action + .one + click: (event) -> + event.preventDefault() + mousedown: -> + $(@).data('clicked', true) + mouseup: => + @[action]() + .powerTip + smartPlacement: true + fadeInTime: 50 + fadeOutTime: 50 + + $buttons: -> + @$actions().find(".fa-#{@action}, .fa-cancel") + + $actions: -> + @$el.find('.actions') + + $amount: -> + @$el.find('td.amount') + + # Payment show view + class ShowPaymentView extends PaymentView + action: 'edit' + + render: -> + super + @set_actions_display() + @show_actions() + @show_amount() + + set_actions_display: -> + width = @$actions().width() + @$actions().width(width).css('text-align', 'left') + + show_actions: -> + @$actions().find('a').show() + + show_amount: -> + amount = $('') + .html(@payment.display_amount()) + .one('click', => @edit().$input().focus()) + @$amount().html(amount) + + # Payment edit view + class EditPaymentView extends PaymentView + action: 'save' + + render: -> + super + @hide_actions() + @edit_amount() + @add_cancel_button() + + add_cancel_button: -> + @$actions().append @$new_button('cancel') + + hide_actions: -> + @$actions().find('a').not(@$buttons()).hide() + + edit_amount: -> + amount = @$amount() + amount.html(@$new_input(amount.find('span').width())) + + save: (event) -> + @payment.update(amount: @$input().val()) + .done(=> @show()) + + cancel: @::show + + $new_input: (width) -> + amount = @constructor.normalize_amount(@payment.display_amount()) + $('') + .prop(id: 'amount', value: amount) + .width(width) + .one + blur: => + clicked = (@$buttons().filter -> $(@).data('clicked')).length + @save() unless clicked + .css('text-align': 'right') + + $input: -> + @$amount().find('input') + + @normalize_amount: (amount) -> + separator = Spree.translations.currency_separator + amount.replace(///[^\d#{separator}]///g, '') + + # Attach ShowPaymentView to each editable payment in the table + $('.admin tr[data-hook=payments_row]').each -> + $el = $(@) + payment = new Payment($el.prop('id').match(/\d+$/)) + payment.if_editable -> new ShowPaymentView($el, payment) diff --git a/backend/app/assets/javascripts/spree/backend/payments/new.js b/backend/app/assets/javascripts/spree/backend/payments/new.js new file mode 100644 index 00000000000..cbb8bf2d0c5 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/payments/new.js @@ -0,0 +1,50 @@ +//= require jquery.payment +$(document).ready(function() { + if ($("#new_payment").is("*")) { + $(".cardNumber").payment('formatCardNumber'); + $(".cardExpiry").payment('formatCardExpiry'); + $(".cardCode").payment('formatCardCVC'); + + $(".cardNumber").change(function() { + $(".ccType").val($.payment.cardType(this.value)) + }) + + $('.payment_methods_radios').click( + function() { + $('.payment-methods').hide(); + $('.payment-methods :input').prop('disabled', true); + if (this.checked) { + $('#payment_method_' + this.value + ' :input').prop('disabled', false); + $('#payment_method_' + this.value).show(); + } + } + ); + + $('.payment_methods_radios').each( + function() { + if (this.checked) { + $('#payment_method_' + this.value + ' :input').prop('disabled', false); + $('#payment_method_' + this.value).show(); + } else { + $('#payment_method_' + this.value).hide(); + $('#payment_method_' + this.value + ' :input').prop('disabled', true); + } + + if ($("#card_new" + this.value).is("*")) { + $("#card_new" + this.value).radioControlsVisibilityOfElement('#card_form' + this.value); + } + } + ); + + $('.cvvLink').click(function(event){ + window_name = 'cvv_info'; + window_options = 'left=20,top=20,width=500,height=500,toolbar=0,resizable=0,scrollbars=1'; + window.open($(this).prop('href'), window_name, window_options); + event.preventDefault(); + }); + + $('select.jump_menu').change(function(){ + window.location = this.options[this.selectedIndex].value; + }); + } +}); diff --git a/backend/app/assets/javascripts/spree/backend/product_picker.js b/backend/app/assets/javascripts/spree/backend/product_picker.js new file mode 100644 index 00000000000..45945c457ba --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/product_picker.js @@ -0,0 +1,46 @@ +$.fn.productAutocomplete = function () { + 'use strict'; + + this.select2({ + minimumInputLength: 1, + multiple: true, + initSelection: function (element, callback) { + $.get(Spree.routes.product_search, { + ids: element.val().split(','), + token: Spree.api_key + }, function (data) { + callback(data.products); + }); + }, + ajax: { + url: Spree.routes.product_search, + datatype: 'json', + data: function (term, page) { + return { + q: { + name_cont: term, + sku_cont: term + }, + m: 'OR', + token: Spree.api_key + }; + }, + results: function (data, page) { + var products = data.products ? data.products : []; + return { + results: products + }; + } + }, + formatResult: function (product) { + return product.name; + }, + formatSelection: function (product) { + return product.name; + } + }); +}; + +$(document).ready(function () { + $('.product_picker').productAutocomplete(); +}); diff --git a/backend/app/assets/javascripts/spree/backend/progress.coffee b/backend/app/assets/javascripts/spree/backend/progress.coffee new file mode 100644 index 00000000000..04db8b0f602 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/progress.coffee @@ -0,0 +1,27 @@ +$(document).ready -> + opts = + lines: 11 + length: 2 + width: 3 + radius: 9 + corners: 1 + rotate: 0 + color: '#fff' + speed: 0.8 + trail: 48 + shadow: false + hwaccel: true + className: 'spinner' + zIndex: 2e9 + top: 'auto' + left: 'auto' + + target = document.getElementById("spinner") + spinner = new Spinner(opts).spin(target) + + $(document).ajaxStart -> + $("#progress").stop(true, true).fadeIn() + + $(document).ajaxStop -> + $("#progress").fadeOut() + diff --git a/backend/app/assets/javascripts/spree/backend/promotions.js b/backend/app/assets/javascripts/spree/backend/promotions.js new file mode 100644 index 00000000000..af59769036c --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/promotions.js @@ -0,0 +1,121 @@ +var initProductActions = function () { + 'use strict'; + + // Add classes on promotion items for design + $(document).on('mouseover mouseout', 'a.delete', function (event) { + if (event.type === 'mouseover') { + $(this).parent().addClass('action-remove'); + } else { + $(this).parent().removeClass('action-remove'); + } + }); + + $('#promotion-filters').find('.variant_autocomplete').variantAutocomplete(); + + $('.calculator-fields').each(function () { + var $fields_container = $(this); + var $type_select = $fields_container.find('.type-select'); + var $settings = $fields_container.find('.settings'); + var $warning = $fields_container.find('.warning'); + var originalType = $type_select.val(); + + $warning.hide(); + $type_select.change(function () { + if ($(this).val() === originalType) { + $warning.hide(); + $settings.show(); + $settings.find('input').removeProp('disabled'); + } else { + $warning.show(); + $settings.hide(); + $settings.find('input').prop('disabled', 'disabled'); + } + }); + }); + + // + // Tiered Calculator + // + if ($('#tier-fields-template').length && $('#tier-input-name').length) { + var tierFieldsTemplate = Handlebars.compile($('#tier-fields-template').html()); + var tierInputNameTemplate = Handlebars.compile($('#tier-input-name').html()); + + var originalTiers = $('.js-original-tiers').data('original-tiers'); + $.each(originalTiers, function(base, value) { + var fieldName = tierInputNameTemplate({base: base}).trim(); + $('.js-tiers').append(tierFieldsTemplate({ + baseField: {value: base}, + valueField: {name: fieldName, value: value} + })); + }); + + $(document).on('click', '.js-add-tier', function(event) { + event.preventDefault(); + $('.js-tiers').append(tierFieldsTemplate({valueField: {name: null}})); + }); + + $(document).on('click', '.js-remove-tier', function(event) { + $(this).parents('.tier').remove(); + }); + + $(document).on('change', '.js-base-input', function(event) { + var valueInput = $(this).parents('.tier').find('.js-value-input'); + valueInput.attr('name', tierInputNameTemplate({base: $(this).val()}).trim()); + }); + } + + // + // CreateLineItems Promotion Action + // + (function () { + var hideOrShowItemTables = function () { + $('.promotion_action table').each(function () { + if ($(this).find('td').length === 0) { + $(this).hide(); + } else { + $(this).show(); + } + }); + }; + hideOrShowItemTables(); + + // Remove line item + var setupRemoveLineItems = function () { + $('.remove_promotion_line_item').on('click', function () { + var line_items_el = $($('.line_items_string')[0]); + var finder = new RegExp($(this).data('variant-id') + "x\\d+"); + line_items_el.val(line_items_el.val().replace(finder, '')); + $(this).parents('tr').remove(); + hideOrShowItemTables(); + }); + }; + + setupRemoveLineItems(); + // Add line item to list + $('.promotion_action.create_line_items button.add').unbind('click').click(function () { + var $container = $(this).parents('.promotion_action'); + var product_name = $container.find('input[name="add_product_name"]').val(); + var variant_id = $container.find('input[name="add_variant_id"]').val(); + var quantity = $container.find('input[name="add_quantity"]').val(); + if (variant_id) { + // Add to the table + var newRow = '' + product_name + '' + quantity + ''; + $container.find('table').append(newRow); + // Add to serialized string in hidden text field + var $hiddenField = $container.find('.line_items_string'); + $hiddenField.val($hiddenField.val() + ',' + variant_id + 'x' + quantity); + setupRemoveLineItems(); + hideOrShowItemTables(); + } + return false; + }); + + })(); + +}; + +$(document).ready(function () { + + initProductActions(); + +}); diff --git a/backend/app/assets/javascripts/spree/backend/returns/expedited_exchanges_warning.coffee b/backend/app/assets/javascripts/spree/backend/returns/expedited_exchanges_warning.coffee new file mode 100644 index 00000000000..b8684e11461 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/returns/expedited_exchanges_warning.coffee @@ -0,0 +1,4 @@ +$ -> + $(document).on("change", ".return-items-table .return-item-exchange-selection", -> + $(".expedited-exchanges-warning").fadeIn() + ) diff --git a/backend/app/assets/javascripts/spree/backend/returns/return_item_selection.js b/backend/app/assets/javascripts/spree/backend/returns/return_item_selection.js new file mode 100644 index 00000000000..e0e419e689b --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/returns/return_item_selection.js @@ -0,0 +1,35 @@ +$(document).ready(function() { + var formFields = $("[data-hook='admin_customer_return_form_fields'], \ + [data-hook='admin_return_authorization_form_fields']"); + + if(formFields.length > 0) { + function checkAddItemBox() { + $(this).closest('tr').find('input.add-item').attr('checked', 'checked'); + updateSuggestedAmount(); + } + + function updateSuggestedAmount() { + var totalPretaxRefund = 0; + var checkedItems = formFields.find('input.add-item:checked'); + $.each(checkedItems, function(i, checkbox) { + totalPretaxRefund += parseFloat($(checkbox).parents('tr').find('.refund-amount-input').val()); + }); + + var displayTotal = isNaN(totalPretaxRefund) ? '' : totalPretaxRefund.toFixed(2); + formFields.find('span#total_pre_tax_refund').html(displayTotal); + } + + updateSuggestedAmount(); + + formFields.find('input#select-all').on('change', function(ev) { + var checkBoxes = $(ev.currentTarget).parents('table:first').find('input.add-item'); + checkBoxes.prop('checked', this.checked); + updateSuggestedAmount(); + }); + + formFields.find('input.add-item').on('change', updateSuggestedAmount); + formFields.find('.refund-amount-input').on('keyup', updateSuggestedAmount); + + formFields.find('input, select').not('.add-item').on('change', checkAddItemBox); + } +}); diff --git a/backend/app/assets/javascripts/spree/backend/shipments.js.erb b/backend/app/assets/javascripts/spree/backend/shipments.js.erb new file mode 100644 index 00000000000..c0089e5a441 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/shipments.js.erb @@ -0,0 +1,348 @@ +// Shipments AJAX API +$(document).ready(function () { + 'use strict'; + + // handle variant selection, show stock level. + $('#add_variant_id').change(function(){ + var variant_id = $(this).val(); + + var variant = _.find(window.variants, function(variant){ + return variant.id == variant_id + }) + $('#stock_details').html(variantStockTemplate({variant: variant})); + $('#stock_details').show(); + + $('button.add_variant').click(addVariantFromStockLocation); + + // Add some tips + $('.with-tip').powerTip({ + smartPlacement: true, + fadeInTime: 50, + fadeOutTime: 50, + intentPollInterval: 300 + }); + + }); + + //handle edit click + $('a.edit-item').click(toggleItemEdit); + + //handle cancel click + $('a.cancel-item').click(toggleItemEdit); + + //handle split click + $('a.split-item').click(startItemSplit); + + //handle save click + $('a.save-item').click(function(){ + var save = $(this); + var shipment_number = save.data('shipment-number'); + var variant_id = save.data('variant-id'); + + var quantity = parseInt(save.parents('tr').find('input.line_item_quantity').val()); + + toggleItemEdit(); + adjustShipmentItems(shipment_number, variant_id, quantity); + return false; + }); + + //handle delete click + $('a.delete-item').click(function(event){ + if (confirm(Spree.translations.are_you_sure_delete)) { + var del = $(this); + var shipment_number = del.data('shipment-number'); + var variant_id = del.data('variant-id'); + + toggleItemEdit(); + adjustShipmentItems(shipment_number, variant_id, 0); + } + return false; + }); + + // handle ship click + $('[data-hook=admin_shipment_form] a.ship').on('click', function () { + var link = $(this); + var shipment_number = link.data('shipment-number'); + var url = Spree.url(Spree.routes.shipments_api + '/' + shipment_number + '/ship.json'); + $.ajax({ + type: 'PUT', + url: url, + data: { + token: Spree.api_key + } + }).done(function () { + window.location.reload(); + }).error(function (msg) { + console.log(msg); + }); + }); + + // handle shipping method edit click + $('a.edit-method').click(toggleMethodEdit); + $('a.cancel-method').click(toggleMethodEdit); + + // handle shipping method save + $('[data-hook=admin_shipment_form] a.save-method').on('click', function (event) { + event.preventDefault(); + + var link = $(this); + var shipment_number = link.data('shipment-number'); + var selected_shipping_rate_id = link.parents('tbody').find("select#selected_shipping_rate_id[data-shipment-number='" + shipment_number + "']").val(); + var unlock = link.parents('tbody').find("input[name='open_adjustment'][data-shipment-number='" + shipment_number + "']:checked").val(); + var url = Spree.url(Spree.routes.shipments_api + '/' + shipment_number + '.json'); + + $.ajax({ + type: 'PUT', + url: url, + data: { + shipment: { + selected_shipping_rate_id: selected_shipping_rate_id, + unlock: unlock + }, + token: Spree.api_key + } + }).done(function () { + window.location.reload(); + }).error(function (msg) { + console.log(msg); + }); + }); + + var toggleTrackingEdit = function(event) { + event.preventDefault(); + + var link = $(this); + link.parents('tbody').find('tr.edit-tracking').toggle(); + link.parents('tbody').find('tr.show-tracking').toggle(); + } + + // handle tracking edit click + $('a.edit-tracking').click(toggleTrackingEdit); + $('a.cancel-tracking').click(toggleTrackingEdit); + + // handle tracking save + $('[data-hook=admin_shipment_form] a.save-tracking').on('click', function (event) { + event.preventDefault(); + + var link = $(this); + var shipment_number = link.data('shipment-number'); + var tracking = link.parents('tbody').find('input#tracking').val(); + var url = Spree.url(Spree.routes.shipments_api + '/' + shipment_number + '.json'); + + $.ajax({ + type: 'PUT', + url: url, + data: { + shipment: { + tracking: tracking + }, + token: Spree.api_key + } + }).done(function (data) { + link.parents('tbody').find('tr.edit-tracking').toggle(); + + var show = link.parents('tbody').find('tr.show-tracking'); + show.toggle(); + show.find('.tracking-value').html($("").html("<%= Spree.t(:tracking) %>: ")).append(data.tracking); + }); + }); +}); + +adjustShipmentItems = function(shipment_number, variant_id, quantity){ + var shipment = _.findWhere(shipments, {number: shipment_number + ''}); + var inventory_units = _.where(shipment.inventory_units, {variant_id: variant_id}); + + var url = Spree.routes.shipments_api + "/" + shipment_number; + + var new_quantity = 0; + if(inventory_units.lengthquantity){ + url += "/remove" + new_quantity = (inventory_units.length - quantity); + } + url += '.json'; + + if(new_quantity!=0){ + $.ajax({ + type: "PUT", + url: Spree.url(url), + data: { + variant_id: variant_id, + quantity: new_quantity, + token: Spree.api_key + } + }).done(function( msg ) { + window.location.reload(); + }); + } +} + +toggleMethodEdit = function(){ + var link = $(this); + link.parents('tbody').find('tr.edit-method').toggle(); + link.parents('tbody').find('tr.show-method').toggle(); + + return false; +} + +toggleItemEdit = function(){ + var link = $(this); + link.parent().find('a.edit-item').toggle(); + link.parent().find('a.cancel-item').toggle(); + link.parent().find('a.split-item').toggle(); + link.parent().find('a.save-item').toggle(); + link.parent().find('a.delete-item').toggle(); + link.parents('tr').find('td.item-qty-show').toggle(); + link.parents('tr').find('td.item-qty-edit').toggle(); + + return false; +} + +startItemSplit = function(event){ + event.preventDefault(); + var link = $(this); + link.parent().find('a.edit-item').toggle(); + link.parent().find('a.split-item').toggle(); + link.parent().find('a.delete-item').toggle(); + var variant_id = link.data('variant-id'); + + var variant = {}; + $.ajax({ + type: "GET", + async: false, + url: Spree.url(Spree.routes.variants_api), + data: { + q: { + "id_eq": variant_id + }, + token: Spree.api_key + } + }).success(function( data ) { + variant = data['variants'][0]; + }).error(function( msg ) { + console.log(msg); + }); + + var max_quantity = link.closest('tr').data('item-quantity'); + var split_item_template = Handlebars.compile($('#variant_split_template').text()); + link.closest('tr').after(split_item_template({ variant: variant, shipments: shipments, max_quantity: max_quantity })); + $('a.cancel-split').click(cancelItemSplit); + $('a.save-split').click(completeItemSplit); + + // Add some tips + $('.with-tip').powerTip({ + smartPlacement: true, + fadeInTime: 50, + fadeOutTime: 50, + intentPollInterval: 300 + }); + $('#item_stock_location').select2({ width: 'resolve', placeholder: Spree.translations.item_stock_placeholder }); +} + +completeItemSplit = function(event) { + event.preventDefault(); + + if($('#item_stock_location').val() === ""){ + alert('Please select the split destination.'); + return false; + } + + var link = $(this); + var stock_item_row = link.closest('tr'); + var variant_id = stock_item_row.data('variant-id'); + var quantity = stock_item_row.find('#item_quantity').val(); + + var stock_location_id = stock_item_row.find('#item_stock_location').val(); + var original_shipment_number = link.closest('tbody').data('shipment-number'); + + var selected_shipment = stock_item_row.find($('#item_stock_location').select2('data').element); + var target_shipment_number = selected_shipment.data('shipment-number'); + var new_shipment = selected_shipment.data('new-shipment'); + + if (stock_location_id != 'new_shipment') { + if (new_shipment != undefined) { + // TRANSFER TO A NEW LOCATION + $.ajax({ + type: "POST", + async: false, + url: Spree.url(Spree.routes.shipments_api + "/transfer_to_location"), + data: { + original_shipment_number: original_shipment_number, + variant_id: variant_id, + quantity: quantity, + stock_location_id: stock_location_id, + token: Spree.api_key + } + }).error(function(msg) { + alert(msg.responseJSON['message']); + }).done(function(msg) { + window.Spree.advanceOrder(); + }); + } else { + // TRANSFER TO AN EXISTING SHIPMENT + $.ajax({ + type: "POST", + async: false, + url: Spree.url(Spree.routes.shipments_api + "/transfer_to_shipment"), + data: { + original_shipment_number: original_shipment_number, + target_shipment_number: target_shipment_number, + variant_id: variant_id, + quantity: quantity, + token: Spree.api_key + } + }).error(function(msg) { + alert(msg.responseJSON['message']); + }).done(function(msg) { + window.Spree.advanceOrder(); + }); + } + } +} + +cancelItemSplit = function(event) { + event.preventDefault(); + var link = $(this); + var prev_row = link.closest('tr').prev(); + link.closest('tr').remove(); + prev_row.find('a.edit-item').toggle(); + prev_row.find('a.split-item').toggle(); + prev_row.find('a.delete-item').toggle(); +} + +addVariantFromStockLocation = function(event) { + event.preventDefault(); + + $('#stock_details').hide(); + + var variant_id = $('input.variant_autocomplete').val(); + var stock_location_id = $(this).data('stock-location-id'); + var quantity = $("input.quantity[data-stock-location-id='" + stock_location_id + "']").val(); + + var shipment = _.find(shipments, function(shipment){ + return shipment.stock_location_id == stock_location_id && (shipment.state == 'ready' || shipment.state == 'pending'); + }); + + if(shipment==undefined){ + $.ajax({ + type: "POST", + url: Spree.url(Spree.routes.shipments_api + "?shipment[order_id]=" + order_number), + data: { + variant_id: variant_id, + quantity: quantity, + stock_location_id: stock_location_id, + token: Spree.api_key + } + }).done(function( msg ) { + window.location.reload(); + }).error(function( msg ) { + console.log(msg); + }); + }else{ + //add to existing shipment + adjustShipmentItems(shipment.number, variant_id, quantity); + } + return 1 +} diff --git a/backend/app/assets/javascripts/spree/backend/spree-select2.js.erb b/backend/app/assets/javascripts/spree/backend/spree-select2.js.erb new file mode 100644 index 00000000000..65b09d12c7b --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/spree-select2.js.erb @@ -0,0 +1,23 @@ +//= require select2 +jQuery(function($) { + // Make select beautiful + $('select.select2').select2({ + allowClear: true, + dropdownAutoWidth: true + }); + + function format_taxons(taxon) { + new_taxon = taxon.text.replace('->', '') + return new_taxon; + } + + $("#product_taxon_ids").on({ + change: function(e){ + $('.select2-search-choice .with-tip').powerTip({ + smartPlacement: true, + fadeInTime: 50, + fadeOutTime: 50 + }) + } + }) +}) diff --git a/backend/app/assets/javascripts/spree/backend/states.js b/backend/app/assets/javascripts/spree/backend/states.js new file mode 100755 index 00000000000..08c931ccae1 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/states.js @@ -0,0 +1,11 @@ +$(document).ready(function () { + 'use strict'; + + $('#country').on('change', function () { + var new_state_link_href = $('#new_state_link a').prop('href'); + var selected_country_id = $('#country option:selected').prop('value'); + var new_link = new_state_link_href.replace(/countries\/(\d+)/, + 'countries/' + selected_country_id); + $('#new_state_link a').attr('href', new_link); + }); +}); \ No newline at end of file diff --git a/backend/app/assets/javascripts/spree/backend/stock_management.js.coffee b/backend/app/assets/javascripts/spree/backend/stock_management.js.coffee new file mode 100644 index 00000000000..6602ed4be23 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/stock_management.js.coffee @@ -0,0 +1,9 @@ +jQuery -> + $('.stock_item_backorderable').on 'click', -> + $(@).parent('form').submit() + $('.toggle_stock_item_backorderable').on 'submit', -> + $.ajax + type: @method + url: @action + data: $(@).serialize() + false diff --git a/backend/app/assets/javascripts/spree/backend/stock_movement.js.coffee b/backend/app/assets/javascripts/spree/backend/stock_movement.js.coffee new file mode 100644 index 00000000000..490272ac1fb --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/stock_movement.js.coffee @@ -0,0 +1,19 @@ +jQuery -> + el = $('#stock_movement_stock_item_id') + el.select2 + placeholder: "Find a stock item" # translate + ajax: + url: Spree.url(Spree.routes.stock_items_api(el.data('stock-location-id'))) + data: (term, page) -> + q: + variant_product_name_cont: term + per_page: 50 + page: page + token: Spree.api_key + results: (data, page) -> + more = (page * 50) < data.count + return { results: data.stock_items, more: more } + formatResult: (stock_item) -> + variantTemplate({ variant: stock_item.variant }) + formatSelection: (stock_item) -> + "#{stock_item.variant.name} (#{stock_item.variant.options_text})" diff --git a/backend/app/assets/javascripts/spree/backend/stock_transfer.js.coffee b/backend/app/assets/javascripts/spree/backend/stock_transfer.js.coffee new file mode 100644 index 00000000000..5907db653ee --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/stock_transfer.js.coffee @@ -0,0 +1,196 @@ +$ -> + # Base Model for transfer line items + class TransferVariant + constructor: (@variant) -> + @id = @variant.id + @name = "#{@variant.name} - #{@variant.sku}" + @quantity = 0 + + add: (quantity) -> + @quantity += quantity + + # Model for stock items which validate quantity with count on hand + class TransferStockItem extends TransferVariant + constructor: (@stock_item) -> + super(@stock_item.variant) + @count_on_hand = @stock_item.count_on_hand + @name = "#{@variant.name} - #{@variant.sku} (#{@count_on_hand})" + + add: (quantity) -> + @quantity += quantity + @quantity = @count_on_hand if @quantity > @count_on_hand + + # Manages source and destination selections + class TransferLocations + constructor: -> + @source = $('#transfer_source_location_id') + @destination = $('#transfer_destination_location_id') + + @source.change => @populate_destination() + + $('#transfer_receive_stock').change (event) => @receive_stock_change(event) + + $.getJSON Spree.url(Spree.routes.stock_locations_api) + '?token=' + Spree.api_key, (data) => + @locations = (location for location in data.stock_locations) + @force_receive_stock() if @locations.length < 2 + + @populate_source() + @populate_destination() + + force_receive_stock: -> + $('#receive_stock_field').hide() + $('#transfer_receive_stock').prop('checked', true) + @toggle_source_location true + + is_source_location_hidden: -> + $('#transfer_source_location_id_field').css('visibility') == 'hidden' + + toggle_source_location: (hide=false) -> + @source.trigger('change') + if @is_source_location_hidden() and not hide + $('#transfer_source_location_id_field').css('visibility', 'visible') + else + $('#transfer_source_location_id_field').css('visibility', 'hidden') + + receive_stock_change: (event) -> + @toggle_source_location event.target.checked + @populate_destination(!event.target.checked) + + populate_source: -> + @populate_select @source + @source.trigger('change') + + populate_destination: (except_source=true) -> + if @is_source_location_hidden() + @populate_select @destination + else + @populate_select @destination, parseInt(@source.val()) + + populate_select: (select, except=0) -> + select.children('option').remove() + for location in @locations when location.id isnt except + select.append $('').text(location.name).attr('value', location.id) + select.select2() + + # Populates variants drop down + class TransferVariants + constructor: -> + $('#transfer_source_location_id').change => @refresh_variants() + + receiving_stock: -> + $( "#transfer_receive_stock:checked" ).length > 0 + + refresh_variants: -> + if @receiving_stock() + @_search_transfer_variants() + else + @_search_transfer_stock_items() + + _search_transfer_variants: -> + @build_select(Spree.url(Spree.routes.variants_api), 'product_name_or_sku_cont') + + _search_transfer_stock_items: -> + stock_location_id = $('#transfer_source_location_id').val() + @build_select(Spree.url(Spree.routes.stock_locations_api + "/#{stock_location_id}/stock_items"), + 'variant_product_name_or_variant_sku_cont') + + format_variant_result: (result) -> + "#{result.name} - #{result.sku}" + + build_select: (url, query) -> + $('#transfer_variant').select2 + minimumInputLength: 3 + ajax: + url: url + datatype: "json" + data: (term, page) -> + query_object = {} + query_object[query] = term + q: query_object + token: Spree.api_key + + results: (data, page) -> + result = data["variants"] || data["stock_items"] + # Format stock items as variants + if data["stock_items"]? + result = _(result).map (variant) -> + variant.variant + window.variants = result + results: result + + formatResult: @format_variant_result + formatSelection: (variant) -> + if !!variant.options_text + variant.name + " (#{variant.options_text})" + " - #{variant.sku}" + else + variant.name + " - #{variant.sku}" + + + # Add/Remove variant line items + class TransferAddVariants + constructor: -> + @variants = [] + @template = Handlebars.compile $('#transfer_variant_template').html() + + $('#transfer_source_location_id').change (event) => @clear_variants() + + $('button.transfer_add_variant').click (event) => + event.preventDefault() + if $('#transfer_variant').select2('data')? + @add_variant() + else + alert('Please select a variant first') + + $('#transfer-variants-table').on 'click', '.transfer_remove_variant', (event) => + event.preventDefault() + @remove_variant $(event.target) + + $('button.transfer_transfer').click => + unless @variants.length > 0 + alert('no variants to transfer') + false + + add_variant: -> + variant = $('#transfer_variant').select2('data') + quantity = parseInt $('#transfer_variant_quantity').val() + + variant = @find_or_add(variant) + variant.add(quantity) + @render() + + find_or_add: (variant) -> + if existing = _.find(@variants, (v) -> v.id == variant.id) + return existing + else + variant = new TransferVariant($.extend({}, variant)) + @variants.push variant + return variant + + remove_variant: (target) -> + variant_id = parseInt(target.data('variantId')) + @variants = (v for v in @variants when v.id isnt variant_id) + @render() + + clear_variants: -> + @variants = [] + @render() + + contains: (id) -> + _.contains(_.pluck(@variants, 'id'), id) + + render: -> + if @variants.length is 0 + $('#transfer-variants-table').hide() + $('.no-objects-found').show() + else + $('#transfer-variants-table').show() + $('.no-objects-found').hide() + + rendered = @template { variants: @variants } + $('#transfer_variants_tbody').html(rendered) + + # Main + if $('#transfer_source_location_id').length > 0 + transfer_locations = new TransferLocations + transfer_variants = new TransferVariants + transfer_add_variants = new TransferAddVariants diff --git a/backend/app/assets/javascripts/spree/backend/taxon_autocomplete.js.erb b/backend/app/assets/javascripts/spree/backend/taxon_autocomplete.js.erb new file mode 100644 index 00000000000..3dd164f23a0 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/taxon_autocomplete.js.erb @@ -0,0 +1,51 @@ +'use strict'; + +var set_taxon_select = function(){ + if ($('#product_taxon_ids').length > 0) { + $('#product_taxon_ids').select2({ + placeholder: Spree.translations.taxon_placeholder, + multiple: true, + initSelection: function (element, callback) { + var url = Spree.url(Spree.routes.taxons_search, { + ids: element.val(), + token: Spree.api_key + }); + return $.getJSON(url, null, function (data) { + return callback(data['taxons']); + }); + }, + ajax: { + url: Spree.routes.taxons_search, + datatype: 'json', + data: function (term, page) { + return { + per_page: 50, + page: page, + without_children: true, + q: { + name_cont: term + }, + token: Spree.api_key + }; + }, + results: function (data, page) { + var more = page < data.pages; + return { + results: data['taxons'], + more: more + }; + } + }, + formatResult: function (taxon) { + return taxon.pretty_name; + }, + formatSelection: function (taxon) { + return taxon.pretty_name; + } + }); + } +} + +$(document).ready(function () { + set_taxon_select() +}); diff --git a/backend/app/assets/javascripts/spree/backend/taxon_tree_menu.js.coffee b/backend/app/assets/javascripts/spree/backend/taxon_tree_menu.js.coffee new file mode 100644 index 00000000000..0f5397d6673 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/taxon_tree_menu.js.coffee @@ -0,0 +1,22 @@ +root = exports ? this + +root.taxon_tree_menu = (obj, context) -> + + base_url = Spree.url(Spree.routes.taxonomy_taxons_path) + admin_base_url = Spree.url(Spree.routes.admin_taxonomy_taxons_path) + edit_url = admin_base_url.clone() + edit_url.setPath(edit_url.path() + '/' + obj.attr("id") + "/edit"); + + create: + label: " " + Spree.translations.add, + action: (obj) -> context.create(obj) + rename: + label: " " + Spree.translations.rename, + action: (obj) -> context.rename(obj) + remove: + label: " " + Spree.translations.remove, + action: (obj) -> context.remove(obj) + edit: + separator_before: true, + label: " " + Spree.translations.edit, + action: (obj) -> window.location = edit_url.toString() diff --git a/backend/app/assets/javascripts/spree/backend/taxonomy.js.coffee b/backend/app/assets/javascripts/spree/backend/taxonomy.js.coffee new file mode 100644 index 00000000000..4c8bcf0aeee --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/taxonomy.js.coffee @@ -0,0 +1,147 @@ +handle_ajax_error = (XMLHttpRequest, textStatus, errorThrown) -> + $.jstree.rollback(last_rollback) + $("#ajax_error").show().html("" + server_error + "
" + taxonomy_tree_error) + +handle_move = (e, data) -> + last_rollback = data.rlbk + position = data.rslt.cp + node = data.rslt.o + new_parent = data.rslt.np + + url = Spree.url(base_url).clone() + url.setPath url.path() + '/' + node.prop("id") + $.ajax + type: "POST", + dataType: "json", + url: url.toString(), + data: { + _method: "put", + "taxon[parent_id]": new_parent.prop("id"), + "taxon[child_index]": position, + token: Spree.api_key + }, + error: handle_ajax_error + + true + +handle_create = (e, data) -> + last_rollback = data.rlbk + node = data.rslt.obj + name = data.rslt.name + position = data.rslt.position + new_parent = data.rslt.parent + + $.ajax + type: "POST", + dataType: "json", + url: base_url.toString(), + data: { + "taxon[name]": name, + "taxon[parent_id]": new_parent.prop("id"), + "taxon[child_index]": position, + token: Spree.api_key + }, + error: handle_ajax_error, + success: (data,result) -> + node.prop('id', data.id) + +handle_rename = (e, data) -> + last_rollback = data.rlbk + node = data.rslt.obj + name = data.rslt.new_name + + url = Spree.url(base_url).clone() + url.setPath(url.path() + '/' + node.prop("id")) + + $.ajax + type: "POST", + dataType: "json", + url: url.toString(), + data: { + _method: "put", + "taxon[name]": name, + token: Spree.api_key + }, + error: handle_ajax_error + +handle_delete = (e, data) -> + last_rollback = data.rlbk + node = data.rslt.obj + delete_url = base_url.clone() + delete_url.setPath delete_url.path() + '/' + node.prop("id") + jConfirm Spree.translations.are_you_sure_delete, Spree.translations.confirm_delete, (r) -> + if r + $.ajax + type: "POST", + dataType: "json", + url: delete_url.toString(), + data: { + _method: "delete", + token: Spree.api_key + }, + error: handle_ajax_error + else + $.jstree.rollback(last_rollback) + last_rollback = null + +root = exports ? this +root.setup_taxonomy_tree = (taxonomy_id) -> + if taxonomy_id != undefined + # this is defined within admin/taxonomies/edit + root.base_url = Spree.url(Spree.routes.taxonomy_taxons_path) + + $.ajax + url: Spree.url(base_url.path().replace("/taxons", "/jstree")).toString(), + data: + token: Spree.api_key + success: (taxonomy) -> + last_rollback = null + + conf = + json_data: + data: taxonomy, + ajax: + url: (e) -> + Spree.url(base_url.path() + '/' + e.prop('id') + '/jstree' + '?token=' + Spree.api_key).toString() + themes: + theme: "apple", + url: Spree.url(Spree.routes.jstree_theme_path) + strings: + new_node: new_taxon, + loading: Spree.translations.loading + "..." + crrm: + move: + check_move: (m) -> + position = m.cp + node = m.o + new_parent = m.np + + # no parent or cant drag and drop + if !new_parent || node.prop("rel") == "root" + return false + + # can't drop before root + if new_parent.prop("id") == "taxonomy_tree" && position == 0 + return false + + true + contextmenu: + items: (obj) -> + taxon_tree_menu(obj, this) + plugins: ["themes", "json_data", "dnd", "crrm", "contextmenu"] + + $("#taxonomy_tree").jstree(conf) + .bind("move_node.jstree", handle_move) + .bind("remove.jstree", handle_delete) + .bind("create.jstree", handle_create) + .bind("rename.jstree", handle_rename) + .bind "loaded.jstree", -> + $(this).jstree("core").toggle_node($('.jstree-icon').first()) + + $("#taxonomy_tree a").on "dblclick", (e) -> + $("#taxonomy_tree").jstree("rename", this) + + # surpress form submit on enter/return + $(document).keypress (e) -> + if e.keyCode == 13 + e.preventDefault() diff --git a/backend/app/assets/javascripts/spree/backend/taxons.js.coffee b/backend/app/assets/javascripts/spree/backend/taxons.js.coffee new file mode 100644 index 00000000000..417c55e5653 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/taxons.js.coffee @@ -0,0 +1,53 @@ +$(document).ready -> + window.productTemplate = Handlebars.compile($('#product_template').text()); + $('#taxon_products').sortable(); + $('#taxon_products').on "sortstop", (event, ui) -> + $.ajax + url: Spree.routes.classifications_api, + method: 'PUT', + data: + token: Spree.api_key, + product_id: ui.item.data('product-id'), + taxon_id: $('#taxon_id').val(), + position: ui.item.index() + + if $('#taxon_id').length > 0 + $('#taxon_id').select2 + dropdownCssClass: "taxon_select_box", + placeholder: Spree.translations.find_a_taxon, + ajax: + url: Spree.routes.taxons_search, + datatype: 'json', + data: (term, page) -> + per_page: 50, + page: page, + token: Spree.api_key, + q: + name_cont: term + results: (data, page) -> + more = page < data.pages; + results: data['taxons'], + more: more + formatResult: (taxon) -> + taxon.pretty_name; + formatSelection: (taxon) -> + taxon.pretty_name; + + $('#taxon_id').on "change", (e) -> + el = $('#taxon_products') + $.ajax + url: Spree.routes.taxon_products_api, + data: + id: e.val, + token: Spree.api_key + success: (data) -> + el.empty(); + if data.products.length == 0 + $('#sorting_explanation').hide() + $('#taxon_products').html("

" + Spree.translations.no_results + "

") + else + for product in data.products + if product.master.images[0] != undefined && product.master.images[0].small_url != undefined + product.image = product.master.images[0].small_url + el.append(productTemplate({ product: product })) + $('#sorting_explanation').show() diff --git a/backend/app/assets/javascripts/spree/backend/underscore-min.js b/backend/app/assets/javascripts/spree/backend/underscore-min.js new file mode 100644 index 00000000000..32ca0c1b142 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/underscore-min.js @@ -0,0 +1,1227 @@ +// Underscore.js 1.4.4 +// =================== + +// > http://underscorejs.org +// > (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// > Underscore may be freely distributed under the MIT license. + +// Baseline setup +// -------------- +(function() { + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.4.4'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? null : []; + return _[first ? 'find' : 'filter'](obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity, value: -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity, value: Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + index : index, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index < right.index ? -1 : 1; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(obj, value, context, behavior) { + var result = {}; + var iterator = lookupIterator(value || _.identity); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); + }; + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = function(obj, value, context) { + return group(obj, value, context, function(result, key) { + if (!_.has(result, key)) result[key] = 0; + result[key]++; + }); + }; + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + each(input, function(value) { + if (_.isArray(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(concat.apply(ArrayProto, arguments)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(args, "" + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, l = list.length; i < l; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, l = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < l; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + var args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, result; + var previous = 0; + var later = function() { + previous = new Date; + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) result = func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(context, args); + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var values = []; + for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var pairs = []; + for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(n); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named property is a function then invoke it; + // otherwise, return it. + _.result = function(object, property) { + if (object == null) return null; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + +}).call(this); diff --git a/backend/app/assets/javascripts/spree/backend/user_picker.js b/backend/app/assets/javascripts/spree/backend/user_picker.js new file mode 100644 index 00000000000..2fdc768e0bf --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/user_picker.js @@ -0,0 +1,40 @@ +$.fn.userAutocomplete = function () { + 'use strict'; + + this.select2({ + minimumInputLength: 1, + multiple: true, + initSelection: function (element, callback) { + $.get(Spree.routes.user_search, { + ids: element.val() + }, function (data) { + callback(data); + }); + }, + ajax: { + url: Spree.routes.user_search, + datatype: 'json', + data: function (term) { + return { + q: term, + token: Spree.api_key + }; + }, + results: function (data) { + return { + results: data + }; + } + }, + formatResult: function (user) { + return user.email; + }, + formatSelection: function (user) { + return user.email; + } + }); +}; + +$(document).ready(function () { + $('.user_picker').userAutocomplete(); +}); diff --git a/backend/app/assets/javascripts/spree/backend/variant_autocomplete.js.coffee.erb b/backend/app/assets/javascripts/spree/backend/variant_autocomplete.js.coffee.erb new file mode 100644 index 00000000000..700d3ad552a --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/variant_autocomplete.js.coffee.erb @@ -0,0 +1,37 @@ +# variant autocompletion +$(document).ready -> + if $("#variant_autocomplete_template").length > 0 + window.variantTemplate = Handlebars.compile($("#variant_autocomplete_template").text()) + window.variantStockTemplate = Handlebars.compile($("#variant_autocomplete_stock_template").text()) + window.variantLineItemTemplate = Handlebars.compile($("#variant_line_items_autocomplete_stock_template").text()) + return + +formatVariantResult = (variant) -> + variant.image = variant.images[0].mini_url if variant["images"][0] isnt `undefined` and variant["images"][0].mini_url isnt `undefined` + variantTemplate variant: variant + +$.fn.variantAutocomplete = -> + @select2 + placeholder: Spree.translations.variant_placeholder + minimumInputLength: 3 + initSelection: (element, callback) -> + $.get Spree.routes.variants_search + "/" + element.val(), {}, (data) -> + callback data + ajax: + url: Spree.url(Spree.routes.variants_api) + datatype: "json" + data: (term, page) -> + q: + product_name_or_sku_cont: term + token: Spree.api_key + + results: (data, page) -> + window.variants = data["variants"] + results: data["variants"] + + formatResult: formatVariantResult + formatSelection: (variant) -> + if !!variant.options_text + variant.name + " (#{variant.options_text})" + else + variant.name diff --git a/backend/app/assets/javascripts/spree/backend/variant_management.js.coffee b/backend/app/assets/javascripts/spree/backend/variant_management.js.coffee new file mode 100644 index 00000000000..46990003908 --- /dev/null +++ b/backend/app/assets/javascripts/spree/backend/variant_management.js.coffee @@ -0,0 +1,10 @@ +jQuery -> + $('.track_inventory_checkbox').on 'click', -> + $(@).siblings('.variant_track_inventory').val($(@).is(':checked')) + $(@).parent('form').submit() + $('.toggle_variant_track_inventory').on 'submit', -> + $.ajax + type: @method + url: @action + data: $(@).serialize() + false diff --git a/core/app/assets/javascripts/admin/zone.js.coffee b/backend/app/assets/javascripts/spree/backend/zone.js.coffee similarity index 90% rename from core/app/assets/javascripts/admin/zone.js.coffee rename to backend/app/assets/javascripts/spree/backend/zone.js.coffee index 2c70c78828a..885521190d7 100644 --- a/core/app/assets/javascripts/admin/zone.js.coffee +++ b/backend/app/assets/javascripts/spree/backend/zone.js.coffee @@ -1,14 +1,18 @@ $ -> - if ($ '#country_based').is(':checked') - show_country() - else - show_state() ($ '#country_based').click -> show_country() ($ '#state_based').click -> show_state() + if ($ '#country_based').is(':checked') + show_country() + else if ($ '#state_based').is(':checked') + show_state() + else + show_state() + ($ '#state_based').click() + show_country = -> ($ '#state_members :input').each -> diff --git a/backend/app/assets/javascripts/spree/frontend/backend.js b/backend/app/assets/javascripts/spree/frontend/backend.js new file mode 100644 index 00000000000..e44d478def9 --- /dev/null +++ b/backend/app/assets/javascripts/spree/frontend/backend.js @@ -0,0 +1 @@ +// Placeholder for backend dummy application diff --git a/backend/app/assets/stylesheets/spree/backend.css b/backend/app/assets/stylesheets/spree/backend.css new file mode 100644 index 00000000000..eeb84ea1520 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend.css @@ -0,0 +1,17 @@ +/* + * This is a manifest file that'll automatically include all the stylesheets available in this directory + * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at + * the top of the compiled file, but it's generally better to create a new file per style scope. + + *= require jquery.alerts/jquery.alerts + *= require jquery.alerts/jquery.alerts.spree + *= require responsive-tables + *= require normalize + *= require skeleton + *= require jquery-ui/datepicker + *= require jquery-ui/autocomplete + *= require jquery.powertip + *= require select2 + + *= require spree/backend/spree_admin +*/ diff --git a/core/app/assets/stylesheets/admin/components/_actions.scss b/backend/app/assets/stylesheets/spree/backend/components/_actions.scss similarity index 91% rename from core/app/assets/stylesheets/admin/components/_actions.scss rename to backend/app/assets/stylesheets/spree/backend/components/_actions.scss index 811ef0af236..1aeac5e630f 100644 --- a/core/app/assets/stylesheets/admin/components/_actions.scss +++ b/backend/app/assets/stylesheets/spree/backend/components/_actions.scss @@ -8,7 +8,7 @@ table tbody tr { } } - &.action-remove td, &.action-void td { + &.action-remove td, &.action-void td { text-decoration: line-through; &.actions { @@ -28,4 +28,4 @@ table tbody tr { td.actions { background-color: transparent !important; } -} \ No newline at end of file +} diff --git a/core/app/assets/stylesheets/admin/components/_date-picker.scss b/backend/app/assets/stylesheets/spree/backend/components/_date-picker.scss similarity index 95% rename from core/app/assets/stylesheets/admin/components/_date-picker.scss rename to backend/app/assets/stylesheets/spree/backend/components/_date-picker.scss index 90ba9b2579c..09149b69389 100644 --- a/core/app/assets/stylesheets/admin/components/_date-picker.scss +++ b/backend/app/assets/stylesheets/spree/backend/components/_date-picker.scss @@ -1,6 +1,6 @@ .date-range-filter { .range-divider { - padding: 0 6px 0 5px; + padding: 0; } input.datepicker { width: 96px !important; @@ -9,6 +9,7 @@ #ui-datepicker-div { @include border-radius($border-radius); + border-color: $color-3; padding: 0; margin-top: 10px; @@ -23,6 +24,7 @@ margin-top: -10px; left: 25px; z-index: 1; + display: block; } .ui-datepicker-header { @@ -47,13 +49,15 @@ } .ui-icon { + @extend [class^="icon-"]:before; + @extend .fa; + background-image: none; text-indent: 0; color: $color-1; width: 10px; margin-left: -5px; - @extend [class^="icon-"]:before; - + &:hover { color: very-light($color-2, 25); } @@ -63,14 +67,15 @@ left: 0; .ui-icon { - @extend .icon-arrow-left; + @extend .fa-arrow-left; } } + .ui-datepicker-next { right: 0; .ui-icon { - @extend .icon-arrow-right; + @extend .fa-arrow-right; } &:hover { diff --git a/core/app/assets/stylesheets/admin/components/_messages.scss b/backend/app/assets/stylesheets/spree/backend/components/_messages.scss similarity index 81% rename from core/app/assets/stylesheets/admin/components/_messages.scss rename to backend/app/assets/stylesheets/spree/backend/components/_messages.scss index 8300434d62c..15397ea513c 100644 --- a/core/app/assets/stylesheets/admin/components/_messages.scss +++ b/backend/app/assets/stylesheets/spree/backend/components/_messages.scss @@ -25,7 +25,7 @@ } } -.flash { +.flash, .alert { position: fixed; top: 0; left: 0; @@ -40,4 +40,15 @@ &.notice { background-color: rgba($color-notice, 0.8) } &.success { background-color: rgba($color-success, 0.8) } &.error { background-color: rgba($color-error, 0.8) } +} + +.alert { + position: relative; + font-weight: normal !important; + + a { + text-decoration: underline; + } + + &.error a { color: very-light($color-error, 10) } } \ No newline at end of file diff --git a/backend/app/assets/stylesheets/spree/backend/components/_navigation.scss b/backend/app/assets/stylesheets/spree/backend/components/_navigation.scss new file mode 100644 index 00000000000..41b7488c9ac --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/components/_navigation.scss @@ -0,0 +1,169 @@ +// Navigation +//--------------------------------------------------- +.inline-menu { + margin: 0; + -webkit-margin-before: 0; + -webkit-padding-start: 0; +} + +nav.menu { + ul { + list-style: none; + + li { + a { + padding: 10px 0; + display: block; + position: relative; + text-align: left; + border: 1px solid transparent; + text-transform: uppercase; + font-weight: 600; + font-size: 90%; + } + + &.active a { + color: $color-2; + border-left-width: 0; + border-bottom-color: $color-2; + } + + &:hover a { + color: $color-2; + } + } + } +} + +[data-hook="admin_login_navigation_bar"] { + ul { + text-align: right; + + li { + padding: 5px 0 5px 10px; + text-align: right; + font-size: 90%; + color: $color-link; + margin-top: 8px; + + &[data-hook="user-logged-in-as"] { + width: 50%; + color: $color-body-text; + } + + &.fa:before { + padding-right: 4px; + } + + &:hover { + i { + color: $color-2; + } + } + } + } +} + +#admin-menu { + background-color: $color-3; + + ul{ + display: table; + table-layout: fixed; + width: 100%; + } + + li { + display: table-cell; + + a { + display: block; + padding: 25px 15px; + color: $color-1 !important; + text-transform: uppercase; + position: relative; + text-align: center; + + @media(max-width: 1009px) { + top: -3px; + margin-bottom: -5px; + } + + i { + display: inline; + } + + &:hover { + background-color: $color-2; + + &:after { + content: ''; + position: absolute; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 5px solid $color-2; + bottom: 0px; + margin-bottom: -5px; + left: 50%; + margin-left: -10px; + z-index: 1; + } + } + + span.text { + font-weight: 600; + } + } + + .dropdown { + width: 300px; + background-color: $color-3; + width: 200px; + z-index: 100000; + + > li { + width: 200px !important; + + a:after { + display: none; + } + } + } + + &.selected a { + @extend a:hover; + } + } +} + +#sub-menu { + background-color: $color-2; + padding-bottom: 0; + + li { + a { + display: block; + padding: 12px 20px; + color: $color-1; + text-align: center; + text-transform: uppercase; + position: relative; + font-size: 85%; + } + + &.selected a, a:hover { + &:after { + content: ''; + position: absolute; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 5px solid $color-2; + bottom: 0px; + margin-bottom: -5px; + left: 50%; + margin-left: -10px; + z-index: 1; + } + } + } +} diff --git a/core/app/assets/stylesheets/admin/components/_pagination.scss b/backend/app/assets/stylesheets/spree/backend/components/_pagination.scss similarity index 100% rename from core/app/assets/stylesheets/admin/components/_pagination.scss rename to backend/app/assets/stylesheets/spree/backend/components/_pagination.scss diff --git a/core/app/assets/stylesheets/admin/components/_product_autocomplete.scss b/backend/app/assets/stylesheets/spree/backend/components/_product_autocomplete.scss similarity index 100% rename from core/app/assets/stylesheets/admin/components/_product_autocomplete.scss rename to backend/app/assets/stylesheets/spree/backend/components/_product_autocomplete.scss diff --git a/core/app/assets/stylesheets/admin/components/_progress.scss b/backend/app/assets/stylesheets/spree/backend/components/_progress.scss similarity index 100% rename from core/app/assets/stylesheets/admin/components/_progress.scss rename to backend/app/assets/stylesheets/spree/backend/components/_progress.scss diff --git a/backend/app/assets/stylesheets/spree/backend/components/_sidebar.scss b/backend/app/assets/stylesheets/spree/backend/components/_sidebar.scss new file mode 100644 index 00000000000..960de96ab01 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/components/_sidebar.scss @@ -0,0 +1,26 @@ +// Sidebar +//--------------------------------------------------- +#sidebar { + overflow: visible; + border-top: 1px solid $color-border; + margin-top: 17px; + + .sidebar-title { + color: $color-2; + text-transform: uppercase; + text-align: center; + font-size: 14px; + font-weight: 600; + + > span { + display: inline; + background: #fff; + padding: 5px 10px; + position: relative; + top: -14px; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + } +} diff --git a/backend/app/assets/stylesheets/spree/backend/components/_states.scss b/backend/app/assets/stylesheets/spree/backend/components/_states.scss new file mode 100644 index 00000000000..97ad366abfb --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/components/_states.scss @@ -0,0 +1,34 @@ +.state { + text-transform: uppercase; + font-size: 80%; + font-weight: 600; + + &:before { + content: ''; + position: relative; + display: inline-block; + margin-right: 3px; + border-radius: $body-font-size/2; + width: $body-font-size - 4px; + height: $body-font-size - 4px; + } + + @each $state in $states { + &.#{$state}:before { + background-color: get-value($states, $states-bg-colors, $state); + + // &, a { + // color: get-value($states, $states-text-colors, $state); + // } + } + } +} + +table tbody tr { + &[class*="state"] td:first-child { + border-left-width: 3px; + } + &.state-complete td:first-child { border-left-color: $color-success } + &.state-cart td:first-child { border-left-color: very-light($color-notice, 6) } + &.state-canceled td:first-child { border-left-color: $color-error } +} diff --git a/core/app/assets/stylesheets/admin/components/_table-filter.scss b/backend/app/assets/stylesheets/spree/backend/components/_table-filter.scss similarity index 100% rename from core/app/assets/stylesheets/admin/components/_table-filter.scss rename to backend/app/assets/stylesheets/spree/backend/components/_table-filter.scss diff --git a/core/app/assets/stylesheets/admin/globals/_functions.scss b/backend/app/assets/stylesheets/spree/backend/globals/_functions.scss similarity index 100% rename from core/app/assets/stylesheets/admin/globals/_functions.scss rename to backend/app/assets/stylesheets/spree/backend/globals/_functions.scss diff --git a/core/app/assets/stylesheets/admin/globals/_mixins.scss b/backend/app/assets/stylesheets/spree/backend/globals/_mixins.scss similarity index 100% rename from core/app/assets/stylesheets/admin/globals/_mixins.scss rename to backend/app/assets/stylesheets/spree/backend/globals/_mixins.scss diff --git a/backend/app/assets/stylesheets/spree/backend/globals/_variables.scss b/backend/app/assets/stylesheets/spree/backend/globals/_variables.scss new file mode 100644 index 00000000000..414c35a65a2 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/globals/_variables.scss @@ -0,0 +1,171 @@ +// ------------------------------------------------------------- +// Variables used in all other files +//-------------------------------------------------------------- + +// Fonts +//-------------------------------------------------------------- +$base-font-family: "Open Sans", "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif !default; + +// Colors +//-------------------------------------------------------------- + +// Basic color palette for admin +$color-1: #FFFFFF !default; // White +$color-2: #9FC820 !default; // Green +$color-3: #5498DA !default; // Light Blue +$color-4: #6788A2 !default; // Dark Blue +$color-5: #C60F13 !default; // Red +$color-6: #FF9300 !default; // Yellow + +// Body base colors +$color-body-bg: $color-1 !default; +$color-body-text: $color-4 !default; +$color-headers: $color-4 !default; +$color-link: $color-3 !default; +$color-link-hover: $color-2 !default; +$color-link-active: $color-2 !default; +$color-link-focus: $color-2 !default; +$color-link-visited: $color-3 !default; +$color-border: very-light($color-3, 12) !default; + +// Basic flash colors +$color-success: $color-2 !default; +$color-notice: $color-6 !default; +$color-error: $color-5 !default; + +// Table colors +$color-tbl-odd: $color-1 !default; +$color-tbl-even: very-light($color-3, 4) !default; +$color-tbl-thead: very-light($color-3, 4) !default; + +// Button colors +$color-btn-bg: $color-3 !default; +$color-btn-text: $color-1 !default; +$color-btn-hover-bg: $color-2 !default; +$color-btn-hover-text: $color-1 !default; + +// Actions colors +$color-action-edit-bg: very-light($color-success, 5 ) !default; +$color-action-edit-brd: very-light($color-success, 20 ) !default; +$color-action-clone-bg: very-light($color-notice, 5 ) !default; +$color-action-clone-brd: very-light($color-notice, 15 ) !default; +$color-action-remove-bg: very-light($color-error, 5 ) !default; +$color-action-remove-brd: very-light($color-error, 10 ) !default; +$color-action-void-bg: very-light($color-error, 10 ) !default; +$color-action-void-brd: very-light($color-error, 20 ) !default; +$color-action-cancel-bg: very-light($color-notice, 10 ) !default; +$color-action-cancel-brd: very-light($color-notice, 20 ) !default; +$color-action-capture-bg: very-light($color-success, 5 ) !default; +$color-action-capture-brd: very-light($color-success, 20 ) !default; +$color-action-save-bg: very-light($color-success, 5 ) !default; +$color-action-save-brd: very-light($color-success, 20 ) !default; +$color-action-mail-bg: very-light($color-success, 5 ) !default; +$color-action-mail-brd: very-light($color-success, 20 ) !default; + +// Select2 select field colors +$color-sel-bg: $color-3 !default; +$color-sel-text: $color-1 !default; +$color-sel-hover-bg: $color-2 !default; +$color-sel-hover-text: $color-1 !default; + +// Text inputs colors +$color-txt-brd: $color-border !default; +$color-txt-text: $color-3 !default; +$color-txt-hover-brd: $color-2 !default; + +// States label colors +$color-ste-complete-bg: $color-success !default; +$color-ste-complete-text: $color-1 !default; +$color-ste-completed-bg: $color-success !default; +$color-ste-completed-text: $color-1 !default; +$color-ste-sold-bg: $color-success !default; +$color-ste-sold-text: $color-1 !default; +$color-ste-pending-bg: $color-notice !default; +$color-ste-pending-text: $color-1 !default; +$color-ste-awaiting_return-bg: $color-notice !default; +$color-ste-awaiting_return-text: $color-1 !default; +$color-ste-returned-bg: $color-notice !default; +$color-ste-returned-text: $color-1 !default; +$color-ste-credit_owed-bg: $color-notice !default; +$color-ste-credit_owed-text: $color-1 !default; +$color-ste-paid-bg: $color-success !default; +$color-ste-paid-text: $color-1 !default; +$color-ste-shipped-bg: $color-success !default; +$color-ste-shipped-text: $color-1 !default; +$color-ste-balance_due-bg: $color-notice !default; +$color-ste-balance_due-text: $color-1 !default; +$color-ste-backorder-bg: $color-notice !default; +$color-ste-backorder-text: $color-1 !default; +$color-ste-none-bg: $color-error !default; +$color-ste-none-text: $color-1 !default; +$color-ste-ready-bg: $color-success !default; +$color-ste-ready-text: $color-1 !default; +$color-ste-void-bg: $color-error !default; +$color-ste-void-text: $color-1 !default; +$color-ste-canceled-bg: $color-error !default; +$color-ste-canceled-text: $color-1 !default; +$color-ste-failed-bg: $color-error !default; +$color-ste-failed-text: $color-1 !default; +$color-ste-address-bg: $color-error !default; +$color-ste-address-text: $color-1 !default; +$color-ste-checkout-bg: $color-notice !default; +$color-ste-checkout-text: $color-1 !default; +$color-ste-cart-bg: $color-notice !default; +$color-ste-cart-text: $color-1 !default; +$color-ste-payment-bg: $color-error !default; +$color-ste-payment-text: $color-1 !default; +$color-ste-delivery-bg: $color-success !default; +$color-ste-delivery-text: $color-1 !default; +$color-ste-confirm-bg: $color-error !default; +$color-ste-confirm-text: $color-1 !default; +$color-ste-active-bg: $color-success !default; +$color-ste-active-text: $color-1 !default; +$color-ste-inactive-bg: $color-notice !default; +$color-ste-inactive-text: $color-1 !default; +$color-ste-considered_risky-bg: $color-error !default; +$color-ste-considered_risky-text:$color-1 !default; +$color-ste-considered_safe-bg: $color-success !default; +$color-ste-considered_safe-text: $color-1 !default; +$color-ste-success-bg: $color-success !default; +$color-ste-success-text: $color-1 !default; +$color-ste-notice-bg: $color-notice !default; +$color-ste-notice-text: $color-1 !default; +$color-ste-error-bg: $color-error !default; +$color-ste-error-text: $color-1 !default; + +// Available states +$states: completed, complete, sold, pending, awaiting_return, returned, credit_owed, paid, shipped, balance_due, backorder, checkout, cart, address, +delivery, payment, confirm, canceled, failed, ready, void, active, inactive, considered_risky, considered_safe, success, notice, error !default; + +$states-bg-colors: $color-ste-completed-bg, $color-ste-complete-bg, $color-ste-sold-bg, $color-ste-pending-bg, $color-ste-awaiting_return-bg, +$color-ste-returned-bg, $color-ste-credit_owed-bg, $color-ste-paid-bg, $color-ste-shipped-bg, $color-ste-balance_due-bg, $color-ste-backorder-bg, +$color-ste-checkout-bg, $color-ste-cart-bg, $color-ste-address-bg, $color-ste-delivery-bg, $color-ste-payment-bg, $color-ste-confirm-bg, +$color-ste-canceled-bg, $color-ste-failed-bg, $color-ste-ready-bg, $color-ste-void-bg, $color-ste-active-bg, $color-ste-inactive-bg, $color-ste-considered_risky-bg, +$color-ste-considered_safe-bg, $color-ste-success-bg, $color-ste-notice-bg, $color-ste-error-bg !default; + +$states-text-colors: $color-ste-completed-text, $color-ste-complete-text, $color-ste-sold-text, $color-ste-pending-text, $color-ste-awaiting_return-text, +$color-ste-returned-text, $color-ste-credit_owed-text, $color-ste-paid-text, $color-ste-shipped-text, $color-ste-balance_due-text, $color-ste-backorder-text, +$color-ste-checkout-text, $color-ste-cart-text, $color-ste-address-text, $color-ste-delivery-text, $color-ste-payment-text, $color-ste-confirm-text, +$color-ste-canceled-text, $color-ste-failed-text, $color-ste-ready-text, $color-ste-void-text, $color-ste-active-text, $color-ste-inactive-text, $color-ste-considered_risky-text, +$color-ste-considered_safe-text, $color-ste-success-text, $color-ste-notice-text, $color-ste-error-text !default; + +// Available actions +$actions: edit, clone, remove, void, capture, save, cancel, mail !default; +$actions-bg-colors: $color-action-edit-bg, $color-action-clone-bg, $color-action-remove-bg, $color-action-void-bg, $color-action-capture-bg, $color-action-save-bg, $color-action-cancel-bg, $color-action-mail-bg !default; +$actions-brd-colors: $color-action-edit-brd, $color-action-clone-brd, $color-action-remove-brd, $color-action-void-brd, $color-action-capture-brd, $color-action-save-brd, $color-action-cancel-brd, $color-action-mail-brd !default; + +// Sizes +//-------------------------------------------------------------- +$body-font-size: 13px !default; + +$h6-size: $body-font-size + 2 !default; +$h5-size: $h6-size + 2 !default; +$h4-size: $h5-size + 2 !default; +$h3-size: $h4-size + 2 !default; +$h2-size: $h3-size + 2 !default; +$h1-size: $h2-size + 2 !default; + +$border-radius: 3px !default; + +$font-weight-bold: 600 !default; +$font-weight-normal: 400 !default; diff --git a/backend/app/assets/stylesheets/spree/backend/globals/_variables_override.scss b/backend/app/assets/stylesheets/spree/backend/globals/_variables_override.scss new file mode 100644 index 00000000000..0a49b49d2f5 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/globals/_variables_override.scss @@ -0,0 +1,7 @@ +/*--------------------------------------------------------- + Empty file to override variables in user applications. + + To set your own colors, sizes or fonts just override this + file in your application and set variables according to + globals/_variables.scss file. + --------------------------------------------------------- */ diff --git a/core/app/assets/stylesheets/admin/hacks/_ie.scss b/backend/app/assets/stylesheets/spree/backend/hacks/_ie.scss similarity index 92% rename from core/app/assets/stylesheets/admin/hacks/_ie.scss rename to backend/app/assets/stylesheets/spree/backend/hacks/_ie.scss index 15fc1f2601b..ed71a30318f 100644 --- a/core/app/assets/stylesheets/admin/hacks/_ie.scss +++ b/backend/app/assets/stylesheets/spree/backend/hacks/_ie.scss @@ -2,10 +2,10 @@ html.ie { // Properly align icons in circle - table td.actions .no-text.icon-edit { + table td.actions .no-text.fa-edit { padding-top: 3px !important; } - table td.actions .no-text.icon-envelope-alt { + table td.actions .no-text.fa-envelope-alt { padding-top: 4px !important; } @@ -18,7 +18,6 @@ html.ie { } .select2-search { &:before { - position: relative; z-index: 10000; content: '\f002' !important; } @@ -70,4 +69,4 @@ html.ie8 { } } -} \ No newline at end of file +} diff --git a/core/app/assets/stylesheets/admin/hacks/_mozilla.scss b/backend/app/assets/stylesheets/spree/backend/hacks/_mozilla.scss similarity index 88% rename from core/app/assets/stylesheets/admin/hacks/_mozilla.scss rename to backend/app/assets/stylesheets/spree/backend/hacks/_mozilla.scss index a3e37cfacc5..c9523c63701 100644 --- a/core/app/assets/stylesheets/admin/hacks/_mozilla.scss +++ b/backend/app/assets/stylesheets/spree/backend/hacks/_mozilla.scss @@ -6,7 +6,7 @@ html.firefox { } // Properly align icons in circle - table td.actions .no-text.icon-edit { + table td.actions .no-text.fa-edit { padding-left: 1px; } @@ -22,6 +22,7 @@ html.firefox { // Fix select2 search input left padding to not overlap search icon .select2-search input.select2-input { + padding-bottom: 12px !important; padding-left: 25px !important; } @@ -29,4 +30,4 @@ html.firefox { input#image_attachment { width: 80%; } -} \ No newline at end of file +} diff --git a/core/app/assets/stylesheets/admin/hacks/_opera.scss b/backend/app/assets/stylesheets/spree/backend/hacks/_opera.scss similarity index 83% rename from core/app/assets/stylesheets/admin/hacks/_opera.scss rename to backend/app/assets/stylesheets/spree/backend/hacks/_opera.scss index af8c878ec2d..fbd601f6a1a 100644 --- a/core/app/assets/stylesheets/admin/hacks/_opera.scss +++ b/backend/app/assets/stylesheets/spree/backend/hacks/_opera.scss @@ -6,7 +6,7 @@ html.opera { } // Properly align icons in circle - table td.actions .no-text.icon-edit { + table td.actions .no-text.fa-edit { padding-left: 1px; } diff --git a/core/app/assets/stylesheets/admin/plugins/_jstree.scss b/backend/app/assets/stylesheets/spree/backend/plugins/_jstree.scss similarity index 86% rename from core/app/assets/stylesheets/admin/plugins/_jstree.scss rename to backend/app/assets/stylesheets/spree/backend/plugins/_jstree.scss index cc2e70238df..e8ad28a03cd 100644 --- a/core/app/assets/stylesheets/admin/plugins/_jstree.scss +++ b/backend/app/assets/stylesheets/spree/backend/plugins/_jstree.scss @@ -2,16 +2,17 @@ > ul, .jstree-icon { background-image: none; } - + .jstree-icon { @extend [class^="icon-"]:before; + @extend .fa; } - .jstree-open > .jstree-icon { - @extend .icon-caret-down; + .jstree-open > .jstree-icon { + @extend .fa-caret-down; } - .jstree-closed > .jstree-icon { - @extend .icon-caret-right; + .jstree-closed > .jstree-icon { + @extend .fa-caret-right; } li { @@ -26,36 +27,38 @@ width: 90%; height: auto; line-height: inherit; - text-transform: uppercase; padding: 5px 0 5px 10px; margin-bottom: 10px; .jstree-icon { padding-left: 0px; - @extend .icon-move; + @extend .fa; + @extend .fa-arrows; } } } } -#vakata-dragged.jstree-apple .jstree-invalid, +#vakata-dragged.jstree-apple .jstree-invalid, #vakata-dragged.jstree-apple .jstree-ok, #jstree-marker { background-image: none !important; background-color: transparent !important; - @extend [class^="icon-"]:before; + @extend [class^="icon-"]:before; } #vakata-dragged.jstree-apple .jstree-invalid { - @extend .icon-remove; + @extend .fa; + @extend .fa-bars; color: $color-5; } #vakata-dragged.jstree-apple .jstree-ok { - @extend .icon-ok; + @extend .fa; + @extend .fa-check; color: $color-2; } #jstree-marker { - @extend .icon-caret-right; + @extend .fa-caret-right; color: $color-body-text !important; width: 4px !important; } @@ -72,7 +75,7 @@ -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; - + } #vakata-contextmenu { @@ -110,7 +113,7 @@ -webkit-box-shadow: none !important; line-height: inherit !important; padding: 5px 10px !important; - margin: 0 !important; + margin: 0 !important; } } @@ -129,4 +132,4 @@ li.vakata-separator { display: none; } -} \ No newline at end of file +} diff --git a/core/app/assets/stylesheets/admin/plugins/_powertip.scss b/backend/app/assets/stylesheets/spree/backend/plugins/_powertip.scss similarity index 86% rename from core/app/assets/stylesheets/admin/plugins/_powertip.scss rename to backend/app/assets/stylesheets/spree/backend/plugins/_powertip.scss index 06839de79d3..b53f47a7e4b 100644 --- a/core/app/assets/stylesheets/admin/plugins/_powertip.scss +++ b/backend/app/assets/stylesheets/spree/backend/plugins/_powertip.scss @@ -17,7 +17,7 @@ &.s:before, &.se:before, &.sw:before { border-bottom-width: 5px; border-bottom-color: $color-3; - top: -5px; + top: -5px; } &.w:before { border-left-width: 5px; @@ -32,33 +32,33 @@ &.nw:before, &.sw:before { border-left-width: 5px; border-right-color: $color-3; - right: -5px; - } + right: -5px; + } - &.clone, &.yellow { + &.clone, &.yellow, &.cancel { background-color: $color-notice; &.n:before, &.ne:before, &.nw:before { border-top-color: $color-notice; } &.e:before, &.nw:before, &.sw:before { - border-right-color: $color-notice; + border-right-color: $color-notice; } &.s:before, &.se:before, &.sw:before { border-bottom-color: $color-notice; } &.w:before { - border-left-color: $color-notice; + border-left-color: $color-notice; } } - &.edit, &.green, &.capture { + &.edit, &.green, &.capture, &.save, &.add { background-color: $color-success; &.n:before, &.ne:before, &.nw:before { border-top-color: $color-success; } &.e:before, &.nw:before, &.sw:before { - border-right-color: $color-success; + border-right-color: $color-success; } &.s:before, &.se:before, &.sw:before { border-bottom-color: $color-success; @@ -67,7 +67,7 @@ border-left-color: $color-success; } } - &.remove, &.red, &.void { + &.remove, &.red, &.void { background-color: $color-error; &.n:before, &.ne:before, &.nw:before { @@ -83,4 +83,4 @@ border-left-color: $color-error; } } -} \ No newline at end of file +} diff --git a/backend/app/assets/stylesheets/spree/backend/plugins/_select2.scss b/backend/app/assets/stylesheets/spree/backend/plugins/_select2.scss new file mode 100644 index 00000000000..087cee4b2ed --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/plugins/_select2.scss @@ -0,0 +1,198 @@ +.select2-container { + &:hover .select2-choice, &.select2-container-active .select2-choice { + background-color: $color-sel-hover-bg !important; + border-color: $color-sel-hover-bg !important; + } + .select2-choice { + background-image: none !important; + background-color: $color-sel-bg; + border: none !important; + box-shadow: none !important; + @include border-radius($border-radius); + color: $color-1 !important; + font-size: 90%; + height: 31px; + line-height: inherit !important; + padding: 5px 15px 7px; + + span { + display: block; + padding: 2px; + } + + .select2-search-choice-close { + @extend .fa; + @extend .fa-times; + margin-top: 2px; + font-size: 100% !important; + background-image: none !important; + } + } + + &.select2-container-active { + .select2-choice { + box-shadow: none !important; + } + + &.select2-dropdown-open .select2-choice div b { + @extend .fa-caret-up + } + } +} + +.select2-drop { + box-shadow: none !important; + z-index: 1000000; + max-width: auto !important; + border-top: 1px solid; + border-color: $color-sel-hover-bg; + + &.select2-drop-above { + border-color: $color-sel-hover-bg; + } +} + +.select2-search { + @extend .fa; + @extend .fa-search; + + font-size: 100%; + color: darken($color-border, 15); + padding: 0 9px 0 0; + + &:before { + @extend [class^="icon-"]:before; + + position: absolute; + top: 16px; + left: 13px; + } + + input { + @extend input[type="text"]; + margin-top: 5px; + margin-left: 4px; + padding-left: 25px; + padding-top: 6px; + padding-bottom: 6px; + font-family: $base-font-family; + font-size: 90%; + box-shadow: none; + background-image: none; + } +} + +.select2-container .select2-choice .select2-arrow { + background-image: none; + background: transparent; + border: 0; + + b { + padding-top: 7px; + display: block; + width: 100%; + height: 100%; + background: none !important; + font-family: FontAwesome; + font-weight: 200 !important; + + &:before { + content: "\f0d7"; + } + } +} + +.select2-results { + padding-left: 0 !important; + + li { + font-size: 85% !important; + + + &:nth-child(odd) { + background: #efefef; + } + + &.select2-highlighted { + .select2-result-label { + &, h6 { + color: $color-1 !important; + } + } + } + + .select2-result-label { + color: $color-body-text; + min-height: 22px; + clear: both; + overflow: auto; + } + + &.select2-no-results, &.select2-searching { + padding: 5px; + background-color: transparent; + color: $color-body-text; + text-align: center; + font-weight: $font-weight-bold; + text-transform: uppercase; + } + } + + .select2-highlighted { + background-color: $color-sel-bg !important; + } +} + +.select2-container-multi { + &.select2-container-active, &.select2-dropdown-open { + .select2-choices { + border-color: $color-sel-hover-bg !important; + box-shadow: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } + .select2-choices { + @extend input[type="text"]; + padding: 6px 3px 3px 3px; + box-shadow: none; + background-image: none !important; + + .select2-search-choice { + @include border-radius($border-radius); + margin: 0 0 3px 3px; + background-image: none; + background-color: $color-sel-bg; + border: none; + box-shadow: none; + color: $color-1 !important; + font-size: 85%; + + &:hover { + background-color: $color-sel-hover-bg; + } + + .select2-search-choice-close { + background-image: none !important; + font-size: 85% !important; + @extend .fa; + @extend .fa-times; + margin-left: 2px; + color: $color-1; + &:before { + font-size: 11px; + } + } + } + } +} + +label .select2-container { + margin-top: -6px; + .select2-choice { + span { + text-transform: none; + font-weight: normal; + } + } +} diff --git a/core/app/assets/stylesheets/admin/plugins/_token-input.scss b/backend/app/assets/stylesheets/spree/backend/plugins/_token-input.scss similarity index 100% rename from core/app/assets/stylesheets/admin/plugins/_token-input.scss rename to backend/app/assets/stylesheets/spree/backend/plugins/_token-input.scss diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_adjustments_table.scss b/backend/app/assets/stylesheets/spree/backend/sections/_adjustments_table.scss new file mode 100644 index 00000000000..9948dd0cc15 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_adjustments_table.scss @@ -0,0 +1,8 @@ +[data-hook="adjustment_buttons"] { + tr { + div { + width: 50%; + float: left; + } + } +} diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_alerts.scss b/backend/app/assets/stylesheets/spree/backend/sections/_alerts.scss new file mode 100644 index 00000000000..91259a2a288 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_alerts.scss @@ -0,0 +1,27 @@ +.alert { + padding: 10px; + color: white; + font-weight: bold; + font-size: 125%; + + &.security { + background: #cc0000; + border-bottom: 3px solid darken(#cc0000, 10); + a { + color: lighten($color-3, 20); + } + } + + &.release, &.news { + background: $color-2; + border-bottom: 3px solid darken($color-2, 10); + a { + color: darken($color-3, 10); + } + } + + .dismiss { + float: right; + color: white !important; + } +} \ No newline at end of file diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_bulk_transfer.scss b/backend/app/assets/stylesheets/spree/backend/sections/_bulk_transfer.scss new file mode 100644 index 00000000000..654001399d1 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_bulk_transfer.scss @@ -0,0 +1,8 @@ +#add-variant-to-transfer { + .field { + .bulk_add_variant { + margin-top: 19px; + width: 100%; + } + } +} diff --git a/core/app/assets/stylesheets/admin/sections/_edit_checkouts.scss b/backend/app/assets/stylesheets/spree/backend/sections/_edit_checkouts.scss similarity index 100% rename from core/app/assets/stylesheets/admin/sections/_edit_checkouts.scss rename to backend/app/assets/stylesheets/spree/backend/sections/_edit_checkouts.scss diff --git a/core/app/assets/stylesheets/admin/sections/_image_settings.scss b/backend/app/assets/stylesheets/spree/backend/sections/_image_settings.scss similarity index 100% rename from core/app/assets/stylesheets/admin/sections/_image_settings.scss rename to backend/app/assets/stylesheets/spree/backend/sections/_image_settings.scss diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_log_entries.scss b/backend/app/assets/stylesheets/spree/backend/sections/_log_entries.scss new file mode 100644 index 00000000000..979802e8e6a --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_log_entries.scss @@ -0,0 +1,17 @@ +.log_entry { + &.success { + background: lighten($color-2, 15); + + td h4 { + color: darken($color-body-text, 25); + } + } + + &.fail { + background: lighten($color-5, 25); + + td h4 { + color: lighten($color-body-text, 50); + } + } +} \ No newline at end of file diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_orders.scss b/backend/app/assets/stylesheets/spree/backend/sections/_orders.scss new file mode 100644 index 00000000000..549c8e73fd2 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_orders.scss @@ -0,0 +1,64 @@ +// Customize orders filter +[data-hook="admin_orders_index_search"] { + select[data-placeholder="Status"] { + width: 100%; + } + + .select2-container { + width: 100% !important; + } +} + +// Order-total +[data-hook="order_details_total"]{ + text-align: center; + + .order-total { + font-size: 35px; + font-weight: 600; + color: $color-success; + } +} + +[data-hook="admin_order_form_fields"] { + legend.stock-location { + color: $color-body-text; + + .shipment-number { + color: $color-success; + } + .stock-location-name { + color: $color-success; + } + } +} + +// Customize orduct add fieldset +#add-line-item { + fieldset { + padding: 10px 0; + + .field { + margin-bottom: 0; + + input[type="text"], input[type="number"] { + width: 100%; + } + } + .actions { + .button { + margin-top: 28px; + } + } + } +} + +[data-hook="adjustments_new_coupon_code"] { + margin-bottom: 18px; +} + +#risk_analysis legend { + background-color: #c00; + color: #FFF; + font-size: 2em; +} \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/sections/_overview.scss b/backend/app/assets/stylesheets/spree/backend/sections/_overview.scss similarity index 100% rename from core/app/assets/stylesheets/admin/sections/_overview.scss rename to backend/app/assets/stylesheets/spree/backend/sections/_overview.scss diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_products.scss b/backend/app/assets/stylesheets/spree/backend/sections/_products.scss new file mode 100644 index 00000000000..3e54fd54096 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_products.scss @@ -0,0 +1,123 @@ +[data-hook="admin_products_sidebar"] { + // .field.checkbox:first-child { + // margin-top: 36px; + // } + .actions { + padding: 0 !important; + } +} + +[data-hook="admin_product_form_fields"] { + label { + display: inline-block; + } + input, select, textarea, .select2-container { + width: 100%; + } + input[type="checkbox"] { + width: auto; + vertical-align: bottom; + } +} + +[data-hook="admin_product_form_multiple_variants"] { + .info-actions { + text-align: right; + } +} + +.outstanding-balance { + margin-bottom: 15px; + text-transform: uppercase; + + strong { + color: $color-2; + } +} + +#listing_product_stock { + > tbody { + > tr { + &.even { + td { + background-color: $color-tbl-even; + } + } + &.odd:hover > td { + background-color: $color-tbl-even; + } + > td { + background-color: $color-tbl-odd; + + > table { + > tbody { + > tr { + > td { + border-color: lighten($color-border, 4); + } + } + > tr.even { + td { + background-color: lighten($color-tbl-even, 2); + } + } + > tr.odd { + td { + background-color: $color-tbl-odd; + } + } + } + } + } + } + } + td { + vertical-align: top; + + &.stock_location_info { + padding: 0; + + table { + margin-bottom: 0; + border-collapse: collapse; + min-height: 102px; + + thead { + th { + border-top: none; + padding: 5px 10px; + text-transform: none; + + &:first-child { + border-left: none; + } + &:last-child { + border-right: none; + } + } + } + tbody { + tr { + td { + padding: 5px 10px; + vertical-align: middle; + + &:first-child { + border-left: none; + } + &:last-child { + border-right: none; + } + } + + &:last-child { + td { + border-bottom: none; + } + } + } + } + } + } + } +} diff --git a/core/app/assets/stylesheets/admin/sections/_promotions.scss b/backend/app/assets/stylesheets/spree/backend/sections/_promotions.scss similarity index 86% rename from core/app/assets/stylesheets/admin/sections/_promotions.scss rename to backend/app/assets/stylesheets/spree/backend/sections/_promotions.scss index 12de22124ff..607257f934c 100644 --- a/core/app/assets/stylesheets/admin/sections/_promotions.scss +++ b/backend/app/assets/stylesheets/spree/backend/sections/_promotions.scss @@ -34,6 +34,25 @@ margin-bottom: 0; } + .tier { + position: relative; + padding-bottom: 10px; + + .remove { + position: absolute; + left: -15px; + top: 10px; + + &:hover { + color: #c60f13; + } + } + } + + .right-align { + text-align: right; + } + .field { padding-bottom: 10px; @@ -97,4 +116,4 @@ border: none; padding-bottom: 0; } -} \ No newline at end of file +} diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_return_authorizations.scss b/backend/app/assets/stylesheets/spree/backend/sections/_return_authorizations.scss new file mode 100644 index 00000000000..9debd607c3d --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_return_authorizations.scss @@ -0,0 +1,24 @@ +@import "spree/backend/globals/variables"; + +.return-items-table { + .refund-amount-input { + width: 80px; + } + .fa-thumbs-up { + margin-bottom: 10px; + } + .select2-container { + text-align: left; + max-width: 150px; + } +} +.expedited-exchanges-warning { + display: none; + color: black; + padding: 15px; + margin: 10px 0; + font-weight: bold; + background-color: $color-6; + border-radius: 4px; + opacity: 0.6; +} diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_tax_zones.scss b/backend/app/assets/stylesheets/spree/backend/sections/_tax_zones.scss new file mode 100644 index 00000000000..0c42a1c7a14 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_tax_zones.scss @@ -0,0 +1,15 @@ +#ul-nested-country { + > li { + margin-bottom: 10px; + } + + .select2-container, select { + width: 90%; + } + + a.remove { + display: inline-block; + margin-top: 6px; + } + +} diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_taxons.scss b/backend/app/assets/stylesheets/spree/backend/sections/_taxons.scss new file mode 100644 index 00000000000..354725f07f0 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_taxons.scss @@ -0,0 +1,21 @@ +#sorting_explanation { + font-size: 133%; + font-style: italic; +} + +.small_product { + text-align: center; + + img { + border: 1px solid #d9d9db; + padding: 5px; + } + + margin: 10px; +} + +#taxon_products { + > li { + height: 170px; + } +} diff --git a/backend/app/assets/stylesheets/spree/backend/sections/_users.scss b/backend/app/assets/stylesheets/spree/backend/sections/_users.scss new file mode 100644 index 00000000000..8e5be5caf14 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/sections/_users.scss @@ -0,0 +1,5 @@ +#listing_orders, #listing_items { + td.order-state { + height: 60px; + } +} diff --git a/backend/app/assets/stylesheets/spree/backend/shared/_forms.scss b/backend/app/assets/stylesheets/spree/backend/shared/_forms.scss new file mode 100644 index 00000000000..19fb811e867 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/shared/_forms.scss @@ -0,0 +1,307 @@ +input[type="text"], +input[type="password"], +input[type="email"], +input[type="date"], +input[type="datetime"], +input[type="time"], +input[type="url"], +input[type="number"], +input[type="tel"], +textarea, fieldset { + @include border-radius($border-radius); + padding: 7px 10px; + border: 1px solid $color-txt-brd; + color: $color-txt-text; + font-size: 90%; + + &:focus { + outline: none; + border-color: $color-txt-hover-brd; + } + + &[disabled] { + opacity: 0.7; + } +} + +textarea { + line-height: 19px; +} + +.fullwidth { + width: 100%; +} + +.input-group { + position: relative; + display: table; + border-collapse: separate; + + .form-control, + .input-group-addon { + display: table-cell; + + &:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + &:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + .form-control { + width: 100%; + } + + .input-group-addon { + padding: 6px 10px; + border: 1px solid #cee1f4; + background: #eff5fc; + border-radius: 3px; + + &:first-child { + border-right: none; + } + + &:last-child { + border-left: none; + } + } +} + +label { + font-weight: 600; + text-transform: uppercase; + font-size: 85%; + display: inline; + margin-bottom: 5px; + color: $color-4; + + &.inline { + display: inline-block !important; + } + + &.block { + display: block !important; + } +} + +.label-block label { display: block } + +input[type="submit"], +input[type="button"], +button, .button { + @include border-radius($border-radius); + display: inline-block; + padding: 8px 15px; + border: none; + background-color: $color-btn-bg; + color: $color-btn-text; + text-transform: uppercase; + font-weight: 600 !important; + white-space: nowrap; + + &:before { + font-weight: normal !important; + } + + &:visited, &:active, &:focus { color: $color-btn-text } + + &:hover { + background-color: $color-btn-hover-bg; + color: $color-btn-hover-text; + } + + &:active:focus { + box-shadow: 0 0 8px 0 darken($color-btn-hover-bg, 5) inset; + } + + &.fullwidth { + width: 100%; + text-align: center; + } +} + +span.info { + font-style: italic; + font-size: 85%; + color: lighten($color-body-text, 15); + display: block; + line-height: 20px; + margin: 5px 0; +} + +.field { + padding: 10px 0; + + &.checkbox { + min-height: 73px; + + input[type="checkbox"] { + display: inline-block; + width: auto; + } + + label { + cursor: pointer; + display: block; + } + } + + ul { + border-top: 1px solid $color-border; + list-style: none; + padding-top: 5px; + + li { + display: inline-block; + padding-right: 10px; + + + label { + font-weight: normal; + text-transform: none; + } + &.white-space-nowrap { + white-space: nowrap; + } + } + } + + &.withError { + .field_with_errors { + label { + color: very-light($color-error, 30); + } + + input { + border-color: very-light($color-error, 15); + } + } + .formError { + color: very-light($color-error, 30); + font-style: italic; + font-size: 85%; + } + } +} + +fieldset { + box-shadow: none; + box-sizing: border-box; + border-color: $color-border; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + margin-left: 0; + margin-right: 0; + position: relative; + margin-bottom: 35px; + padding: 10px 0 15px 0; + background-color: transparent; + border-left: none; + border-right: none; + border-radius: 0; + + &.no-border-bottom { + border-bottom: none; + margin-bottom: 0; + } + + &.no-border-top { + border-top: none; + padding-top: 0; + } + + legend { + background-color: $color-1; + color: $color-2; + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + text-align: center; + padding: 8px 15px; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + i { + color: $color-link; + } + } + + label { + color: lighten($color-body-text, 8); + } + + .filter-actions { + margin-bottom: -32px; + margin-top: 15px; + text-align: center; + + form { + display: inline-block; + } + + button, .button, input[type="submit"], input[type="button"], span.or { + @include border-radius($border-radius); + + -webkit-box-shadow: 0 0 0 15px $color-1; + -moz-box-shadow: 0 0 0 15px $color-1; + -ms-box-shadow: 0 0 0 15px $color-1; + -o-box-shadow: 0 0 0 15px $color-1; + box-shadow: 0 0 0 15px $color-1; + + &:hover { + border-color: $color-1; + } + } + + span.or { + background-color: $color-1; + border-width: 5px; + margin-left: 5px; + margin-right: 5px; + position: relative; + + -webkit-box-shadow: 0 0 0 5px $color-1; + -moz-box-shadow: 0 0 0 5px $color-1; + -ms-box-shadow: 0 0 0 5px $color-1; + -o-box-shadow: 0 0 0 5px $color-1; + box-shadow: 0 0 0 5px $color-1; + } + } + + &.labels-inline { + .field { + margin-bottom: 0; + display: table; + width: 100%; + + label, input { + display: table-cell !important; + } + input { + width: 100%; + } + + &.checkbox { + input { + width: auto !important + } + } + } + .actions { + padding: 0; + text-align: right; + } + } +} + +.form-actions { + margin-top: 18px; +} +.form-buttons { + text-align: center; +} diff --git a/backend/app/assets/stylesheets/spree/backend/shared/_icons.scss b/backend/app/assets/stylesheets/spree/backend/shared/_icons.scss new file mode 100644 index 00000000000..a8fdeed5735 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/shared/_icons.scss @@ -0,0 +1,53 @@ +// Some fixes for fontwesome stylesheets +[class^="icon-"], [class*=" icon-"] { + &:before { + padding-right: 5px; + } + + &.button, &.icon_link { + width: auto; + + &:before { + padding-right: 5px; + padding-top: 3px; + } + } +} + +.fa-email:before { @extend .fa-envelope:before } +.fa-resume:before { @extend .fa-refresh:before } +.fa-approve:before { @extend .fa-check:before } + +.fa-remove:before, +.fa-cancel:before, +.fa-void:before { @extend .fa-times:before } + +.fa-trash:before { @extend .fa-trash-o:before } + +.fa-capture:before { @extend .fa-check:before } +.fa-credit:before { @extend .fa-check:before } +.fa-approve:before { @extend .fa-check:before } +.fa-icon-cogs:before { @extend .fa-cogs:before } +.fa-ok:before, +.fa-icon-ok:before { @extend .fa-check:before } + +button, a { + &.fa:before { + padding-right: 5px; + } +} + +// Admin navigation fixes +.icon-user, +.icon-signout, +.icon-external-link { + @extend .fa; +} +.icon-user { @extend .fa-user; } +.icon-signout { @extend .fa-sign-out; } +.icon-external-link { @extend .fa-external-link; } + +// Avoid ugly default browser font (usually a serif) when an element has an icon AND text +.fa { + font-family: "FontAwesome", $base-font-family !important; +} diff --git a/backend/app/assets/stylesheets/spree/backend/shared/_layout.scss b/backend/app/assets/stylesheets/spree/backend/shared/_layout.scss new file mode 100644 index 00000000000..f54c796b672 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/shared/_layout.scss @@ -0,0 +1,99 @@ +// Basics +//--------------------------------------------------- +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +// Helpers +.block-table { + display: table; + width: 100%; + + .table-cell { + display: table-cell; + vertical-align: middle; + padding: 0 10px; + + &:first-child { + padding-left: 0; + } + &:last-child { + padding-right: 0; + } + } +} + +.hidden { + display: none; +} + +// For block grids +.frameless { + margin-left: -10px; + margin-right: -10px; +} + +.container .column, +.container .columns { + // Float container right instead of left. + .right { + float: right; + } +} + +// Header +//--------------------------------------------------- +#header { + background-color: $color-1; + padding: 5px 0; +} + +#logo { height: 40px } + +[data-hook="admin-title"] { font-size: 14px } + +.page-title { + i { + color: $color-2; + } +} + +// Content +//--------------------------------------------------- +#content { + background-color: $color-1; + position: relative; + z-index: 0; + padding: 0; + margin-top: 15px; +} + +#content-header { + padding: 15px 0; + background-color: very-light($color-3, 4); + border-bottom: 1px solid $color-border; + + .page-title { + font-size: 20px; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + .page-actions { + text-align: right; + line-height: 38px; + form { + display: inline-block; + } + } +} + +// Footer +//--------------------------------------------------- +#footer { + margin-top: 15px; + border-top: 1px solid $color-border; + padding: 10px 0; +} diff --git a/backend/app/assets/stylesheets/spree/backend/shared/_tables.scss b/backend/app/assets/stylesheets/spree/backend/shared/_tables.scss new file mode 100644 index 00000000000..5b0a8875109 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/shared/_tables.scss @@ -0,0 +1,214 @@ +table { + width: 100%; + margin-bottom: 15px; + border-collapse: separate; + + th, td { + padding: 7px 5px; + border-right: 1px solid $color-border; + border-bottom: 1px solid $color-border; + vertical-align: middle; + text-overflow: ellipsis; + + img { + border: 1px solid transparent; + } + + &:first-child { + border-left: 1px solid $color-border; + } + + a { + border-bottom: 1px dotted lighten($color-link, 10); + + &:hover { + border-color: lighten($color-link-hover, 10); + } + } + + .handle { + display: block !important; + text-align: center; + padding-right: 0; + } + + &.actions { + background-color: transparent; + border: none !important; + text-align: center; + + span.text { + font-size: $body-font-size; + } + + [class*='fa-'].no-text { + font-size: 120%; + background-color: very-light($color-3); + border: 1px solid $color-border; + border-radius: 15px; + width: 29px; + height: 29px; + display: inline-block; + padding-top: 6px; + + &:before { + text-align: center !important; + width: 27px; + display: inline-block; + } + + &:hover { + border-color: transparent; + } + } + + button[class*='fa-'] { + color: $color-link; + padding: 0 !important; + } + + .fa-envelope-alt, .fa-eye-open { + color: $color-link; + padding-left: 0px; + + &:hover { + background-color: $color-3; + color: $color-1; + } + } + .fa-trash:hover, .fa-void:hover { + background-color: $color-error; + color: $color-1; + } + .fa-cancel:hover { + background-color: $color-notice; + color: $color-1; + } + .fa-edit:hover, .fa-capture:hover, .fa-ok:hover, .fa-plus:hover, .fa-save:hover { + background-color: $color-success; + color: $color-1; + } + .fa-copy:hover { + background-color: $color-notice; + color: $color-1; + } + .fa-thumbs-up:hover { + background-color: $color-success; + color: $color-1; + } + .fa-thumbs-down:hover { + background-color: $color-error; + color: $color-1; + } + } + + input[type="number"], + input[type="text"] { + width: 100%; + } + + &.no-border { + border-right: none; + } + + .handle { + @extend [class^="icon-"]:before; + @extend .fa; + @extend .fa-bars; + cursor: move; + } + + } + + &.no-borders { + td, th { + border: none !important; + } + + } + + thead { + th { + padding: 10px; + border-top: 1px solid $color-border; + border-bottom: none; + background-color: $color-tbl-thead; + text-transform: uppercase; + font-size: 85%; + font-weight: $font-weight-bold; + } + } + + tbody { + tr { + &:first-child th, + &:first-child td { + border-top: 1px solid $color-border; + } + &.even td { + background-color: $color-tbl-even; + + img { + border: 1px solid very-light($color-3, 6); + } + } + + &:hover td { + background-color: very-light($color-3, 5); + + img { + border: 1px solid $color-border; + } + } + + &.deleted td { + background-color: very-light($color-error, 6); + border-color: very-light($color-error, 15); + } + + &.ui-sortable-placeholder td { + border: 1px solid $color-2 !important; + visibility: visible !important; + + &.actions { + background-color: transparent; + border-right: none !important; + border-top: none !important; + border-bottom: none !important; + border-left: 1px solid $color-2 !important; + } + } + + &.ui-sortable-helper { + width: 100%; + + td { + background-color: lighten($color-3, 33); + border-bottom: 1px solid $color-border; + + &.actions { + display: none; + } + } + } + } + + &.no-border-top tr:first-child td { + border-top: none; + } + + &.grand-total { + td { + border-color: $color-2 !important; + text-transform: uppercase; + font-size: 110%; + font-weight: 600; + background-color: lighten($color-2, 50); + } + .total { + background-color: $color-2; + color: $color-1; + } + } + } +} diff --git a/backend/app/assets/stylesheets/spree/backend/shared/_typography.scss b/backend/app/assets/stylesheets/spree/backend/shared/_typography.scss new file mode 100644 index 00000000000..24074e59db2 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/shared/_typography.scss @@ -0,0 +1,138 @@ +// Base +//-------------------------------------------------------------- +body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; font-size: 13px; } + +body { + font-family: $base-font-family; + font-size: $body-font-size; + font-weight: 400; + color: $color-body-text; + text-rendering: optimizeLegibility; +} + +hr { + border-top: 1px solid $color-border; + border-bottom: 1px solid white; + border-left: none; +} + +strong, b { + font-weight: 600; +} + +// links +//-------------------------------------------------------------- +a { + color: $color-link; + text-decoration: none; + line-height: inherit; + + &, &:hover, &:active, &:visited, &:focus { + outline: none; + } + + &:visited { + color: $color-link-visited; + } + &:focus { + color: $color-link-focus; + } + &:active { + color: $color-link-active; + } + &:hover { + color: $color-link-hover; + } +} + +// Headings +//-------------------------------------------------------------- + +h1,h2,h3,h4,h5,h6 { + font-weight: 600; + color: $color-headers; + line-height: 1.1; +} + +h1 { font-size: $h1-size; line-height: $h1-size + 6 } +h2 { font-size: $h2-size; line-height: $h1-size + 4 } +h3 { font-size: $h3-size; line-height: $h1-size + 2 } +h4 { font-size: $h4-size; line-height: $h1-size } +h5 { font-size: $h5-size; line-height: $h1-size } +h6 { font-size: $h6-size; line-height: $h1-size } + + +// Lists +//-------------------------------------------------------------- +ul { + &.inline-menu { + li { + display: inline-block; + } + } + &.fields { + list-style: none; + padding: 0; + margin: 0; + } +} + +ul.text_list { + border-top: none; + margin: 0; + list-style: disc inside none; +} + +dl { + width: 100%; + overflow: hidden; + margin: 5px 0; + color: lighten($color-body-text, 15); + + dt, dd { + float: left; + line-height: 16px; + padding: 5px; + text-align: justify; + } + + dt { + width: 40%; + font-weight: 600; + padding-left: 0; + text-transform: uppercase; + font-size: 85%; + } + + dd { + width: 60%; + padding-right: 0; + } + + dd:after { + content: ''; + clear: both; + } + +} + +// Helpers +.align-center { text-align: center } +.align-right { text-align: right } +.align-left { text-align: left } +.align-justify { text-align: justify } + +.uppercase { text-transform: uppercase } + +.green { color: $color-2 } +.blue { color: $color-3 } +.red { color: $color-5 } +.yellow { color: $color-6 } + +.no-objects-found { + text-align: center; + font-size: 120%; + text-transform: uppercase; + padding: 40px 0px; + color: lighten($color-body-text, 15); +} diff --git a/backend/app/assets/stylesheets/spree/backend/spree_admin.scss b/backend/app/assets/stylesheets/spree/backend/spree_admin.scss new file mode 100644 index 00000000000..9d90aaf5719 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/backend/spree_admin.scss @@ -0,0 +1,45 @@ +@import 'spree/backend/globals/functions'; +@import 'spree/backend/globals/variables_override'; +@import 'spree/backend/globals/variables'; +@import 'spree/backend/globals/mixins'; + +@import 'spree/backend/shared/typography'; +@import 'spree/backend/shared/tables'; +@import 'spree/backend/shared/icons'; +@import 'spree/backend/shared/forms'; +@import 'spree/backend/shared/layout'; + +@import 'spree/backend/components/states'; +@import 'spree/backend/components/actions'; +@import 'spree/backend/components/date-picker'; +@import 'spree/backend/components/messages'; +@import 'spree/backend/components/pagination'; +@import 'spree/backend/components/progress'; +@import 'spree/backend/components/table-filter'; +@import 'spree/backend/components/navigation'; +@import 'spree/backend/components/sidebar'; +@import 'spree/backend/components/product_autocomplete'; + +@import 'font-awesome'; +@import 'spree/backend/plugins/powertip'; +@import 'spree/backend/plugins/select2'; +@import 'spree/backend/plugins/token-input'; +@import 'spree/backend/plugins/jstree'; + +@import 'spree/backend/sections/adjustments_table'; +@import 'spree/backend/sections/alerts'; +@import 'spree/backend/sections/orders'; +@import 'spree/backend/sections/overview'; +@import 'spree/backend/sections/products'; +@import 'spree/backend/sections/promotions'; +@import 'spree/backend/sections/edit_checkouts'; +@import 'spree/backend/sections/bulk_transfer'; +@import 'spree/backend/sections/return_authorizations'; +@import 'spree/backend/sections/tax_zones'; +@import 'spree/backend/sections/log_entries'; +@import 'spree/backend/sections/taxons'; +@import 'spree/backend/sections/users'; + +@import 'spree/backend/hacks/mozilla'; +@import 'spree/backend/hacks/opera'; +@import 'spree/backend/hacks/ie'; diff --git a/backend/app/assets/stylesheets/spree/frontend/backend.css b/backend/app/assets/stylesheets/spree/frontend/backend.css new file mode 100644 index 00000000000..63eac109f71 --- /dev/null +++ b/backend/app/assets/stylesheets/spree/frontend/backend.css @@ -0,0 +1 @@ +/* Placeholder for backend dummy app */ diff --git a/backend/app/controllers/spree/admin/adjustments_controller.rb b/backend/app/controllers/spree/admin/adjustments_controller.rb new file mode 100644 index 00000000000..9e061c18471 --- /dev/null +++ b/backend/app/controllers/spree/admin/adjustments_controller.rb @@ -0,0 +1,38 @@ +module Spree + module Admin + class AdjustmentsController < ResourceController + + belongs_to 'spree/order', find_by: :number + + create.after :update_totals + destroy.after :update_totals + update.after :update_totals + + skip_before_action :load_resource, only: [:toggle_state, :edit, :update, :destroy] + + before_action :find_adjustment, only: [:destroy, :edit, :update] + + def index + @adjustments = @order.all_adjustments.order("created_at ASC") + end + + private + + def find_adjustment + # Need to assign to @object here to keep ResourceController happy + @adjustment = @object = parent.all_adjustments.find(params[:id]) + end + + def update_totals + @order.reload.update! + end + + # Override method used to create a new instance to correctly + # associate adjustment with order + def build_resource + parent.adjustments.build(order: parent) + end + + end + end +end diff --git a/backend/app/controllers/spree/admin/base_controller.rb b/backend/app/controllers/spree/admin/base_controller.rb new file mode 100644 index 00000000000..1455a106651 --- /dev/null +++ b/backend/app/controllers/spree/admin/base_controller.rb @@ -0,0 +1,68 @@ +module Spree + module Admin + class BaseController < Spree::BaseController + ssl_required + + helper 'spree/admin/navigation' + helper 'spree/admin/tables' + layout '/spree/layouts/admin' + + before_action :authorize_admin + + protected + + def action + params[:action].to_sym + end + + def authorize_admin + if respond_to?(:model_class, true) && model_class + record = model_class + else + record = controller_name.to_sym + end + authorize! :admin, record + authorize! action, record + end + + # Need to generate an API key for a user due to some backend actions + # requiring authentication to the Spree API + def generate_admin_api_key + if (user = try_spree_current_user) && user.spree_api_key.blank? + user.generate_spree_api_key! + end + end + + def flash_message_for(object, event_sym) + resource_desc = object.class.model_name.human + resource_desc += " \"#{object.name}\"" if object.respond_to?(:name) && object.name.present? + Spree.t(event_sym, resource: resource_desc) + end + + def render_js_for_destroy + render partial: '/spree/admin/shared/destroy' + end + + # Index request for JSON needs to pass a CSRF token in order to prevent JSON Hijacking + def check_json_authenticity + return unless request.format.js? || request.format.json? + return unless protect_against_forgery? + auth_token = params[request_forgery_protection_token] + unless auth_token && form_authenticity_token == URI.unescape(auth_token) + raise(ActionController::InvalidAuthenticityToken) + end + end + + def config_locale + Spree::Backend::Config[:locale] + end + + def can_not_transition_without_customer_info + unless @order.billing_address.present? + flash[:notice] = Spree.t(:fill_in_customer_info) + redirect_to edit_admin_order_customer_url(@order) + end + end + end + end +end diff --git a/core/app/controllers/spree/admin/countries_controller.rb b/backend/app/controllers/spree/admin/countries_controller.rb similarity index 100% rename from core/app/controllers/spree/admin/countries_controller.rb rename to backend/app/controllers/spree/admin/countries_controller.rb diff --git a/backend/app/controllers/spree/admin/customer_returns_controller.rb b/backend/app/controllers/spree/admin/customer_returns_controller.rb new file mode 100644 index 00000000000..5ac6af5ab82 --- /dev/null +++ b/backend/app/controllers/spree/admin/customer_returns_controller.rb @@ -0,0 +1,67 @@ +module Spree + module Admin + class CustomerReturnsController < ResourceController + belongs_to 'spree/order', find_by: :number + + before_action :parent # ensure order gets loaded to support our pseudo parent-child relationship + before_action :load_form_data, only: [:new, :edit] + + create.before :build_return_items_from_params + create.fails :load_form_data + + def edit + @pending_return_items = @customer_return.return_items.select(&:pending?) + @accepted_return_items = @customer_return.return_items.select(&:accepted?) + @rejected_return_items = @customer_return.return_items.select(&:rejected?) + @manual_intervention_return_items = @customer_return.return_items.select(&:manual_intervention_required?) + @pending_reimbursements = @customer_return.reimbursements.select(&:pending?) + + super + end + + private + + def location_after_save + url_for([:edit, :admin, @order, @customer_return]) + end + + def build_resource + Spree::CustomerReturn.new + end + + def find_resource + Spree::CustomerReturn.accessible_by(current_ability, :read).find(params[:id]) + end + + def collection + parent # trigger loading the order + @collection ||= Spree::ReturnItem + .accessible_by(current_ability, :read) + .where(inventory_unit_id: @order.inventory_units.pluck(:id)) + .map(&:customer_return).uniq.compact + @customer_returns = @collection + end + + def load_form_data + return_items = @order.inventory_units.map(&:current_or_new_return_item).reject(&:customer_return_id) + @rma_return_items = return_items.select(&:return_authorization_id) + end + + def permitted_resource_params + @permitted_resource_params ||= params.require('customer_return').permit(permitted_customer_return_attributes) + end + + def build_return_items_from_params + return_items_params = permitted_resource_params.delete(:return_items_attributes).values + + @customer_return.return_items = return_items_params.map do |item_params| + next unless item_params.delete('returned') == '1' + return_item = item_params[:id] ? Spree::ReturnItem.find(item_params[:id]) : Spree::ReturnItem.new + return_item.attributes = item_params + return_item + end.compact + end + + end + end +end diff --git a/backend/app/controllers/spree/admin/general_settings_controller.rb b/backend/app/controllers/spree/admin/general_settings_controller.rb new file mode 100644 index 00000000000..12bc0d42950 --- /dev/null +++ b/backend/app/controllers/spree/admin/general_settings_controller.rb @@ -0,0 +1,45 @@ +module Spree + module Admin + class GeneralSettingsController < Spree::Admin::BaseController + include Spree::Backend::Callbacks + + before_action :set_store + + def edit + @preferences_security = [:allow_ssl_in_production, :allow_ssl_in_staging, :allow_ssl_in_development_and_test] + @preferences_currency = [:display_currency, :hide_cents] + end + + def update + params.each do |name, value| + next unless Spree::Config.has_preference? name + Spree::Config[name] = value + end + + current_store.update_attributes store_params + + flash[:success] = Spree.t(:successfully_updated, resource: Spree.t(:general_settings)) + redirect_to edit_admin_general_settings_path + end + + def clear_cache + Rails.cache.clear + invoke_callbacks(:clear_cache, :after) + head :no_content + end + + private + def store_params + params.require(:store).permit(permitted_params) + end + + def permitted_params + Spree::PermittedAttributes.store_attributes + end + + def set_store + @store = current_store + end + end + end +end diff --git a/backend/app/controllers/spree/admin/images_controller.rb b/backend/app/controllers/spree/admin/images_controller.rb new file mode 100644 index 00000000000..a9401bd0c22 --- /dev/null +++ b/backend/app/controllers/spree/admin/images_controller.rb @@ -0,0 +1,34 @@ +module Spree + module Admin + class ImagesController < ResourceController + before_action :load_data + + create.before :set_viewable + update.before :set_viewable + + private + + def location_after_destroy + admin_product_images_url(@product) + end + + def location_after_save + admin_product_images_url(@product) + end + + def load_data + @product = Product.friendly.find(params[:product_id]) + @variants = @product.variants.collect do |variant| + [variant.sku_and_options_text, variant.id] + end + @variants.insert(0, [Spree.t(:all), @product.master.id]) + end + + def set_viewable + @image.viewable_type = 'Spree::Variant' + @image.viewable_id = params[:image][:viewable_id] + end + + end + end +end diff --git a/backend/app/controllers/spree/admin/log_entries_controller.rb b/backend/app/controllers/spree/admin/log_entries_controller.rb new file mode 100644 index 00000000000..6e064efe4c3 --- /dev/null +++ b/backend/app/controllers/spree/admin/log_entries_controller.rb @@ -0,0 +1,19 @@ +module Spree + module Admin + class LogEntriesController < Spree::Admin::BaseController + before_action :find_order_and_payment + + def index + @log_entries = @payment.log_entries + end + + + private + + def find_order_and_payment + @order = Spree::Order.where(:number => params[:order_id]).first! + @payment = @order.payments.find(params[:payment_id]) + end + end + end +end diff --git a/core/app/controllers/spree/admin/option_types_controller.rb b/backend/app/controllers/spree/admin/option_types_controller.rb similarity index 95% rename from core/app/controllers/spree/admin/option_types_controller.rb rename to backend/app/controllers/spree/admin/option_types_controller.rb index 367ea03dd59..cb7bc4db330 100644 --- a/core/app/controllers/spree/admin/option_types_controller.rb +++ b/backend/app/controllers/spree/admin/option_types_controller.rb @@ -1,7 +1,7 @@ module Spree module Admin class OptionTypesController < ResourceController - before_filter :setup_new_option_value, :only => [:edit] + before_action :setup_new_option_value, only: :edit def update_values_positions params[:positions].each do |id, index| diff --git a/backend/app/controllers/spree/admin/option_values_controller.rb b/backend/app/controllers/spree/admin/option_values_controller.rb new file mode 100644 index 00000000000..06ee314edb7 --- /dev/null +++ b/backend/app/controllers/spree/admin/option_values_controller.rb @@ -0,0 +1,11 @@ +module Spree + module Admin + class OptionValuesController < Spree::Admin::BaseController + def destroy + option_value = Spree::OptionValue.find(params[:id]) + option_value.destroy + render :text => nil + end + end + end +end diff --git a/backend/app/controllers/spree/admin/orders/customer_details_controller.rb b/backend/app/controllers/spree/admin/orders/customer_details_controller.rb new file mode 100644 index 00000000000..401430b094a --- /dev/null +++ b/backend/app/controllers/spree/admin/orders/customer_details_controller.rb @@ -0,0 +1,57 @@ +module Spree + module Admin + module Orders + class CustomerDetailsController < Spree::Admin::BaseController + before_action :load_order + + def show + edit + render :action => :edit + end + + def edit + country_id = Address.default.country.id + @order.build_bill_address(:country_id => country_id) if @order.bill_address.nil? + @order.build_ship_address(:country_id => country_id) if @order.ship_address.nil? + + @order.bill_address.country_id = country_id if @order.bill_address.country.nil? + @order.ship_address.country_id = country_id if @order.ship_address.country.nil? + end + + def update + if @order.update_attributes(order_params) + if params[:guest_checkout] == "false" + @order.associate_user!(Spree.user_class.find(params[:user_id]), @order.email.blank?) + end + @order.next + @order.refresh_shipment_rates + flash[:success] = Spree.t('customer_details_updated') + redirect_to edit_admin_order_url(@order) + else + render :action => :edit + end + + end + + private + def order_params + params.require(:order).permit( + :email, + :use_billing, + :bill_address_attributes => permitted_address_attributes, + :ship_address_attributes => permitted_address_attributes + ) + end + + def load_order + @order = Order.includes(:adjustments).find_by_number!(params[:order_id]) + end + + def model_class + Spree::Order + end + + end + end + end +end diff --git a/backend/app/controllers/spree/admin/orders_controller.rb b/backend/app/controllers/spree/admin/orders_controller.rb new file mode 100644 index 00000000000..dea0a0cd0fb --- /dev/null +++ b/backend/app/controllers/spree/admin/orders_controller.rb @@ -0,0 +1,149 @@ +module Spree + module Admin + class OrdersController < Spree::Admin::BaseController + before_action :initialize_order_events + before_action :load_order, only: [:edit, :update, :cancel, :resume, :approve, :resend, :open_adjustments, :close_adjustments, :cart] + + respond_to :html + + def index + params[:q] ||= {} + params[:q][:completed_at_not_null] ||= '1' if Spree::Config[:show_only_complete_orders_by_default] + @show_only_completed = params[:q][:completed_at_not_null] == '1' + params[:q][:s] ||= @show_only_completed ? 'completed_at desc' : 'created_at desc' + params[:q][:completed_at_not_null] = '' unless @show_only_completed + + # As date params are deleted if @show_only_completed, store + # the original date so we can restore them into the params + # after the search + created_at_gt = params[:q][:created_at_gt] + created_at_lt = params[:q][:created_at_lt] + + params[:q].delete(:inventory_units_shipment_id_null) if params[:q][:inventory_units_shipment_id_null] == "0" + + if params[:q][:created_at_gt].present? + params[:q][:created_at_gt] = Time.zone.parse(params[:q][:created_at_gt]).beginning_of_day rescue "" + end + + if params[:q][:created_at_lt].present? + params[:q][:created_at_lt] = Time.zone.parse(params[:q][:created_at_lt]).end_of_day rescue "" + end + + if @show_only_completed + params[:q][:completed_at_gt] = params[:q].delete(:created_at_gt) + params[:q][:completed_at_lt] = params[:q].delete(:created_at_lt) + end + + @search = Order.accessible_by(current_ability, :index).ransack(params[:q]) + + # lazyoading other models here (via includes) may result in an invalid query + # e.g. SELECT DISTINCT DISTINCT "spree_orders".id, "spree_orders"."created_at" AS alias_0 FROM "spree_orders" + # see https://github.com/spree/spree/pull/3919 + @orders = @search.result(distinct: true). + page(params[:page]). + per(params[:per_page] || Spree::Config[:orders_per_page]) + + # Restore dates + params[:q][:created_at_gt] = created_at_gt + params[:q][:created_at_lt] = created_at_lt + end + + def new + @order = Order.create(order_params) + redirect_to cart_admin_order_url(@order) + end + + def edit + can_not_transition_without_customer_info + + unless @order.completed? + @order.refresh_shipment_rates + end + end + + def cart + unless @order.completed? + @order.refresh_shipment_rates + end + if @order.shipped_shipments.count > 0 + redirect_to edit_admin_order_url(@order) + end + end + + def update + if @order.update_attributes(params[:order]) && @order.line_items.present? + @order.update! + unless @order.completed? + # Jump to next step if order is not completed. + redirect_to admin_order_customer_path(@order) and return + end + else + @order.errors.add(:line_items, Spree.t('errors.messages.blank')) if @order.line_items.empty? + end + + render :action => :edit + end + + def cancel + @order.canceled_by(try_spree_current_user) + flash[:success] = Spree.t(:order_canceled) + redirect_to :back + end + + def resume + @order.resume! + flash[:success] = Spree.t(:order_resumed) + redirect_to :back + end + + def approve + @order.approved_by(try_spree_current_user) + flash[:success] = Spree.t(:order_approved) + redirect_to :back + end + + def resend + OrderMailer.confirm_email(@order.id, true).deliver + flash[:success] = Spree.t(:order_email_resent) + + redirect_to :back + end + + def open_adjustments + adjustments = @order.all_adjustments.where(state: 'closed') + adjustments.update_all(state: 'open') + flash[:success] = Spree.t(:all_adjustments_opened) + + respond_with(@order) { |format| format.html { redirect_to :back } } + end + + def close_adjustments + adjustments = @order.all_adjustments.where(state: 'open') + adjustments.update_all(state: 'closed') + flash[:success] = Spree.t(:all_adjustments_closed) + + respond_with(@order) { |format| format.html { redirect_to :back } } + end + + private + def order_params + params[:created_by_id] = try_spree_current_user.try(:id) + params.permit(:created_by_id) + end + + def load_order + @order = Order.includes(:adjustments).find_by_number!(params[:id]) + authorize! action, @order + end + + # Used for extensions which need to provide their own custom event links on the order details view. + def initialize_order_events + @order_events = %w{approve cancel resume} + end + + def model_class + Spree::Order + end + end + end +end diff --git a/backend/app/controllers/spree/admin/payment_methods_controller.rb b/backend/app/controllers/spree/admin/payment_methods_controller.rb new file mode 100644 index 00000000000..9097921d707 --- /dev/null +++ b/backend/app/controllers/spree/admin/payment_methods_controller.rb @@ -0,0 +1,72 @@ +module Spree + module Admin + class PaymentMethodsController < ResourceController + skip_before_action :load_resource, only: :create + before_action :load_data + before_action :validate_payment_method_provider, only: :create + + respond_to :html + + def create + @payment_method = params[:payment_method].delete(:type).constantize.new(payment_method_params) + @object = @payment_method + invoke_callbacks(:create, :before) + if @payment_method.save + invoke_callbacks(:create, :after) + flash[:success] = Spree.t(:successfully_created, :resource => Spree.t(:payment_method)) + redirect_to edit_admin_payment_method_path(@payment_method) + else + invoke_callbacks(:create, :fails) + respond_with(@payment_method) + end + end + + def update + invoke_callbacks(:update, :before) + payment_method_type = params[:payment_method].delete(:type) + if @payment_method['type'].to_s != payment_method_type + @payment_method.update_columns( + type: payment_method_type, + updated_at: Time.now, + ) + @payment_method = PaymentMethod.find(params[:id]) + end + + update_params = params[ActiveModel::Naming.param_key(@payment_method)] || {} + attributes = payment_method_params.merge(update_params) + attributes.each do |k,v| + if k.include?("password") && attributes[k].blank? + attributes.delete(k) + end + end + + if @payment_method.update_attributes(attributes) + invoke_callbacks(:update, :after) + flash[:success] = Spree.t(:successfully_updated, :resource => Spree.t(:payment_method)) + redirect_to edit_admin_payment_method_path(@payment_method) + else + invoke_callbacks(:update, :fails) + respond_with(@payment_method) + end + end + + private + + def load_data + @providers = Gateway.providers.sort{|p1, p2| p1.name <=> p2.name } + end + + def validate_payment_method_provider + valid_payment_methods = Rails.application.config.spree.payment_methods.map(&:to_s) + if !valid_payment_methods.include?(params[:payment_method][:type]) + flash[:error] = Spree.t(:invalid_payment_provider) + redirect_to new_admin_payment_method_path + end + end + + def payment_method_params + params.require(:payment_method).permit! + end + end + end +end diff --git a/backend/app/controllers/spree/admin/payments_controller.rb b/backend/app/controllers/spree/admin/payments_controller.rb new file mode 100644 index 00000000000..9b35c0b3406 --- /dev/null +++ b/backend/app/controllers/spree/admin/payments_controller.rb @@ -0,0 +1,103 @@ +module Spree + module Admin + class PaymentsController < Spree::Admin::BaseController + include Spree::Backend::Callbacks + + before_action :load_order, only: [:create, :new, :index, :fire] + before_action :load_payment, except: [:create, :new, :index] + before_action :load_data + before_action :can_not_transition_without_customer_info + + respond_to :html + + def index + @payments = @order.payments.includes(:refunds => :reason) + @refunds = @payments.flat_map(&:refunds) + redirect_to new_admin_order_payment_url(@order) if @payments.empty? + end + + def new + @payment = @order.payments.build + end + + def create + invoke_callbacks(:create, :before) + @payment ||= @order.payments.build(object_params) + if @payment.payment_method.source_required? && params[:card].present? and params[:card] != 'new' + @payment.source = @payment.payment_method.payment_source_class.find_by_id(params[:card]) + end + + begin + if @payment.save + invoke_callbacks(:create, :after) + # Transition order as far as it will go. + while @order.next; end + # If "@order.next" didn't trigger payment processing already (e.g. if the order was + # already complete) then trigger it manually now + @payment.process! if @order.completed? && @payment.checkout? + flash[:success] = flash_message_for(@payment, :successfully_created) + redirect_to admin_order_payments_path(@order) + else + invoke_callbacks(:create, :fails) + flash[:error] = Spree.t(:payment_could_not_be_created) + render :new + end + rescue Spree::Core::GatewayError => e + invoke_callbacks(:create, :fails) + flash[:error] = "#{e.message}" + redirect_to new_admin_order_payment_path(@order) + end + end + + def fire + return unless event = params[:e] and @payment.payment_source + + # Because we have a transition method also called void, we do this to avoid conflicts. + event = "void_transaction" if event == "void" + if @payment.send("#{event}!") + flash[:success] = Spree.t(:payment_updated) + else + flash[:error] = Spree.t(:cannot_perform_operation) + end + rescue Spree::Core::GatewayError => ge + flash[:error] = "#{ge.message}" + ensure + redirect_to admin_order_payments_path(@order) + end + + private + + def object_params + if params[:payment] and params[:payment_source] and source_params = params.delete(:payment_source)[params[:payment][:payment_method_id]] + params[:payment][:source_attributes] = source_params + end + + params.require(:payment).permit(permitted_payment_attributes) + end + + def load_data + @amount = params[:amount] || load_order.total + @payment_methods = PaymentMethod.available(:back_end) + if @payment and @payment.payment_method + @payment_method = @payment.payment_method + else + @payment_method = @payment_methods.first + end + end + + def load_order + @order = Order.find_by_number!(params[:order_id]) + authorize! action, @order + @order + end + + def load_payment + @payment = Payment.find(params[:id]) + end + + def model_class + Spree::Payment + end + end + end +end diff --git a/backend/app/controllers/spree/admin/product_properties_controller.rb b/backend/app/controllers/spree/admin/product_properties_controller.rb new file mode 100644 index 00000000000..4f5480f0c1e --- /dev/null +++ b/backend/app/controllers/spree/admin/product_properties_controller.rb @@ -0,0 +1,18 @@ +module Spree + module Admin + class ProductPropertiesController < ResourceController + belongs_to 'spree/product', :find_by => :slug + before_action :find_properties + before_action :setup_property, only: :index + + private + def find_properties + @properties = Spree::Property.pluck(:name) + end + + def setup_property + @product.product_properties.build + end + end + end +end diff --git a/backend/app/controllers/spree/admin/products_controller.rb b/backend/app/controllers/spree/admin/products_controller.rb new file mode 100644 index 00000000000..566cb76a78c --- /dev/null +++ b/backend/app/controllers/spree/admin/products_controller.rb @@ -0,0 +1,147 @@ +module Spree + module Admin + class ProductsController < ResourceController + helper 'spree/products' + + before_action :load_data, except: :index + create.before :create_before + update.before :update_before + helper_method :clone_object_url + + def show + session[:return_to] ||= request.referer + redirect_to action: :edit + end + + def index + session[:return_to] = request.url + respond_with(@collection) + end + + def update + if params[:product][:taxon_ids].present? + params[:product][:taxon_ids] = params[:product][:taxon_ids].split(',') + end + if params[:product][:option_type_ids].present? + params[:product][:option_type_ids] = params[:product][:option_type_ids].split(',') + end + invoke_callbacks(:update, :before) + if @object.update_attributes(permitted_resource_params) + invoke_callbacks(:update, :after) + flash[:success] = flash_message_for(@object, :successfully_updated) + respond_with(@object) do |format| + format.html { redirect_to location_after_save } + format.js { render layout: false } + end + else + # Stops people submitting blank slugs, causing errors when they try to + # update the product again + @product.slug = @product.slug_was if @product.slug.blank? + invoke_callbacks(:update, :fails) + respond_with(@object) + end + end + + def destroy + @product = Product.friendly.find(params[:id]) + @product.destroy + + flash[:success] = Spree.t('notice_messages.product_deleted') + + respond_with(@product) do |format| + format.html { redirect_to collection_url } + format.js { render_js_for_destroy } + end + end + + def clone + @new = @product.duplicate + + if @new.save + flash[:success] = Spree.t('notice_messages.product_cloned') + else + flash[:error] = Spree.t('notice_messages.product_not_cloned') + end + + redirect_to edit_admin_product_url(@new) + end + + def stock + @variants = @product.variants.includes(*variant_stock_includes) + @variants = [@product.master] if @variants.empty? + @stock_locations = StockLocation.accessible_by(current_ability, :read) + if @stock_locations.empty? + flash[:error] = Spree.t(:stock_management_requires_a_stock_location) + redirect_to admin_stock_locations_path + end + end + + protected + + def find_resource + Product.with_deleted.friendly.find(params[:id]) + end + + def location_after_save + spree.edit_admin_product_url(@product) + end + + def load_data + @taxons = Taxon.order(:name) + @option_types = OptionType.order(:name) + @tax_categories = TaxCategory.order(:name) + @shipping_categories = ShippingCategory.order(:name) + end + + def collection + return @collection if @collection.present? + params[:q] ||= {} + params[:q][:deleted_at_null] ||= "1" + + params[:q][:s] ||= "name asc" + @collection = super + # Don't delete params[:q][:deleted_at_null] here because it is used in view to check the + # checkbox for 'q[deleted_at_null]'. This also messed with pagination when deleted_at_null is checked. + if params[:q][:deleted_at_null] == '0' + @collection = @collection.with_deleted + end + # @search needs to be defined as this is passed to search_form_for + # Temporarily remove params[:q][:deleted_at_null] from params[:q] to ransack products. + # This is to include all products and not just deleted products. + @search = @collection.ransack(params[:q].reject { |k, _v| k.to_s == 'deleted_at_null' }) + @collection = @search.result. + distinct_by_product_ids(params[:q][:s]). + includes(product_includes). + page(params[:page]). + per(params[:per_page] || Spree::Config[:admin_products_per_page]) + @collection + end + + def create_before + return if params[:product][:prototype_id].blank? + @prototype = Spree::Prototype.find(params[:product][:prototype_id]) + end + + def update_before + # note: we only reset the product properties if we're receiving a post + # from the form on that tab + return unless params[:clear_product_properties] + params[:product] ||= {} + end + + def product_includes + [{ variants: [:images], master: [:images, :default_price] }] + end + + def clone_object_url(resource) + clone_admin_product_url resource + end + + private + + def variant_stock_includes + [:images, stock_items: :stock_location, option_values: :option_type] + end + end + end +end diff --git a/backend/app/controllers/spree/admin/promotion_actions_controller.rb b/backend/app/controllers/spree/admin/promotion_actions_controller.rb new file mode 100644 index 00000000000..ec52c0a2e72 --- /dev/null +++ b/backend/app/controllers/spree/admin/promotion_actions_controller.rb @@ -0,0 +1,45 @@ +class Spree::Admin::PromotionActionsController < Spree::Admin::BaseController + before_action :load_promotion, only: [:create, :destroy] + before_action :validate_promotion_action_type, only: :create + + def create + @calculators = Spree::Promotion::Actions::CreateAdjustment.calculators + @promotion_action = params[:action_type].constantize.new(params[:promotion_action]) + @promotion_action.promotion = @promotion + if @promotion_action.save + flash[:success] = Spree.t(:successfully_created, :resource => Spree.t(:promotion_action)) + end + respond_to do |format| + format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} + format.js { render :layout => false } + end + end + + def destroy + @promotion_action = @promotion.promotion_actions.find(params[:id]) + if @promotion_action.destroy + flash[:success] = Spree.t(:successfully_removed, :resource => Spree.t(:promotion_action)) + end + respond_to do |format| + format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} + format.js { render :layout => false } + end + end + + private + + def load_promotion + @promotion = Spree::Promotion.find(params[:promotion_id]) + end + + def validate_promotion_action_type + valid_promotion_action_types = Rails.application.config.spree.promotions.actions.map(&:to_s) + if !valid_promotion_action_types.include?(params[:action_type]) + flash[:error] = Spree.t(:invalid_promotion_action) + respond_to do |format| + format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} + format.js { render :layout => false } + end + end + end +end diff --git a/backend/app/controllers/spree/admin/promotion_categories_controller.rb b/backend/app/controllers/spree/admin/promotion_categories_controller.rb new file mode 100644 index 00000000000..024be2974de --- /dev/null +++ b/backend/app/controllers/spree/admin/promotion_categories_controller.rb @@ -0,0 +1,6 @@ +module Spree + module Admin + class PromotionCategoriesController < ResourceController + end + end +end diff --git a/backend/app/controllers/spree/admin/promotion_rules_controller.rb b/backend/app/controllers/spree/admin/promotion_rules_controller.rb new file mode 100644 index 00000000000..f390c0da811 --- /dev/null +++ b/backend/app/controllers/spree/admin/promotion_rules_controller.rb @@ -0,0 +1,50 @@ +class Spree::Admin::PromotionRulesController < Spree::Admin::BaseController + helper 'spree/promotion_rules' + + before_action :load_promotion, only: [:create, :destroy] + before_action :validate_promotion_rule_type, only: :create + + def create + # Remove type key from this hash so that we don't attempt + # to set it when creating a new record, as this is raises + # an error in ActiveRecord 3.2. + promotion_rule_type = params[:promotion_rule].delete(:type) + @promotion_rule = promotion_rule_type.constantize.new(params[:promotion_rule]) + @promotion_rule.promotion = @promotion + if @promotion_rule.save + flash[:success] = Spree.t(:successfully_created, :resource => Spree.t(:promotion_rule)) + end + respond_to do |format| + format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} + format.js { render :layout => false } + end + end + + def destroy + @promotion_rule = @promotion.promotion_rules.find(params[:id]) + if @promotion_rule.destroy + flash[:success] = Spree.t(:successfully_removed, :resource => Spree.t(:promotion_rule)) + end + respond_to do |format| + format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} + format.js { render :layout => false } + end + end + + private + + def load_promotion + @promotion = Spree::Promotion.find(params[:promotion_id]) + end + + def validate_promotion_rule_type + valid_promotion_rule_types = Rails.application.config.spree.promotions.rules.map(&:to_s) + if !valid_promotion_rule_types.include?(params[:promotion_rule][:type]) + flash[:error] = Spree.t(:invalid_promotion_rule) + respond_to do |format| + format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} + format.js { render :layout => false } + end + end + end +end diff --git a/backend/app/controllers/spree/admin/promotions_controller.rb b/backend/app/controllers/spree/admin/promotions_controller.rb new file mode 100644 index 00000000000..bd4a2c0dbec --- /dev/null +++ b/backend/app/controllers/spree/admin/promotions_controller.rb @@ -0,0 +1,38 @@ +module Spree + module Admin + class PromotionsController < ResourceController + before_action :load_data + + helper 'spree/promotion_rules' + + protected + def location_after_save + spree.edit_admin_promotion_url(@promotion) + end + + def load_data + @calculators = Rails.application.config.spree.calculators.promotion_actions_create_adjustments + @promotion_categories = Spree::PromotionCategory.order(:name) + end + + def collection + return @collection if defined?(@collection) + params[:q] ||= HashWithIndifferentAccess.new + params[:q][:s] ||= 'id desc' + + @collection = super + @search = @collection.ransack(params[:q]) + @collection = @search.result(distinct: true). + includes(promotion_includes). + page(params[:page]). + per(params[:per_page] || Spree::Config[:promotions_per_page]) + + @collection + end + + def promotion_includes + [:promotion_actions] + end + end + end +end diff --git a/backend/app/controllers/spree/admin/properties_controller.rb b/backend/app/controllers/spree/admin/properties_controller.rb new file mode 100644 index 00000000000..fdf01614eb1 --- /dev/null +++ b/backend/app/controllers/spree/admin/properties_controller.rb @@ -0,0 +1,25 @@ +module Spree + module Admin + class PropertiesController < ResourceController + def index + respond_with(@collection) + end + + private + + def collection + return @collection if @collection.present? + # params[:q] can be blank upon pagination + params[:q] = {} if params[:q].blank? + + @collection = super + @search = @collection.ransack(params[:q]) + @collection = @search.result. + page(params[:page]). + per(Spree::Config[:properties_per_page]) + + @collection + end + end + end +end diff --git a/core/app/controllers/spree/admin/prototypes_controller.rb b/backend/app/controllers/spree/admin/prototypes_controller.rb similarity index 94% rename from core/app/controllers/spree/admin/prototypes_controller.rb rename to backend/app/controllers/spree/admin/prototypes_controller.rb index b799e5ce5a6..fb657df760c 100644 --- a/core/app/controllers/spree/admin/prototypes_controller.rb +++ b/backend/app/controllers/spree/admin/prototypes_controller.rb @@ -20,8 +20,6 @@ def available def select @prototype ||= Prototype.find(params[:id]) @prototype_properties = @prototype.properties - - respond_with(@prototypes) end end diff --git a/backend/app/controllers/spree/admin/refund_reasons_controller.rb b/backend/app/controllers/spree/admin/refund_reasons_controller.rb new file mode 100644 index 00000000000..e08a6a69ce3 --- /dev/null +++ b/backend/app/controllers/spree/admin/refund_reasons_controller.rb @@ -0,0 +1,6 @@ +module Spree + module Admin + class RefundReasonsController < ResourceController + end + end +end diff --git a/backend/app/controllers/spree/admin/refunds_controller.rb b/backend/app/controllers/spree/admin/refunds_controller.rb new file mode 100644 index 00000000000..eb29286887a --- /dev/null +++ b/backend/app/controllers/spree/admin/refunds_controller.rb @@ -0,0 +1,38 @@ +module Spree + module Admin + class RefundsController < ResourceController + belongs_to 'spree/payment' + before_action :load_order + + helper_method :refund_reasons + + rescue_from Spree::Core::GatewayError, with: :spree_core_gateway_error, only: :create + + private + + def location_after_save + admin_order_payments_path(@payment.order) + end + + def load_order + # the spree/admin/shared/order_tabs partial expects the @order instance variable to be set + @order = @payment.order if @payment + end + + def refund_reasons + @refund_reasons ||= RefundReason.active.all + end + + def build_resource + super.tap do |refund| + refund.amount = refund.payment.credit_allowed + end + end + + def spree_core_gateway_error(error) + flash[:error] = error.message + render :new + end + end + end +end diff --git a/backend/app/controllers/spree/admin/reimbursement_types_controller.rb b/backend/app/controllers/spree/admin/reimbursement_types_controller.rb new file mode 100644 index 00000000000..59f25080d8a --- /dev/null +++ b/backend/app/controllers/spree/admin/reimbursement_types_controller.rb @@ -0,0 +1,6 @@ +module Spree + module Admin + class ReimbursementTypesController < ResourceController + end + end +end diff --git a/backend/app/controllers/spree/admin/reimbursements_controller.rb b/backend/app/controllers/spree/admin/reimbursements_controller.rb new file mode 100644 index 00000000000..cfc145b53ab --- /dev/null +++ b/backend/app/controllers/spree/admin/reimbursements_controller.rb @@ -0,0 +1,45 @@ +module Spree + module Admin + class ReimbursementsController < ResourceController + belongs_to 'spree/order', find_by: :number + + before_action :load_simulated_refunds, only: :edit + + rescue_from Spree::Core::GatewayError, with: :spree_core_gateway_error, only: :perform + + def perform + @reimbursement.perform! + redirect_to location_after_save + end + + private + + def build_resource + if params[:build_from_customer_return_id].present? + customer_return = CustomerReturn.find(params[:build_from_customer_return_id]) + + Reimbursement.build_from_customer_return(customer_return) + else + super + end + end + + def location_after_save + if @reimbursement.reimbursed? + admin_order_reimbursement_path(parent, @reimbursement) + else + edit_admin_order_reimbursement_path(parent, @reimbursement) + end + end + + def load_simulated_refunds + @reimbursement_objects = @reimbursement.simulate + end + + def spree_core_gateway_error(error) + flash[:error] = error.message + redirect_to edit_admin_order_reimbursement_path(parent, @reimbursement) + end + end + end +end diff --git a/backend/app/controllers/spree/admin/reports_controller.rb b/backend/app/controllers/spree/admin/reports_controller.rb new file mode 100644 index 00000000000..1fc312f5c1c --- /dev/null +++ b/backend/app/controllers/spree/admin/reports_controller.rb @@ -0,0 +1,65 @@ +module Spree + module Admin + class ReportsController < Spree::Admin::BaseController + respond_to :html + + class << self + def available_reports + @@available_reports + end + + def add_available_report!(report_key, report_description_key = nil) + if report_description_key.nil? + report_description_key = "#{report_key}_description" + end + @@available_reports[report_key] = {name: Spree.t(report_key), description: Spree.t(report_description_key)} + end + end + + def initialize + super + ReportsController.add_available_report!(:sales_total) + end + + def index + @reports = ReportsController.available_reports + end + + def sales_total + params[:q] = {} unless params[:q] + + if params[:q][:completed_at_gt].blank? + params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month + else + params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month + end + + if params[:q] && !params[:q][:completed_at_lt].blank? + params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" + end + + params[:q][:s] ||= "completed_at desc" + + @search = Order.complete.ransack(params[:q]) + @orders = @search.result + + @totals = {} + @orders.each do |order| + @totals[order.currency] = { :item_total => ::Money.new(0, order.currency), :adjustment_total => ::Money.new(0, order.currency), :sales_total => ::Money.new(0, order.currency) } unless @totals[order.currency] + @totals[order.currency][:item_total] += order.display_item_total.money + @totals[order.currency][:adjustment_total] += order.display_adjustment_total.money + @totals[order.currency][:sales_total] += order.display_total.money + end + end + + private + + def model_class + Spree::Admin::ReportsController + end + + @@available_reports = {} + + end + end +end diff --git a/backend/app/controllers/spree/admin/resource_controller.rb b/backend/app/controllers/spree/admin/resource_controller.rb new file mode 100644 index 00000000000..cff55367234 --- /dev/null +++ b/backend/app/controllers/spree/admin/resource_controller.rb @@ -0,0 +1,258 @@ +class Spree::Admin::ResourceController < Spree::Admin::BaseController + include Spree::Backend::Callbacks + + helper_method :new_object_url, :edit_object_url, :object_url, :collection_url + before_action :load_resource, except: :update_positions + rescue_from ActiveRecord::RecordNotFound, :with => :resource_not_found + + respond_to :html + + def new + invoke_callbacks(:new_action, :before) + respond_with(@object) do |format| + format.html { render :layout => !request.xhr? } + if request.xhr? + format.js { render :layout => false } + end + end + end + + def edit + respond_with(@object) do |format| + format.html { render :layout => !request.xhr? } + if request.xhr? + format.js { render :layout => false } + end + end + end + + def update + invoke_callbacks(:update, :before) + if @object.update_attributes(permitted_resource_params) + invoke_callbacks(:update, :after) + flash[:success] = flash_message_for(@object, :successfully_updated) + respond_with(@object) do |format| + format.html { redirect_to location_after_save } + format.js { render :layout => false } + end + else + invoke_callbacks(:update, :fails) + respond_with(@object) do |format| + format.html do + flash.now[:error] = @object.errors.full_messages.join(", ") + render action: 'edit' + end + format.js { render layout: false } + end + end + end + + def create + invoke_callbacks(:create, :before) + @object.attributes = permitted_resource_params + if @object.save + invoke_callbacks(:create, :after) + flash[:success] = flash_message_for(@object, :successfully_created) + respond_with(@object) do |format| + format.html { redirect_to location_after_save } + format.js { render :layout => false } + end + else + invoke_callbacks(:create, :fails) + respond_with(@object) do |format| + format.html do + flash.now[:error] = @object.errors.full_messages.join(", ") + render action: 'new' + end + format.js { render layout: false } + end + end + end + + def update_positions + ActiveRecord::Base.transaction do + params[:positions].each do |id, index| + model_class.find(id).set_list_position(index) + end + end + + respond_to do |format| + format.js { render text: 'Ok' } + end + end + + def destroy + invoke_callbacks(:destroy, :before) + if @object.destroy + invoke_callbacks(:destroy, :after) + flash[:success] = flash_message_for(@object, :successfully_removed) + respond_with(@object) do |format| + format.html { redirect_to location_after_destroy } + format.js { render :partial => "spree/admin/shared/destroy" } + end + else + invoke_callbacks(:destroy, :fails) + respond_with(@object) do |format| + format.html { redirect_to location_after_destroy } + end + end + end + + protected + + class << self + attr_accessor :parent_data + + def belongs_to(model_name, options = {}) + @parent_data ||= {} + @parent_data[:model_name] = model_name + @parent_data[:model_class] = model_name.to_s.classify.constantize + @parent_data[:find_by] = options[:find_by] || :id + end + end + + def resource_not_found + flash[:error] = flash_message_for(model_class.new, :not_found) + redirect_to collection_url + end + + def model_class + "Spree::#{controller_name.classify}".constantize + end + + def model_name + parent_data[:model_name].gsub('spree/', '') + end + + def object_name + controller_name.singularize + end + + def load_resource + if member_action? + @object ||= load_resource_instance + + # call authorize! a third time (called twice already in Admin::BaseController) + # this time we pass the actual instance so fine-grained abilities can control + # access to individual records, not just entire models. + authorize! action, @object + + instance_variable_set("@#{object_name}", @object) + else + @collection ||= collection + + # note: we don't call authorize here as the collection method should use + # CanCan's accessible_by method to restrict the actual records returned + + instance_variable_set("@#{controller_name}", @collection) + end + end + + def load_resource_instance + if new_actions.include?(action) + build_resource + elsif params[:id] + find_resource + end + end + + def parent_data + self.class.parent_data + end + + def parent + if parent_data.present? + @parent ||= parent_data[:model_class].send("find_by_#{parent_data[:find_by]}", params["#{model_name}_id"]) + instance_variable_set("@#{model_name}", @parent) + else + nil + end + end + + def find_resource + if parent_data.present? + parent.send(controller_name).find(params[:id]) + else + model_class.find(params[:id]) + end + end + + def build_resource + if parent_data.present? + parent.send(controller_name).build + else + model_class.new + end + end + + def collection + return parent.send(controller_name) if parent_data.present? + if model_class.respond_to?(:accessible_by) && !current_ability.has_block?(params[:action], model_class) + model_class.accessible_by(current_ability, action) + else + model_class.where(nil) + end + end + + def location_after_destroy + collection_url + end + + def location_after_save + collection_url + end + + # URL helpers + + def new_object_url(options = {}) + if parent_data.present? + spree.new_polymorphic_url([:admin, parent, model_class], options) + else + spree.new_polymorphic_url([:admin, model_class], options) + end + end + + def edit_object_url(object, options = {}) + if parent_data.present? + spree.send "edit_admin_#{model_name}_#{object_name}_url", parent, object, options + else + spree.send "edit_admin_#{object_name}_url", object, options + end + end + + def object_url(object = nil, options = {}) + target = object ? object : @object + if parent_data.present? + spree.send "admin_#{model_name}_#{object_name}_url", parent, target, options + else + spree.send "admin_#{object_name}_url", target, options + end + end + + def collection_url(options = {}) + if parent_data.present? + spree.polymorphic_url([:admin, parent, model_class], options) + else + spree.polymorphic_url([:admin, model_class], options) + end + end + + # Allow all attributes to be updatable. + # + # Other controllers can, should, override it to set custom logic + def permitted_resource_params + params[object_name].present? ? params.require(object_name).permit! : ActionController::Parameters.new + end + + def collection_actions + [:index] + end + + def member_action? + !collection_actions.include? action + end + + def new_actions + [:new, :create] + end +end diff --git a/backend/app/controllers/spree/admin/return_authorization_reasons_controller.rb b/backend/app/controllers/spree/admin/return_authorization_reasons_controller.rb new file mode 100644 index 00000000000..fd7a7b135d6 --- /dev/null +++ b/backend/app/controllers/spree/admin/return_authorization_reasons_controller.rb @@ -0,0 +1,6 @@ +module Spree + module Admin + class ReturnAuthorizationReasonsController < ResourceController + end + end +end diff --git a/backend/app/controllers/spree/admin/return_authorizations_controller.rb b/backend/app/controllers/spree/admin/return_authorizations_controller.rb new file mode 100644 index 00000000000..09c0bd4fc96 --- /dev/null +++ b/backend/app/controllers/spree/admin/return_authorizations_controller.rb @@ -0,0 +1,51 @@ +module Spree + module Admin + class ReturnAuthorizationsController < ResourceController + belongs_to 'spree/order', :find_by => :number + + before_action :load_form_data, only: [:new, :edit] + create.fails :load_form_data + update.fails :load_form_data + + def fire + @return_authorization.send("#{params[:e]}!") + flash[:success] = Spree.t(:return_authorization_updated) + redirect_to :back + end + + private + + def load_form_data + load_return_items + load_reimbursement_types + load_return_authorization_reasons + end + + # To satisfy how nested attributes works we want to create placeholder ReturnItems for + # any InventoryUnits that have not already been added to the ReturnAuthorization. + def load_return_items + all_inventory_units = @return_authorization.order.inventory_units + associated_inventory_units = @return_authorization.return_items.map(&:inventory_unit) + unassociated_inventory_units = all_inventory_units - associated_inventory_units + + new_return_items = unassociated_inventory_units.map do |new_unit| + Spree::ReturnItem.new(inventory_unit: new_unit).tap(&:set_default_pre_tax_amount) + end + + @form_return_items = (@return_authorization.return_items + new_return_items).sort_by(&:inventory_unit_id) + end + + def load_reimbursement_types + @reimbursement_types = Spree::ReimbursementType.accessible_by(current_ability, :read).active + end + + def load_return_authorization_reasons + @reasons = Spree::ReturnAuthorizationReason.active + # Only allow an inactive reason if it's already associated to the RMA + if @return_authorization.reason && !@return_authorization.reason.active? + @reasons << @return_authorization.reason + end + end + end + end +end diff --git a/backend/app/controllers/spree/admin/return_items_controller.rb b/backend/app/controllers/spree/admin/return_items_controller.rb new file mode 100644 index 00000000000..9e1f3b941c3 --- /dev/null +++ b/backend/app/controllers/spree/admin/return_items_controller.rb @@ -0,0 +1,9 @@ +module Spree + module Admin + class ReturnItemsController < ResourceController + def location_after_save + url_for([:edit, :admin, @return_item.customer_return.order, @return_item.customer_return]) + end + end + end +end diff --git a/backend/app/controllers/spree/admin/root_controller.rb b/backend/app/controllers/spree/admin/root_controller.rb new file mode 100644 index 00000000000..2390765b13d --- /dev/null +++ b/backend/app/controllers/spree/admin/root_controller.rb @@ -0,0 +1,17 @@ +module Spree + module Admin + class RootController < Spree::Admin::BaseController + + skip_before_filter :authorize_admin + + def index + redirect_to admin_root_redirect_path + end + + protected + def admin_root_redirect_path + spree.admin_orders_path + end + end + end +end diff --git a/core/app/controllers/spree/admin/search_controller.rb b/backend/app/controllers/spree/admin/search_controller.rb similarity index 93% rename from core/app/controllers/spree/admin/search_controller.rb rename to backend/app/controllers/spree/admin/search_controller.rb index 49abde0ef13..fd946d62526 100644 --- a/core/app/controllers/spree/admin/search_controller.rb +++ b/backend/app/controllers/spree/admin/search_controller.rb @@ -2,7 +2,7 @@ module Spree module Admin class SearchController < Spree::Admin::BaseController # http://spreecommerce.com/blog/2010/11/02/json-hijacking-vulnerability/ - before_filter :check_json_authenticity, :only => :index + before_action :check_json_authenticity, only: :index respond_to :json # TODO: Clean this up by moving searching out to user_class_extensions diff --git a/core/app/controllers/spree/admin/shipping_categories_controller.rb b/backend/app/controllers/spree/admin/shipping_categories_controller.rb similarity index 100% rename from core/app/controllers/spree/admin/shipping_categories_controller.rb rename to backend/app/controllers/spree/admin/shipping_categories_controller.rb diff --git a/backend/app/controllers/spree/admin/shipping_methods_controller.rb b/backend/app/controllers/spree/admin/shipping_methods_controller.rb new file mode 100644 index 00000000000..7e07b6233dc --- /dev/null +++ b/backend/app/controllers/spree/admin/shipping_methods_controller.rb @@ -0,0 +1,46 @@ +module Spree + module Admin + class ShippingMethodsController < ResourceController + before_action :load_data, except: :index + before_action :set_shipping_category, only: [:create, :update] + before_action :set_zones, only: [:create, :update] + + def destroy + @object.destroy + + flash[:success] = flash_message_for(@object, :successfully_removed) + + respond_with(@object) do |format| + format.html { redirect_to collection_url } + format.js { render_js_for_destroy } + end + end + + private + + def set_shipping_category + return true if params["shipping_method"][:shipping_categories] == "" + @shipping_method.shipping_categories = Spree::ShippingCategory.where(:id => params["shipping_method"][:shipping_categories]) + @shipping_method.save + params[:shipping_method].delete(:shipping_categories) + end + + def set_zones + return true if params["shipping_method"][:zones] == "" + @shipping_method.zones = Spree::Zone.where(:id => params["shipping_method"][:zones]) + @shipping_method.save + params[:shipping_method].delete(:zones) + end + + def location_after_save + edit_admin_shipping_method_path(@shipping_method) + end + + def load_data + @available_zones = Zone.order(:name) + @tax_categories = Spree::TaxCategory.order(:name) + @calculators = ShippingMethod.calculators.sort_by(&:name) + end + end + end +end diff --git a/backend/app/controllers/spree/admin/states_controller.rb b/backend/app/controllers/spree/admin/states_controller.rb new file mode 100644 index 00000000000..cf82bf52718 --- /dev/null +++ b/backend/app/controllers/spree/admin/states_controller.rb @@ -0,0 +1,29 @@ +module Spree + module Admin + class StatesController < ResourceController + belongs_to 'spree/country' + before_action :load_data + + def index + respond_with(@collection) do |format| + format.html + format.js { render :partial => 'state_list' } + end + end + + protected + + def location_after_save + admin_country_states_url(@country) + end + + def collection + super.order(:name) + end + + def load_data + @countries = Country.order(:name) + end + end + end +end diff --git a/backend/app/controllers/spree/admin/stock_items_controller.rb b/backend/app/controllers/spree/admin/stock_items_controller.rb new file mode 100644 index 00000000000..3d008f2481c --- /dev/null +++ b/backend/app/controllers/spree/admin/stock_items_controller.rb @@ -0,0 +1,51 @@ +module Spree + module Admin + class StockItemsController < Spree::Admin::BaseController + before_action :determine_backorderable, only: :update + + def update + stock_item.save + respond_to do |format| + format.js { head :ok } + end + end + + def create + variant = Variant.find(params[:variant_id]) + stock_location = StockLocation.find(params[:stock_location_id]) + stock_movement = stock_location.stock_movements.build(stock_movement_params) + stock_movement.stock_item = stock_location.set_up_stock_item(variant) + + if stock_movement.save + flash[:success] = flash_message_for(stock_movement, :successfully_created) + else + flash[:error] = Spree.t(:could_not_create_stock_movement) + end + + redirect_to :back + end + + def destroy + stock_item.destroy + + respond_with(@stock_item) do |format| + format.html { redirect_to :back } + format.js + end + end + + private + def stock_movement_params + params.require(:stock_movement).permit(permitted_stock_movement_attributes) + end + + def stock_item + @stock_item ||= StockItem.find(params[:id]) + end + + def determine_backorderable + stock_item.backorderable = params[:stock_item].present? && params[:stock_item][:backorderable].present? + end + end + end +end diff --git a/backend/app/controllers/spree/admin/stock_locations_controller.rb b/backend/app/controllers/spree/admin/stock_locations_controller.rb new file mode 100644 index 00000000000..f5fdc0ec662 --- /dev/null +++ b/backend/app/controllers/spree/admin/stock_locations_controller.rb @@ -0,0 +1,25 @@ +module Spree + module Admin + class StockLocationsController < ResourceController + + before_action :set_country, only: :new + + private + + def set_country + begin + if Spree::Config[:default_country_id].present? + @stock_location.country = Spree::Country.find(Spree::Config[:default_country_id]) + else + @stock_location.country = Spree::Country.find_by!(iso: 'US') + end + + rescue ActiveRecord::RecordNotFound + flash[:error] = Spree.t(:stock_locations_need_a_default_country) + redirect_to admin_stock_locations_path and return + end + end + + end + end +end diff --git a/backend/app/controllers/spree/admin/stock_movements_controller.rb b/backend/app/controllers/spree/admin/stock_movements_controller.rb new file mode 100644 index 00000000000..897a440785f --- /dev/null +++ b/backend/app/controllers/spree/admin/stock_movements_controller.rb @@ -0,0 +1,39 @@ +module Spree + module Admin + class StockMovementsController < Spree::Admin::BaseController + respond_to :html + helper_method :stock_location + + def index + @stock_movements = stock_location.stock_movements.recent. + includes(:stock_item => { :variant => :product }). + page(params[:page]) + end + + def new + @stock_movement = stock_location.stock_movements.build + end + + def create + @stock_movement = stock_location.stock_movements.build(stock_movement_params) + @stock_movement.save + flash[:success] = flash_message_for(@stock_movement, :successfully_created) + redirect_to admin_stock_location_stock_movements_path(stock_location) + end + + def edit + @stock_movement = StockMovement.find(params[:id]) + end + + private + + def stock_location + @stock_location ||= StockLocation.find(params[:stock_location_id]) + end + + def stock_movement_params + params.require(:stock_movement).permit(:quantity, :stock_item_id, :action) + end + end + end +end diff --git a/backend/app/controllers/spree/admin/stock_transfers_controller.rb b/backend/app/controllers/spree/admin/stock_transfers_controller.rb new file mode 100644 index 00000000000..8fb7499c582 --- /dev/null +++ b/backend/app/controllers/spree/admin/stock_transfers_controller.rb @@ -0,0 +1,53 @@ +module Spree + module Admin + class StockTransfersController < Admin::BaseController + before_action :load_stock_locations, only: :index + + def index + @q = StockTransfer.ransack(params[:q]) + + @stock_transfers = @q.result + .includes(:stock_movements => { :stock_item => :stock_location }) + .order('created_at DESC') + .page(params[:page]) + end + + def show + @stock_transfer = StockTransfer.find_by_param(params[:id]) + end + + def new + + end + + def create + variants = Hash.new(0) + params[:variant].each_with_index do |variant_id, i| + variants[variant_id] += params[:quantity][i].to_i + end + + stock_transfer = StockTransfer.create(:reference => params[:reference]) + stock_transfer.transfer(source_location, + destination_location, + variants) + + flash[:success] = Spree.t(:stock_successfully_transferred) + redirect_to admin_stock_transfer_path(stock_transfer) + end + + private + def load_stock_locations + @stock_locations = Spree::StockLocation.active.order_default + end + + def source_location + @source_location ||= params.has_key?(:transfer_receive_stock) ? nil : + StockLocation.find(params[:transfer_source_location_id]) + end + + def destination_location + @destination_location ||= StockLocation.find(params[:transfer_destination_location_id]) + end + end + end +end diff --git a/core/app/controllers/spree/admin/tax_categories_controller.rb b/backend/app/controllers/spree/admin/tax_categories_controller.rb similarity index 94% rename from core/app/controllers/spree/admin/tax_categories_controller.rb rename to backend/app/controllers/spree/admin/tax_categories_controller.rb index 187ed6b25de..74e5e83c0b9 100644 --- a/core/app/controllers/spree/admin/tax_categories_controller.rb +++ b/backend/app/controllers/spree/admin/tax_categories_controller.rb @@ -2,7 +2,7 @@ module Spree module Admin class TaxCategoriesController < ResourceController def destroy - if @object.mark_deleted! + if @object.destroy flash[:success] = flash_message_for(@object, :successfully_removed) respond_with(@object) do |format| format.html { redirect_to collection_url } diff --git a/backend/app/controllers/spree/admin/tax_rates_controller.rb b/backend/app/controllers/spree/admin/tax_rates_controller.rb new file mode 100644 index 00000000000..15db5a1493b --- /dev/null +++ b/backend/app/controllers/spree/admin/tax_rates_controller.rb @@ -0,0 +1,15 @@ +module Spree + module Admin + class TaxRatesController < ResourceController + before_action :load_data + + private + + def load_data + @available_zones = Zone.order(:name) + @available_categories = TaxCategory.order(:name) + @calculators = TaxRate.calculators.sort_by(&:name) + end + end + end +end diff --git a/core/app/controllers/spree/admin/taxonomies_controller.rb b/backend/app/controllers/spree/admin/taxonomies_controller.rb old mode 100755 new mode 100644 similarity index 93% rename from core/app/controllers/spree/admin/taxonomies_controller.rb rename to backend/app/controllers/spree/admin/taxonomies_controller.rb index 0824538517c..3010db2a52a --- a/core/app/controllers/spree/admin/taxonomies_controller.rb +++ b/backend/app/controllers/spree/admin/taxonomies_controller.rb @@ -5,8 +5,6 @@ class TaxonomiesController < ResourceController def get_children @taxons = Taxon.find(params[:parent_id]).children - - respond_with(@taxons) end private diff --git a/backend/app/controllers/spree/admin/taxons_controller.rb b/backend/app/controllers/spree/admin/taxons_controller.rb new file mode 100644 index 00000000000..f73dbea356c --- /dev/null +++ b/backend/app/controllers/spree/admin/taxons_controller.rb @@ -0,0 +1,107 @@ +module Spree + module Admin + class TaxonsController < Spree::Admin::BaseController + + respond_to :html, :json, :js + + def index + + end + + def search + if params[:ids] + @taxons = Spree::Taxon.where(:id => params[:ids].split(',')) + else + @taxons = Spree::Taxon.limit(20).ransack(:name_cont => params[:q]).result + end + end + + def create + @taxonomy = Taxonomy.find(params[:taxonomy_id]) + @taxon = @taxonomy.taxons.build(params[:taxon]) + if @taxon.save + respond_with(@taxon) do |format| + format.json {render :json => @taxon.to_json } + end + else + flash[:error] = Spree.t('errors.messages.could_not_create_taxon') + respond_with(@taxon) do |format| + format.html { redirect_to @taxonomy ? edit_admin_taxonomy_url(@taxonomy) : admin_taxonomies_url } + end + end + end + + def edit + @taxonomy = Taxonomy.find(params[:taxonomy_id]) + @taxon = @taxonomy.taxons.find(params[:id]) + @permalink_part = @taxon.permalink.split("/").last + end + + def update + @taxonomy = Taxonomy.find(params[:taxonomy_id]) + @taxon = @taxonomy.taxons.find(params[:id]) + parent_id = params[:taxon][:parent_id] + new_position = params[:taxon][:position] + + if parent_id + @taxon.parent = Taxon.find(parent_id.to_i) + end + + if new_position + @taxon.child_index = new_position.to_i + end + + @taxon.save! + + # regenerate permalink + if parent_id + @taxon.reload + @taxon.set_permalink + @taxon.save! + @update_children = true + end + + if params.key? "permalink_part" + parent_permalink = @taxon.permalink.split("/")[0...-1].join("/") + parent_permalink += "/" unless parent_permalink.blank? + params[:taxon][:permalink] = parent_permalink + params[:permalink_part] + end + #check if we need to rename child taxons if parent name or permalink changes + @update_children = true if params[:taxon][:name] != @taxon.name || params[:taxon][:permalink] != @taxon.permalink + + if @taxon.update_attributes(taxon_params) + flash[:success] = flash_message_for(@taxon, :successfully_updated) + end + + #rename child taxons + if @update_children + @taxon.descendants.each do |taxon| + taxon.reload + taxon.set_permalink + taxon.save! + end + end + + respond_with(@taxon) do |format| + format.html {redirect_to edit_admin_taxonomy_url(@taxonomy) } + format.json {render :json => @taxon.to_json } + end + end + + def destroy + @taxon = Taxon.find(params[:id]) + @taxon.destroy + respond_with(@taxon) { |format| format.json { render :json => '' } } + end + + private + def taxon_params + params.require(:taxon).permit(permitted_params) + end + + def permitted_params + Spree::PermittedAttributes.taxon_attributes + end + end + end +end diff --git a/core/app/controllers/spree/admin/trackers_controller.rb b/backend/app/controllers/spree/admin/trackers_controller.rb similarity index 100% rename from core/app/controllers/spree/admin/trackers_controller.rb rename to backend/app/controllers/spree/admin/trackers_controller.rb diff --git a/backend/app/controllers/spree/admin/users_controller.rb b/backend/app/controllers/spree/admin/users_controller.rb new file mode 100644 index 00000000000..7ee8833bf4e --- /dev/null +++ b/backend/app/controllers/spree/admin/users_controller.rb @@ -0,0 +1,159 @@ +module Spree + module Admin + class UsersController < ResourceController + rescue_from Spree::Core::DestroyWithOrdersError, :with => :user_destroy_with_orders_error + + after_action :sign_in_if_change_own_password, only: :update + + # http://spreecommerce.com/blog/2010/11/02/json-hijacking-vulnerability/ + before_action :check_json_authenticity, only: :index + before_action :load_roles + + def index + respond_with(@collection) do |format| + format.html + format.json { render :json => json_data } + end + end + + def show + redirect_to edit_admin_user_path(@user) + end + + def create + if params[:user] + roles = params[:user].delete("spree_role_ids") + end + + @user = Spree.user_class.new(user_params) + if @user.save + + if roles + @user.spree_roles = roles.reject(&:blank?).collect{|r| Spree::Role.find(r)} + end + + flash.now[:success] = Spree.t(:created_successfully) + render :edit + else + render :new + end + end + + def update + if params[:user] + roles = params[:user].delete("spree_role_ids") + end + + if @user.update_attributes(user_params) + if roles + @user.spree_roles = roles.reject(&:blank?).collect{|r| Spree::Role.find(r)} + end + flash.now[:success] = Spree.t(:account_updated) + end + + render :edit + end + + def addresses + if request.put? + if @user.update_attributes(user_params) + flash.now[:success] = Spree.t(:account_updated) + end + + render :addresses + end + end + + def orders + params[:q] ||= {} + @search = Spree::Order.reverse_chronological.ransack(params[:q].merge(user_id_eq: @user.id)) + @orders = @search.result.page(params[:page]).per(Spree::Config[:admin_products_per_page]) + end + + def items + params[:q] ||= {} + @search = Spree::Order.includes( + line_items: { + variant: [:product, { option_values: :option_type }] + }).ransack(params[:q].merge(user_id_eq: @user.id)) + @orders = @search.result.page(params[:page]).per(Spree::Config[:admin_products_per_page]) + end + + def generate_api_key + if @user.generate_spree_api_key! + flash[:success] = Spree.t('api.key_generated') + end + redirect_to edit_admin_user_path(@user) + end + + def clear_api_key + if @user.clear_spree_api_key! + flash[:success] = Spree.t('api.key_cleared') + end + redirect_to edit_admin_user_path(@user) + end + + def model_class + Spree.user_class + end + + protected + + def collection + return @collection if @collection.present? + if request.xhr? && params[:q].present? + @collection = Spree.user_class.includes(:bill_address, :ship_address) + .where("spree_users.email #{LIKE} :search + OR (spree_addresses.firstname #{LIKE} :search AND spree_addresses.id = spree_users.bill_address_id) + OR (spree_addresses.lastname #{LIKE} :search AND spree_addresses.id = spree_users.bill_address_id) + OR (spree_addresses.firstname #{LIKE} :search AND spree_addresses.id = spree_users.ship_address_id) + OR (spree_addresses.lastname #{LIKE} :search AND spree_addresses.id = spree_users.ship_address_id)", + { :search => "#{params[:q].strip}%" }) + .limit(params[:limit] || 100) + else + @search = Spree.user_class.ransack(params[:q]) + @collection = @search.result.page(params[:page]).per(Spree::Config[:admin_products_per_page]) + end + end + + private + def user_params + params.require(:user).permit(PermittedAttributes.user_attributes | + [:spree_role_ids, + ship_address_attributes: PermittedAttributes.address_attributes, + bill_address_attributes: PermittedAttributes.address_attributes]) + end + + # handling raise from Spree::Admin::ResourceController#destroy + def user_destroy_with_orders_error + invoke_callbacks(:destroy, :fails) + render :status => :forbidden, :text => Spree.t(:error_user_destroy_with_orders) + end + + # Allow different formats of json data to suit different ajax calls + def json_data + json_format = params[:json_format] or 'default' + case json_format + when 'basic' + collection.map { |u| { 'id' => u.id, 'name' => u.email } }.to_json + else + address_fields = [:firstname, :lastname, :address1, :address2, :city, :zipcode, :phone, :state_name, :state_id, :country_id] + includes = { :only => address_fields , :include => { :state => { :only => :name }, :country => { :only => :name } } } + + collection.to_json(:only => [:id, :email], :include => + { :bill_address => includes, :ship_address => includes }) + end + end + + def sign_in_if_change_own_password + if try_spree_current_user == @user && @user.password.present? + sign_in(@user, :event => :authentication, :bypass => true) + end + end + + def load_roles + @roles = Spree::Role.all + end + end + end +end diff --git a/backend/app/controllers/spree/admin/variants_controller.rb b/backend/app/controllers/spree/admin/variants_controller.rb new file mode 100644 index 00000000000..6d620189e77 --- /dev/null +++ b/backend/app/controllers/spree/admin/variants_controller.rb @@ -0,0 +1,49 @@ +module Spree + module Admin + class VariantsController < ResourceController + belongs_to 'spree/product', :find_by => :slug + new_action.before :new_before + before_action :load_data, only: [:new, :create, :edit, :update] + + # override the destroy method to set deleted_at value + # instead of actually deleting the product. + def destroy + @variant = Variant.find(params[:id]) + if @variant.destroy + flash[:success] = Spree.t('notice_messages.variant_deleted') + else + flash[:success] = Spree.t('notice_messages.variant_not_deleted') + end + + respond_with(@variant) do |format| + format.html { redirect_to admin_product_variants_url(params[:product_id]) } + format.js { render_js_for_destroy } + end + end + + protected + def new_before + @object.attributes = @object.product.master.attributes.except('id', 'created_at', 'deleted_at', + 'sku', 'is_master') + # Shallow Clone of the default price to populate the price field. + @object.default_price = @object.product.master.default_price.clone + end + + def collection + @deleted = (params.key?(:deleted) && params[:deleted] == "on") ? "checked" : "" + + if @deleted.blank? + @collection ||= super + else + @collection ||= Variant.only_deleted.where(:product_id => parent.id) + end + @collection + end + + private + def load_data + @tax_categories = TaxCategory.order(:name) + end + end + end +end diff --git a/backend/app/controllers/spree/admin/variants_including_master_controller.rb b/backend/app/controllers/spree/admin/variants_including_master_controller.rb new file mode 100644 index 00000000000..e5047572aa0 --- /dev/null +++ b/backend/app/controllers/spree/admin/variants_including_master_controller.rb @@ -0,0 +1,15 @@ +module Spree + module Admin + class VariantsIncludingMasterController < VariantsController + + def model_class + Spree::Variant + end + + def object_name + "variant" + end + + end + end +end diff --git a/backend/app/controllers/spree/admin/zones_controller.rb b/backend/app/controllers/spree/admin/zones_controller.rb new file mode 100644 index 00000000000..b566e975fc1 --- /dev/null +++ b/backend/app/controllers/spree/admin/zones_controller.rb @@ -0,0 +1,26 @@ +module Spree + module Admin + class ZonesController < ResourceController + before_action :load_data, except: :index + + def new + @zone.zone_members.build + end + + protected + + def collection + params[:q] ||= {} + params[:q][:s] ||= "ascend_by_name" + @search = super.ransack(params[:q]) + @zones = @search.result.page(params[:page]).per(params[:per_page]) + end + + def load_data + @countries = Country.order(:name) + @states = State.order(:name) + @zones = Zone.order(:name) + end + end + end +end diff --git a/backend/app/helpers/spree/admin/adjustments_helper.rb b/backend/app/helpers/spree/admin/adjustments_helper.rb new file mode 100644 index 00000000000..40ced24d7fa --- /dev/null +++ b/backend/app/helpers/spree/admin/adjustments_helper.rb @@ -0,0 +1,42 @@ +module Spree + module Admin + module AdjustmentsHelper + def adjustment_state(adjustment) + state = adjustment.state.to_sym + icon = { closed: 'lock', open: 'unlock' } + content_tag(:span, '', class: "fa fa-#{ icon[state] }") + end + + def display_adjustable(adjustable) + case adjustable + when Spree::LineItem + display_line_item(adjustable) + when Spree::Shipment + display_shipment(adjustable) + when Spree::Order + display_order(adjustable) + end + + end + + private + + def display_line_item(line_item) + variant = line_item.variant + parts = [] + parts << variant.product.name + parts << "(#{variant.options_text})" if variant.options_text.present? + parts << line_item.display_total + parts.join("
").html_safe + end + + def display_shipment(shipment) + "#{Spree.t(:shipment)} ##{shipment.number}
#{shipment.display_cost}".html_safe + end + + def display_order(order) + Spree.t(:order) + end + end + end +end diff --git a/core/app/helpers/spree/admin/base_helper.rb b/backend/app/helpers/spree/admin/base_helper.rb similarity index 82% rename from core/app/helpers/spree/admin/base_helper.rb rename to backend/app/helpers/spree/admin/base_helper.rb index feb613d1a36..0c5f896f8f5 100644 --- a/core/app/helpers/spree/admin/base_helper.rb +++ b/backend/app/helpers/spree/admin/base_helper.rb @@ -22,6 +22,14 @@ def error_message_on(object, method, options = {}) end end + def datepicker_field_value(date) + unless date.blank? + l(date, :format => Spree.t('date_picker.format', default: '%Y/%m/%d')) + else + nil + end + end + # This method demonstrates the use of the :child_index option to render a # form partial for, for instance, client side addition of new nested # records. @@ -54,7 +62,7 @@ def generate_template(form_builder, method, options = {}) def remove_nested(fields) out = '' out << fields.hidden_field(:_destroy) unless fields.object.new_record? - out << (link_to icon('delete'), "#", :class => 'remove') + out << (link_to icon('remove'), "#", :class => 'remove') out.html_safe end @@ -63,7 +71,7 @@ def preference_field_tag(name, value, options) when :integer text_field_tag(name, value, preference_field_options(options)) when :boolean - hidden_field_tag(name, 0) + + hidden_field_tag(name, 0, id: "#{name}_hidden") + check_box_tag(name, 1, value, preference_field_options(options)) when :string text_field_tag(name, value, preference_field_options(options)) @@ -126,23 +134,16 @@ def preference_fields(object, form) return unless object.respond_to?(:preferences) object.preferences.keys.map{ |key| - form.label("preferred_#{key}", t(key) + ": ") + + form.label("preferred_#{key}", Spree.t(key) + ": ") + preference_field_for(form, "preferred_#{key}", :type => object.preference_type(key)) }.join("
").html_safe end - def product_picker_field(name, value) - products = Product.with_ids(value.split(',')) - product_names = products.inject({}){|memo,item| memo[item.id] = item.name; memo} - product_rules = products.collect{ |p| { :id => p.id, :name => p.name } } - %().html_safe - end - def link_to_add_fields(name, target, options = {}) name = '' if options[:no_text] - css_classes = options[:class] ? options[:class] + " add_fields" : "add_fields" - link_to_with_icon('icon-plus', name, 'javascript:', :data => { :target => target }, :class => css_classes) + css_classes = options[:class] ? options[:class] + " spree_add_fields" : "spree_add_fields" + link_to_with_icon('plus', name, 'javascript:', :data => { :target => target }, :class => css_classes) end # renders hidden field and link to remove record using nested_attributes @@ -150,13 +151,20 @@ def link_to_remove_fields(name, f, options = {}) name = '' if options[:no_text] options[:class] = '' unless options[:class] options[:class] += 'no-text with-tip' if options[:no_text] - link_to_with_icon('icon-trash', name, '#', :class => "remove_fields #{options[:class]}", :data => {:action => 'remove'}, :title => t(:remove)) + f.hidden_field(:_destroy) + url = f.object.persisted? ? [:admin, f.object] : '#' + link_to_with_icon('trash', name, url, :class => "spree_remove_fields #{options[:class]}", :data => {:action => 'remove'}, :title => Spree.t(:remove)) + f.hidden_field(:_destroy) end def spree_dom_id(record) dom_id(record, 'spree') end + def rails_environments + @@rails_environments ||= Dir.glob("#{Rails.root}/config/environments/*.rb") + .map { |f| File.basename(f, ".rb") } + .sort + end + private def attribute_name_for(field_name) field_name.gsub(' ', '_').downcase diff --git a/backend/app/helpers/spree/admin/customer_returns_helper.rb b/backend/app/helpers/spree/admin/customer_returns_helper.rb new file mode 100644 index 00000000000..8880f046e84 --- /dev/null +++ b/backend/app/helpers/spree/admin/customer_returns_helper.rb @@ -0,0 +1,9 @@ +module Spree + module Admin + module CustomerReturnsHelper + def reimbursement_types + @reimbursement_types ||= Spree::ReimbursementType.accessible_by(current_ability, :read).active + end + end + end +end diff --git a/core/app/helpers/spree/admin/general_settings_helper.rb b/backend/app/helpers/spree/admin/general_settings_helper.rb similarity index 100% rename from core/app/helpers/spree/admin/general_settings_helper.rb rename to backend/app/helpers/spree/admin/general_settings_helper.rb diff --git a/backend/app/helpers/spree/admin/images_helper.rb b/backend/app/helpers/spree/admin/images_helper.rb new file mode 100644 index 00000000000..36e031b6e9b --- /dev/null +++ b/backend/app/helpers/spree/admin/images_helper.rb @@ -0,0 +1,18 @@ +module Spree + module Admin + module ImagesHelper + def options_text_for(image) + if image.viewable.is_a?(Spree::Variant) + if image.viewable.is_master? + Spree.t(:all) + else + image.viewable.sku_and_options_text + end + else + Spree.t(:all) + end + end + end + end +end + diff --git a/backend/app/helpers/spree/admin/inventory_settings_helper.rb b/backend/app/helpers/spree/admin/inventory_settings_helper.rb new file mode 100644 index 00000000000..a603f407c9e --- /dev/null +++ b/backend/app/helpers/spree/admin/inventory_settings_helper.rb @@ -0,0 +1,9 @@ +module Spree + module Admin + module InventorySettingsHelper + def show_not(true_or_false) + true_or_false ? '' : Spree.t(:not) + end + end + end +end diff --git a/backend/app/helpers/spree/admin/navigation_helper.rb b/backend/app/helpers/spree/admin/navigation_helper.rb new file mode 100644 index 00000000000..2cfc89dacd9 --- /dev/null +++ b/backend/app/helpers/spree/admin/navigation_helper.rb @@ -0,0 +1,158 @@ +module Spree + module Admin + module NavigationHelper + # Make an admin tab that coveres one or more resources supplied by symbols + # Option hash may follow. Valid options are + # * :label to override link text, otherwise based on the first resource name (translated) + # * :route to override automatically determining the default route + # * :match_path as an alternative way to control when the tab is active, /products would match /admin/products, /admin/products/5/variants etc. + def tab(*args) + options = {:label => args.first.to_s} + + # Return if resource is found and user is not allowed to :admin + return '' if klass = klass_for(options[:label]) and cannot?(:admin, klass) + + if args.last.is_a?(Hash) + options = options.merge(args.pop) + end + options[:route] ||= "admin_#{args.first}" + + destination_url = options[:url] || spree.send("#{options[:route]}_path") + titleized_label = Spree.t(options[:label], :default => options[:label], :scope => [:admin, :tab]).titleize + + css_classes = [] + + if options[:icon] + link = link_to_with_icon(options[:icon], titleized_label, destination_url) + css_classes << 'tab-with-icon' + else + link = link_to(titleized_label, destination_url) + end + + selected = if options[:match_path].is_a? Regexp + request.fullpath =~ options[:match_path] + elsif options[:match_path] + request.fullpath.starts_with?("#{admin_path}#{options[:match_path]}") + else + args.include?(controller.controller_name.to_sym) + end + css_classes << 'selected' if selected + + if options[:css_class] + css_classes << options[:css_class] + end + content_tag('li', link, :class => css_classes.join(' ')) + end + + # finds class for a given symbol / string + # + # Example : + # :products returns Spree::Product + # :my_products returns MyProduct if MyProduct is defined + # :my_products returns My::Product if My::Product is defined + # if cannot constantize it returns nil + # This will allow us to use cancan abilities on tab + def klass_for(name) + model_name = name.to_s + + ["Spree::#{model_name.classify}", model_name.classify, model_name.gsub('_', '/').classify].find do |t| + t.safe_constantize + end.try(:safe_constantize) + end + + def link_to_clone(resource, options={}) + options[:data] = {:action => 'clone'} + link_to_with_icon('copy', Spree.t(:clone), clone_object_url(resource), options) + end + + def link_to_new(resource) + options[:data] = {:action => 'new'} + link_to_with_icon('plus', Spree.t(:new), edit_object_url(resource)) + end + + def link_to_edit(resource, options={}) + url = options[:url] || edit_object_url(resource) + options[:data] = {:action => 'edit'} + link_to_with_icon('edit', Spree.t(:edit), url, options) + end + + def link_to_edit_url(url, options={}) + options[:data] = {:action => 'edit'} + link_to_with_icon('edit', Spree.t(:edit), url, options) + end + + def link_to_delete(resource, options={}) + url = options[:url] || object_url(resource) + name = options[:name] || Spree.t(:delete) + options[:class] = "delete-resource" + options[:data] = { :confirm => Spree.t(:are_you_sure), :action => 'remove' } + link_to_with_icon 'trash', name, url, options + end + + def link_to_with_icon(icon_name, text, url, options = {}) + options[:class] = (options[:class].to_s + " fa fa-#{icon_name} icon_link with-tip").strip + options[:class] += ' no-text' if options[:no_text] + options[:title] = text if options[:no_text] + text = options[:no_text] ? '' : raw("#{text}") + options.delete(:no_text) + link_to(text, url, options) + end + + def icon(icon_name) + icon_name ? content_tag(:i, '', :class => icon_name) : '' + end + + def button(text, icon_name = nil, button_type = 'submit', options={}) + button_tag(text, options.merge(:type => button_type, :class => "fa fa-#{icon_name} button")) + end + + def button_link_to(text, url, html_options = {}) + if (html_options[:method] && + html_options[:method].to_s.downcase != 'get' && + !html_options[:remote]) + form_tag(url, :method => html_options.delete(:method)) do + button(text, html_options.delete(:icon), nil, html_options) + end + else + if html_options['data-update'].nil? && html_options[:remote] + object_name, action = url.split('/')[-2..-1] + html_options['data-update'] = [action, object_name.singularize].join('_') + end + + html_options.delete('data-update') unless html_options['data-update'] + + html_options[:class] = 'button' + + if html_options[:icon] + html_options[:class] += " fa fa-#{html_options[:icon]}" + end + link_to(text_for_button_link(text, html_options), url, html_options) + end + end + + def text_for_button_link(text, html_options) + s = '' + s << text + raw(s) + end + + def configurations_menu_item(link_text, url, description = '') + %( + #{link_to(link_text, url)} + #{description} + + ).html_safe + end + + def configurations_sidebar_menu_item(link_text, url, options = {}) + is_active = url.ends_with?(controller.controller_name) || + url.ends_with?("#{controller.controller_name}/edit") || + url.ends_with?("#{controller.controller_name.singularize}/edit") + options.merge!(:class => is_active ? 'active' : nil) + content_tag(:li, options) do + link_to(link_text, url) + end + end + end + end +end diff --git a/backend/app/helpers/spree/admin/orders_helper.rb b/backend/app/helpers/spree/admin/orders_helper.rb new file mode 100644 index 00000000000..6570aa2d09e --- /dev/null +++ b/backend/app/helpers/spree/admin/orders_helper.rb @@ -0,0 +1,65 @@ +module Spree + module Admin + module OrdersHelper + # Renders all the extension partials that may have been specified in the extensions + def event_links + links = [] + @order_events.sort.each do |event| + if @order.send("can_#{event}?") + links << button_link_to(Spree.t(event), [event, :admin, @order], + :method => :put, + :icon => "#{event}", + :data => { :confirm => Spree.t(:order_sure_want_to, :event => Spree.t(event)) }) + end + end + links.join(' ').html_safe + end + + def line_item_shipment_price(line_item, quantity) + Spree::Money.new(line_item.price * quantity, { currency: line_item.currency }) + end + + def avs_response_code + { + "A" => "Street address matches, but 5-digit and 9-digit postal code do not match.", + "B" => "Street address matches, but postal code not verified.", + "C" => "Street address and postal code do not match.", + "D" => "Street address and postal code match. ", + "E" => "AVS data is invalid or AVS is not allowed for this card type.", + "F" => "Card member's name does not match, but billing postal code matches.", + "G" => "Non-U.S. issuing bank does not support AVS.", + "H" => "Card member's name does not match. Street address and postal code match.", + "I" => "Address not verified.", + "J" => "Card member's name, billing address, and postal code match.", + "K" => "Card member's name matches but billing address and billing postal code do not match.", + "L" => "Card member's name and billing postal code match, but billing address does not match.", + "M" => "Street address and postal code match. ", + "N" => "Street address and postal code do not match.", + "O" => "Card member's name and billing address match, but billing postal code does not match.", + "P" => "Postal code matches, but street address not verified.", + "Q" => "Card member's name, billing address, and postal code match.", + "R" => "System unavailable.", + "S" => "Bank does not support AVS.", + "T" => "Card member's name does not match, but street address matches.", + "U" => "Address information unavailable. Returned if the U.S. bank does not support non-U.S. AVS or if the AVS in a U.S. bank is not functioning properly.", + "V" => "Card member's name, billing address, and billing postal code match.", + "W" => "Street address does not match, but 9-digit postal code matches.", + "X" => "Street address and 9-digit postal code match.", + "Y" => "Street address and 5-digit postal code match.", + "Z" => "Street address does not match, but 5-digit postal code matches." + } + end + + def cvv_response_code + { + "M" => "CVV2 Match", + "N" => "CVV2 No Match", + "P" => "Not Processed", + "S" => "Issuer indicates that CVV2 data should be present on the card, but the merchant has indicated data is not present on the card", + "U" => "Issuer has not certified for CVV2 or Issuer has not provided Visa with the CVV2 encryption keys", + "" => "Transaction failed because wrong CVV2 number was entered or no CVV2 number was entered" + } + end + end + end +end diff --git a/core/app/helpers/spree/admin/payments_helper.rb b/backend/app/helpers/spree/admin/payments_helper.rb similarity index 100% rename from core/app/helpers/spree/admin/payments_helper.rb rename to backend/app/helpers/spree/admin/payments_helper.rb diff --git a/core/app/helpers/spree/admin/products_helper.rb b/backend/app/helpers/spree/admin/products_helper.rb similarity index 91% rename from core/app/helpers/spree/admin/products_helper.rb rename to backend/app/helpers/spree/admin/products_helper.rb index bb5ab8aa044..30b07def3b9 100644 --- a/core/app/helpers/spree/admin/products_helper.rb +++ b/backend/app/helpers/spree/admin/products_helper.rb @@ -7,7 +7,7 @@ def taxon_options_for(product) content_tag(:option, :value => taxon.id, :selected => ('selected' if selected)) do - (taxon.ancestors.map(&:name) + [taxon.name]).join(" -> ") + (taxon.ancestors.pluck(:name) + [taxon.name]).join(" -> ") end end.join("").html_safe end diff --git a/backend/app/helpers/spree/admin/reimbursement_type_helper.rb b/backend/app/helpers/spree/admin/reimbursement_type_helper.rb new file mode 100644 index 00000000000..43ddab201ff --- /dev/null +++ b/backend/app/helpers/spree/admin/reimbursement_type_helper.rb @@ -0,0 +1,9 @@ +module Spree + module Admin + module ReimbursementTypeHelper + def reimbursement_type_name(reimbursement_type) + reimbursement_type.present? ? reimbursement_type.name.humanize : '' + end + end + end +end diff --git a/backend/app/helpers/spree/admin/reimbursements_helper.rb b/backend/app/helpers/spree/admin/reimbursements_helper.rb new file mode 100644 index 00000000000..99890c933ef --- /dev/null +++ b/backend/app/helpers/spree/admin/reimbursements_helper.rb @@ -0,0 +1,14 @@ +module Spree + module Admin + module ReimbursementsHelper + def reimbursement_status_color(reimbursement) + case reimbursement.reimbursement_status + when 'reimbursed' then 'success' + when 'pending' then 'notice' + when 'errored' then 'error' + else raise "unknown reimbursement status: #{reimbursement.reimbursement_status}" + end + end + end + end +end diff --git a/backend/app/helpers/spree/admin/stock_locations_helper.rb b/backend/app/helpers/spree/admin/stock_locations_helper.rb new file mode 100644 index 00000000000..2fe8daf4194 --- /dev/null +++ b/backend/app/helpers/spree/admin/stock_locations_helper.rb @@ -0,0 +1,15 @@ +module Spree + module Admin + module StockLocationsHelper + def display_name(stock_location) + name_parts = [stock_location.admin_name, stock_location.name] + name_parts.delete_if(&:blank?) + name_parts.join(' / ') + end + + def state(stock_location) + stock_location.active? ? 'active' : 'inactive' + end + end + end +end \ No newline at end of file diff --git a/backend/app/helpers/spree/admin/stock_movements_helper.rb b/backend/app/helpers/spree/admin/stock_movements_helper.rb new file mode 100644 index 00000000000..f4b96e4242b --- /dev/null +++ b/backend/app/helpers/spree/admin/stock_movements_helper.rb @@ -0,0 +1,24 @@ +module Spree + module Admin + module StockMovementsHelper + def pretty_originator(stock_movement) + if stock_movement.originator.respond_to?(:number) + if stock_movement.originator.respond_to?(:order) + link_to stock_movement.originator.number, [:edit, :admin, stock_movement.originator.order] + else + stock_movement.originator.number + end + else + "" + end + end + + def display_variant(stock_movement) + variant = stock_movement.stock_item.variant + output = variant.name + output += "
(#{variant.options_text})" unless variant.options_text.blank? + output.html_safe + end + end + end +end diff --git a/core/app/helpers/spree/admin/tables_helper.rb b/backend/app/helpers/spree/admin/tables_helper.rb similarity index 100% rename from core/app/helpers/spree/admin/tables_helper.rb rename to backend/app/helpers/spree/admin/tables_helper.rb diff --git a/core/app/helpers/spree/admin/taxons_helper.rb b/backend/app/helpers/spree/admin/taxons_helper.rb similarity index 100% rename from core/app/helpers/spree/admin/taxons_helper.rb rename to backend/app/helpers/spree/admin/taxons_helper.rb diff --git a/backend/app/helpers/spree/promotion_rules_helper.rb b/backend/app/helpers/spree/promotion_rules_helper.rb new file mode 100644 index 00000000000..62352e2d4be --- /dev/null +++ b/backend/app/helpers/spree/promotion_rules_helper.rb @@ -0,0 +1,13 @@ +module Spree + module PromotionRulesHelper + + def options_for_promotion_rule_types(promotion) + existing = promotion.rules.map { |rule| rule.class.name } + rule_names = Rails.application.config.spree.promotions.rules.map(&:name).reject{ |r| existing.include? r } + options = rule_names.map { |name| [ Spree.t("promotion_rule_types.#{name.demodulize.underscore}.name"), name] } + options_for_select(options) + end + + end +end + diff --git a/backend/app/models/spree/backend_configuration.rb b/backend/app/models/spree/backend_configuration.rb new file mode 100644 index 00000000000..98feb9ed759 --- /dev/null +++ b/backend/app/models/spree/backend_configuration.rb @@ -0,0 +1,21 @@ +module Spree + class BackendConfiguration < Preferences::Configuration + preference :locale, :string, default: Rails.application.config.i18n.default_locale + + ORDER_TABS ||= [:orders, :payments, :creditcard_payments, + :shipments, :credit_cards, :return_authorizations, + :customer_returns, :adjustments, :customer_details] + PRODUCT_TABS ||= [:products, :option_types, :properties, :prototypes, + :variants, :product_properties, :taxonomies, + :taxons] + REPORT_TABS ||= [:reports] + CONFIGURATION_TABS ||= [:configurations, :general_settings, :tax_categories, + :tax_rates, :zones, :countries, :states, + :payment_methods, :shipping_methods, + :shipping_categories, :stock_transfers, + :stock_locations, :trackers, :refund_reasons, + :reimbursement_types, :return_authorization_reasons] + PROMOTION_TABS ||= [:promotions, :promotion_categories] + USER_TABS ||= [:users] + end +end diff --git a/backend/app/views/spree/admin/adjustments/_adjustment.html.erb b/backend/app/views/spree/admin/adjustments/_adjustment.html.erb new file mode 100644 index 00000000000..3d5c73653a8 --- /dev/null +++ b/backend/app/views/spree/admin/adjustments/_adjustment.html.erb @@ -0,0 +1,16 @@ +<% + @edit_url = edit_admin_order_adjustment_path(@order, adjustment) + @delete_url = admin_order_adjustment_path(@order, adjustment) +%> + + <%= display_adjustable(adjustment.adjustable) %> + <%= adjustment.label %> + <%= adjustment.display_amount.to_html %> + <%= adjustment_state(adjustment) %> + + <% if adjustment.open? %> + <%= link_to_edit adjustment, :no_text => true %> + <%= link_to_delete adjustment, :no_text => true %> + <% end %> + + \ No newline at end of file diff --git a/backend/app/views/spree/admin/adjustments/_adjustments_table.html.erb b/backend/app/views/spree/admin/adjustments/_adjustments_table.html.erb new file mode 100644 index 00000000000..3cd766fe328 --- /dev/null +++ b/backend/app/views/spree/admin/adjustments/_adjustments_table.html.erb @@ -0,0 +1,25 @@ + + + + + + + + + + + + <%= render :partial => "adjustment", :collection => @adjustments %> + + + + + +
<%= Spree.t(:adjustable) %><%= Spree.t(:description) %><%= Spree.t(:amount) %><%= Spree.t(:state) %>
+
+ <%= button_to Spree.t(:open_all_adjustments), open_adjustments_admin_order_path(@order), method: :get %> +
+
+ <%= button_to Spree.t(:close_all_adjustments), close_adjustments_admin_order_path(@order), method: :get %> +
+
 
diff --git a/backend/app/views/spree/admin/adjustments/_form.html.erb b/backend/app/views/spree/admin/adjustments/_form.html.erb new file mode 100644 index 00000000000..6d8dbefb8f4 --- /dev/null +++ b/backend/app/views/spree/admin/adjustments/_form.html.erb @@ -0,0 +1,16 @@ +
+
+ <%= f.field_container :amount do %> + <%= f.label :amount, raw(Spree.t(:amount) + content_tag(:span, " *", :class => "required")) %> + <%= text_field :adjustment, :amount, :class => 'fullwidth' %> + <%= f.error_message_on :amount %> + <% end %> +
+
+ <%= f.field_container :label do %> + <%= f.label :label, raw(Spree.t(:description) + content_tag(:span, " *", :class => "required")) %> + <%= text_field :adjustment, :label, :class => 'fullwidth' %> + <%= f.error_message_on :label %> + <% end %> +
+
diff --git a/backend/app/views/spree/admin/adjustments/edit.html.erb b/backend/app/views/spree/admin/adjustments/edit.html.erb new file mode 100644 index 00000000000..94a52c650c1 --- /dev/null +++ b/backend/app/views/spree/admin/adjustments/edit.html.erb @@ -0,0 +1,22 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Adjustments' } %> + +<% content_for :page_title do %> + <%= Spree.t(:edit) %> <%= Spree.t(:adjustment) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_adjustments_list), spree.admin_order_adjustments_url(@order), :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @adjustment } %> +<%= form_for @adjustment, :url => admin_order_adjustment_path(@order, @adjustment), :method => :put do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= button Spree.t(:continue), 'arrow-right' %> + <%= Spree.t(:or) %> + <%= link_to_with_icon 'remove', Spree.t('actions.cancel'), admin_order_adjustments_url(@order), :class => 'button' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/adjustments/index.html.erb b/backend/app/views/spree/admin/adjustments/index.html.erb new file mode 100644 index 00000000000..86af8f37983 --- /dev/null +++ b/backend/app/views/spree/admin/adjustments/index.html.erb @@ -0,0 +1,22 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Adjustments' } %> + +<% content_for :page_title do %> + <%= Spree.t(:adjustments) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:new_adjustment), new_admin_order_adjustment_url(@order), :icon => 'plus' %>
  • +
  • <%= button_link_to Spree.t(:back_to_orders_list), admin_orders_path, :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'adjustments_table' %> + +<% if @order.can_add_coupon? %> +
    + <%= text_field_tag "coupon_code", "", :placeholder => Spree.t(:coupon_code) %> + <%= button Spree.t(:add_coupon_code), 'plus', 'submit', :id => "add_coupon_code" %> +
    +<% end %> +<%= javascript_tag do -%> + var order_number = '<%= @order.number %>'; +<% end -%> diff --git a/backend/app/views/spree/admin/adjustments/new.html.erb b/backend/app/views/spree/admin/adjustments/new.html.erb new file mode 100644 index 00000000000..5b21d029b5e --- /dev/null +++ b/backend/app/views/spree/admin/adjustments/new.html.erb @@ -0,0 +1,23 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Adjustments' } %> + +<% content_for :page_title do %> + <%= Spree.t(:new_adjustment) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_adjustments_list), spree.admin_order_adjustments_url(@order), :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @adjustment } %> + +<%= form_for @adjustment, :url => admin_order_adjustments_path do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= button Spree.t(:continue), 'arrow-right' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_order_adjustments_url(@order), :icon => 'remove' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/countries/_form.html.erb b/backend/app/views/spree/admin/countries/_form.html.erb new file mode 100644 index 00000000000..347d5e6577c --- /dev/null +++ b/backend/app/views/spree/admin/countries/_form.html.erb @@ -0,0 +1,22 @@ +
    +
    +
    + <%= f.label :name, Spree.t(:name) %> + <%= f.text_field :name, :class => 'fullwidth' %> +
    +
    +
    +
    + <%= f.label :iso_name, Spree.t(:iso_name) %> + <%= f.text_field :iso_name, :class => 'fullwidth' %> +
    +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/backend/app/views/spree/admin/countries/edit.html.erb b/backend/app/views/spree/admin/countries/edit.html.erb new file mode 100644 index 00000000000..63367c0315d --- /dev/null +++ b/backend/app/views/spree/admin/countries/edit.html.erb @@ -0,0 +1,21 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_country) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_countries_list), spree.admin_countries_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @country } %> + +<%= form_for [:admin, @country] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> +
    + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/countries/index.html.erb b/backend/app/views/spree/admin/countries/index.html.erb new file mode 100644 index 00000000000..484a2f60a2f --- /dev/null +++ b/backend/app/views/spree/admin/countries/index.html.erb @@ -0,0 +1,43 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:listing_countries) %> +<% end %> + +<% content_for :page_actions do %> +
      +
    • + <%= button_link_to Spree.t(:new_country), new_object_url, { :icon => 'plus', :id => 'admin_new_country' } %> +
    • +
    +<% end %> + + + + + + + + + + + + + + + + + + <% @countries.each do |country| %> + + + + + + + <% end %> + +
    <%= Spree.t(:country_name) %><%= Spree.t(:iso_name) %><%= Spree.t(:states_required) %>
    <%= country.name %><%= country.iso_name %><%= country.states_required? ? Spree.t(:say_yes) : Spree.t(:say_no) %> + <%= link_to_edit country, :no_text => true %> + <%= link_to_delete country, :no_text => true %> +
    diff --git a/backend/app/views/spree/admin/countries/new.html.erb b/backend/app/views/spree/admin/countries/new.html.erb new file mode 100644 index 00000000000..fb8b96fd8e8 --- /dev/null +++ b/backend/app/views/spree/admin/countries/new.html.erb @@ -0,0 +1,18 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_country) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_countries_list), admin_countries_path, :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @countries } %> + +<%= form_for [:admin, @country] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/customer_returns/_reimbursements_table.html.erb b/backend/app/views/spree/admin/customer_returns/_reimbursements_table.html.erb new file mode 100644 index 00000000000..d82753f0bec --- /dev/null +++ b/backend/app/views/spree/admin/customer_returns/_reimbursements_table.html.erb @@ -0,0 +1,36 @@ + + + + + + + + + + + + <% reimbursements.each do |reimbursement| %> + + + + + + + + <% end %> + +
    <%= Spree.t(:number) %><%= Spree.t(:total) %><%= Spree.t(:status) %><%= "#{Spree.t('date')}/#{Spree.t('time')}" %>
    + <% if reimbursement.reimbursed? %> + <%= link_to reimbursement.number, url_for([:admin, @order, reimbursement]) %> + <% else %> + <%= reimbursement.number %> + <% end %> + <%= reimbursement.display_total %> + + <%= reimbursement.reimbursement_status %> + + <%= pretty_time(reimbursement.created_at) %> + <% if !reimbursement.reimbursed? %> + <%= link_to_edit_url url_for([:edit, :admin, @order, reimbursement]), title: "admin_edit_#{dom_id(reimbursement)}", no_text: true %> + <% end %> +
    diff --git a/backend/app/views/spree/admin/customer_returns/_return_item_decision.html.erb b/backend/app/views/spree/admin/customer_returns/_return_item_decision.html.erb new file mode 100644 index 00000000000..e2cb07153e6 --- /dev/null +++ b/backend/app/views/spree/admin/customer_returns/_return_item_decision.html.erb @@ -0,0 +1,50 @@ + + + + + + + + + + <% if show_decision %> + + <% end %> + + + + <% return_items.each do |return_item| %> + + + + + + + + <% if show_decision %> + + <% end %> + + <% end %> + +
    <%= Spree.t(:product) %><%= Spree.t(:sku) %><%= Spree.t(:pre_tax_amount) %><%= Spree.t(:preferred_reimbursement_type) %><%= Spree.t(:exchange_for) %><%= Spree.t(:acceptance_errors) %>
    +
    <%= return_item.inventory_unit.variant.name %>
    +
    <%= return_item.inventory_unit.variant.options_text %>
    +
    + <%= return_item.inventory_unit.variant.sku %> + + <%= return_item.display_pre_tax_amount %> + + <%= reimbursement_type_name(return_item.preferred_reimbursement_type) %> + + <%= return_item.exchange_variant.try(:exchange_name) %> + + <%= return_item.acceptance_status_errors %> + + <%= button_to [:admin, return_item], { class: 'fa fa-thumbs-up icon_link no-text with-tip', params: { "return_item[acceptance_status]" => 'accepted' }, "data-action" => 'save', title: Spree.t(:accept), method: 'put' } do %> + Spree.t(:accept) + <% end %> + <%= button_to [:admin, return_item], { class: 'fa fa-thumbs-down icon_link no-text with-tip', params: { "return_item[acceptance_status]" => 'rejected' }, "data-action" => 'remove', title: Spree.t(:reject), method: 'put' } do %> + Spree.t(:reject) + <% end %> +
    diff --git a/backend/app/views/spree/admin/customer_returns/_return_item_selection.html.erb b/backend/app/views/spree/admin/customer_returns/_return_item_selection.html.erb new file mode 100644 index 00000000000..cf03dcc2eb0 --- /dev/null +++ b/backend/app/views/spree/admin/customer_returns/_return_item_selection.html.erb @@ -0,0 +1,43 @@ + + + + + + + + + + + + <%= f.fields_for :return_items, return_items do |item_fields| %> + <% return_item = item_fields.object %> + + + + + + + + + <% end %> + +
    + <%= check_box_tag 'select-all' %> + <%= Spree.t(:product) %><%= Spree.t(:sku) %><%= Spree.t(:pre_tax_amount) %><%= Spree.t(:exchange_for) %>
    +
    + <%= item_fields.hidden_field :inventory_unit_id %> + <%= item_fields.hidden_field :return_authorization_id %> + <%= item_fields.hidden_field :pre_tax_amount %> +
    + + <%= item_fields.check_box :returned, {checked: false, class: 'add-item', "data-price" => return_item.pre_tax_amount}, '1', '0' %> +
    +
    <%= return_item.inventory_unit.variant.name %>
    +
    <%= return_item.inventory_unit.variant.options_text %>
    +
    + <%= return_item.inventory_unit.variant.sku %> + + <%= return_item.display_pre_tax_amount %> + + <%= return_item.exchange_variant.try(:exchange_name) %> +
    diff --git a/backend/app/views/spree/admin/customer_returns/edit.html.erb b/backend/app/views/spree/admin/customer_returns/edit.html.erb new file mode 100644 index 00000000000..1edd5cc2ba3 --- /dev/null +++ b/backend/app/views/spree/admin/customer_returns/edit.html.erb @@ -0,0 +1,65 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Customer Returns' } %> + +<% content_for :page_title do %> + <%= Spree.t(:customer_return) %> #<%= @customer_return.number %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_customer_return_list), spree.admin_order_customer_returns_url(@order), :icon => 'arrow-left' %>
  • +<% end %> + +<% if @manual_intervention_return_items.any? %> +
    + <%= Spree.t(:manual_intervention_required) %> + <%= render partial: 'return_item_decision', locals: {return_items: @manual_intervention_return_items, show_decision: true} %> +
    +<% end %> + +<% if @pending_return_items.any? %> +
    + <%= Spree.t(:pending) %> + <%= render partial: 'return_item_decision', locals: {return_items: @pending_return_items, show_decision: true} %> +
    +<% end %> + +<% if @accepted_return_items.any? %> +
    + <%= Spree.t(:accepted) %> + <%= render partial: 'return_item_decision', locals: {return_items: @accepted_return_items, show_decision: false} %> +
    +<% end %> + +<% if @rejected_return_items.any? %> +
    + <%= Spree.t(:rejected) %> + <%= render partial: 'return_item_decision', locals: {return_items: @rejected_return_items, show_decision: false} %> +
    +<% end %> + +<% if !@customer_return.fully_reimbursed? && @pending_reimbursements.empty? %> +
    + <% if @customer_return.completely_decided? %> + <%= form_for [:admin, @order, Spree::Reimbursement.new] do |f| %> + <%= hidden_field_tag :build_from_customer_return_id, @customer_return.id %> + <%= f.button class: 'button fa fa-reply' do %> + <%= Spree.t(:create_reimbursement) %> + <% end %> + <% end %> + <% else %> +
    + <%= Spree.t(:unable_to_create_reimbursements) %> +
    + <% end %> +
    +<% end %> + +
    + <%= Spree.t(:reimbursements) %> + <% if @customer_return.reimbursements.any? %> + <%= render partial: 'reimbursements_table', locals: {reimbursements: @customer_return.reimbursements} %> + <% else %> +
    + <%= Spree.t(:none) %> +
    + <% end %> +
    diff --git a/backend/app/views/spree/admin/customer_returns/index.html.erb b/backend/app/views/spree/admin/customer_returns/index.html.erb new file mode 100644 index 00000000000..7595bc72885 --- /dev/null +++ b/backend/app/views/spree/admin/customer_returns/index.html.erb @@ -0,0 +1,60 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Customer Returns' } %> + +<% content_for :page_actions do %> + <% if @order.shipments.any?(&:shipped?) %> +
  • + <%= button_link_to Spree.t(:new_customer_return), spree.new_admin_order_customer_return_path(@order), icon: 'plus' %> +
  • + <% end %> +
  • <%= button_link_to Spree.t(:back_to_orders_list), spree.admin_orders_path, :icon => 'arrow-left' %>
  • +<% end %> + +<% content_for :page_title do %> + <%= Spree.t(:customer_returns) %> +<% end %> + +<% if @order.shipments.any?(&:shipped?) %> + + <% if @customer_returns.any? %> + + + + + + + + + + + + <% @customer_returns.each do |customer_return| %> + + + + + + + + <% end %> + +
    <%= Spree.t(:return_number) %><%= Spree.t(:pre_tax_total) %><%= "#{Spree.t('date')}/#{Spree.t('time')}" %><%= Spree.t(:reimbursement_status) %>
    <%= link_to customer_return.number, edit_admin_order_customer_return_path(@order, customer_return) %><%= customer_return.display_pre_tax_total.to_html %><%= pretty_time(customer_return.created_at) %> + <% if customer_return.fully_reimbursed? %> + <%= Spree.t(:reimbursed) %> + <% else %> + <%= Spree.t(:incomplete) %> + <% end %> + + <%= link_to_edit_url edit_admin_order_customer_return_path(@order, customer_return), title: "admin_edit_#{dom_id(customer_return)}", no_text: true %> +
    + <% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/customer_return')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_order_customer_return_path(@order) %>! +
    + <% end %> + +<% else %> +
    + <%= Spree.t(:cannot_create_customer_returns) %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/customer_returns/new.html.erb b/backend/app/views/spree/admin/customer_returns/new.html.erb new file mode 100644 index 00000000000..5b4ad454a19 --- /dev/null +++ b/backend/app/views/spree/admin/customer_returns/new.html.erb @@ -0,0 +1,50 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Customer Returns' } %> + +<% content_for :page_title do %> + <%= Spree.t(:new_customer_return) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_customer_return_list), spree.admin_order_customer_returns_url(@order), :icon => 'arrow-left' %>
  • +<% end %> + +<% if @rma_return_items.any? %> + + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @customer_return } %> + <%= form_for [:admin, @order, @customer_return] do |f| %> +
    +
    +
    + <%= Spree.t(:items_in_rmas) %> + <% if @rma_return_items.any? %> + <%= render partial: 'return_item_selection', locals: {f: f, return_items: @rma_return_items} %> + <% else %> +
    <%= Spree.t(:none) %>
    + <% end %> +
    + + <%= f.field_container :stock_location do %> + <%= f.label :stock_location, Spree.t(:stock_location) %> + <%= f.select :stock_location_id, Spree::StockLocation.order_default.active.to_a.collect{|l|[l.name.humanize, l.id]}, {include_blank: true}, {class: 'select2 fullwidth', "data-placeholder" => Spree.t(:select_a_stock_location)} %> + <%= f.error_message_on :stock_location_id %> + <% end %> +
    + +
    + <%= button Spree.t(:create), 'ok' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_order_customer_returns_url(@order), :icon => 'remove' %> +
    +
    + <% end %> + +<% else %> + +
    +
    + <%= Spree.t(:all_items_have_been_returned) %>, + <%= link_to Spree.t(:back_to_customer_return_list), spree.admin_order_customer_returns_path(@order) %>. +
    +
    + +<% end %> diff --git a/backend/app/views/spree/admin/general_settings/edit.html.erb b/backend/app/views/spree/admin/general_settings/edit.html.erb new file mode 100644 index 00000000000..89e4035a169 --- /dev/null +++ b/backend/app/views/spree/admin/general_settings/edit.html.erb @@ -0,0 +1,131 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:general_settings) %> +<% end %> + +<%= form_tag admin_general_settings_path, method: :put do %> +
    + +
    + + <%= fields_for :store do |f| %> +
    + <%= f.label :name %> +
    + <%= f.text_field :name, class: 'fullwidth' %> +
    + +
    + <%= f.label :seo_title %> +
    + <%= f.text_field :seo_title, class: 'fullwidth' %> +
    + +
    + <%= f.label :meta_keywords %> +
    + <%= f.text_field :meta_keywords, class: 'fullwidth' %> +
    + +
    + <%= f.label :meta_description %> +
    + <%= f.text_field :meta_description, class: 'fullwidth' %> +
    + +
    + <%= f.label :url %> +
    + <%= f.text_field :url, class: 'fullwidth' %> +
    + +
    + <%= f.label :mail_from_address %> +
    + <%= f.text_field :mail_from_address, class: 'fullwidth' %> +
    + <% end %> + +
    +
    +
    + <%= Spree.t(:security_settings)%> + <% @preferences_security.each do |key| + type = Spree::Config.preference_type(key) %> +
    + <%= label_tag(key, Spree.t(key)) + tag(:br) if type != :boolean %> + <%= preference_field_tag(key, Spree::Config[key], :type => type) %> + <%= label_tag(key, Spree.t(key)) + tag(:br) if type == :boolean %> +
    + <% end %> +
    +
    + <%= Spree.t(:clear_cache)%> +
    + <%= Spree.t(:clear_cache_warning) %> +
    +
    + <%= button Spree.t(:clear_cache), '', 'button', id: "clear_cache" %> +
    +
    +
    +
    +
    + <%= Spree.t(:currency_settings)%> + <% @preferences_currency.each do |key| + type = Spree::Config.preference_type(key) %> +
    + <%= label_tag(key, Spree.t(key)) + tag(:br) if type != :boolean %> + <%= preference_field_tag(key, Spree::Config[key], :type => type) %> + <%= label_tag(key, Spree.t(key)) + tag(:br) if type == :boolean %> +
    + <% end %> +
    + <%= label_tag :currency, Spree.t(:choose_currency) %>
    + <%= select_tag :currency, currency_options, :class => 'fullwidth' %> +
    +
    + <%= label_tag Spree.t(:currency_symbol_position) %>
    +
    +
      +
    • + <%= radio_button_tag :currency_symbol_position, "before", Spree::Config[:currency_symbol_position] == "before" %> + <%= label_tag :currency_symbol_position_before, Spree::Money.new(10, :symbol_position => "before") %> +
    • +
    • + <%= radio_button_tag :currency_symbol_position, "after", Spree::Config[:currency_symbol_position] == "after" %> + <%= label_tag :currency_symbol_position_after, Spree::Money.new(10, :symbol_position => "after") %> +
    • +
    +
    +
    +
    + <%= label_tag :currency_decimal_mark, Spree.t(:currency_decimal_mark) %>
    + <%= text_field_tag :currency_decimal_mark, Spree::Config[:currency_decimal_mark], :size => 3 %> +
    +
    + <%= label_tag :currency_thousands_separator, Spree.t(:currency_thousands_separator) %>
    + <%= text_field_tag :currency_thousands_separator, Spree::Config[:currency_thousands_separator], :size => 3 %> +
    +
    +
    +
    + +
    + <%= button Spree.t('actions.update'), 'refresh' %> + <%= Spree.t(:or) %> + <%= link_to_with_icon 'remove', Spree.t('actions.cancel'), edit_admin_general_settings_url, :class => 'button' %> +
    + +
    + +
    + + +<% end %> + + diff --git a/backend/app/views/spree/admin/images/_form.html.erb b/backend/app/views/spree/admin/images/_form.html.erb new file mode 100644 index 00000000000..d4f6f77799e --- /dev/null +++ b/backend/app/views/spree/admin/images/_form.html.erb @@ -0,0 +1,18 @@ +
    +
    +
    + <%= f.label :attachment, Spree.t(:filename) %>
    + <%= f.file_field :attachment %> +
    +
    + <%= f.label :viewable_id, Spree::Variant.model_name.human %>
    + <%= f.select :viewable_id, @variants, {}, { class: 'select2 fullwidth' } %> +
    +
    +
    + <%= f.label :alt, Spree.t(:alt_text) %>
    + <%= f.text_area :alt, rows: 4, class: 'fullwidth' %> +
    +
    + +
    diff --git a/backend/app/views/spree/admin/images/edit.html.erb b/backend/app/views/spree/admin/images/edit.html.erb new file mode 100644 index 00000000000..ef416a7a760 --- /dev/null +++ b/backend/app/views/spree/admin/images/edit.html.erb @@ -0,0 +1,28 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Images' } %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @image } %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_images_list), admin_product_images_url(@product), :icon => 'arrow-left' %>
  • +<% end %> + +<%= form_for [:admin, @product, @image], :html => { :multipart => true } do |f| %> +
    + <%= @image.attachment_file_name%> +
    + <%= f.label Spree.t(:thumbnail) %>
    + <%= link_to image_tag(@image.attachment.url(:small)), @image.attachment.url(:product) %> +
    +
    + <%= render :partial => 'form', :locals => { :f => f } %> +
    +
    +
    + <%= button Spree.t('actions.update'), 'refresh' %> + <%= Spree.t(:or) %> + <%= link_to Spree.t('actions.cancel'), admin_product_images_url(@product), :id => 'cancel_link', :class => 'button remove' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/images/index.html.erb b/backend/app/views/spree/admin/images/index.html.erb new file mode 100644 index 00000000000..adced175622 --- /dev/null +++ b/backend/app/views/spree/admin/images/index.html.erb @@ -0,0 +1,58 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => 'Images'} %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon('plus', Spree.t(:new_image), new_admin_product_image_url(@product), :id => 'new_image_link', :class => 'button') %>
  • +<% end %> + +
    + +<% unless @product.images.any? || @product.variant_images.any? %> +
    + <%= Spree.t(:no_images_found) %>. +
    +<% else %> + + + + + <% if @product.has_variants? %> + + <% end %> + + + + + + + <% if @product.has_variants? %> + + <% end %> + + + + + + + <% (@product.variant_images).each do |image| %> + + + + <% if @product.has_variants? %> + + <% end %> + + + + <% end %> + +
    <%= Spree.t(:thumbnail) %><%= Spree::Variant.model_name.human %><%= Spree.t(:alt_text) %>
    + + + <%= link_to image_tag(image.attachment.url(:mini)), image.attachment.url(:product) %> + <%= options_text_for(image) %><%= image.alt %> + <%= link_to_with_icon 'edit', Spree.t(:edit), edit_admin_product_image_url(@product, image), :no_text => true, :data => {:action => 'edit'} %> + <%= link_to_delete image, { :url => admin_product_image_url(@product, image), :no_text => true } %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/images/new.html.erb b/backend/app/views/spree/admin/images/new.html.erb new file mode 100644 index 00000000000..0c43a67affb --- /dev/null +++ b/backend/app/views/spree/admin/images/new.html.erb @@ -0,0 +1,15 @@ +<%= form_for [:admin, @product, @image], :html => { :multipart => true } do |f| %> +
    + <%= Spree.t(:new_image) %> + + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= button Spree.t('actions.update'), 'refresh' %> + <%= Spree.t(:or) %> + <%= link_to_with_icon 'remove', Spree.t('actions.cancel'), admin_product_images_url(@product), :id => 'cancel_link', :class => 'button' %> +
    +
    +<% end %> + +<%= javascript_include_tag 'spree/backend/images/new.js' %> \ No newline at end of file diff --git a/core/app/views/spree/admin/line_items/create.js.erb b/backend/app/views/spree/admin/line_items/create.js.erb similarity index 100% rename from core/app/views/spree/admin/line_items/create.js.erb rename to backend/app/views/spree/admin/line_items/create.js.erb diff --git a/core/app/views/spree/admin/line_items/destroy.js.erb b/backend/app/views/spree/admin/line_items/destroy.js.erb similarity index 100% rename from core/app/views/spree/admin/line_items/destroy.js.erb rename to backend/app/views/spree/admin/line_items/destroy.js.erb diff --git a/backend/app/views/spree/admin/log_entries/index.html.erb b/backend/app/views/spree/admin/log_entries/index.html.erb new file mode 100644 index 00000000000..fddd6a10d2b --- /dev/null +++ b/backend/app/views/spree/admin/log_entries/index.html.erb @@ -0,0 +1,31 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', locals: { current: 'Payments' }%> + +<% content_for :page_title do %> + + <%= I18n.t(:one, scope: "activerecord.models.spree/payment") %> + + <%= Spree.t(:log_entries) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:logs), spree.admin_order_payment_log_entries_url(@order, @payment), :icon => 'archive' %>
  • +
  • <%= button_link_to Spree.t(:back_to_payment), spree.admin_order_payment_url(@order, @payment), :icon => 'arrow-left' %>
  • +<% end %> + + + <% @log_entries.each do |entry| %> + + + + + + + + + + + + <% end %> +
    +

    '> <%= pretty_time(entry.created_at) %>

    +
    Message<%= entry.parsed_details.message %>
    diff --git a/backend/app/views/spree/admin/option_types/_form.html.erb b/backend/app/views/spree/admin/option_types/_form.html.erb new file mode 100644 index 00000000000..0365b4ec9ea --- /dev/null +++ b/backend/app/views/spree/admin/option_types/_form.html.erb @@ -0,0 +1,17 @@ +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= f.text_field :name, :class => "fullwidth" %> + <%= f.error_message_on :name %> + <% end %> +
    + +
    + <%= f.field_container :presentation do %> + <%= f.label :presentation, Spree.t(:presentation) %> *
    + <%= f.text_field :presentation, :class => "fullwidth" %> + <%= f.error_message_on :presentation %> + <% end %> +
    +
    \ No newline at end of file diff --git a/backend/app/views/spree/admin/option_types/_option_value_fields.html.erb b/backend/app/views/spree/admin/option_types/_option_value_fields.html.erb new file mode 100644 index 00000000000..2ebfaaf0a2e --- /dev/null +++ b/backend/app/views/spree/admin/option_types/_option_value_fields.html.erb @@ -0,0 +1,11 @@ + + <% if f.object.persisted? %> + + + <%= f.hidden_field :id %> + + <% end %> + <%= f.text_field :name %> + <%= f.text_field :presentation %> + <%= link_to_remove_fields Spree.t(:remove), f, :no_text => true %> + diff --git a/backend/app/views/spree/admin/option_types/edit.html.erb b/backend/app/views/spree/admin/option_types/edit.html.erb new file mode 100644 index 00000000000..b6ad6b700fd --- /dev/null +++ b/backend/app/views/spree/admin/option_types/edit.html.erb @@ -0,0 +1,43 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_option_type) %> "<%= @option_type.name %>" +<% end %> + +<% content_for :page_actions do %> +
  • + + <%= link_to_add_fields Spree.t(:add_option_value), "tbody#option_values", :class => 'button fa fa-plus' %> + +
  • +
  • + <%= button_link_to Spree.t(:back_to_option_types_list), spree.admin_option_types_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @option_type } %> + +<%= form_for [:admin, @option_type] do |f| %> +
    + <%= Spree.t(:option_values) %> + + <%= render :partial => 'form', :locals => { :f => f } %> + + + + + + + + + + + <%= f.fields_for :option_values, @option_values do |option_value_form| %> + <%= render :partial => 'option_value_fields', :locals => { :f => option_value_form } %> + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:display) %>
    + + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/option_types/index.html.erb b/backend/app/views/spree/admin/option_types/index.html.erb new file mode 100644 index 00000000000..12dd2a5680e --- /dev/null +++ b/backend/app/views/spree/admin/option_types/index.html.erb @@ -0,0 +1,50 @@ +<% content_for :page_title do %> + <%= Spree.t(:option_types) %> +<% end %> + +<% content_for :page_actions do %> + +<% end %> + +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +
    + +<% if @option_types.any? %> + + + + + + + + + + + + + + + + + <% @option_types.each do |option_type| %> + + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:presentation) %>
    <%= option_type.name %><%= option_type.presentation %> + <%= link_to_edit(option_type, :class => 'admin_edit_option_type', :no_text => true) %> + <%= link_to_delete(option_type, :no_text => true) %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/option_type')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_option_type_path %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/option_types/new.html.erb b/backend/app/views/spree/admin/option_types/new.html.erb new file mode 100644 index 00000000000..4e3549009b6 --- /dev/null +++ b/backend/app/views/spree/admin/option_types/new.html.erb @@ -0,0 +1,11 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @option_type } %> + +<%= form_for [:admin, @option_type] do |f| %> +
    + <%= Spree.t(:new_option_type) %> + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/core/app/views/spree/admin/option_types/new.js.erb b/backend/app/views/spree/admin/option_types/new.js.erb similarity index 100% rename from core/app/views/spree/admin/option_types/new.js.erb rename to backend/app/views/spree/admin/option_types/new.js.erb diff --git a/backend/app/views/spree/admin/orders/_add_line_item.html.erb b/backend/app/views/spree/admin/orders/_add_line_item.html.erb new file mode 100644 index 00000000000..5b20786841c --- /dev/null +++ b/backend/app/views/spree/admin/orders/_add_line_item.html.erb @@ -0,0 +1,18 @@ +<%= render :partial => "spree/admin/variants/autocomplete", :formats => :js %> + +<%= render :partial => "spree/admin/variants/autocomplete_line_items_stock", :formats => :js %> + + +
    +
    + <%= Spree.t(:add_product) %> + +
    + <%= label_tag :add_line_item_variant_id, Spree.t(:name_or_sku) %> + <%= hidden_field_tag :add_line_item_variant_id, "", :class => "variant_autocomplete fullwidth" %> +
    + +
    + +
    +
    diff --git a/backend/app/views/spree/admin/orders/_add_product.html.erb b/backend/app/views/spree/admin/orders/_add_product.html.erb new file mode 100644 index 00000000000..34114e290a0 --- /dev/null +++ b/backend/app/views/spree/admin/orders/_add_product.html.erb @@ -0,0 +1,16 @@ +<%= render :partial => "spree/admin/variants/autocomplete", :formats => :js %> +<%= render :partial => "spree/admin/variants/autocomplete_stock", :formats => :js %> + +
    +
    + <%= Spree.t(:add_product) %> + +
    + <%= label_tag :add_variant_id, Spree.t(:name_or_sku) %> + <%= hidden_field_tag :add_variant_id, "", :class => "variant_autocomplete fullwidth" %> +
    + +
    + +
    +
    diff --git a/backend/app/views/spree/admin/orders/_adjustments.html.erb b/backend/app/views/spree/admin/orders/_adjustments.html.erb new file mode 100644 index 00000000000..5535f34c8f0 --- /dev/null +++ b/backend/app/views/spree/admin/orders/_adjustments.html.erb @@ -0,0 +1,19 @@ +<% if adjustments.eligible.exists? %> +
    + <%= title %> + + + + + + + <% adjustments.eligible.group_by(&:label).each do |label, adjustments| %> + + + + + <% end %> + +
    <%= Spree.t('name')%><%= Spree.t('amount')%>
    <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: adjustments.first.order.try(:currency)) %>
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/orders/_form.html.erb b/backend/app/views/spree/admin/orders/_form.html.erb new file mode 100644 index 00000000000..13b4f2dcc31 --- /dev/null +++ b/backend/app/views/spree/admin/orders/_form.html.erb @@ -0,0 +1,43 @@ +
    + <% if @line_item.try(:errors).present? %> + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @line_item } %> + <% end %> + + <% if Spree::Order.checkout_step_names.include?(:delivery) %> + <%= render :partial => "spree/admin/orders/shipment", :collection => @order.shipments.order(:created_at), :locals => { :order => order } %> + <% else %> + <%= render :partial => "spree/admin/orders/line_items", :locals => { :order => order } %> + <% end %> + <%= render :partial => "spree/admin/orders/adjustments", :locals => { + :adjustments => @order.line_item_adjustments, + :order => order, + :title => Spree.t(:line_item_adjustments) + } %> + <%= render :partial => "spree/admin/orders/adjustments", :locals => { + :adjustments => @order.shipment_adjustments, + :order => order, + :title => Spree.t(:shipment_adjustments) + } %> + <%= render :partial => "spree/admin/orders/adjustments", :locals => { + :adjustments => @order.adjustments, + :order => order, + :title => Spree.t(:order_adjustments) + } %> + + <% if order.line_items.exists? %> +
    + <%= Spree.t(:order_total) %> + <%= order.display_total %> +
    + <% end %> + + <%= javascript_tag do -%> + var order_number = '<%= @order.number %>'; + var shipments = []; + + <% @order.shipments.each do |shipment| %> + shipments.push(<%== shipment.as_json(:root => false, :only => [:id, :tracking, :number, :state, :stock_location_id], :include => [:inventory_units, :stock_location]).to_json %>); + <% end %> + <%= render :partial => 'spree/admin/shared/update_order_state', :handlers => [:js] %> + <% end -%> +
    diff --git a/backend/app/views/spree/admin/orders/_line_item.html.erb b/backend/app/views/spree/admin/orders/_line_item.html.erb new file mode 100644 index 00000000000..d06fdd56507 --- /dev/null +++ b/backend/app/views/spree/admin/orders/_line_item.html.erb @@ -0,0 +1,9 @@ + + <%=f.object.variant.product.name%> <%= "(#{f.object.variant.options_text})" unless f.object.variant.option_values.empty? %> + <%= f.object.single_money %> + <%= f.number_field :quantity, :min => 0, :class => "qty" %> + <%= f.object.display_amount %> + + <%= link_to_delete f.object, {:url => admin_order_line_item_url(@order.number, f.object), :no_text => true} %> + + diff --git a/backend/app/views/spree/admin/orders/_line_items.html.erb b/backend/app/views/spree/admin/orders/_line_items.html.erb new file mode 100644 index 00000000000..b7ccbd9b87f --- /dev/null +++ b/backend/app/views/spree/admin/orders/_line_items.html.erb @@ -0,0 +1,46 @@ +<% if order.line_items.exists? %> + + + + + + + + + + + + + + + + + + <% order.line_items.each do |item| %> + + + + + + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:price) %><%= Spree.t(:quantity) %><%= Spree.t(:total_price) %> 
    <%= mini_image(item.variant) %> + <%= item.variant.product.name %>
    <%= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? %> +
    <%= item.single_money.to_html %> + <%= item.quantity %> + <%= line_item_shipment_price(item, item.quantity) %> + <% if can? :update, item %> + <%= link_to '', '#', :class => 'save-line-item fa fa-ok no-text with-tip', :data => { 'line-item-id' => item.id, :action => 'save'}, :title => Spree.t('actions.save'), :style => 'display: none' %> + <%= link_to '', '#', :class => 'cancel-line-item fa fa-cancel no-text with-tip', :data => {:action => 'cancel'}, :title => Spree.t('actions.cancel'), :style => 'display: none' %> + <%= link_to '', '#', :class => 'edit-line-item fa fa-edit no-text with-tip', :data => {:action => 'edit'}, :title => Spree.t('edit') %> + <%= link_to '', '#', :class => 'delete-line-item fa fa-trash no-text with-tip', :data => { 'line-item-id' => item.id, :action => 'remove'}, :title => Spree.t('delete') %> + <% end %> +
    +<% end %> \ No newline at end of file diff --git a/backend/app/views/spree/admin/orders/_line_items_edit_form.html.erb b/backend/app/views/spree/admin/orders/_line_items_edit_form.html.erb new file mode 100644 index 00000000000..2e53a78e04c --- /dev/null +++ b/backend/app/views/spree/admin/orders/_line_items_edit_form.html.erb @@ -0,0 +1,40 @@ +
    + <% if @line_item.try(:errors).present? %> + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @line_item } %> + <% end %> + + <%= render :partial => "spree/admin/orders/line_items", :locals => { :order => order } %> + <%= render :partial => "spree/admin/orders/adjustments", :locals => { + :adjustments => @order.line_item_adjustments, + :order => order, + :title => Spree.t(:line_item_adjustments) + } %> + <%= render :partial => "spree/admin/orders/adjustments", :locals => { + :adjustments => @order.shipment_adjustments, + :order => order, + :title => Spree.t(:shipment_adjustments) + } %> + <%= render :partial => "spree/admin/orders/adjustments", :locals => { + :adjustments => @order.adjustments, + :order => order, + :title => Spree.t(:order_adjustments) + } %> + + <% if order.line_items.exists? %> +
    + <%= Spree.t(:order_total) %> + <%= order.display_total %> +
    + <% end %> + + <%= javascript_tag do -%> + var order_number = '<%= @order.number %>'; + var shipments = []; + + <% @order.shipments.each do |shipment| %> + shipments.push(<%== shipment.as_json(:root => false, :only => [:id, :tracking, :number, :state, :stock_location_id], :include => [:inventory_units, :stock_location]).to_json %>); + <% end %> + + <%= render :partial => 'spree/admin/shared/update_order_state', :handlers => [:js] %> + <% end -%> +
    diff --git a/backend/app/views/spree/admin/orders/_risk_analysis.html.erb b/backend/app/views/spree/admin/orders/_risk_analysis.html.erb new file mode 100644 index 00000000000..39ee342ed8d --- /dev/null +++ b/backend/app/views/spree/admin/orders/_risk_analysis.html.erb @@ -0,0 +1,47 @@ +
    + <%= "#{Spree.t(:risk_analysis)}: #{Spree.t(:not) unless @order.is_risky?} #{Spree.t(:risky)}" %> + + + + + + + + + + + + + + + + + + + + + +
    <%= Spree.t('risk')%><%= Spree.t('status')%>
    + <%= Spree.t(:failed_payment_attempts) %>: + + + <%= link_to "#{Spree.t 'payments_count', count: @order.payments.failed.count, default: pluralize(@order.payments.failed.count, Spree.t(:payment))}", spree.admin_order_payments_path(@order) %> + +
    <%= Spree.t(:avs_response) %>: + + <% if latest_payment.is_avs_risky? %> + <%= "#{Spree.t(:error)}: #{avs_response_code[latest_payment.avs_response]}" %> + <% else %> + <%= Spree.t(:success) %> + <% end %> + +
    <%= Spree.t(:cvv_response) %>: + + <% if latest_payment.is_cvv_risky? %> + <%= "#{Spree.t(:error)}: #{cvv_response_code[latest_payment.cvv_response_code]}" %> + <% else %> + <%= Spree.t(:success) %> + <% end %> + +
    +
    diff --git a/backend/app/views/spree/admin/orders/_shipment.html.erb b/backend/app/views/spree/admin/orders/_shipment.html.erb new file mode 100644 index 00000000000..e8e481ac0b3 --- /dev/null +++ b/backend/app/views/spree/admin/orders/_shipment.html.erb @@ -0,0 +1,116 @@ +
    " data-hook="admin_shipment_form"> + <%= render :partial => "spree/admin/variants/split", :formats => :js %> +
    + + <%= shipment.number %> + - + <%= Spree.t("shipment_states.#{shipment.state}") %> + <%= Spree.t(:package_from) %> + '<%= shipment.stock_location.name %>' + <% if shipment.ready? and can? :update, shipment %> + - + <%= link_to Spree.t(:ship), '#', :class => 'ship button fa fa-arrow-right', :data => {'shipment-number' => shipment.number} %> + <% end %> + +
    + + + + + + + + + + + + + + + + + + + + + <%= render 'spree/admin/orders/shipment_manifest', shipment: shipment %> + + <% unless shipment.shipped? %> + + + + + <% end %> + + + <% if rate = shipment.selected_shipping_rate %> + + + <% else %> + + <% end %> + + + + + + + + + + + <% if order.special_instructions.present? %> + + + + <% end %> + + + + + + +
    <%= Spree.t(:item_description) %><%= Spree.t(:price) %><%= Spree.t(:quantity) %><%= Spree.t(:total) %>
    + <%= rate.name %> + + <%= shipment.display_cost %> + <%= Spree.t(:no_shipping_method_selected) %> + <% if( (can? :update, shipment) and !shipment.shipped?) %> + <%= link_to '', '#', :class => 'edit-method fa fa-edit no-text with-tip', :data => {:action => 'edit'}, :title => Spree.t('edit') %> + <% end %> +
    + <%= Spree.t(:special_instructions) %>: <%= order.special_instructions %> +
    + <% if shipment.tracking.present? %> + <%= Spree.t(:tracking) %>: <%= link_to_tracking(shipment, target: '_blank') %> + <% else %> + <%= Spree.t(:no_tracking_present) %> + <% end %> + + <% if can? :update, shipment %> + <%= link_to '', '#', :class => 'edit-tracking fa fa-edit no-text with-tip', :data => {:action => 'edit'}, :title => Spree.t('edit') %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/orders/_shipment_manifest.html.erb b/backend/app/views/spree/admin/orders/_shipment_manifest.html.erb new file mode 100644 index 00000000000..7ee629a1b4a --- /dev/null +++ b/backend/app/views/spree/admin/orders/_shipment_manifest.html.erb @@ -0,0 +1,35 @@ +<% shipment.manifest.each do |item| %> + + + <%= mini_image(item.variant) %> + + + <%= item.variant.product.name %>
    <%= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? %> + <% if item.variant.sku.present? %> + <%= Spree.t(:sku) %>: <%= item.variant.sku %> + <% end %> + + <%= item.line_item.single_money.to_html %> + + <% item.states.each do |state,count| %> + <%= count %> x <%= Spree.t(state).downcase %> + <% end %> + + <% unless shipment.shipped? %> + + <%= number_field_tag :quantity, item.quantity, :min => 0, :class => "line_item_quantity", :size => 5 %> + + <% end %> + <%= line_item_shipment_price(item.line_item, item.quantity) %> + + <% if !shipment.shipped? && can?(:update, item.line_item) %> + <%= link_to '', '#', :class => 'save-item fa fa-ok no-text with-tip', :data => {'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'save'}, :title => Spree.t('actions.save'), :style => 'display: none' %> + <%= link_to '', '#', :class => 'cancel-item fa fa-cancel no-text with-tip', :data => {:action => 'cancel'}, :title => Spree.t('actions.cancel'), :style => 'display: none' %> + <% if shipment.order.shipped_shipments.count == 0 %> + <%= link_to '', '#', :class => 'split-item icon_link fa fa-arrows-h no-text with-tip', :data => {:action => 'split', 'variant-id' => item.variant.id}, :title => Spree.t('split') %> + <%= link_to '', '#', :class => 'delete-item fa fa-trash no-text with-tip', :data => { 'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'remove'}, :title => Spree.t('delete') %> + <% end %> + <% end %> + + +<% end %> diff --git a/backend/app/views/spree/admin/orders/cart.html.erb b/backend/app/views/spree/admin/orders/cart.html.erb new file mode 100644 index 00000000000..3012606c525 --- /dev/null +++ b/backend/app/views/spree/admin/orders/cart.html.erb @@ -0,0 +1,39 @@ +<% content_for :page_actions do %> + <% if can?(:fire, @order) %> +
  • <%= event_links %>
  • + <% end %> + <% if can?(:resend, @order) %> +
  • <%= button_link_to Spree.t(:resend), resend_admin_order_url(@order), :method => :post, :icon => 'email' %>
  • + <% end %> + <% if can?(:admin, Spree::Order) %> +
  • <%= button_link_to Spree.t(:back_to_orders_list), admin_orders_path, :icon => 'arrow-left' %>
  • + <% end %> +<% end %> + +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Cart' } %> + +
    + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> +
    + +<% if @order.payments.exists? && @order.considered_risky? %> + <%= render 'spree/admin/orders/risk_analysis', latest_payment: @order.payments.order("created_at DESC").first %> +<% end %> + +<%= render :partial => 'add_line_item' if can?(:update, @order) %> + +<% if @order.line_items.empty? %> +
    + <%= Spree.t(:your_order_is_empty_add_product)%> +
    +<% end %> + +
    +
    + <%= render :partial => 'line_items_edit_form', :locals => { :order => @order } %> +
    +
    + +<% content_for :head do %> + <%= javascript_tag 'var expand_variants = true;' %> +<% end %> diff --git a/backend/app/views/spree/admin/orders/customer_details/_form.html.erb b/backend/app/views/spree/admin/orders/customer_details/_form.html.erb new file mode 100644 index 00000000000..e1f9631d452 --- /dev/null +++ b/backend/app/views/spree/admin/orders/customer_details/_form.html.erb @@ -0,0 +1,73 @@ +
    + +
    + <%= Spree.t(:account) %> + +
    +
    +
    + <%= f.label :email, Spree.t(:email) %> + <%= f.email_field :email, :class => 'fullwidth' %> +
    +
    +
    +
    + <%= label_tag nil, Spree.t(:guest_checkout) %> +
      + <% if @order.completed? %> +
    • + <%= @order.user.nil? ? Spree.t(:say_yes) : Spree.t(:say_no) %> +
    • + <% else %> + <% guest = @order.user.nil? %> +
    • + <%= radio_button_tag :guest_checkout, true, guest %> + <%= label_tag :guest_checkout_true, Spree.t(:say_yes) %> +
    • +
    • + <%= radio_button_tag :guest_checkout, false, !guest, :disabled => @order.cart? %> + <%= label_tag :guest_checkout_false, Spree.t(:say_no) %> +
    • + <%= hidden_field_tag :user_id, @order.user_id %> + <% end %> +
    +
    +
    +
    +
    + +
    +
    + <%= Spree.t(:billing_address) %> + <%= f.fields_for :bill_address do |ba_form| %> + <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => ba_form, :type => "billing" } %> + <% end %> +
    +
    + +
    +
    + <%= Spree.t(:shipping_address) %> + <%= f.fields_for :ship_address do |sa_form| %> +
    + + <%= check_box_tag 'order[use_billing]', '1', ((@order.bill_address.empty? && @order.ship_address.empty?) && @order.bill_address.same_as?(@order.ship_address)) %> + <%= label_tag 'order[use_billing]', Spree.t(:use_billing_address) %> + +
    + + <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => sa_form, :type => 'shipping' } %> + <% end %> +
    +
    + +
    + +
    + <%= button Spree.t('actions.update'), 'refresh' %> +
    + + <% content_for :head do %> + <%= javascript_include_tag 'spree/backend/address_states.js' %> + <% end %> +
    diff --git a/backend/app/views/spree/admin/orders/customer_details/edit.html.erb b/backend/app/views/spree/admin/orders/customer_details/edit.html.erb new file mode 100644 index 00000000000..df06528c574 --- /dev/null +++ b/backend/app/views/spree/admin/orders/customer_details/edit.html.erb @@ -0,0 +1,23 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Customer Details' } %> + +<% content_for :page_title do %> + <%= Spree.t(:customer_details) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_orders_list), admin_orders_path, :icon => 'arrow-left' %>
  • +<% end %> + +
    +
    + <%= Spree.t(:customer_search) %> + <%= hidden_field_tag :customer_search, nil, :class => 'fullwidth title' %> + <%= render :partial => "spree/admin/orders/customer_details/autocomplete", :formats => :js %> +
    +
    + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> + +<%= form_for @order, :url => admin_order_customer_url(@order) do |f| %> + <%= render 'form', :f => f %> +<% end %> diff --git a/backend/app/views/spree/admin/orders/edit.html.erb b/backend/app/views/spree/admin/orders/edit.html.erb new file mode 100644 index 00000000000..d28f9bada96 --- /dev/null +++ b/backend/app/views/spree/admin/orders/edit.html.erb @@ -0,0 +1,39 @@ +<% content_for :page_actions do %> + <% if can?(:fire, @order) %> +
  • <%= event_links %>
  • + <% end %> + <% if can?(:resend, @order) %> +
  • <%= button_link_to Spree.t(:resend), resend_admin_order_url(@order), method: :post, icon: 'email' %>
  • + <% end %> + <% if can?(:admin, Spree::Order) %> +
  • <%= button_link_to Spree.t(:back_to_orders_list), admin_orders_path, icon: 'arrow-left' %>
  • + <% end %> +<% end %> + +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Shipments' } %> + +
    + <%= render partial: 'spree/shared/error_messages', locals: { target: @order } %> +
    + +<% if @order.payments.valid.any? && @order.considered_risky? %> + <%= render 'spree/admin/orders/risk_analysis', latest_payment: @order.payments.valid.last %> +<% end %> + +<%= render partial: 'add_product' if @order.shipment_state != 'shipped' && can?(:update, @order) %> + +<% if @order.line_items.empty? %> +
    + <%= Spree.t(:your_order_is_empty_add_product)%> +
    +<% end %> + +
    +
    + <%= render partial: 'form', locals: { order: @order } %> +
    +
    + +<% content_for :head do %> + <%= javascript_tag 'var expand_variants = true;' %> +<% end %> diff --git a/backend/app/views/spree/admin/orders/index.html.erb b/backend/app/views/spree/admin/orders/index.html.erb new file mode 100644 index 00000000000..f6bc698d64a --- /dev/null +++ b/backend/app/views/spree/admin/orders/index.html.erb @@ -0,0 +1,163 @@ +<% content_for :page_title do %> + <%= Spree.t(:listing_orders) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_order), new_admin_order_url, :icon => 'plus', :id => 'admin_new_order' %> +
  • +<% end if can? :edit, Spree::Order.new %> + +<% content_for :table_filter_title do %> + <%= Spree.t(:search) %> +<% end %> + +<% content_for :table_filter do %> +
    + <%= search_form_for [:admin, @search] do |f| %> +
    +
    + <%= label_tag :q_created_at_gt, Spree.t(:date_range) %> +
    + <%= f.text_field :created_at_gt, :class => 'datepicker datepicker-from', :value => params[:q][:created_at_gt], :placeholder => Spree.t(:start) %> + + + + + + <%= f.text_field :created_at_lt, :class => 'datepicker datepicker-to', :value => params[:q][:created_at_lt], :placeholder => Spree.t(:stop) %> +
    +
    + +
    + <%= label_tag :q_state_eq, Spree.t(:status) %> + <%= f.select :state_eq, Spree::Order.state_machines[:state].states.collect {|s| [Spree.t("order_state.#{s.name}"), s.value]}, {:include_blank => true}, :class => 'select2' %> +
    + +
    + <%= label_tag :q_promotions_id_in, Spree.t(:promotion) %> + <%= f.select :promotions_id_in, Spree::Promotion.applied.pluck(:name, :id), {:include_blank => true}, :class => 'select2' %> +
    +
    + +
    +
    + <%= label_tag :q_number_cont, Spree.t(:order_number, :number => '') %> + <%= f.text_field :number_cont %> +
    + +
    + <%= label_tag :q_email_cont, Spree.t(:email) %> + <%= f.text_field :email_cont %> +
    + +
    + <%= label_tag :q_line_items_variant_id_in, Spree.t(:sku) %> + <%= f.select :line_items_variant_id_in, Spree::Variant.having_orders.order(:sku).pluck(:sku, :id), {:include_blank => true}, :class => 'select2' %> +
    +
    + +
    +
    + <%= label_tag :q_bill_address_firstname_start, Spree.t(:first_name_begins_with) %> + <%= f.text_field :bill_address_firstname_start, :size => 25 %> +
    +
    + <%= label_tag :q_bill_address_lastname_start, Spree.t(:last_name_begins_with) %> + <%= f.text_field :bill_address_lastname_start, :size => 25%> +
    +
    + +
    +
    + + +
    +
    + +
    + +
    +
    + <%= button Spree.t(:filter_results), 'search' %> +
    +
    + <% end %> +
    +<% end %> + +<%= paginate @orders %> + +<% if @orders.any? %> + + + + + + + <% if Spree::Order.checkout_step_names.include?(:delivery) %> + + <% end %> + + + + + + + <% if @show_only_completed %> + + <% else %> + + <% end %> + + + + + <% if Spree::Order.checkout_step_names.include?(:delivery) %> + + <% end %> + + + + + + + <% @orders.each do |order| %> + + + + + + + <% if Spree::Order.checkout_step_names.include?(:delivery) %> + + <% end %> + + + + + <% end %> + +
    <%= sort_link @search, :completed_at, I18n.t(:completed_at, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :created_at, I18n.t(:created_at, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :number, I18n.t(:number, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :considered_risky, I18n.t(:considered_risky, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :state, I18n.t(:state, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :payment_state, I18n.t(:payment_state, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :shipment_state, I18n.t(:shipment_state, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :email, I18n.t(:email, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :total, I18n.t(:total, :scope => 'activerecord.attributes.spree/order') %>
    <%= l (@show_only_completed ? order.completed_at : order.created_at).to_date %><%= link_to order.number, edit_admin_order_path(order) %><%= Spree.t("order_state.#{order.state.downcase}") %><%= link_to Spree.t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) if order.payment_state %><%= Spree.t("shipment_states.#{order.shipment_state}") if order.shipment_state %> + <% if order.user %> + <%= link_to order.email, edit_admin_user_path(order.user) %> + <% else %> + <%= mail_to order.email %> + <% end %> + <%= order.display_total.to_html %> + <%= link_to_edit_url edit_admin_order_path(order), :title => "admin_edit_#{dom_id(order)}", :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/order')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_order_path %>! +
    +<% end %> + +<%= paginate @orders %> diff --git a/backend/app/views/spree/admin/payment_methods/_form.html.erb b/backend/app/views/spree/admin/payment_methods/_form.html.erb new file mode 100644 index 00000000000..a12b6e5b1d1 --- /dev/null +++ b/backend/app/views/spree/admin/payment_methods/_form.html.erb @@ -0,0 +1,59 @@ +
    + +
    + +
    +
    + <%= f.label :type, Spree.t(:provider) %> + <%= collection_select(:payment_method, :type, @providers, :to_s, :name, {}, {:id => 'gtwy-type', :class => 'select2 fullwidth'}) %> + + <% unless @object.new_record? %> + <%= preference_fields(@object, f) %> + + <% if @object.respond_to?(:preferences) %> +
    <%= Spree.t(:provider_settings_warning) %>
    + <% end %> + <% end %> +
    +
    + <%= label_tag :payment_method_environment, Spree.t(:environment) %> + <%= collection_select(:payment_method, :environment, rails_environments, :to_s, :titleize, {}, {:id => 'gtwy-env', :class => 'select2 fullwidth'}) %> +
    +
    + <%= label_tag :payment_method_display_on, Spree.t(:display) %> + <%= select(:payment_method, :display_on, Spree::PaymentMethod::DISPLAY.collect { |display| [Spree.t(display), display == :both ? nil : display.to_s] }, {}, {:class => 'select2 fullwidth'}) %> +
    +
    + <%= label_tag :payment_method_auto_capture, Spree.t(:auto_capture) %> + <%= select(:payment_method, :auto_capture, [["#{Spree.t(:use_app_default)} (#{Spree::Config[:auto_capture]})", ''], [Spree.t(:say_yes), true], [Spree.t(:say_no), false]], {}, {:class => 'select2 fullwidth'}) %> +
    +
    + <%= label_tag nil, Spree.t(:active) %> +
      +
    • + <%= radio_button :payment_method, :active, true %> + <%= label_tag :payment_method_active_true, Spree.t(:say_yes) %> +
    • +
    • + <%= radio_button :payment_method, :active, false %> + <%= label_tag :payment_method_active_false, Spree.t(:say_no) %> +
    • +
    +
    +
    + +
    +
    + <%= label_tag :payment_method_name, Spree.t(:name) %> + <%= text_field :payment_method, :name, :class => 'fullwidth' %> +
    +
    + <%= label_tag :payment_method_description, Spree.t(:description) %> + <%= text_area :payment_method, :description, {:cols => 60, :rows => 6, :class => 'fullwidth'} %> +
    +
    + +
    +
    + +
    diff --git a/backend/app/views/spree/admin/payment_methods/edit.html.erb b/backend/app/views/spree/admin/payment_methods/edit.html.erb new file mode 100644 index 00000000000..6710fc9fcc3 --- /dev/null +++ b/backend/app/views/spree/admin/payment_methods/edit.html.erb @@ -0,0 +1,23 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_payment_method) %> <%= @payment_method.name %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_payment_methods_list), spree.admin_payment_methods_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @payment_method } %> + +<%= form_for @payment_method, :url => admin_payment_method_path(@payment_method) do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> +
    + <%= button Spree.t('actions.update'), 'refresh' %> +
    +
    + +<% end %> diff --git a/backend/app/views/spree/admin/payment_methods/index.html.erb b/backend/app/views/spree/admin/payment_methods/index.html.erb new file mode 100644 index 00000000000..8a7e18a240f --- /dev/null +++ b/backend/app/views/spree/admin/payment_methods/index.html.erb @@ -0,0 +1,55 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:payment_methods) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_payment_method), new_object_url, :icon => 'plus', :id => 'admin_new_payment_methods_link' %> +
  • +<% end %> + +<% if @payment_methods.any? %> + + + + + + + + + + + + + + + + + + + + + <% @payment_methods.each do |method|%> + + + + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:provider) %><%= Spree.t(:environment) %><%= Spree.t(:display) %><%= Spree.t(:active) %>
    <%= method.name %><%= method.type %><%= method.environment.to_s.titleize %><%= method.display_on.blank? ? Spree.t(:both) : Spree.t(method.display_on) %><%= method.active ? Spree.t(:say_yes) : Spree.t(:say_no) %> + <%= link_to_edit method, :no_text => true %> + <%= link_to_delete method, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/payment_method')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_payment_method_path %>! +
    +<% end %> + diff --git a/backend/app/views/spree/admin/payment_methods/new.html.erb b/backend/app/views/spree/admin/payment_methods/new.html.erb new file mode 100644 index 00000000000..7e101fd62d9 --- /dev/null +++ b/backend/app/views/spree/admin/payment_methods/new.html.erb @@ -0,0 +1,22 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_payment_method) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_payment_methods_list), admin_payment_methods_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @payment_method } %> + +<%= form_for @payment_method, :url => collection_url do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> +
    + <%= button Spree.t(:create), 'ok' %> +
    +
    +<% end %> diff --git a/core/app/views/spree/admin/payments/_bill_address_form.html.erb b/backend/app/views/spree/admin/payments/_bill_address_form.html.erb similarity index 79% rename from core/app/views/spree/admin/payments/_bill_address_form.html.erb rename to backend/app/views/spree/admin/payments/_bill_address_form.html.erb index 6ae4433c660..aa6ce294f30 100644 --- a/core/app/views/spree/admin/payments/_bill_address_form.html.erb +++ b/backend/app/views/spree/admin/payments/_bill_address_form.html.erb @@ -2,7 +2,7 @@ <% order_form.fields_for :checkout do |checkout_form| %> <% checkout_form.fields_for :bill_address do |ba_form| %> - <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => ba_form, :name => t(:billing_address) } %> + <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => ba_form, :name => Spree.t(:billing_address) } %> <% end %> <% end %> diff --git a/backend/app/views/spree/admin/payments/_capture_events.html.erb b/backend/app/views/spree/admin/payments/_capture_events.html.erb new file mode 100644 index 00000000000..e4768f9938f --- /dev/null +++ b/backend/app/views/spree/admin/payments/_capture_events.html.erb @@ -0,0 +1,19 @@ +<% if @payment.capture_events.exists? %> +

    <%= Spree.t(:capture_events) %>

    + + + + + + + + + <% @payment.capture_events.each do |capture_event| %> + + + + + <% end %> + +
    <%= "#{Spree.t('date')}/#{Spree.t('time')}" %><%= Spree.t(:amount) %>
    <%= pretty_time(capture_event.created_at) %><%= capture_event.display_amount %>
    +<% end %> \ No newline at end of file diff --git a/backend/app/views/spree/admin/payments/_form.html.erb b/backend/app/views/spree/admin/payments/_form.html.erb new file mode 100644 index 00000000000..07c2ceed890 --- /dev/null +++ b/backend/app/views/spree/admin/payments/_form.html.erb @@ -0,0 +1,36 @@ +
    +
    +
    + <%= f.label :amount, Spree.t(:amount) %> + <%= f.text_field :amount, :value => @order.display_outstanding_balance.money, :class => 'fullwidth' %> +
    +
    +
    +
    + +
      + <% @payment_methods.each do |method| %> +
    • + +
    • + <% end %> +
    + +
    + <% @payment_methods.each do |method| %> + +
    + <% if method.source_required? %> +
    + <%= render :partial => "spree/admin/payments/source_forms/#{method.method_type}", + :locals => { :payment_method => method, previous_cards: method.reusable_sources(@order) } %> + <% end %> +
    + <% end %> +
    +
    +
    +
    diff --git a/backend/app/views/spree/admin/payments/_list.html.erb b/backend/app/views/spree/admin/payments/_list.html.erb new file mode 100644 index 00000000000..becf6ba9567 --- /dev/null +++ b/backend/app/views/spree/admin/payments/_list.html.erb @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + <% payments.each do |payment| %> + + + + + + + + + + <% end %> + +
    <%= Spree.t(:identifier) %><%= "#{Spree.t('date')}/#{Spree.t('time')}" %><%= Spree.t(:amount) %><%= Spree.t(:payment_method) %><%= Spree.t(:transaction_id) %><%= Spree.t(:payment_state) %>
    <%= link_to payment.identifier, spree.admin_order_payment_path(@order, payment) %><%= pretty_time(payment.created_at) %><%= payment.display_amount.to_html %><%= payment_method_name(payment) %><%= payment.transaction_id %> <%= Spree.t(payment.state, :scope => :payment_states, :default => payment.state.capitalize) %> + <% payment.actions.each do |action| %> + <% if action == 'credit' %> + <%= link_to_with_icon 'reply', Spree.t(:refund), new_admin_order_payment_refund_path(@order, payment), no_text: true %> + <% else %> + <%= link_to_with_icon action, Spree.t(action), fire_admin_order_payment_path(@order, payment, :e => action), :method => :put, :no_text => true, :data => {:action => action} %> + <% end %> + <% end %> +
    diff --git a/backend/app/views/spree/admin/payments/credit.html.erb b/backend/app/views/spree/admin/payments/credit.html.erb new file mode 100644 index 00000000000..46de92b4e88 --- /dev/null +++ b/backend/app/views/spree/admin/payments/credit.html.erb @@ -0,0 +1,18 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Creditcards' } %> + +<% content_for :page_title do %> + <%= Spree.t(:refund) %> +<% end %> + +<%= form_tag do %> +

    <%= Spree.t(:refund) %>

    +
    +

    + <%= label_tag :amount, Spree.t(:amount) %> + <%= text_field_tag :amount, @payment.amount %> +

    +

    + <%= button Spree.t(:make_refund) %> +

    +
    +<% end %> diff --git a/backend/app/views/spree/admin/payments/index.html.erb b/backend/app/views/spree/admin/payments/index.html.erb new file mode 100644 index 00000000000..cde3855ec61 --- /dev/null +++ b/backend/app/views/spree/admin/payments/index.html.erb @@ -0,0 +1,36 @@ +<%= render partial: 'spree/admin/shared/order_tabs', locals: { current: "Payments" } %> + +<% content_for :page_actions do %> + <% if @order.outstanding_balance? %> +
  • + <%= button_link_to Spree.t(:new_payment), new_admin_order_payment_url(@order), :icon => 'plus' %> +
  • + <% end %> +
  • <%= button_link_to Spree.t(:back_to_orders_list), admin_orders_path, :icon => 'arrow-left' %>
  • +<% end %> + +<% content_for :page_title do %> + <%= Spree.t(:payments) %> +<% end %> + +<% if @order.outstanding_balance? %> +
    <%= @order.outstanding_balance < 0 ? Spree.t(:credit_owed) : Spree.t(:balance_due) %>: <%= @order.display_outstanding_balance %>
    +<% end %> + +<% if @payments.any? %> + +
    + <%= Spree.t(:payments) %> + <%= render :partial => 'list', :locals => { :payments => @payments } %> +
    + + <% if @refunds.any? %> +
    + <%= Spree.t(:refunds) %> + <%= render :partial => 'spree/admin/shared/refunds', :locals => { :refunds => @refunds, show_actions: true } %> +
    + <% end %> + +<% else %> +
    <%= Spree.t(:order_has_no_payments) %>
    +<% end %> diff --git a/backend/app/views/spree/admin/payments/new.html.erb b/backend/app/views/spree/admin/payments/new.html.erb new file mode 100644 index 00000000000..a9fecc16976 --- /dev/null +++ b/backend/app/views/spree/admin/payments/new.html.erb @@ -0,0 +1,26 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Payments' } %> + +<% content_for :page_title do %> + <%= Spree.t(:new_payment) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_payments_list), spree.admin_order_payments_url(@order), :icon => 'arrow-left' %>
  • +<% end %> + +<% if @payment_methods.any? %> + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @payment } %> + + <%= form_for @payment, :url => admin_order_payments_path(@order) do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= button @order.cart? ? Spree.t('actions.continue') : Spree.t('actions.update'), @order.cart? ? 'arrow-right' : 'refresh' %> +
    +
    + <% end %> +<% else %> + <%= Spree.t(:cannot_create_payment_without_payment_methods) %> + <%= link_to Spree.t(:please_define_payment_methods), admin_payment_methods_url %> +<% end %> diff --git a/backend/app/views/spree/admin/payments/show.html.erb b/backend/app/views/spree/admin/payments/show.html.erb new file mode 100644 index 00000000000..4ecf4918137 --- /dev/null +++ b/backend/app/views/spree/admin/payments/show.html.erb @@ -0,0 +1,25 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Payments' } %> + +<% content_for :page_title do %> + + <%= I18n.t(:one, scope: "activerecord.models.spree/payment") %> + + <%= payment_method_name(@payment) %> +   + + <%= Spree.t(@payment.state, :scope => :payment_states, :default => @payment.state.capitalize) %> + +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:logs), spree.admin_order_payment_log_entries_url(@order, @payment), :icon => 'archive' %>
  • +
  • <%= button_link_to Spree.t(:back_to_payments_list), spree.admin_order_payments_url(@order), :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => "spree/admin/payments/source_views/#{@payment.payment_method.method_type}", :locals => { :payment => @payment.source.is_a?(Spree::Payment) ? @payment.source : @payment } %> + +
    +
    <%= label_tag nil, Spree.t(:amount) %>: <%= @payment.display_amount.to_html %>
    +
    + +<%= render 'spree/admin/payments/capture_events' %> \ No newline at end of file diff --git a/core/app/views/spree/admin/payments/source_forms/_check.html.erb b/backend/app/views/spree/admin/payments/source_forms/_check.html.erb similarity index 100% rename from core/app/views/spree/admin/payments/source_forms/_check.html.erb rename to backend/app/views/spree/admin/payments/source_forms/_check.html.erb diff --git a/backend/app/views/spree/admin/payments/source_forms/_gateway.html.erb b/backend/app/views/spree/admin/payments/source_forms/_gateway.html.erb new file mode 100644 index 00000000000..d6ec4d73984 --- /dev/null +++ b/backend/app/views/spree/admin/payments/source_forms/_gateway.html.erb @@ -0,0 +1,60 @@ +
    +
    + <% if previous_cards.any? %> + <% previous_cards.each do |card| %> + + <% end %> + + <% end %> +
    + +
    +
    diff --git a/core/app/views/spree/admin/payments/source_views/_check.html.erb b/backend/app/views/spree/admin/payments/source_views/_check.html.erb similarity index 100% rename from core/app/views/spree/admin/payments/source_views/_check.html.erb rename to backend/app/views/spree/admin/payments/source_views/_check.html.erb diff --git a/backend/app/views/spree/admin/payments/source_views/_gateway.html.erb b/backend/app/views/spree/admin/payments/source_views/_gateway.html.erb new file mode 100644 index 00000000000..5e0cb9a04a2 --- /dev/null +++ b/backend/app/views/spree/admin/payments/source_views/_gateway.html.erb @@ -0,0 +1,21 @@ +
    + <%= Spree.t(:credit_card) %> + +
    +
    +
    +
    <%= Spree.t(:name_on_card) %>:
    +
    <%= payment.source.name %>
    + +
    <%= Spree.t(:card_type) %>:
    +
    <%= payment.source.cc_type %>
    + +
    <%= Spree.t(:card_number) %>:
    +
    <%= payment.source.display_number %>
    + +
    <%= Spree.t(:expiration) %>:
    +
    <%= payment.source.month %>/<%= payment.source.year %>
    +
    +
    +
    +
    diff --git a/backend/app/views/spree/admin/product_properties/_product_property_fields.html.erb b/backend/app/views/spree/admin/product_properties/_product_property_fields.html.erb new file mode 100644 index 00000000000..fe48c6a523a --- /dev/null +++ b/backend/app/views/spree/admin/product_properties/_product_property_fields.html.erb @@ -0,0 +1,19 @@ + + + <% if f.object.persisted? %> + + <%= f.hidden_field :id %> + <% end %> + + + <%= f.text_field :property_name, class: 'autocomplete' %> + + + <%= f.text_field :value %> + + + <% if f.object.persisted? %> + <%= link_to_delete f.object, no_text: true %> + <% end %> + + diff --git a/backend/app/views/spree/admin/product_properties/index.html.erb b/backend/app/views/spree/admin/product_properties/index.html.erb new file mode 100644 index 00000000000..a6adc81a2f4 --- /dev/null +++ b/backend/app/views/spree/admin/product_properties/index.html.erb @@ -0,0 +1,55 @@ +<%= render 'spree/admin/shared/product_sub_menu' %> +<%= render 'spree/admin/shared/product_tabs', :current => 'Product Properties' %> +<%= render 'spree/shared/error_messages', :target => @product %> + +<% content_for :page_actions do %> +
      +
    • + <%= link_to_add_fields Spree.t(:add_product_properties), 'tbody#product_properties', :class => 'plus button' %> +
    • +
    • + + <%= link_to Spree.t(:select_from_prototype), available_admin_prototypes_url, :remote => true, 'data-update' => 'prototypes', :class => 'button fa fa-copy' %> + +
    • +
    +<% end %> + +<%= form_for @product, :url => admin_product_url(@product), :method => :put do |f| %> +
    +
    + +
    + <%= image_tag 'select2-spinner.gif', :plugin => 'spree', :style => 'display:none;', :id => 'busy_indicator' %> + + + + + + + + + + + <%= f.fields_for :product_properties do |pp_form| %> + <%= render 'product_property_fields', :f => pp_form %> + <% end %> + +
    <%= Spree.t(:property) %><%= Spree.t(:value) %>
    + + <%= render 'spree/admin/shared/edit_resource_links' %> + + <%= hidden_field_tag 'clear_product_properties', 'true' %> +
    +<% end %> + +<%= javascript_tag do -%> + var properties = <%= raw(@properties.to_json) %>; + $('#product_properties').on('keydown', 'input.autocomplete', function() { + already_auto_completed = $(this).is('ac_input'); + if (!already_auto_completed) { + $(this).autocomplete({source: properties}); + $(this).focus(); + } + }); +<% end -%> diff --git a/backend/app/views/spree/admin/products/_add_stock_form.html.erb b/backend/app/views/spree/admin/products/_add_stock_form.html.erb new file mode 100644 index 00000000000..9c5ab1cc8e2 --- /dev/null +++ b/backend/app/views/spree/admin/products/_add_stock_form.html.erb @@ -0,0 +1,35 @@ +<%= form_for [:admin, Spree::StockMovement.new], url: admin_stock_items_path do |f| %> +
    + <%= Spree.t(:add_stock_management) %> + +
    +
    + <%= f.field_container :quantity do %> + <%= f.label :quantity, Spree.t(:quantity) %> + <%= f.number_field :quantity, class: 'fullwidth', value: 1 %> + <% end %> +
    +
    + <%= f.field_container :stock_location do %> + <%= label_tag :stock_location_id, Spree.t(:stock_location) %> + <%= select_tag 'stock_location_id', options_from_collection_for_select(@stock_locations, :id, :name), + class: 'select2 fullwidth' %> + <% end %> +
    + +
    + <%= f.field_container :variant_id do %> + <%= label_tag 'variant_id', Spree.t(:variant) %> + <%= select_tag 'variant_id', options_from_collection_for_select(@variants, :id, :sku_and_options_text), + class: 'select2 fullwidth' %> + <% end %> +
    +
    + +
    + <%= button Spree.t(:add_stock), 'plus' %> + <%= Spree.t(:or) %> + <%= link_to_with_icon 'remove', Spree.t('actions.cancel'), collection_url, :class => 'button' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/products/_autocomplete.js.erb b/backend/app/views/spree/admin/products/_autocomplete.js.erb new file mode 100644 index 00000000000..aa52cdf2226 --- /dev/null +++ b/backend/app/views/spree/admin/products/_autocomplete.js.erb @@ -0,0 +1,14 @@ + + diff --git a/backend/app/views/spree/admin/products/_form.html.erb b/backend/app/views/spree/admin/products/_form.html.erb new file mode 100644 index 00000000000..1ec1960e79b --- /dev/null +++ b/backend/app/views/spree/admin/products/_form.html.erb @@ -0,0 +1,187 @@ +
    + +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, raw(Spree.t(:name) + content_tag(:span, ' *', :class => 'required')) %> + <%= f.text_field :name, :class => 'fullwidth title', :required => true %> + <%= f.error_message_on :name %> + <% end %> +
    + +
    + <%= f.field_container :slug do %> + <%= f.label :slug, raw(Spree.t(:slug) + content_tag(:span, ' *', :class => "required")) %> + <%= f.text_field :slug, :class => 'fullwidth title', :required => true %> + <%= f.error_message_on :slug %> + <% end %> +
    + +
    + <%= f.field_container :description do %> + <%= f.label :description, Spree.t(:description) %> + <%= f.text_area :description, {:rows => "#{unless @product.has_variants? then '22' else '15' end}", :class => 'fullwidth'} %> + <%= f.error_message_on :description %> + <% end %> +
    +
    + +
    +
    + <%= f.field_container :price do %> + <%= f.label :price, raw(Spree.t(:master_price) + content_tag(:span, ' *', :class => "required")) %> + <%= f.text_field :price, :value => number_to_currency(@product.price, :unit => ''), :required => true %> + <%= f.error_message_on :price %> + <% end %> +
    + +
    + <%= f.field_container :cost_price do %> + <%= f.label :cost_price, Spree.t(:cost_price) %> + <%= f.text_field :cost_price, :value => number_to_currency(@product.cost_price, :unit => '') %> + <%= f.error_message_on :cost_price %> + <% end %> +
    + +
    + <%= f.field_container :cost_currency do %> + <%= f.label :cost_currency, Spree.t(:cost_currency) %> + <%= f.text_field :cost_currency %> + <%= f.error_message_on :cost_currency %> + <% end %> +
    + +
    + +
    + <%= f.field_container :available_on do %> + <%= f.label :available_on, Spree.t(:available_on) %> + <%= f.error_message_on :available_on %> + <%= f.text_field :available_on, :value => datepicker_field_value(@product.available_on), :class => 'datepicker' %> + <% end %> +
    + +
    + <%= f.field_container :promotionable do %> + <%= f.label :promotionable do %> + <%= f.check_box :promotionable %> <%= Spree.t(:promotionable) %> + <% end %> + <% end %> +
    + + <% if @product.has_variants? %> +
    + <%= f.label :skus, Spree.t(:skus) %> + + <%= Spree.t(:info_product_has_multiple_skus, count: @product.variants.count) %> +
      + <% @product.variants.first(5).each do |variant| %> +
    • <%= variant.sku %>
    • + <% end %> +
    + <% if @product.variants.count > 5 %> + <%= Spree.t(:info_number_of_skus_not_shown, count: @product.variants.count - 5) %> + <% end %> +
    +
    + <% if can?(:admin, Spree::Variant) %> + <%= link_to_with_icon 'th-large', Spree.t(:manage_variants), admin_product_variants_url(@product) %> + <% end %> +
    +
    + <% else %> +
    + <%= f.field_container :sku do %> + <%= f.label :sku, Spree.t(:sku) %> + <%= f.text_field :sku, :size => 16 %> + <% end %> +
    + +
      +
    • + <%= f.label :weight, Spree.t(:weight) %> + <%= f.text_field :weight, :size => 4 %> +
    • +
    • + <%= f.label :height, Spree.t(:height) %> + <%= f.text_field :height, :size => 4 %> +
    • +
    • + <%= f.label :width, Spree.t(:width) %> + <%= f.text_field :width, :size => 4 %> +
    • +
    • + <%= f.label :depth, Spree.t(:depth) %> + <%= f.text_field :depth, :size => 4 %> +
    • +
    + <% end %> + +
    + <%= f.field_container :shipping_categories do %> + <%= f.label :shipping_category_id, Spree.t(:shipping_categories) %> + <%= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, { :include_blank => Spree.t('match_choices.none') }, { :class => 'select2' }) %> + <%= f.error_message_on :shipping_category %> + <% end %> +
    + +
    + <%= f.field_container :tax_category do %> + <%= f.label :tax_category_id, Spree.t(:tax_category) %> + <%= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { :include_blank => Spree.t('match_choices.none') }, { :class => 'select2' }) %> + <%= f.error_message_on :tax_category %> + <% end %> +
    +
    + +
    +
    + <%= f.field_container :taxons do %> + <%= f.label :taxon_ids, Spree.t(:taxons) %>
    + <%= f.hidden_field :taxon_ids, :value => @product.taxon_ids.join(',') %> + <% end %> +
    + +
    + <%= f.field_container :option_types do %> + <%= f.label :option_type_ids, Spree.t(:option_types) %> + <%= f.hidden_field :option_type_ids, :value => @product.option_type_ids.join(',') %> + <% end %> +
    +
    + +
    +
    + <%= f.field_container :meta_title do %> + <%= f.label :meta_title, Spree.t(:meta_title) %> + <%= f.text_field :meta_title, :class => 'fullwidth' %> + <% end %> +
    + +
    + <%= f.field_container :meta_keywords do %> + <%= f.label :meta_keywords, Spree.t(:meta_keywords) %> + <%= f.text_field :meta_keywords, :class => 'fullwidth' %> + <% end %> +
    + +
    + <%= f.field_container :meta_description do %> + <%= f.label :meta_description, Spree.t(:meta_description) %> + <%= f.text_field :meta_description, :class => 'fullwidth' %> + <% end %> +
    +
    + +
    + +
    + +
    +
    + +<% unless Rails.env.test? %> + +<% end %> diff --git a/core/app/views/spree/admin/products/_properties_form.erb b/backend/app/views/spree/admin/products/_properties_form.erb similarity index 90% rename from core/app/views/spree/admin/products/_properties_form.erb rename to backend/app/views/spree/admin/products/_properties_form.erb index b80c894af78..b019a3937df 100644 --- a/core/app/views/spree/admin/products/_properties_form.erb +++ b/backend/app/views/spree/admin/products/_properties_form.erb @@ -1,5 +1,5 @@ <% content_for :page_title do %> - <%= t(:properties) %> + <%= Spree.t(:properties) %> <% end %> <% f.fields_for :product_properties do |properties_form| %> diff --git a/backend/app/views/spree/admin/products/edit.html.erb b/backend/app/views/spree/admin/products/edit.html.erb new file mode 100644 index 00000000000..1669da5625e --- /dev/null +++ b/backend/app/views/spree/admin/products/edit.html.erb @@ -0,0 +1,20 @@ +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_products_list), session[:return_to] || admin_products_url, :icon => 'arrow-left' %>
  • + <% if can?(:create, Spree::Product) %> + + <% end %> +<% end %> + +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Product Details' } %> +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @product } %> + +<%= form_for [:admin, @product], :method => :put, :html => { :multipart => true } do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/products/index.html.erb b/backend/app/views/spree/admin/products/index.html.erb new file mode 100644 index 00000000000..f34a5f6a6c3 --- /dev/null +++ b/backend/app/views/spree/admin/products/index.html.erb @@ -0,0 +1,102 @@ +<% content_for :page_title do %> + <%= Spree.t(:listing_products) %> +<% end %> + +<% content_for :page_actions do %> + +<% end if can?(:create, Spree::Product) %> + +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<% content_for :table_filter_title do %> + <%= Spree.t(:search) %> +<% end %> + +<% content_for :table_filter do %> +
    + + <%= search_form_for [:admin, @search] do |f| %> + + <%- locals = {:f => f} %> + +
    +
    +
    + <%= f.label :name_cont, Spree.t(:name) %> + <%= f.text_field :name_cont, :size => 15 %> +
    +
    +
    +
    + <%= f.label :variants_including_master_sku_cont, Spree.t(:sku) %> + <%= f.text_field :variants_including_master_sku_cont, :size => 15 %> +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    + <%= button Spree.t(:search), 'search' %> +
    + <% end %> +
    +<% end %> + +
    + +<%= paginate @collection %> + +<% if @collection.any? %> + + + + + + + + + + + + + + + + + + <% @collection.each do |product| %> + id="<%= spree_dom_id product %>" data-hook="admin_products_index_rows" class="<%= cycle('odd', 'even') %>"> + + + + + + + <% end %> + +
    <%= Spree.t(:sku) %><%= sort_link @search,:name, Spree.t(:name), { :default_order => "desc" }, {:title => 'admin_products_listing_name_title'} %><%= sort_link @search,:master_default_price_amount, Spree.t(:master_price), {}, {:title => 'admin_products_listing_price_title'} %>
    <%= product.sku rescue '' %><%= mini_image(product) %><%= link_to product.try(:name), edit_admin_product_path(product) %><%= product.display_price.to_html rescue '' %> + <%= link_to_edit product, :no_text => true, :class => 'edit' if can?(:edit, product) && !product.deleted? %> +   + <%= link_to_clone product, :no_text => true, :class => 'clone' if can?(:clone, product) %> +   + <%= link_to_delete product, :no_text => true if can?(:delete, product) && !product.deleted? %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/product')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_product_path %>! +
    +<% end %> + +<%= paginate @collection %> diff --git a/backend/app/views/spree/admin/products/new.html.erb b/backend/app/views/spree/admin/products/new.html.erb new file mode 100644 index 00000000000..b2bb01eaf99 --- /dev/null +++ b/backend/app/views/spree/admin/products/new.html.erb @@ -0,0 +1,92 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @product } %> + +<%= form_for [:admin, @product], method: :post, url: admin_products_path, :html => { :multipart => true } do |f| %> + +
    + + <%= Spree.t(:new_product) %> + + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= f.text_field :name, :class => 'fullwidth title', :required => true %> + <%= f.error_message_on :name %> + <% end %> + +
    + <% unless @product.has_variants? %> +
    + <%= f.field_container :sku do %> + <%= f.label :sku, Spree.t(:sku) %>
    + <%= f.text_field :sku, :size => 16, :class => 'fullwidth' %> + <%= f.error_message_on :sku %> + <% end %> +
    + <% end %> + +
    + <%= f.field_container :prototype do %> + <%= f.label :prototype_id, Spree.t(:prototype) %>
    + <%= f.collection_select :prototype_id, Spree::Prototype.all, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth'} %> + <% end %> +
    + +
    + <%= f.field_container :price do %> + <%= f.label :price, Spree.t(:master_price) %> *
    + <%= f.text_field :price, :value => number_to_currency(@product.price, :unit => ''), :class => 'fullwidth', :required => true %> + <%= f.error_message_on :price %> + <% end %> +
    + +
    + <%= f.field_container :available_on do %> + <%= f.label :available_on, Spree.t(:available_on) %> + <%= f.error_message_on :available_on %> + <%= f.text_field :available_on, :class => 'datepicker fullwidth' %> + <% end %> +
    + +
    + +
    +
    + <%= f.field_container :shipping_category do %> + <%= f.label :shipping_category_id, Spree.t(:shipping_categories) %>*
    + <%= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, { :include_blank => Spree.t('match_choices.none') }, { :class => 'select2 fullwidth', :required => true }) %> + <%= f.error_message_on :shipping_category_id %> + <% end %> +
    +
    + +
    + <%= render :file => 'spree/admin/prototypes/show' if @prototype %> +
    + + <%= render :partial => 'spree/admin/shared/new_resource_links' %> + +
    +<% end %> + + diff --git a/backend/app/views/spree/admin/products/new.js.erb b/backend/app/views/spree/admin/products/new.js.erb new file mode 100644 index 00000000000..325ad02a196 --- /dev/null +++ b/backend/app/views/spree/admin/products/new.js.erb @@ -0,0 +1,7 @@ +$("#new_product_wrapper").html('<%= escape_javascript(render :template => "spree/admin/products/new", :formats => [:html], :handlers => [:erb]) %>'); +handle_date_picker_fields(); +<% unless Rails.env.test? %> + $('.select2').select2(); +<% end %> +$("#table-filter").hide(); +$("#admin_new_product").parent().hide(); diff --git a/backend/app/views/spree/admin/products/stock.html.erb b/backend/app/views/spree/admin/products/stock.html.erb new file mode 100644 index 00000000000..4aba3675723 --- /dev/null +++ b/backend/app/views/spree/admin/products/stock.html.erb @@ -0,0 +1,91 @@ +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_products_list), session[:return_to] || admin_products_url, :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> +<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Stock Management' } %> +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @product } %> + +
    + <%= render 'add_stock_form' %> +
    + + + + + + + + + + + + + + + + <% @variants.each do |variant| %> + <% if variant.stock_items.present? %> + + + + <% reset_cycle("stock_locations") %> + <% end %> + + <% end %> + +
    <%= Spree.t(:variant) %><%= Spree.t(:stock_location_info) %>
    + <%= variant.sku_and_options_text %> +
    + <% if variant.images.present? %> + <%= image_tag variant.images.first.attachment.url(:mini) %> + <% end %> +
    + <%= form_tag admin_product_variants_including_master_path(@product, variant, format: :js), method: :put, class: 'toggle_variant_track_inventory' do %> + <%= check_box_tag 'track_inventory', 1, variant.track_inventory?, + class: 'track_inventory_checkbox' %> + <%= label_tag :track_inventory, Spree.t(:track_inventory) %> + <%= hidden_field_tag 'variant[track_inventory]', variant.track_inventory?, + class: 'variant_track_inventory', + id: "variant_track_inventory_#{variant.id}" %> + <% end %> +
    + + + + + + + + + + + + + + + <% variant.stock_items.each do |item| %> + <% next unless @stock_locations.include?(item.stock_location) %> + + + + + + + + <% end %> + +
    <%= Spree.t(:stock_location) %><%= Spree.t(:count_on_hand) %><%= Spree.t(:backorderable) %>
    <%= item.stock_location.name %><%= item.count_on_hand %> + <%= form_tag admin_stock_item_path(item), method: :put, class: 'toggle_stock_item_backorderable' do %> + <%= check_box_tag 'stock_item[backorderable]', true, + item.backorderable?, + class: 'stock_item_backorderable', + id: "stock_item_backorderable_#{item.stock_location.id}" %> + <% end %> + + <%= link_to(icon('delete'), [:admin, item], + method: :delete, remote: true, + class: 'icon_link with-tip fa fa-trash no-text', + title: Spree.t(:remove), data: { action: :remove, confirm: Spree.t(:are_you_sure) }) %> +
    +
    diff --git a/backend/app/views/spree/admin/promotion_actions/create.js.erb b/backend/app/views/spree/admin/promotion_actions/create.js.erb new file mode 100644 index 00000000000..9700fc67170 --- /dev/null +++ b/backend/app/views/spree/admin/promotion_actions/create.js.erb @@ -0,0 +1,13 @@ +$('#actions').append('<%= escape_javascript( render(:partial => 'spree/admin/promotions/promotion_action', :object => @promotion_action) ) %>'); +$('#actions .no-objects-found').hide(); +$(document).ready(function(){ + $(".variant_autocomplete").variantAutocomplete(); + //enable select2 functions for recently added box + $('.type-select.select2').last().select2(); +}); +initProductActions(); + + +$('#<%= dom_id @promotion_action %>').hide(); +$('#<%= dom_id @promotion_action %>').fadeIn(); + diff --git a/promo/app/views/spree/admin/promotion_actions/destroy.js.erb b/backend/app/views/spree/admin/promotion_actions/destroy.js.erb similarity index 100% rename from promo/app/views/spree/admin/promotion_actions/destroy.js.erb rename to backend/app/views/spree/admin/promotion_actions/destroy.js.erb diff --git a/backend/app/views/spree/admin/promotion_categories/_form.html.erb b/backend/app/views/spree/admin/promotion_categories/_form.html.erb new file mode 100644 index 00000000000..d273b1c1e5c --- /dev/null +++ b/backend/app/views/spree/admin/promotion_categories/_form.html.erb @@ -0,0 +1,10 @@ +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @promotion } %> + +
    +
    + <%= f.field_container :name do %> + <%= f.label :name %> + <%= f.text_field :name, :class => 'fullwidth' %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/promotion_categories/edit.html.erb b/backend/app/views/spree/admin/promotion_categories/edit.html.erb new file mode 100644 index 00000000000..3de0a4cf583 --- /dev/null +++ b/backend/app/views/spree/admin/promotion_categories/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :page_title do %> + <%= Spree.t(:editing_promotion_category) %> +<% end %> + +<%= render 'spree/admin/shared/promotion_sub_menu' %> + +<%= form_for @promotion_category, :url => object_url, :method => :put do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/promotion_categories/index.html.erb b/backend/app/views/spree/admin/promotion_categories/index.html.erb new file mode 100644 index 00000000000..f7ec4ee6123 --- /dev/null +++ b/backend/app/views/spree/admin/promotion_categories/index.html.erb @@ -0,0 +1,33 @@ +<% content_for :page_title do %> + <%= Spree::PromotionCategory.model_name.human(count: :many) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_promotion_category), spree.new_admin_promotion_category_path, :icon => 'plus' %> +
  • +<% end %> + +<%= render 'spree/admin/shared/promotion_sub_menu' %> + + + + + + + + + + + + <% @promotion_categories.each do |promotion_category| %> + + + + + <% end %> + +
    <%= Spree::PromotionCategory.human_attribute_name :name %>
    <%= promotion_category.name %> + <%= link_to_edit promotion_category, :no_text => true %> + <%= link_to_delete promotion_category, :no_text => true %> +
    diff --git a/backend/app/views/spree/admin/promotion_categories/new.html.erb b/backend/app/views/spree/admin/promotion_categories/new.html.erb new file mode 100644 index 00000000000..30fc2c16a4e --- /dev/null +++ b/backend/app/views/spree/admin/promotion_categories/new.html.erb @@ -0,0 +1,12 @@ +<% content_for :page_title do %> + <%= Spree.t(:new_promotion_category) %> +<% end %> + +<%= render 'spree/admin/shared/promotion_sub_menu' %> + +<%= form_for :promotion_category, :url => collection_url do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/promotion_rules/create.js.erb b/backend/app/views/spree/admin/promotion_rules/create.js.erb new file mode 100644 index 00000000000..b0adb281c57 --- /dev/null +++ b/backend/app/views/spree/admin/promotion_rules/create.js.erb @@ -0,0 +1,10 @@ +$('#rules').append('<%= escape_javascript( render(:partial => 'spree/admin/promotions/promotion_rule', :object => @promotion_rule) ) %>'); +$('#rules .no-objects-found').hide(); + +$('.product_picker').productAutocomplete(); +$('.user_picker').userAutocomplete(); + +$('#promotion_rule_type').html('<%= escape_javascript options_for_promotion_rule_types(@promotion) %>'); +$('#promotion_rule_type').select2(); + +set_taxon_select() diff --git a/promo/app/views/spree/admin/promotion_rules/destroy.js.erb b/backend/app/views/spree/admin/promotion_rules/destroy.js.erb similarity index 100% rename from promo/app/views/spree/admin/promotion_rules/destroy.js.erb rename to backend/app/views/spree/admin/promotion_rules/destroy.js.erb diff --git a/backend/app/views/spree/admin/promotions/_actions.html.erb b/backend/app/views/spree/admin/promotions/_actions.html.erb new file mode 100644 index 00000000000..439b77093a5 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/_actions.html.erb @@ -0,0 +1,32 @@ +
    + + <%= form_tag spree.admin_promotion_promotion_actions_path(@promotion), :remote => true, :id => 'new_promotion_action_form' do %> + <% options = options_for_select( Rails.application.config.spree.promotions.actions.map(&:name).map {|name| [ Spree.t("promotion_action_types.#{name.demodulize.underscore}.name"), name] } ) %> +
    + <%= Spree.t(:promotion_actions) %> +
    + <%= label_tag :action_type, Spree.t(:add_action_of_type)%> + <%= select_tag 'action_type', options, :class => 'select2 fullwidth' %> +
    +
    + <%= button Spree.t(:add), 'plus' %> +
    +
    + <% end %> + + <%= form_for @promotion, :url => spree.admin_promotion_path(@promotion), :method => :put do |f| %> +
    + <% if @promotion.actions.any? %> + <%= render :partial => 'promotion_action', :collection => @promotion.actions %> + <% else %> +
    + <%= Spree.t(:no_actions_added) %> +
    + <% end %> +
    +
    + <%= button Spree.t('actions.update'), 'refresh' %> +
    + <% end %> + +
    diff --git a/backend/app/views/spree/admin/promotions/_form.html.erb b/backend/app/views/spree/admin/promotions/_form.html.erb new file mode 100644 index 00000000000..2097d75ef64 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/_form.html.erb @@ -0,0 +1,58 @@ +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @promotion } %> +
    +
    +
    + <%= f.field_container :name do %> + <%= f.label :name %> + <%= f.text_field :name, :class => 'fullwidth' %> + <% end %> + + <%= f.field_container :code do %> + <%= f.label :code %> + <%= f.text_field :code, :class => 'fullwidth' %> + <% end %> + + <%= f.field_container :path do %> + <%= f.label :path %> + <%= f.text_field :path, :class => 'fullwidth' %> + <% end %> + + <%= f.field_container :advertise do %> + <%= f.check_box :advertise %> + <%= f.label :advertise %> + <% end %> +
    + +
    + <%= f.field_container :description do %> + <%= f.label :description %>
    + <%= f.text_area :description, :rows => 7, :class => 'fullwidth' %> + <% end %> + + <%= f.field_container :category do %> + <%= f.label :promotion_category %>
    + <%= f.collection_select(:promotion_category_id, @promotion_categories, :id, :name, { :include_blank => Spree.t('match_choices.none') }, { :class => 'select2 fullwidth' }) %> + <% end %> +
    +
    + +
    + <%= f.field_container :usage_limit do %> + <%= f.label :usage_limit %>
    + <%= f.number_field :usage_limit, :min => 0, :class => 'fullwidth' %>
    + + <%= Spree.t(:current_promotion_usage, :count => @promotion.credits_count) %> + + <% end %> + +
    + <%= f.label :starts_at %> + <%= f.text_field :starts_at, :value => datepicker_field_value(@promotion.starts_at), :class => 'datepicker datepicker-from fullwidth' %> +
    + +
    + <%= f.label :expires_at %> + <%= f.text_field :expires_at, :value => datepicker_field_value(@promotion.expires_at), :class => 'datepicker datepicker-to fullwidth' %> +
    +
    +
    diff --git a/backend/app/views/spree/admin/promotions/_promotion_action.html.erb b/backend/app/views/spree/admin/promotions/_promotion_action.html.erb new file mode 100644 index 00000000000..6b6e4da1faf --- /dev/null +++ b/backend/app/views/spree/admin/promotions/_promotion_action.html.erb @@ -0,0 +1,12 @@ +
    + <% type_name = promotion_action.class.name.demodulize.underscore %> +
    <%= Spree.t("promotion_action_types.#{type_name}.description") %>
    + <%= link_to_with_icon 'trash', '', spree.admin_promotion_promotion_action_path(@promotion, promotion_action), :remote => true, :method => :delete, :class => 'delete' %> + + <% param_prefix = "promotion[promotion_actions_attributes][#{promotion_action.id}]" %> + <%= hidden_field_tag "#{param_prefix}[id]", promotion_action.id %> + + <%= render :partial => "spree/shared/error_messages", :locals => { :target => promotion_action } %> + <%= render :partial => "spree/admin/promotions/actions/#{type_name}", + :locals => { :promotion_action => promotion_action, :param_prefix => param_prefix } %> +
    diff --git a/backend/app/views/spree/admin/promotions/_promotion_rule.html.erb b/backend/app/views/spree/admin/promotions/_promotion_rule.html.erb new file mode 100644 index 00000000000..b0df564377c --- /dev/null +++ b/backend/app/views/spree/admin/promotions/_promotion_rule.html.erb @@ -0,0 +1,10 @@ +
    + <% type_name = promotion_rule.class.name.demodulize.underscore %> +
    '><%= Spree.t("promotion_rule_types.#{type_name}.description") %>
    + <%= link_to_with_icon 'trash', '', spree.admin_promotion_promotion_rule_path(@promotion, promotion_rule), :remote => true, :method => :delete, :class => 'delete' %> + + <% param_prefix = "promotion[promotion_rules_attributes][#{promotion_rule.id}]" %> + <%= hidden_field_tag "#{param_prefix}[id]", promotion_rule.id %> + <%= render :partial => "spree/shared/error_messages", :locals => { :target => promotion_rule } %> + <%= render :partial => "spree/admin/promotions/rules/#{type_name}", :locals => { :promotion_rule => promotion_rule, :param_prefix => param_prefix } %> +
    diff --git a/promo/app/views/spree/admin/promotions/_rules.html.erb b/backend/app/views/spree/admin/promotions/_rules.html.erb similarity index 79% rename from promo/app/views/spree/admin/promotions/_rules.html.erb rename to backend/app/views/spree/admin/promotions/_rules.html.erb index 2def45f2dc1..16946408bfc 100644 --- a/promo/app/views/spree/admin/promotions/_rules.html.erb +++ b/backend/app/views/spree/admin/promotions/_rules.html.erb @@ -2,14 +2,14 @@ <%= form_tag spree.admin_promotion_promotion_rules_path(@promotion), :remote => true, :id => 'new_product_rule_form' do %>
    - <%= t(:rules) %> + <%= Spree.t(:rules) %>
    - <%= label_tag :promotion_rule_type, t(:add_rule_of_type) %> + <%= label_tag :promotion_rule_type, Spree.t(:add_rule_of_type) %> <%= select_tag('promotion_rule[type]', options_for_promotion_rule_types(@promotion), :class => 'select2 fullwidth') %>
    - <%= button t(:add), 'icon-plus' %> + <%= button Spree.t(:add), 'plus' %>
    <% end %> @@ -20,7 +20,7 @@
    <% Spree::Promotion::MATCH_POLICIES.each do |policy| %>
    - +
    <% end %>
    @@ -30,13 +30,13 @@ <%= render :partial => 'promotion_rule', :collection => @promotion.rules, :locals => {} %> <% else %>
    - <%= t(:no_rules_added) %> + <%= Spree.t(:no_rules_added) %>
    <% end %>
    - <%= button t(:update), 'icon-refresh' %> + <%= button Spree.t('actions.update'), 'refresh' %>
    <% end %> diff --git a/promo/app/views/spree/admin/promotions/_tab.html.erb b/backend/app/views/spree/admin/promotions/_tab.html.erb similarity index 100% rename from promo/app/views/spree/admin/promotions/_tab.html.erb rename to backend/app/views/spree/admin/promotions/_tab.html.erb diff --git a/backend/app/views/spree/admin/promotions/actions/_create_adjustment.html.erb b/backend/app/views/spree/admin/promotions/actions/_create_adjustment.html.erb new file mode 100644 index 00000000000..702157934a9 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/actions/_create_adjustment.html.erb @@ -0,0 +1,28 @@ +
    + +
    + <% field_name = "#{param_prefix}[calculator_type]" %> + <%= label_tag field_name, Spree.t(:calculator) %> + <%= select_tag field_name, + options_from_collection_for_select(@calculators, :to_s, :description, promotion_action.calculator.type), + :class => 'type-select select2 fullwidth' %> + <% if promotion_action.calculator.respond_to?(:preferences) %> + <%= Spree.t(:calculator_settings_warning) %> + <% end %> +
    + + <% unless promotion_action.new_record? %> +
    + <% type_name = promotion_action.calculator.type.demodulize.underscore %> + <% if lookup_context.exists?("fields", + ["spree/admin/promotions/calculators/#{type_name}"], true) %> + <%= render "spree/admin/promotions/calculators/#{type_name}/fields", + calculator: promotion_action.calculator, prefix: param_prefix %> + <% else %> + <%= render "spree/admin/promotions/calculators/default_fields", + calculator: promotion_action.calculator, prefix: param_prefix %> + <% end %> + <%= hidden_field_tag "#{param_prefix}[calculator_attributes][id]", promotion_action.calculator.id %> +
    + <% end %> +
    diff --git a/backend/app/views/spree/admin/promotions/actions/_create_item_adjustments.html.erb b/backend/app/views/spree/admin/promotions/actions/_create_item_adjustments.html.erb new file mode 100644 index 00000000000..00d28e130f6 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/actions/_create_item_adjustments.html.erb @@ -0,0 +1,26 @@ +
    + +
    + <% field_name = "#{param_prefix}[calculator_type]" %> + <%= label_tag field_name, Spree.t(:calculator) %> + <%= select_tag field_name, + options_from_collection_for_select(Spree::Promotion::Actions::CreateItemAdjustments.calculators, :to_s, :description, promotion_action.calculator.type), + :class => 'type-select select2 fullwidth' %> + <% if promotion_action.calculator.respond_to?(:preferences) %> + <%= Spree.t(:calculator_settings_warning) %> + <% end %> +
    + + <% unless promotion_action.new_record? %> +
    + <% promotion_action.calculator.preferences.keys.map do |key| %> + <% field_name = "#{param_prefix}[calculator_attributes][preferred_#{key}]" %> + <%= label_tag field_name, Spree.t(key.to_s) %> + <%= preference_field_tag(field_name, + promotion_action.calculator.get_preference(key), + :type => promotion_action.calculator.preference_type(key)) %> + <% end %> + <%= hidden_field_tag "#{param_prefix}[calculator_attributes][id]", promotion_action.calculator.id %> +
    + <% end %> +
    diff --git a/backend/app/views/spree/admin/promotions/actions/_create_line_items.html.erb b/backend/app/views/spree/admin/promotions/actions/_create_line_items.html.erb new file mode 100644 index 00000000000..996a0258a93 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/actions/_create_line_items.html.erb @@ -0,0 +1,25 @@ +<% promotion_action.promotion_action_line_items.each do |item| %> + <%= item.quantity %> x <%= item.variant.product.name %> + <%= item.variant.options_text %> +<% end %> + +<% if promotion_action.promotion_action_line_items.empty? %> + <% line_items = promotion_action.promotion_action_line_items %> + <% line_items.build %> + + <% line_items.each_with_index do |line_item, index| %> +
    +
    + <% line_item_prefix = "#{param_prefix}[promotion_action_line_items_attributes][#{index}]" %> +
    + <%= label_tag "#{line_item_prefix}_variant_id", Spree.t(:variant) %> + <%= hidden_field_tag "#{line_item_prefix}[variant_id]", line_item.variant_id, class: "variant_autocomplete fullwidth" %> +
    +
    + <%= label_tag "#{line_item_prefix}_quantity", Spree.t(:quantity) %> + <%= number_field_tag "#{line_item_prefix}[quantity]", line_item.quantity, min: 1, class: 'fullwidth' %> +
    +
    +
    + <% end %> +<% end %> diff --git a/core/app/views/spree/checkout/payment/_check.html.erb b/backend/app/views/spree/admin/promotions/actions/_free_shipping.html.erb similarity index 100% rename from core/app/views/spree/checkout/payment/_check.html.erb rename to backend/app/views/spree/admin/promotions/actions/_free_shipping.html.erb diff --git a/backend/app/views/spree/admin/promotions/calculators/_default_fields.html.erb b/backend/app/views/spree/admin/promotions/calculators/_default_fields.html.erb new file mode 100644 index 00000000000..0749bf79a5b --- /dev/null +++ b/backend/app/views/spree/admin/promotions/calculators/_default_fields.html.erb @@ -0,0 +1,8 @@ +<% calculator.preferences.keys.map do |key| %> + <% field_name = "#{prefix}[calculator_attributes][preferred_#{key}]" %> + <%= label_tag field_name, Spree.t(key.to_s) %> + <%= preference_field_tag( + field_name, + calculator.get_preference(key), + type: calculator.preference_type(key)) %> +<% end %> diff --git a/backend/app/views/spree/admin/promotions/calculators/tiered_flat_rate/_fields.html.erb b/backend/app/views/spree/admin/promotions/calculators/tiered_flat_rate/_fields.html.erb new file mode 100644 index 00000000000..106f93c7e01 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/calculators/tiered_flat_rate/_fields.html.erb @@ -0,0 +1,37 @@ +<%= label_tag "#{prefix}[calculator_attributes][preferred_base_amount]", + Spree.t(:base_amount) %> +<%= preference_field_tag( + "#{prefix}[calculator_attributes][preferred_base_amount]", + calculator.preferred_base_amount, + type: calculator.preference_type(:base_amount)) %> + +<%= label_tag nil, Spree.t(:tiers) %> +<%= content_tag :div, nil, class: "hidden js-original-tiers", + data: { :'original-tiers' => Hash[calculator.preferred_tiers.sort] } %> +
    + + + + + + diff --git a/backend/app/views/spree/admin/promotions/calculators/tiered_percent/_fields.html.erb b/backend/app/views/spree/admin/promotions/calculators/tiered_percent/_fields.html.erb new file mode 100644 index 00000000000..b179e37dbe9 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/calculators/tiered_percent/_fields.html.erb @@ -0,0 +1,36 @@ +<%= label_tag "#{prefix}[calculator_attributes][preferred_base_percent]", + Spree.t(:base_percent) %> +<%= preference_field_tag( + "#{prefix}[calculator_attributes][preferred_base_percent]", + calculator.preferred_base_percent, + type: calculator.preference_type(:base_percent)) %> + +<%= label_tag nil, Spree.t(:tiers) %> +<%= content_tag :div, nil, class: "hidden js-original-tiers", + data: { :'original-tiers' => Hash[calculator.preferred_tiers.sort] } %> +
    + + + + + diff --git a/backend/app/views/spree/admin/promotions/edit.html.erb b/backend/app/views/spree/admin/promotions/edit.html.erb new file mode 100644 index 00000000000..a383415af09 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/edit.html.erb @@ -0,0 +1,24 @@ +<% content_for :page_title do %> + <%= Spree.t(:editing_promotion) %> +<% end %> + +<%= render 'spree/admin/shared/promotion_sub_menu' %> + +<%= form_for @promotion, :url => object_url, :method => :put do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> + +
    +
    + <%= render :partial => 'rules' %> +
    + +
    + <%= render :partial => 'actions' %> +
    +
    + +<%= render :partial => "spree/admin/variants/autocomplete", :formats => [:js] %> diff --git a/backend/app/views/spree/admin/promotions/index.html.erb b/backend/app/views/spree/admin/promotions/index.html.erb new file mode 100644 index 00000000000..06c39c4b9b3 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/index.html.erb @@ -0,0 +1,106 @@ +<% content_for :page_title do %> + <%= Spree.t(:promotions) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_promotion), spree.new_admin_promotion_path, :icon => 'plus' %> +
  • +<% end %> + +<%= render 'spree/admin/shared/promotion_sub_menu' %> + +<% content_for :table_filter_title do %> + <%= Spree.t(:search) %> +<% end %> + +<% content_for :table_filter do %> +
    + <%= search_form_for [:admin, @search] do |f| %> +
    +
    + <%= label_tag :q_name_cont, Spree.t(:name) %> + <%= f.text_field :name_cont, tabindex: 1 %> +
    +
    + +
    +
    + <%= label_tag :q_code_cont, Spree.t(:code) %> + <%= f.text_field :code_cont, tabindex: 1 %> +
    +
    + +
    +
    + <%= label_tag :q_path_cont, Spree.t(:path) %> + <%= f.text_field :path_cont, tabindex: 1 %> +
    +
    + +
    +
    + <%= label_tag :q_promotion_category_id_eq, 'promotion category' %>
    + <%= f.collection_select(:promotion_category_id_eq, @promotion_categories, :id, :name, { :include_blank => Spree.t('match_choices.all') }, { :class => 'select2 fullwidth' }) %> +
    +
    + +
    + +
    +
    + <%= button Spree.t(:filter_results), 'search' %> +
    +
    + <% end %> +
    +<% end %> + + +<%= paginate @promotions %> + +<% if @promotions.any? %> + + + + + + + + + + + + + + + + + + + + + + + <% @promotions.each do |promotion| %> + + + + + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:code) %><%= Spree.t(:description) %><%= Spree.t(:usage_limit) %><%= Spree.t(:promotion_uses) %><%= Spree.t(:expiration) %>
    <%= promotion.name %><%= promotion.code %><%= promotion.description %><%= promotion.usage_limit.nil? ? "∞" : promotion.usage_limit %><%= Spree.t(:current_promotion_usage, :count => promotion.credits_count) %><%= promotion.expires_at.to_date.to_s(:short_date) if promotion.expires_at %> + <%= link_to_edit promotion, :no_text => true %> + <%= link_to_delete promotion, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/promotion')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_promotion_path %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/promotions/new.html.erb b/backend/app/views/spree/admin/promotions/new.html.erb new file mode 100644 index 00000000000..f101f44ae69 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/new.html.erb @@ -0,0 +1,12 @@ +<% content_for :page_title do %> + <%= Spree.t(:new_promotion) %> +<% end %> + +<%= render 'spree/admin/shared/promotion_sub_menu' %> + +<%= form_for :promotion, :url => collection_url do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/promo/app/views/spree/admin/promotions/rules/_first_order.html.erb b/backend/app/views/spree/admin/promotions/rules/_first_order.html.erb similarity index 100% rename from promo/app/views/spree/admin/promotions/rules/_first_order.html.erb rename to backend/app/views/spree/admin/promotions/rules/_first_order.html.erb diff --git a/backend/app/views/spree/admin/promotions/rules/_item_total.html.erb b/backend/app/views/spree/admin/promotions/rules/_item_total.html.erb new file mode 100644 index 00000000000..20b95d7294f --- /dev/null +++ b/backend/app/views/spree/admin/promotions/rules/_item_total.html.erb @@ -0,0 +1,8 @@ +
    + <%= select_tag "#{param_prefix}[preferred_operator_min]", options_for_select(Spree::Promotion::Rules::ItemTotal::OPERATORS_MIN.map{|o| [Spree.t("item_total_rule.operators.#{o}"),o]}, promotion_rule.preferred_operator_min), { class: 'select2 select_item_total fullwidth' } %> + <%= select_tag "#{param_prefix}[preferred_operator_max]", options_for_select(Spree::Promotion::Rules::ItemTotal::OPERATORS_MAX.map{|o| [Spree.t("item_total_rule.operators.#{o}"),o]}, promotion_rule.preferred_operator_max), { class: 'select2 select_item_total fullwidth' } %> +
    +
    + <%= text_field_tag "#{param_prefix}[preferred_amount_min]", promotion_rule.preferred_amount_min, class: 'fullwidth' %> + <%= text_field_tag "#{param_prefix}[preferred_amount_max]", promotion_rule.preferred_amount_max, class: 'fullwidth' %> +
    diff --git a/backend/app/views/spree/admin/promotions/rules/_landing_page.html.erb b/backend/app/views/spree/admin/promotions/rules/_landing_page.html.erb new file mode 100644 index 00000000000..cc92a514030 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/rules/_landing_page.html.erb @@ -0,0 +1,5 @@ +
    + + <%= text_field_tag "#{param_prefix}[preferred_path]", promotion_rule.preferred_path, :class => 'fullwidth' %> + <%= Spree.t('landing_page_rule.must_have_visited_path') %> +
    \ No newline at end of file diff --git a/core/app/views/spree/shared/unauthorized.html.erb b/backend/app/views/spree/admin/promotions/rules/_one_use_per_user.html.erb similarity index 100% rename from core/app/views/spree/shared/unauthorized.html.erb rename to backend/app/views/spree/admin/promotions/rules/_one_use_per_user.html.erb diff --git a/backend/app/views/spree/admin/promotions/rules/_product.html.erb b/backend/app/views/spree/admin/promotions/rules/_product.html.erb new file mode 100644 index 00000000000..9f4e0eb294a --- /dev/null +++ b/backend/app/views/spree/admin/promotions/rules/_product.html.erb @@ -0,0 +1,9 @@ +
    + <%= label_tag "#{param_prefix}_product_ids_string", Spree.t('product_rule.choose_products') %> + <%= hidden_field_tag "#{param_prefix}[product_ids_string]", promotion_rule.product_ids.join(","), :class => "product_picker fullwidth" %> +
    +
    + +
    diff --git a/backend/app/views/spree/admin/promotions/rules/_taxon.html.erb b/backend/app/views/spree/admin/promotions/rules/_taxon.html.erb new file mode 100644 index 00000000000..82c8183c1a3 --- /dev/null +++ b/backend/app/views/spree/admin/promotions/rules/_taxon.html.erb @@ -0,0 +1,9 @@ +
    + <%= label_tag "#{param_prefix}_taxon_ids_string", Spree.t('taxon_rule.choose_taxons') %> + <%= hidden_field_tag "#{param_prefix}[taxon_ids_string]", promotion_rule.taxon_ids.join(","), :class => "taxon_picker fullwidth", id: 'product_taxon_ids' %> +
    +
    + +
    diff --git a/promo/app/views/spree/admin/promotions/rules/_user.html.erb b/backend/app/views/spree/admin/promotions/rules/_user.html.erb similarity index 76% rename from promo/app/views/spree/admin/promotions/rules/_user.html.erb rename to backend/app/views/spree/admin/promotions/rules/_user.html.erb index d09610ecf1b..f37bcb2ceff 100644 --- a/promo/app/views/spree/admin/promotions/rules/_user.html.erb +++ b/backend/app/views/spree/admin/promotions/rules/_user.html.erb @@ -1,4 +1,4 @@
    -
    +
    diff --git a/promo/app/views/spree/admin/promotions/rules/_user_logged_in.html.erb b/backend/app/views/spree/admin/promotions/rules/_user_logged_in.html.erb similarity index 100% rename from promo/app/views/spree/admin/promotions/rules/_user_logged_in.html.erb rename to backend/app/views/spree/admin/promotions/rules/_user_logged_in.html.erb diff --git a/backend/app/views/spree/admin/properties/_form.html.erb b/backend/app/views/spree/admin/properties/_form.html.erb new file mode 100644 index 00000000000..bb86a4e69ad --- /dev/null +++ b/backend/app/views/spree/admin/properties/_form.html.erb @@ -0,0 +1,16 @@ +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= f.text_field :name, :class => 'fullwidth' %> + <%= f.error_message_on :name %> + <% end %> +
    +
    + <%= f.field_container :presentation do %> + <%= f.label :presentation, Spree.t(:presentation) %> *
    + <%= f.text_field :presentation, :class => 'fullwidth' %> + <%= f.error_message_on :presentation %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/properties/edit.html.erb b/backend/app/views/spree/admin/properties/edit.html.erb new file mode 100644 index 00000000000..595501e421c --- /dev/null +++ b/backend/app/views/spree/admin/properties/edit.html.erb @@ -0,0 +1,18 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_property) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_properties_list), admin_properties_url, :icon => 'arrow-left'%>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @property } %> + +<%= form_for [:admin, @property] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/core/app/views/spree/admin/properties/filtered.html.erb b/backend/app/views/spree/admin/properties/filtered.html.erb similarity index 100% rename from core/app/views/spree/admin/properties/filtered.html.erb rename to backend/app/views/spree/admin/properties/filtered.html.erb diff --git a/backend/app/views/spree/admin/properties/index.html.erb b/backend/app/views/spree/admin/properties/index.html.erb new file mode 100644 index 00000000000..d4bd596b920 --- /dev/null +++ b/backend/app/views/spree/admin/properties/index.html.erb @@ -0,0 +1,78 @@ +<% content_for :page_title do %> + <%= Spree.t(:properties) %> +<% end %> + +<% content_for :page_actions do %> + +<% end %> + +<%= render 'spree/admin/shared/product_sub_menu' %> + +<% content_for :table_filter_title do %> + <%= Spree.t(:search) %> +<% end %> + +<% content_for :table_filter do %> +
    + + <%= search_form_for [:admin, @search] do |f| %> + + <%- locals = {:f => f} %> + +
    +
    +
    + <%= f.label :name_cont, Spree.t(:name) %> + <%= f.text_field :name_cont, :size => 15 %> +
    +
    +
    + +
    + +
    + <%= button Spree.t(:search), 'search' %> +
    + <% end %> +
    +<% end %> + +
    + +<% if @properties.any? %> + + + + + + + + + + + + + + + <% @properties.each do |property| %> + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:presentation) %>
    <%= property.name %><%= property.presentation %> + <%= link_to_edit(property, :no_text => true) %> + <%= link_to_delete(property, :no_text => true) %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/property')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_property_path %>! +
    +<% end %> + +<%= paginate @collection %> diff --git a/backend/app/views/spree/admin/properties/new.html.erb b/backend/app/views/spree/admin/properties/new.html.erb new file mode 100644 index 00000000000..307bb00b171 --- /dev/null +++ b/backend/app/views/spree/admin/properties/new.html.erb @@ -0,0 +1,13 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @property } %> + +<%= form_for [:admin, @property] do |f| %> +
    + <%= Spree.t(:new_property) %> + <%= render :partial => 'form', :locals => { :f => f } %> +
    + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +
    +<% end %> diff --git a/core/app/views/spree/admin/properties/new.js.erb b/backend/app/views/spree/admin/properties/new.js.erb similarity index 100% rename from core/app/views/spree/admin/properties/new.js.erb rename to backend/app/views/spree/admin/properties/new.js.erb diff --git a/backend/app/views/spree/admin/prototypes/_form.html.erb b/backend/app/views/spree/admin/prototypes/_form.html.erb new file mode 100644 index 00000000000..cc490c514cf --- /dev/null +++ b/backend/app/views/spree/admin/prototypes/_form.html.erb @@ -0,0 +1,36 @@ +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= f.text_field :name, class: 'fullwidth' %> + <%= f.error_message_on :name %> + <% end %> +
    + +
    +
    + <%= f.field_container :property_ids do %> + <%= f.label :property_ids, Spree.t(:properties) %>
    + <%= f.select :property_ids, Spree::Property.all.map { |p| ["#{p.presentation} (#{p.name})", p.id] }, {}, { multiple: true, class: "select2 fullwidth" } %> + <% end %> +
    +
    + +
    +
    + <%= f.field_container :option_type_ids do %> + <%= f.label :option_type_ids, Spree.t(:option_types) %>
    + <%= f.select :option_type_ids, Spree::OptionType.all.map { |ot| ["#{ot.presentation} (#{ot.name})", ot.id] }, {}, { multiple: true, class: "select2 fullwidth" } %> + <% end %> +
    +
    + +
    +
    + <%= f.field_container :taxon_ids do %> + <%= f.label :taxon_ids, Spree.t(:taxons) %>
    + <%= f.select :taxon_ids, Spree::Taxon.all.map { |t| [t.name, t.id] }, {}, { multiple: true, class: "select2 fullwidth" } %> + <% end %> +
    +
    +
    diff --git a/backend/app/views/spree/admin/prototypes/_prototypes.html.erb b/backend/app/views/spree/admin/prototypes/_prototypes.html.erb new file mode 100644 index 00000000000..218f044c8c2 --- /dev/null +++ b/backend/app/views/spree/admin/prototypes/_prototypes.html.erb @@ -0,0 +1,25 @@ + + + + + + + + + + + + + <% @prototypes.each do |prototype| %> + + + + + <% end %> + <% if @prototypes.empty? %> + + <% end %> + +
    <%= Spree.t(:name) %>
    <%= prototype.name %> + <%= link_to Spree.t(:select), select_admin_prototype_url(prototype), :class => 'ajax button select_properties_from_prototype fa fa-ok' %> +
    <% Spree.t(:none) %>.
    diff --git a/core/app/views/spree/admin/prototypes/available.js.erb b/backend/app/views/spree/admin/prototypes/available.js.erb similarity index 100% rename from core/app/views/spree/admin/prototypes/available.js.erb rename to backend/app/views/spree/admin/prototypes/available.js.erb diff --git a/backend/app/views/spree/admin/prototypes/edit.html.erb b/backend/app/views/spree/admin/prototypes/edit.html.erb new file mode 100644 index 00000000000..7816fa11646 --- /dev/null +++ b/backend/app/views/spree/admin/prototypes/edit.html.erb @@ -0,0 +1,20 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_prototype) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_prototypes_list), spree.admin_prototypes_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @prototype } %> + +<%= form_for [:admin, @prototype] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/prototypes/index.html.erb b/backend/app/views/spree/admin/prototypes/index.html.erb new file mode 100644 index 00000000000..8503d3775cc --- /dev/null +++ b/backend/app/views/spree/admin/prototypes/index.html.erb @@ -0,0 +1,47 @@ +<% content_for :page_title do %> + <%= Spree.t(:prototypes) %> +<% end %> + +<% content_for :page_actions do %> + +<% end %> + +<%= render 'spree/admin/shared/product_sub_menu' %> + +<%= image_tag 'select2-spinner.gif', :plugin => 'spree', :style => 'display: none', :id => 'busy_indicator' %> + +<%# Placeholder for new prototype form %> +
    + +<% if @prototypes.any? %> + + + + + + + + + + + + + <% @prototypes.each do |prototype| %> + + + + + <% end %> + +
    <%= Spree.t(:name) %>
    <%= prototype.name %> + <%= link_to_edit(prototype, :no_text => true, :class => 'admin_edit_prototype') %> + <%= link_to_delete(prototype, :no_text => true) %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/prototype')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_prototype_path %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/prototypes/new.html.erb b/backend/app/views/spree/admin/prototypes/new.html.erb new file mode 100644 index 00000000000..c59da8af05d --- /dev/null +++ b/backend/app/views/spree/admin/prototypes/new.html.erb @@ -0,0 +1,11 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @prototype } %> + +<%= form_for [:admin, @prototype] do |f| %> +
    + <%= Spree.t(:new_prototype) %> + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/core/app/views/spree/admin/prototypes/new.js.erb b/backend/app/views/spree/admin/prototypes/new.js.erb similarity index 100% rename from core/app/views/spree/admin/prototypes/new.js.erb rename to backend/app/views/spree/admin/prototypes/new.js.erb diff --git a/backend/app/views/spree/admin/prototypes/select.js.erb b/backend/app/views/spree/admin/prototypes/select.js.erb new file mode 100644 index 00000000000..f31df83002d --- /dev/null +++ b/backend/app/views/spree/admin/prototypes/select.js.erb @@ -0,0 +1,4 @@ +<% @prototype_properties.sort_by{ |prop| -prop[:id] }.each do |prop| %> + $("a.spree_add_fields").click(); + $(".product_property.fields:first input[type=text]:first").val("<%= prop.name %>"); +<% end %> diff --git a/backend/app/views/spree/admin/prototypes/show.html.erb b/backend/app/views/spree/admin/prototypes/show.html.erb new file mode 100644 index 00000000000..2236be2813b --- /dev/null +++ b/backend/app/views/spree/admin/prototypes/show.html.erb @@ -0,0 +1,42 @@ +<% if @prototype.option_types.present? %> +

    <%= Spree.t(:variants) %>

    + + + + +<% end %> diff --git a/backend/app/views/spree/admin/refund_reasons/edit.html.erb b/backend/app/views/spree/admin/refund_reasons/edit.html.erb new file mode 100644 index 00000000000..9a29b6b2d48 --- /dev/null +++ b/backend/app/views/spree/admin/refund_reasons/edit.html.erb @@ -0,0 +1,4 @@ +<%= render partial: 'spree/admin/shared/named_types/edit', locals: { + page_title: Spree.t(:editing_refund_reason), + back_button_text: Spree.t(:back_to_refund_reason_list) +} %> diff --git a/backend/app/views/spree/admin/refund_reasons/index.html.erb b/backend/app/views/spree/admin/refund_reasons/index.html.erb new file mode 100644 index 00000000000..c8bddbba48b --- /dev/null +++ b/backend/app/views/spree/admin/refund_reasons/index.html.erb @@ -0,0 +1,5 @@ +<%= render partial: 'spree/admin/shared/named_types/index', locals: { + page_title: Spree.t(:refund_reasons), + new_button_text: Spree.t(:new_refund_reason), + resource_name: I18n.t(:other, scope: 'activerecord.models.spree/refund_reason') +} %> diff --git a/backend/app/views/spree/admin/refund_reasons/new.html.erb b/backend/app/views/spree/admin/refund_reasons/new.html.erb new file mode 100644 index 00000000000..65d3371fdd5 --- /dev/null +++ b/backend/app/views/spree/admin/refund_reasons/new.html.erb @@ -0,0 +1,4 @@ +<%= render partial: 'spree/admin/shared/named_types/new', locals: { + page_title: Spree.t(:new_refund_reason), + back_button_text: Spree.t(:back_to_refund_reason_list) +} %> diff --git a/backend/app/views/spree/admin/refunds/edit.html.erb b/backend/app/views/spree/admin/refunds/edit.html.erb new file mode 100644 index 00000000000..944a2f703d9 --- /dev/null +++ b/backend/app/views/spree/admin/refunds/edit.html.erb @@ -0,0 +1,31 @@ +<%= render partial: 'spree/admin/shared/order_tabs', locals: {current: 'Payments'} %> + +<% content_for :page_title do %> + <%= link_to "#{Spree.t(:payment)} #{@refund.payment.id}", admin_order_payment_path(@refund.payment.order, @refund.payment) %> + <%= Spree.t(:editing_refund) %> <%= @refund.id %> +<% end %> + +<%= form_for [:admin, @refund.payment.order, @refund.payment, @refund] do |f| %> +
    +
    +
    +
    + <%= Spree.t(:amount) %>
    + <%= @refund.amount %> +
    +
    +
    +
    + <%= f.label :refund_reason_id, Spree.t(:reason) %>
    + <%= f.collection_select(:refund_reason_id, refund_reasons, :id, :name, {}, {class: 'select2 fullwidth'}) %> +
    +
    +
    + +
    + <%= button Spree.t('actions.save'), 'ok' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_order_payments_url(@refund.payment.order), icon: 'remove' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/refunds/new.html.erb b/backend/app/views/spree/admin/refunds/new.html.erb new file mode 100644 index 00000000000..8d0e59801f0 --- /dev/null +++ b/backend/app/views/spree/admin/refunds/new.html.erb @@ -0,0 +1,43 @@ +<%= render partial: 'spree/admin/shared/order_tabs', locals: {current: 'Payments'} %> + +<% content_for :page_title do %> + <%= link_to "#{Spree.t(:payment)} #{@refund.payment.id}", admin_order_payment_path(@refund.payment.order, @refund.payment) %> + <%= Spree.t(:new_refund) %> +<% end %> + +<%= form_for [:admin, @refund.payment.order, @refund.payment, @refund] do |f| %> +
    +
    +
    +
    + <%= Spree.t(:payment_amount) %>
    + <%= @refund.payment.amount %> +
    +
    +
    +
    + <%= Spree.t(:credit_allowed) %>
    + <%= @refund.payment.credit_allowed %> +
    +
    +
    +
    + <%= f.label :amount, Spree.t(:amount) %>
    + <%= f.text_field :amount, class: 'fullwidth' %> +
    +
    +
    +
    + <%= f.label :refund_reason_id, Spree.t(:reason) %>
    + <%= f.collection_select(:refund_reason_id, refund_reasons, :id, :name, {include_blank: true}, {class: 'select2 fullwidth'}) %> +
    +
    +
    + +
    + <%= button Spree.t(:refund), 'ok' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_order_payments_url(@refund.payment.order), icon: 'remove' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/reimbursement_types/index.html.erb b/backend/app/views/spree/admin/reimbursement_types/index.html.erb new file mode 100644 index 00000000000..c4cd5d108ce --- /dev/null +++ b/backend/app/views/spree/admin/reimbursement_types/index.html.erb @@ -0,0 +1,32 @@ +<%= render partial: 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:reimbursement_types) %> +<% end %> + +<% if @reimbursement_types.any? %> + + + + + + + + + <% @reimbursement_types.each do |reimbursement_type| %> + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:type) %>
    + <%= reimbursement_type.name.humanize %> + + <%= reimbursement_type.type %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/reimbursement_type')) %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/reimbursements/edit.html.erb b/backend/app/views/spree/admin/reimbursements/edit.html.erb new file mode 100644 index 00000000000..a0486ec57fa --- /dev/null +++ b/backend/app/views/spree/admin/reimbursements/edit.html.erb @@ -0,0 +1,108 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Customer Returns' } %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_reimbursement) %> #<%= @reimbursement.number %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_customer_return), url_for([:edit, :admin, @order, @reimbursement.customer_return]), :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @reimbursement } %> + +<%= form_for [:admin, @order, @reimbursement] do |f| %> +
    + <%= Spree.t(:items_to_be_reimbursed) %> + + + + + + + + + + + + + <%= f.fields_for :return_items, @reimbursement.return_items.sort_by(&:id) do |item_fields| %> + <% return_item = item_fields.object %> + + + + + + + + + + + <% end %> + +
    <%= Spree.t(:product) %><%= Spree.t(:preferred_reimbursement_type) %><%= Spree.t(:reimbursement_type_override) %><%= Spree.t(:pre_tax_refund_amount) %><%= Spree.t(:total) %><%= Spree.t(:exchange_for) %>
    +
    <%= return_item.inventory_unit.variant.name %>
    +
    <%= return_item.inventory_unit.variant.options_text %>
    +
    + <%= reimbursement_type_name(return_item.preferred_reimbursement_type) %> + + <%= item_fields.select(:override_reimbursement_type_id, + reimbursement_types.collect { |r| [r.name.humanize, r.id] }, + {include_blank: true}, + {class: 'select2 fullwidth'} + ) %> + + <%= item_fields.text_field :pre_tax_amount, { class: 'refund-amount-input' } %> + + <%= return_item.display_total %> + + <% if return_item.exchange_processed? %> + <%= return_item.exchange_variant.exchange_name %> + <% else %> + <%= item_fields.collection_select :exchange_variant_id, return_item.eligible_exchange_variants, :id, :exchange_name, { include_blank: true }, { class: "select2 fullwidth return-item-exchange-selection" } %> + <% end %> +
    +
    + +
    + <%= f.button do %> + <%= Spree.t(:update) %> + <% end %> +
    +
    +<% end %> + +
    + <%= Spree.t(:calculated_reimbursements) %> + + + + + + + + + + <% @reimbursement_objects.each do |reimbursement_object| %> + + + + + + <% end %> + +
    <%= Spree.t(:reimbursement_type) %><%= Spree.t(:description) %><%= Spree.t(:amount) %>
    <%= reimbursement_object.class.name.demodulize %><%= reimbursement_object.description %><%= reimbursement_object.display_amount %>
    + <% if @order.has_non_reimbursement_related_refunds? %> + + <%= "#{Spree.t('note')}: #{Spree.t('this_order_has_already_received_a_refund')}. #{Spree.t('make_sure_the_above_reimbursement_amount_is_correct')}." %> + + <% end %> +
    + <% if !@reimbursement.reimbursed? %> + <%= button_to [:perform, :admin, @order, @reimbursement], {class: 'button fa fa-reply', method: 'post'} do %> + <%= Spree.t(:reimburse) %> + <% end %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), url_for([:admin, @order, @reimbursement.customer_return]), :icon => 'remove' %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/reimbursements/index.html.erb b/backend/app/views/spree/admin/reimbursements/index.html.erb new file mode 100644 index 00000000000..1c54185297b --- /dev/null +++ b/backend/app/views/spree/admin/reimbursements/index.html.erb @@ -0,0 +1,34 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Reimbursements' } %> + +<% content_for :page_title do %> + <%= Spree.t(:edit_reimbursement) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_customer_return_list), spree.admin_order_customer_returns_url(@order), :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @customer_return } %> + + + + + + + + + + + + + <% @reimbursements.each do |reimbursement| %> + + + + + + + + <% end %> + +
    <%= Spree.t(:id) %><%= Spree.t(:total) %><%= Spree.t(:status) %><%= "#{Spree.t('date')}/#{Spree.t('time')}" %>
    <%= reimbursement.id %><%= reimbursement.total %><%= reimbursement.reimbursement_status %><%= pretty_time(reimbursement.created_at) %>
    diff --git a/backend/app/views/spree/admin/reimbursements/show.html.erb b/backend/app/views/spree/admin/reimbursements/show.html.erb new file mode 100644 index 00000000000..8c76ffe285e --- /dev/null +++ b/backend/app/views/spree/admin/reimbursements/show.html.erb @@ -0,0 +1,94 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Customer Returns' } %> + +<% content_for :page_title do %> + <%= Spree.t(:reimbursement) %> #<%= @reimbursement.number %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_customer_return), url_for([:edit, :admin, @order, @reimbursement.customer_return]), :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @reimbursement } %> + +
    + <%= Spree.t(:items_reimbursed) %> + + + + + + + + + + + + + <% @reimbursement.return_items.each do |return_item| %> + + + + + + + + + + + <% end %> + +
    <%= Spree.t(:product) %><%= Spree.t(:preferred_reimbursement_type) %><%= Spree.t(:reimbursement_type_override) %><%= Spree.t(:exchange_for) %><%= Spree.t(:pre_tax_amount) %><%= Spree.t(:total) %>
    +
    <%= return_item.inventory_unit.variant.name %>
    +
    <%= return_item.inventory_unit.variant.options_text %>
    +
    + <%= reimbursement_type_name(return_item.preferred_reimbursement_type) %> + + <%= reimbursement_type_name(return_item.override_reimbursement_type) %> + + <%= return_item.exchange_variant.try(:exchange_name) %> + + <%= return_item.display_pre_tax_amount %> + + <%= return_item.display_total %> +
    +
    + +
    + <%= Spree.t(:refunds) %> + + + + + + + + + <% @reimbursement.refunds.each do |refund| %> + + + + + <% end %> + +
    <%= Spree.t(:description) %><%= Spree.t(:amount) %>
    <%= refund.description %><%= refund.display_amount %>
    +
    + +
    + <%= Spree.t(:credits) %> + + + + + + + + + <% @reimbursement.credits.each do |credit| %> + + + + + <% end %> + +
    <%= Spree.t(:description) %><%= Spree.t(:amount) %>
    <%= credit.description %><%= credit.display_amount %>
    +
    diff --git a/backend/app/views/spree/admin/reports/index.html.erb b/backend/app/views/spree/admin/reports/index.html.erb new file mode 100644 index 00000000000..9c496100eb0 --- /dev/null +++ b/backend/app/views/spree/admin/reports/index.html.erb @@ -0,0 +1,20 @@ +<% content_for :page_title do %> + <%= Spree.t(:listing_reports) %> +<% end %> + + + + + + + + + + <% @reports.each do |key, value| %> + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:description) %>
    <%= link_to value[:name], send("#{key}_admin_reports_url".to_sym) %><%= value[:description] %>
    diff --git a/backend/app/views/spree/admin/reports/sales_total.html.erb b/backend/app/views/spree/admin/reports/sales_total.html.erb new file mode 100644 index 00000000000..a0e00b31a6b --- /dev/null +++ b/backend/app/views/spree/admin/reports/sales_total.html.erb @@ -0,0 +1,37 @@ +<% content_for :page_title do %> + <%= Spree.t(:sales_totals) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_reports_list), spree.admin_reports_url, :class => 'button' %>
  • +<% end %> + + +<% content_for :table_filter_title do %> + <%= Spree.t(:date_range) %> +<% end %> + +<% content_for :table_filter do %> + <%= render :partial => 'spree/admin/shared/report_order_criteria', :locals => {} %> +<% end %> + + + + + + + + + + + + <% @totals.each do |key, row| %> + + + + + + + <% end %> + +
    <%= Spree.t(:currency) %><%= Spree.t(:item_total) %><%= Spree.t(:adjustment_total) %><%= Spree.t(:sales_total) %>
    <%= key %><%= row[:item_total].format %><%= row[:adjustment_total].format %><%= row[:sales_total].format %>
    diff --git a/backend/app/views/spree/admin/return_authorization_reasons/edit.html.erb b/backend/app/views/spree/admin/return_authorization_reasons/edit.html.erb new file mode 100644 index 00000000000..e8423b4b36a --- /dev/null +++ b/backend/app/views/spree/admin/return_authorization_reasons/edit.html.erb @@ -0,0 +1,4 @@ +<%= render partial: 'spree/admin/shared/named_types/edit', locals: { + page_title: Spree.t(:editing_rma_reason), + back_button_text: Spree.t(:back_to_rma_reason_list) +} %> diff --git a/backend/app/views/spree/admin/return_authorization_reasons/index.html.erb b/backend/app/views/spree/admin/return_authorization_reasons/index.html.erb new file mode 100644 index 00000000000..d74847db865 --- /dev/null +++ b/backend/app/views/spree/admin/return_authorization_reasons/index.html.erb @@ -0,0 +1,5 @@ +<%= render partial: 'spree/admin/shared/named_types/index', locals: { + page_title: Spree.t(:return_authorization_reasons), + new_button_text: Spree.t(:new_rma_reason), + resource_name: I18n.t(:other, scope: 'activerecord.models.spree/return_authorization_reason') +} %> diff --git a/backend/app/views/spree/admin/return_authorization_reasons/new.html.erb b/backend/app/views/spree/admin/return_authorization_reasons/new.html.erb new file mode 100644 index 00000000000..ac86ad1b484 --- /dev/null +++ b/backend/app/views/spree/admin/return_authorization_reasons/new.html.erb @@ -0,0 +1,4 @@ +<%= render partial: 'spree/admin/shared/named_types/new', locals: { + page_title: Spree.t(:new_rma_reason), + back_button_text: Spree.t(:back_to_rma_reason_list) +} %> diff --git a/backend/app/views/spree/admin/return_authorizations/_form.html.erb b/backend/app/views/spree/admin/return_authorizations/_form.html.erb new file mode 100644 index 00000000000..fbc7f194c7b --- /dev/null +++ b/backend/app/views/spree/admin/return_authorizations/_form.html.erb @@ -0,0 +1,91 @@ +<% allow_return_item_changes = !@return_authorization.customer_returned_items? %> + +
    + + + + + + + + + + + + + + <%= f.fields_for :return_items, @form_return_items do |item_fields| %> + <% return_item = item_fields.object %> + <% inventory_unit = return_item.inventory_unit %> + <% editable = inventory_unit.shipped? && allow_return_item_changes && return_item.reimbursement.nil? %> + + + + + + + + + + <% end %> + +
    + <% if allow_return_item_changes %> + <%= check_box_tag 'select-all' %> + <% end %> + <%= Spree.t(:product) %><%= Spree.t(:state) %><%= Spree.t(:charged) %><%= Spree.t(:pre_tax_refund_amount) %><%= Spree.t(:reimbursement_type) %><%= Spree.t(:exchange_for) %>
    + <% if editable %> + <%= item_fields.hidden_field :inventory_unit_id %> + <%= item_fields.check_box :_destroy, {checked: return_item.persisted?, class: 'add-item', "data-price" => return_item.pre_tax_amount}, '0', '1' %> + <% end %> + +
    <%= inventory_unit.variant.name %>
    +
    <%= inventory_unit.variant.options_text %>
    +
    <%= inventory_unit.state.humanize %> + <%= return_item.display_pre_tax_amount %> + + <% if editable %> + <%= item_fields.text_field :pre_tax_amount, { class: 'refund-amount-input' } %> + <% else %> + <%= return_item.display_pre_tax_amount %> + <% end %> + + <% if editable %> + <%= item_fields.select :preferred_reimbursement_type_id, @reimbursement_types.collect{|r|[r.name.humanize, r.id]}, {include_blank: true}, {class: 'select2 fullwidth'} %> + <% else %> + <%= return_item.preferred_reimbursement_type.try(:name) %> + <% end %> + + <% if editable %> + <%= item_fields.collection_select :exchange_variant_id, return_item.eligible_exchange_variants, :id, :options_text, { include_blank: true }, { class: "select2 fullwidth return-item-exchange-selection" } %> + <% elsif return_item.exchange_processed? %> + <%= return_item.exchange_variant.options_text %> + <% end %> +
    + + <%= f.field_container :amount do %> + <%= Spree.t(:total_pre_tax_refund) %>: 0.00 + <% end %> + + <%= f.field_container :stock_location do %> + <%= f.label :stock_location, Spree.t(:stock_location) %> + <%= f.select :stock_location_id, Spree::StockLocation.order_default.active.to_a.collect{|l|[l.name, l.id]}, {include_blank: true}, {class: 'select2 fullwidth', "data-placeholder" => Spree.t(:select_a_stock_location)} %> + <%= f.error_message_on :stock_location_id %> + <% end %> + + <%= f.field_container :reason do %> + <%= f.label :reason, Spree.t(:reason) %> + <%= f.select :return_authorization_reason_id, @reasons.collect{|r|[r.name, r.id]}, {include_blank: true}, {class: 'select2 fullwidth', "data-placeholder" => Spree.t(:select_a_return_authorization_reason)} %> + <%= f.error_message_on :reason %> + <% end %> + + <%= f.field_container :memo do %> + <%= f.label :memo, Spree.t(:memo) %> + <%= f.text_area :memo, {:style => 'height:100px;', :class => 'fullwidth'} %> + <%= f.error_message_on :memo %> + <% end %> +
    + +<% if Spree::Config[:expedited_exchanges] %> +
    <%= Spree.t(:expedited_exchanges_warning, days_window: Spree::Config[:expedited_exchanges_days_window]) %>
    +<% end %> diff --git a/backend/app/views/spree/admin/return_authorizations/edit.html.erb b/backend/app/views/spree/admin/return_authorizations/edit.html.erb new file mode 100644 index 00000000000..e2d4c7493f2 --- /dev/null +++ b/backend/app/views/spree/admin/return_authorizations/edit.html.erb @@ -0,0 +1,30 @@ +<% content_for :page_actions do %> + <% if @return_authorization.can_cancel? %> +
  • + <%= button_link_to Spree.t('actions.cancel'), fire_admin_order_return_authorization_url(@order, @return_authorization, e: 'cancel'), method: :put, data: { confirm: Spree.t(:are_you_sure) }, icon: 'remove' %> +
  • + <% end %> +
  • + <%= button_link_to Spree.t(:back), spree.admin_order_return_authorizations_url(@order), icon: 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Return Authorizations' } %> + +<% content_for :page_title do %> + <%= Spree.t(:return_authorization) %> <%= @return_authorization.number %> (<%= Spree.t(@return_authorization.state.downcase) %>) +<% end %> + + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @return_authorization } %> +<%= form_for [:admin, @order, @return_authorization] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= button Spree.t('actions.update'), 'repeat' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_order_return_authorizations_url(@order), :icon => 'remove' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/return_authorizations/index.html.erb b/backend/app/views/spree/admin/return_authorizations/index.html.erb new file mode 100644 index 00000000000..e8ba3bd6ada --- /dev/null +++ b/backend/app/views/spree/admin/return_authorizations/index.html.erb @@ -0,0 +1,49 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Return Authorizations' } %> + +<% content_for :page_actions do %> + <% if @order.shipments.any? &:shipped? %> +
  • + <%= button_link_to Spree.t(:new_return_authorization), new_admin_order_return_authorization_url(@order), :icon => 'plus' %> +
  • + <% end %> +
  • <%= button_link_to Spree.t(:back_to_orders_list), spree.admin_orders_path, :icon => 'arrow-left' %>
  • +<% end %> + +<% content_for :page_title do %> + <%= Spree.t(:return_authorizations) %> +<% end %> + +<% if @order.shipments.any?(&:shipped?) || @order.return_authorizations.any? %> + + + + + + + + + + + + <% @return_authorizations.each do |return_authorization| %> + + + + + + + + + <% end %> + +
    <%= Spree.t(:rma_number) %><%= Spree.t(:status) %><%= Spree.t(:pre_tax_total) %><%= "#{Spree.t('date')}/#{Spree.t('time')}" %>
    <%= return_authorization.number %><%= Spree.t(return_authorization.state.downcase) %><%= return_authorization.display_pre_tax_total.to_html %><%= pretty_time(return_authorization.created_at) %> + <%= link_to_edit return_authorization, :no_text => true, :class => 'edit' %> + <% unless return_authorization.customer_returned_items? %> + <%= link_to_delete return_authorization, :no_text => true %> + <% end %> +
    +<% else %> +
    + <%= Spree.t(:cannot_create_returns) %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/return_authorizations/new.html.erb b/backend/app/views/spree/admin/return_authorizations/new.html.erb new file mode 100644 index 00000000000..74bd36a1d49 --- /dev/null +++ b/backend/app/views/spree/admin/return_authorizations/new.html.erb @@ -0,0 +1,22 @@ +<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Return Authorizations' } %> + +<% content_for :page_title do %> + <%= Spree.t(:new_return_authorization) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= button_link_to Spree.t(:back_to_return_authorizations_list), spree.admin_order_return_authorizations_url, :icon => 'arrow-left' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @return_authorization } %> +<%= form_for [:admin, @order, @return_authorization] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= button Spree.t(:create), 'ok' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_order_return_authorizations_url(@order), :icon => 'remove' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/search/users.rabl b/backend/app/views/spree/admin/search/users.rabl new file mode 100644 index 00000000000..a8bf45eae3a --- /dev/null +++ b/backend/app/views/spree/admin/search/users.rabl @@ -0,0 +1,30 @@ +collection(@users) +attributes :email, :id +address_fields = [:firstname, :lastname, + :address1, :address2, + :city, :zipcode, + :phone, :state_name, + :state_id, :country_id, + :company] + +child :ship_address => :ship_address do + attributes *address_fields + child :state do + attributes :name + end + + child :country do + attributes :name + end +end + +child :bill_address => :bill_address do + attributes *address_fields + child :state do + attributes :name + end + + child :country do + attributes :name + end +end diff --git a/core/app/views/spree/admin/shared/_address.html.erb b/backend/app/views/spree/admin/shared/_address.html.erb similarity index 100% rename from core/app/views/spree/admin/shared/_address.html.erb rename to backend/app/views/spree/admin/shared/_address.html.erb diff --git a/backend/app/views/spree/admin/shared/_address_form.html.erb b/backend/app/views/spree/admin/shared/_address_form.html.erb new file mode 100644 index 00000000000..6f645c442c5 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_address_form.html.erb @@ -0,0 +1,70 @@ +<% s_or_b = type.chars.first %> + +
    +
    "> + <%= f.label :firstname, Spree.t(:first_name) %> + <%= f.text_field :firstname, :class => 'fullwidth' %> +
    + +
    "> + <%= f.label :lastname, Spree.t(:last_name) %> + <%= f.text_field :lastname, :class => 'fullwidth' %> +
    + + <% if Spree::Config[:company] %> +
    "> + <%= f.label :company, Spree.t(:company) %> + <%= f.text_field :company, :class => 'fullwidth' %> +
    + <% end %> + +
    "> + <%= f.label :address1, Spree.t(:street_address) %> + <%= f.text_field :address1, :class => 'fullwidth' %> +
    + +
    "> + <%= f.label :address2, Spree.t(:street_address_2) %> + <%= f.text_field :address2, :class => 'fullwidth' %> +
    + +
    "> + <%= f.label :city, Spree.t(:city) %> + <%= f.text_field :city, :class => 'fullwidth' %> +
    + +
    "> + <%= f.label :zipcode, Spree.t(:zip) %> + <%= f.text_field :zipcode, :class => 'fullwidth' %> +
    + +
    "> + <%= f.label :country_id, Spree.t(:country) %> + + <%= f.collection_select :country_id, available_countries, :id, :name, {}, {:class => 'select2 fullwidth'} %> + +
    + +
    "> + <%= f.label :state_id, Spree.t(:state) %> + + <%= f.text_field :state_name, + :style => "display: #{f.object.country.states.empty? ? 'block' : 'none' };", + :disabled => !f.object.country.states.empty?, :class => 'fullwidth state_name' %> + <%= f.collection_select :state_id, f.object.country.states.sort, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth', :style => "display: #{f.object.country.states.empty? ? 'none' : 'block' };", :disabled => f.object.country.states.empty?} %> + +
    + +
    "> + <%= f.label :phone, Spree.t(:phone) %> + <%= f.phone_field :phone, :class => 'fullwidth' %> +
    +
    + +<% content_for :head do %> + <%= javascript_tag do -%> + $(document).ready(function(){ + $('span#<%= s_or_b %>country .select2').on('change', function() { update_state('<%= s_or_b %>'); }); + }); + <% end -%> +<% end %> diff --git a/core/app/views/spree/admin/shared/_calculator_fields.html.erb b/backend/app/views/spree/admin/shared/_calculator_fields.html.erb similarity index 75% rename from core/app/views/spree/admin/shared/_calculator_fields.html.erb rename to backend/app/views/spree/admin/shared/_calculator_fields.html.erb index b3a6c267764..13233bae786 100644 --- a/core/app/views/spree/admin/shared/_calculator_fields.html.erb +++ b/backend/app/views/spree/admin/shared/_calculator_fields.html.erb @@ -1,11 +1,11 @@
    - <%= t(:calculator) %> + <%= Spree.t(:calculator) %>
    - <%= f.label(:calculator_type, t(:calculator), :for => 'calc_type') %> + <%= f.label(:calculator_type, Spree.t(:calculator), :for => 'calc_type') %> <%= f.select(:calculator_type, @calculators.map { |c| [c.description, c.name] }, {}, {:id => 'calc_type', :class => 'select2 fullwidth'}) %> -
    +
    <% if !@object.new_record? %>
    @@ -14,9 +14,9 @@ <% end %>
    <% if @object.calculator.respond_to?(:preferences) %> - <%= t(:calculator_settings_warning) %> + <%= Spree.t(:calculator_settings_warning) %> <% end %>
    - <% end %> + <% end %> -
    \ No newline at end of file + diff --git a/backend/app/views/spree/admin/shared/_configuration_menu.html.erb b/backend/app/views/spree/admin/shared/_configuration_menu.html.erb new file mode 100644 index 00000000000..b5d7972e794 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_configuration_menu.html.erb @@ -0,0 +1,28 @@ +<% content_for :sidebar_title do %> + <%= Spree.t(:configurations) %> +<% end %> + +<% content_for :sidebar do %> + +<% end %> diff --git a/backend/app/views/spree/admin/shared/_content_header.html.erb b/backend/app/views/spree/admin/shared/_content_header.html.erb new file mode 100644 index 00000000000..0e17e1e7e5e --- /dev/null +++ b/backend/app/views/spree/admin/shared/_content_header.html.erb @@ -0,0 +1,23 @@ +<% if content_for?(:page_title) || content_for?(:page_actions) %> +
    +
    +
    +
    + <% if content_for?(:page_title) %> +
    +

    <%= yield :page_title %>

    +
    + <% end %> + + <% if content_for?(:page_actions) %> +
    +
      + <%= yield :page_actions %> +
    +
    + <% end %> +
    +
    +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/shared/_destroy.js.erb b/backend/app/views/spree/admin/shared/_destroy.js.erb new file mode 100644 index 00000000000..f69af291600 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_destroy.js.erb @@ -0,0 +1,6 @@ +<% success = flash.discard(:success) +if success %> + show_flash('success', "<%= j success %>") +<% end %> + +<%= render :partial => '/spree/admin/shared/update_order_state' if @order %> diff --git a/backend/app/views/spree/admin/shared/_edit_resource_links.html.erb b/backend/app/views/spree/admin/shared/_edit_resource_links.html.erb new file mode 100644 index 00000000000..a04770bd7f7 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_edit_resource_links.html.erb @@ -0,0 +1,5 @@ +
    + <%= button Spree.t('actions.update'), 'refresh' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), collection_url, :icon => 'remove' %> +
    diff --git a/backend/app/views/spree/admin/shared/_head.html.erb b/backend/app/views/spree/admin/shared/_head.html.erb new file mode 100644 index 00000000000..b80103655e7 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_head.html.erb @@ -0,0 +1,29 @@ + + +<%= csrf_meta_tags %> + + + <% if content_for? :title %> + <%= yield :title %> + <% else %> + <%= "Spree #{Spree.t('administration')}: " %> + <%= Spree.t(controller.controller_name, :default => controller.controller_name.titleize) %> + <% end %> + + + + + +<%= stylesheet_link_tag 'spree/backend/all' %> + +<%= javascript_include_tag 'spree/backend/all' %> + +<%= render "spree/admin/shared/translations" %> + +<%= javascript_tag do -%> + jQuery.alerts.dialogClass = 'spree'; + <%== "var AUTH_TOKEN = #{form_authenticity_token.inspect};" %> + <%== "Spree.api_key = '#{try_spree_current_user.spree_api_key}';" if try_spree_current_user %> +<% end %> + +<%= yield :head %> diff --git a/backend/app/views/spree/admin/shared/_header.html.erb b/backend/app/views/spree/admin/shared/_header.html.erb new file mode 100644 index 00000000000..b77c4bf166f --- /dev/null +++ b/backend/app/views/spree/admin/shared/_header.html.erb @@ -0,0 +1,9 @@ + diff --git a/backend/app/views/spree/admin/shared/_menu.html.erb b/backend/app/views/spree/admin/shared/_menu.html.erb new file mode 100644 index 00000000000..b98a3455c18 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_menu.html.erb @@ -0,0 +1,9 @@ + diff --git a/backend/app/views/spree/admin/shared/_new_resource_links.html.erb b/backend/app/views/spree/admin/shared/_new_resource_links.html.erb new file mode 100644 index 00000000000..b322a8f88be --- /dev/null +++ b/backend/app/views/spree/admin/shared/_new_resource_links.html.erb @@ -0,0 +1,5 @@ +
    + <%= button Spree.t('actions.create'), 'ok' %> + <%= Spree.t(:or) %> + <%= link_to_with_icon 'remove', Spree.t('actions.cancel'), collection_url, :class => 'button' %> +
    diff --git a/backend/app/views/spree/admin/shared/_order_submenu.html.erb b/backend/app/views/spree/admin/shared/_order_submenu.html.erb new file mode 100644 index 00000000000..072739d6d9f --- /dev/null +++ b/backend/app/views/spree/admin/shared/_order_submenu.html.erb @@ -0,0 +1,49 @@ + diff --git a/backend/app/views/spree/admin/shared/_order_summary.html.erb b/backend/app/views/spree/admin/shared/_order_summary.html.erb new file mode 100644 index 00000000000..5793fece512 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_order_summary.html.erb @@ -0,0 +1,48 @@ +
    +
    +
    <%= Spree.t(:status) %>:
    +
    <%= Spree.t(@order.state, :scope => :order_state) %>
    +
    <%= Spree.t(:subtotal) %>:
    +
    <%= @order.display_item_total.to_html %>
    + <% if checkout_steps.include?("delivery") && @order.ship_total > 0 %> +
    <%= Spree.t(:ship_total) %>:
    +
    <%= @order.display_ship_total.to_html %>
    + <% end %> + + <% if @order.included_tax_total != 0 %> +
    <%= Spree.t(:tax_included) %>:
    +
    <%= @order.display_included_tax_total.to_html %>
    + <% end %> + + <% if @order.additional_tax_total != 0 %> +
    <%= Spree.t(:tax) %>:
    +
    <%= @order.display_additional_tax_total.to_html %>
    + <% end %> + +
    <%= Spree.t(:total) %>:
    +
    <%= @order.display_total.to_html %>
    + + <% if @order.completed? %> +
    <%= Spree.t(:shipment) %>:
    +
    <%= Spree.t(@order.shipment_state, :scope => :shipment_states, :default => [:missing, "none"]) %>
    +
    <%= Spree.t(:payment) %>:
    +
    <%= Spree.t(@order.payment_state, :scope => :payment_states, :default => [:missing, "none"]) %>
    +
    <%= Spree.t(:date_completed) %>:
    +
    <%= pretty_time(@order.completed_at) %>
    + <% end %> + + <% if @order.approved? %> +
    <%= Spree.t(:approver) %>
    +
    <%= @order.approver.email %>
    +
    <%= Spree.t(:approved_at) %>
    +
    <%= pretty_time(@order.approved_at) %>
    + <% end %> + + <% if @order.canceled? && @order.canceler && @order.canceled_at %> +
    <%= Spree.t(:canceler) %>
    +
    <%= @order.canceler.email %> +
    <%= Spree.t(:canceled_at) %>
    +
    <%= pretty_time(@order.canceled_at) %>
    + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/shared/_order_tabs.html.erb b/backend/app/views/spree/admin/shared/_order_tabs.html.erb new file mode 100644 index 00000000000..5d84f9f8740 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_order_tabs.html.erb @@ -0,0 +1,12 @@ +<% content_for :page_title do %> + <%= link_to "#{Spree.t(:order)} ##{@order.number}", spree.edit_admin_order_path(@order) %> +<% end %> + +<% content_for :sidebar_title do %> + <%= Spree.t(:order_information) %> +<% end %> + +<% content_for :sidebar do %> + <%= render "spree/admin/shared/order_summary", checkout_steps: @order.checkout_steps %> + <%= render "spree/admin/shared/order_submenu", current: current, checkout_steps: @order.checkout_steps %> +<% end %> diff --git a/core/app/views/spree/admin/shared/_product_sub_menu.html.erb b/backend/app/views/spree/admin/shared/_product_sub_menu.html.erb similarity index 85% rename from core/app/views/spree/admin/shared/_product_sub_menu.html.erb rename to backend/app/views/spree/admin/shared/_product_sub_menu.html.erb index 7a9954baa8e..1188bd8d7b1 100644 --- a/core/app/views/spree/admin/shared/_product_sub_menu.html.erb +++ b/backend/app/views/spree/admin/shared/_product_sub_menu.html.erb @@ -4,5 +4,7 @@ <%= tab :option_types, :match_path => '/option_types' %> <%= tab :properties %> <%= tab :prototypes %> + <%= tab :taxonomies %> + <%= tab :taxons %> <% end %> \ No newline at end of file diff --git a/backend/app/views/spree/admin/shared/_product_tabs.html.erb b/backend/app/views/spree/admin/shared/_product_tabs.html.erb new file mode 100644 index 00000000000..e3538c98954 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_product_tabs.html.erb @@ -0,0 +1,30 @@ +<% content_for :page_title do %> + <%= Spree.t(:editing_product) %> “<%= @product.name %>” +<% end %> + +<% content_for :sidebar_title do %> + <%= @product.sku %> +<% end %> + +<% content_for :sidebar do %> + + +<% end %> diff --git a/backend/app/views/spree/admin/shared/_promotion_sub_menu.html.erb b/backend/app/views/spree/admin/shared/_promotion_sub_menu.html.erb new file mode 100644 index 00000000000..91dc326c076 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_promotion_sub_menu.html.erb @@ -0,0 +1,6 @@ +<% content_for :sub_menu do %> + +<% end %> diff --git a/backend/app/views/spree/admin/shared/_refunds.html.erb b/backend/app/views/spree/admin/shared/_refunds.html.erb new file mode 100644 index 00000000000..d88c7d71c26 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_refunds.html.erb @@ -0,0 +1,32 @@ + + + + + + + + + + <% if show_actions %> + + <% end %> + + + + <% refunds.each do |refund| %> + + + + + + + + <% if show_actions %> + + <% end %> + + <% end %> + +
    <%= "#{Spree.t('date')}/#{Spree.t('time')}" %><%= Spree.t(:payment_identifier) %><%= Spree.t(:amount) %><%= Spree.t(:payment_method) %><%= Spree.t(:transaction_id) %><%= Spree.t(:reason) %>
    <%= pretty_time(refund.created_at) %><%= refund.payment.identifier %><%= refund.display_amount %><%= payment_method_name(refund.payment) %><%= refund.transaction_id %><%= truncate(refund.reason.name, length: 100) %> + <%= link_to_with_icon 'edit', Spree.t(:edit), edit_admin_order_payment_refund_path(refund.payment.order, refund.payment, refund), no_text: true %> +
    diff --git a/backend/app/views/spree/admin/shared/_report_criteria.html.erb b/backend/app/views/spree/admin/shared/_report_criteria.html.erb new file mode 100644 index 00000000000..52facef01f1 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_report_criteria.html.erb @@ -0,0 +1,17 @@ +<%= search_form_for @search, :url => spree.sales_total_admin_reports_path do |s| %> +
    + <%= label_tag nil, Spree.t(:start), :class => 'inline' %> + <%= s.text_field :created_at_gt, :class => 'datepicker datepicker-from', :value => datepicker_field_value(params[:q][:created_at_gt]) %> + + + + + + <%= s.text_field :created_at_lt, :class => 'datepicker datepicker-to', :value => datepicker_field_value(params[:q][:created_at_lt]) %> + <%= label_tag nil, Spree.t(:end), :class => 'inline' %> +
    + +
    + <%= button Spree.t(:search), 'search' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/shared/_report_order_criteria.html.erb b/backend/app/views/spree/admin/shared/_report_order_criteria.html.erb new file mode 100644 index 00000000000..deb7fb043a5 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_report_order_criteria.html.erb @@ -0,0 +1,17 @@ +<%= search_form_for @search, :url => spree.sales_total_admin_reports_path do |s| %> +
    + <%= label_tag :q_completed_at_gt, Spree.t(:start), :class => 'inline' %> + <%= s.text_field :completed_at_gt, :class => 'datepicker datepicker-from', :value => datepicker_field_value(params[:q][:completed_at_gt]) %> + + + + + + <%= s.text_field :completed_at_lt, :class => 'datepicker datepicker-to', :value => datepicker_field_value(params[:q][:completed_at_lt]) %> + <%= label_tag :q_completed_at_lt, Spree.t(:end), :class => 'inline' %> +
    + +
    + <%= button Spree.t(:search), 'search' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/shared/_show_resource_links.html.erb b/backend/app/views/spree/admin/shared/_show_resource_links.html.erb new file mode 100644 index 00000000000..3db09c204ea --- /dev/null +++ b/backend/app/views/spree/admin/shared/_show_resource_links.html.erb @@ -0,0 +1,5 @@ +

    + <%= link_to Spree.t(:edit), edit_object_url %> | + <%= link_to Spree.t(:back), collection_url %> | + <%= link_to Spree.t(:delete), object_url, :method => :delete, :data => { :confirm => Spree.t(:are_you_sure_you_want_to_delete_this_record) } %> +

    diff --git a/backend/app/views/spree/admin/shared/_sidebar.html.erb b/backend/app/views/spree/admin/shared/_sidebar.html.erb new file mode 100644 index 00000000000..b8b3708178b --- /dev/null +++ b/backend/app/views/spree/admin/shared/_sidebar.html.erb @@ -0,0 +1,10 @@ +<% if content_for?(:sidebar) %> + +<% end %> diff --git a/backend/app/views/spree/admin/shared/_sub_menu.html.erb b/backend/app/views/spree/admin/shared/_sub_menu.html.erb new file mode 100644 index 00000000000..80bc60290bc --- /dev/null +++ b/backend/app/views/spree/admin/shared/_sub_menu.html.erb @@ -0,0 +1,9 @@ +<% if content_for?(:sub_menu) %> + +<% end %> diff --git a/backend/app/views/spree/admin/shared/_table_filter.html.erb b/backend/app/views/spree/admin/shared/_table_filter.html.erb new file mode 100644 index 00000000000..84442da6607 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_table_filter.html.erb @@ -0,0 +1,8 @@ +<% if content_for?(:table_filter) %> +
    +
    + <%= yield :table_filter_title %> + <%= yield :table_filter %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/shared/_tabs.html.erb b/backend/app/views/spree/admin/shared/_tabs.html.erb new file mode 100644 index 00000000000..62bb97e3a96 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_tabs.html.erb @@ -0,0 +1,21 @@ +<% if can? :admin, Spree::Order %> + <%= tab *Spree::BackendConfiguration::ORDER_TABS, icon: 'shopping-cart' %> +<% end %> + +<% if can? :admin, Spree::Product %> + <%= tab *Spree::BackendConfiguration::PRODUCT_TABS, icon: 'th-large' %> +<% end %> + +<% if can? :admin, Spree::Admin::ReportsController %> + <%= tab *Spree::BackendConfiguration::REPORT_TABS, icon: 'file' %> +<% end %> + +<%= tab *Spree::BackendConfiguration::CONFIGURATION_TABS, label: 'configuration', icon: 'wrench', url: spree.edit_admin_general_settings_path %> + +<% if can? :admin, Spree::Promotion %> + <%= tab *Spree::BackendConfiguration::PROMOTION_TABS, url: spree.admin_promotions_path, icon: 'bullhorn' %> +<% end %> + +<% if Spree.user_class && can?(:admin, Spree.user_class) %> + <%= tab *Spree::BackendConfiguration::USER_TABS, url: spree.admin_users_path, icon: 'user' %> +<% end %> diff --git a/backend/app/views/spree/admin/shared/_translations.html.erb b/backend/app/views/spree/admin/shared/_translations.html.erb new file mode 100644 index 00000000000..a364af25e9d --- /dev/null +++ b/backend/app/views/spree/admin/shared/_translations.html.erb @@ -0,0 +1,47 @@ + + +<% if I18n.locale != :en %> + <%= javascript_include_tag "select2_locale_#{I18n.locale}" %> +<% end %> diff --git a/backend/app/views/spree/admin/shared/_update_order_state.js b/backend/app/views/spree/admin/shared/_update_order_state.js new file mode 100644 index 00000000000..32b2d4d574f --- /dev/null +++ b/backend/app/views/spree/admin/shared/_update_order_state.js @@ -0,0 +1,7 @@ +$('#order_tab_summary h5#order_status').html('<%= j Spree.t(:status) %>: <%= j Spree.t(@order.state, :scope => :order_state) %>'); +$('#order_tab_summary h5#order_total').html('<%= j Spree.t(:total) %>: <%= j @order.display_total.to_html %>'); + +<% if @order.completed? %> + $('#order_tab_summary h5#payment_status').html('<%= j Spree.t(:payment) %>: <%= j Spree.t(@order.payment_state, :scope => :payment_states, :default => [:missing, "none"]) %>'); + $('#order_tab_summary h5#shipment_status').html('<%= j Spree.t(:shipment) %>: <%= j Spree.t(@order.shipment_state, :scope => :shipment_state, :default => [:missing, "none"]) %>'); +<% end %> diff --git a/backend/app/views/spree/admin/shared/named_types/_edit.html.erb b/backend/app/views/spree/admin/shared/named_types/_edit.html.erb new file mode 100644 index 00000000000..689b18b5d8b --- /dev/null +++ b/backend/app/views/spree/admin/shared/named_types/_edit.html.erb @@ -0,0 +1,17 @@ +<% content_for :page_title do %> + <%= page_title %> + <%= @object.name %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', back_button_text, collection_url, :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @object } %> + +<%= form_for [:admin, @object] do |f| %> +
    + <%= render :partial => 'spree/admin/shared/named_types/form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/shared/named_types/_form.html.erb b/backend/app/views/spree/admin/shared/named_types/_form.html.erb new file mode 100644 index 00000000000..46b14c176c5 --- /dev/null +++ b/backend/app/views/spree/admin/shared/named_types/_form.html.erb @@ -0,0 +1,16 @@ +
    +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= f.text_field :name, :class => 'fullwidth' %> + <% end %> +
    + +
    +
    +
    +
    diff --git a/backend/app/views/spree/admin/shared/named_types/_index.html.erb b/backend/app/views/spree/admin/shared/named_types/_index.html.erb new file mode 100644 index 00000000000..f3cdc4c1cff --- /dev/null +++ b/backend/app/views/spree/admin/shared/named_types/_index.html.erb @@ -0,0 +1,52 @@ +<%= render partial: 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= page_title %> +<% end %> + +<% content_for :page_actions do %> + +<% end %> + +<% if @collection.any? %> + + + + + + + + + + + + + + + <% @collection.each do |named_type| %> + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:state) %>
    + <%= named_type.name %> + + <%= Spree.t(named_type.active? ? :active : :inactive) %> + + <% if named_type.mutable? %> + <%= link_to_edit named_type, no_text: true %> + <% end %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: resource_name) %>, + <%= link_to Spree.t(:add_one), new_object_url %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/shared/named_types/_new.html.erb b/backend/app/views/spree/admin/shared/named_types/_new.html.erb new file mode 100644 index 00000000000..920e0ce7298 --- /dev/null +++ b/backend/app/views/spree/admin/shared/named_types/_new.html.erb @@ -0,0 +1,16 @@ +<% content_for :page_title do %> + <%= page_title %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', back_button_text, collection_url, :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @object } %> + +<%= form_for [:admin, @object] do |f| %> +
    + <%= render :partial => 'spree/admin/shared/named_types/form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/shipping_categories/_form.html.erb b/backend/app/views/spree/admin/shipping_categories/_form.html.erb new file mode 100644 index 00000000000..8652b135f26 --- /dev/null +++ b/backend/app/views/spree/admin/shipping_categories/_form.html.erb @@ -0,0 +1,6 @@ +
    +
    + <%= label_tag :shipping_category_name, Spree.t(:name) %>
    + <%= f.text_field :name %> +
    +
    diff --git a/backend/app/views/spree/admin/shipping_categories/edit.html.erb b/backend/app/views/spree/admin/shipping_categories/edit.html.erb new file mode 100644 index 00000000000..57a92f5d9fc --- /dev/null +++ b/backend/app/views/spree/admin/shipping_categories/edit.html.erb @@ -0,0 +1,20 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_shipping_category) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_shipping_categories), spree.admin_shipping_categories_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_category } %> + +<%= form_for [:admin, @shipping_category] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/shipping_categories/index.html.erb b/backend/app/views/spree/admin/shipping_categories/index.html.erb new file mode 100644 index 00000000000..642f81baacd --- /dev/null +++ b/backend/app/views/spree/admin/shipping_categories/index.html.erb @@ -0,0 +1,42 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:shipping_categories) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_shipping_category), new_object_url, :icon => 'plus' %> +
  • +<% end %> + +<% if @shipping_categories.any? %> + + + + + + + + + + + + + <% @shipping_categories.each do |shipping_category|%> + + + + + <% end %> + +
    <%= Spree.t(:name) %>
    <%= shipping_category.name %> + <%= link_to_edit shipping_category, :no_text => true %> + <%= link_to_delete shipping_category, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/shipping_category')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_shipping_category_path %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/shipping_categories/new.html.erb b/backend/app/views/spree/admin/shipping_categories/new.html.erb new file mode 100644 index 00000000000..0b2db7cfeec --- /dev/null +++ b/backend/app/views/spree/admin/shipping_categories/new.html.erb @@ -0,0 +1,20 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_shipping_category) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_shipping_categories_list), spree.admin_shipping_categories_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_category } %> + +<%= form_for [:admin, @shipping_category] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/shipping_methods/_form.html.erb b/backend/app/views/spree/admin/shipping_methods/_form.html.erb new file mode 100644 index 00000000000..5356fb61b80 --- /dev/null +++ b/backend/app/views/spree/admin/shipping_methods/_form.html.erb @@ -0,0 +1,94 @@ +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %>
    + <%= f.text_field :name, :class => 'fullwidth' %> + <%= error_message_on :shipping_method, :name %> + <% end %> +
    + +
    + <%= f.field_container :display_on do %> + <%= f.label :display_on, Spree.t(:display) %>
    + <%= select(:shipping_method, :display_on, Spree::ShippingMethod::DISPLAY.collect { |display| [Spree.t(display), display == :both ? nil : display.to_s] }, {}, {:class => 'select2 fullwidth'}) %> + <%= error_message_on :shipping_method, :display_on %> + <% end %> +
    + +
    +
    + <%= f.field_container :admin_name do %> + <%= f.label :admin_name, Spree.t(:internal_name) %>
    + <%= f.text_field :admin_name, :class => 'fullwidth', :label => false %> + <%= error_message_on :shipping_method, :admin_name %> + <% end %> +
    + +
    + <%= f.field_container :code do %> + <%= f.label :code, Spree.t(:code) %>
    + <%= f.text_field :code, :class => 'fullwidth', :label => false %> + <%= error_message_on :shipping_method, :code %> + <% end %> +
    +
    + +
    + <%= f.field_container :tracking_url do %> + <%= f.label :tracking_url, Spree.t(:tracking_url) %>
    + <%= f.text_field :tracking_url, :class => 'fullwidth', :placeholder => Spree.t(:tracking_url_placeholder) %> + <%= error_message_on :shipping_method, :tracking_url %> + <% end %> +
    +
    + +
    +
    +
    + <%= Spree.t(:shipping_categories) %> + <%= f.field_container :categories do %> + <% Spree::ShippingCategory.all.each do |category| %> + <%= label_tag do %> + <%= check_box_tag('shipping_method[shipping_categories][]', category.id, @shipping_method.shipping_categories.include?(category)) %> + <%= category.name %>
    + <% end %> + <% end %> + <%= error_message_on :shipping_method, :shipping_category_id %> + <% end %> +
    +
    + +
    +
    + <%= Spree.t(:zones) %> + <%= f.field_container :zones do %> + <% shipping_method_zones = @shipping_method.zones.to_a %> + <% Spree::Zone.all.each do |zone| %> + <%= label_tag do %> + <%= check_box_tag('shipping_method[zones][]', zone.id, shipping_method_zones.include?(zone)) %> + <%= zone.name %> + <% end %> +
    + <% end %> + <%= error_message_on :shipping_method, :zone_id %> + <% end %> +
    +
    +
    + +
    + <%= render :partial => 'spree/admin/shared/calculator_fields', :locals => { :f => f } %> +
    + +
    +
    +
    + <%= Spree.t(:tax_category) %> + <%= f.field_container :categories do %> + <%= f.select :tax_category_id, @tax_categories.map { |tc| [tc.name, tc.id] }, {include_blank: true}, class: "select2 fullwidth" %> + <%= error_message_on :shipping_method, :tax_category_id %> + <% end %> +
    +
    +
    + diff --git a/backend/app/views/spree/admin/shipping_methods/edit.html.erb b/backend/app/views/spree/admin/shipping_methods/edit.html.erb new file mode 100644 index 00000000000..9f2380e234c --- /dev/null +++ b/backend/app/views/spree/admin/shipping_methods/edit.html.erb @@ -0,0 +1,29 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_shipping_method) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_shipping_methods_list), spree.admin_shipping_methods_path, :icon => 'arrow-left' %> +
  • +<% end %> + +
    + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_method } %> +
    + +
    + <%= form_for [:admin, @shipping_method] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + +
    + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +
    + <% end %> +
    diff --git a/backend/app/views/spree/admin/shipping_methods/index.html.erb b/backend/app/views/spree/admin/shipping_methods/index.html.erb new file mode 100644 index 00000000000..be5df5379e4 --- /dev/null +++ b/backend/app/views/spree/admin/shipping_methods/index.html.erb @@ -0,0 +1,52 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:shipping_methods) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_shipping_method), new_object_url, :icon => 'plus', :id => 'admin_new_shipping_method_link' %> +
  • +<% end %> + +<% if @shipping_methods.any? %> + + + + + + + + + + + + + + + + + + + <% @shipping_methods.includes(:zones, :calculator).each do |shipping_method|%> + + + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:zone) %><%= Spree.t(:calculator) %><%= Spree.t(:display) %>
    <%= shipping_method.admin_name + ' / ' if shipping_method.admin_name.present? %><%= shipping_method.name %><%= shipping_method.zones.collect(&:name).join(", ") if shipping_method.zones %><%= shipping_method.calculator.description %><%= shipping_method.display_on.blank? ? Spree.t(:both) : Spree.t(shipping_method.display_on) %> + <%= link_to_edit shipping_method, :no_text => true %> + <%= link_to_delete shipping_method, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/shipping_method')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_shipping_method_path %>! +
    +<% end %> + diff --git a/backend/app/views/spree/admin/shipping_methods/new.html.erb b/backend/app/views/spree/admin/shipping_methods/new.html.erb new file mode 100644 index 00000000000..506cff473d4 --- /dev/null +++ b/backend/app/views/spree/admin/shipping_methods/new.html.erb @@ -0,0 +1,29 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_shipping_method) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_shipping_methods_list), spree.admin_shipping_methods_path, :icon => 'arrow-left' %> +
  • +<% end %> + +
    + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_method } %> +
    + +
    + <%= form_for [:admin, @shipping_method] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + +
    + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +
    + <% end %> +
    diff --git a/backend/app/views/spree/admin/states/_form.html.erb b/backend/app/views/spree/admin/states/_form.html.erb new file mode 100644 index 00000000000..c9dff9e35cd --- /dev/null +++ b/backend/app/views/spree/admin/states/_form.html.erb @@ -0,0 +1,14 @@ +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> + <%= f.text_field :name, :class => 'fullwidth' %> + <% end %> +
    +
    + <%= f.field_container :abbr do %> + <%= f.label :abbr, Spree.t(:abbreviation) %> + <%= f.text_field :abbr, :class => 'fullwidth' %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/states/_state_list.html.erb b/backend/app/views/spree/admin/states/_state_list.html.erb new file mode 100644 index 00000000000..95dc4b66d59 --- /dev/null +++ b/backend/app/views/spree/admin/states/_state_list.html.erb @@ -0,0 +1,31 @@ +
    + + + + + + + + + + + + + + + + <% @states.each do |state| %> + + + + + + <% end %> + <% if @states.empty? %> + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:abbreviation) %>
    <%= state.name %><%= state.abbr %> + <%= link_to_with_icon 'edit', Spree.t(:edit), edit_admin_country_state_url(@country, state), :no_text => true %> + <%= link_to_delete state, :no_text => true %> +
    <%= Spree.t(:none) %>
    diff --git a/backend/app/views/spree/admin/states/edit.html.erb b/backend/app/views/spree/admin/states/edit.html.erb new file mode 100644 index 00000000000..96c408254ca --- /dev/null +++ b/backend/app/views/spree/admin/states/edit.html.erb @@ -0,0 +1,20 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_state) %> <%= @state.name %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_states_list), spree.admin_country_states_url(@country), :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @state } %> + +<%= form_for [:admin, @country, @state] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/states/index.html.erb b/backend/app/views/spree/admin/states/index.html.erb new file mode 100644 index 00000000000..6b5bde9e4ed --- /dev/null +++ b/backend/app/views/spree/admin/states/index.html.erb @@ -0,0 +1,22 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:states) %> +<% end %> + +<% content_for :page_actions do %> + +<% end %> + +
    + <%= label_tag :country, Spree.t(:country) %> + +
    + +
    + <%= render :partial => 'state_list'%> +
    diff --git a/backend/app/views/spree/admin/states/new.html.erb b/backend/app/views/spree/admin/states/new.html.erb new file mode 100644 index 00000000000..1db852502c8 --- /dev/null +++ b/backend/app/views/spree/admin/states/new.html.erb @@ -0,0 +1,15 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @state } %> + +<% content_for :page_title do %> + <%= Spree.t(:new_state) %> +<% end %> + +<%= form_for [:admin, @country, @state] do |f| %> +
    + <%= Spree.t(:new_state) %> + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/core/app/views/spree/admin/states/new.js.erb b/backend/app/views/spree/admin/states/new.js.erb similarity index 100% rename from core/app/views/spree/admin/states/new.js.erb rename to backend/app/views/spree/admin/states/new.js.erb diff --git a/backend/app/views/spree/admin/stock_items/destroy.js.erb b/backend/app/views/spree/admin/stock_items/destroy.js.erb new file mode 100644 index 00000000000..e3c6c133fdf --- /dev/null +++ b/backend/app/views/spree/admin/stock_items/destroy.js.erb @@ -0,0 +1 @@ +$("#stock-item-<%= @stock_item.id %>").fadeOut(); diff --git a/backend/app/views/spree/admin/stock_locations/_form.html.erb b/backend/app/views/spree/admin/stock_locations/_form.html.erb new file mode 100644 index 00000000000..1c1369ce4e8 --- /dev/null +++ b/backend/app/views/spree/admin/stock_locations/_form.html.erb @@ -0,0 +1,89 @@ +
    +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= f.text_field :name, class: 'fullwidth', required: true %> + <% end %> +
    +
    + <%= f.field_container :admin_name do %> + <%= f.label :admin_name, Spree.t(:internal_name) %> + <%= f.text_field :admin_name, class: 'fullwidth', label: false %> + <% end %> +
    +
    + +
    + <%= f.field_container :active do %> + +
      +
    • + <%= f.check_box :active %> + <%= f.label :active, Spree.t(:active) %> +
    • +
    • + <%= f.check_box :default %> + <%= f.label :default, Spree.t(:default) %> +
    • +
    • + <%= f.check_box :backorderable_default %> + <%= f.label :backorderable_default, Spree.t(:backorderable_default) %> +
    • +
    • + <%= f.check_box :propagate_all_variants %> + <%= f.label :propagate_all_variants, Spree.t(:propagate_all_variants) %> +
    • +
    + <% end %> +
    + +
    + <%= f.label :address1, Spree.t(:street_address) %> + <%= f.text_field :address1, class: 'fullwidth' %> +
    + +
    + <%= f.label :city, Spree.t(:city) %> + <%= f.text_field :city, class: 'fullwidth' %> +
    + +
    + <%= f.label :address2, Spree.t(:street_address_2) %> + <%= f.text_field :address2, class: 'fullwidth' %> +
    + +
    + <%= f.label :zipcode, Spree.t(:zip) %> + <%= f.text_field :zipcode, class: 'fullwidth' %> +
    + +
    + <%= f.label :phone, Spree.t(:phone) %> + <%= f.phone_field :phone, class: 'fullwidth' %> +
    + +
    + <%= f.label :country_id, Spree.t(:country) %> + <%= f.collection_select :country_id, available_countries, :id, :name, {}, { class: 'select2 fullwidth' } %> +
    + +
    + <% if f.object.country %> + <%= f.label :state_id, Spree.t(:state) %> + + <%= f.text_field :state_name, style: "display: #{f.object.country.states.empty? ? 'block' : 'none' };", disabled: !f.object.country.states.empty?, class: 'fullwidth state_name' %> + <%= f.collection_select :state_id, f.object.country.states.sort, :id, :name, { include_blank: true }, {class: 'select2 fullwidth', style: "display: #{f.object.country.states.empty? ? 'none' : 'block' };", disabled: f.object.country.states.empty?} %> + + <% end %> +
    +
    + +<% content_for :head do %> + <%= javascript_include_tag 'spree/backend/address_states.js' %> + <%= javascript_tag do -%> + $(document).ready(function(){ + $('span#country .select2').on('change', function() { update_state(''); }); + }); + <% end -%> +<% end %> diff --git a/backend/app/views/spree/admin/stock_locations/_transfer_stock_form.html.erb b/backend/app/views/spree/admin/stock_locations/_transfer_stock_form.html.erb new file mode 100644 index 00000000000..f283c9287a0 --- /dev/null +++ b/backend/app/views/spree/admin/stock_locations/_transfer_stock_form.html.erb @@ -0,0 +1,39 @@ +<%= form_tag transfer_stock_admin_stock_locations_path do %> +
    + <%= Spree.t(:move_stock_between_locations)%> +
    +
    +
    + <%= label_tag :stock_location_from_id, Spree.t(:transfer_from_location) %> + <%= select_tag :stock_location_from_id, options_from_collection_for_select(@stock_locations, :id, :name), class: 'select2 fullwidth' %> +
    +
    +
    +
    + <%= label_tag :stock_location_to_id, Spree.t(:transfer_to_location) %> + <%= select_tag :stock_location_to_id, options_from_collection_for_select(@stock_locations, :id, :name), class: 'select2 fullwidth' %> +
    +
    + +
    +
    + <%= label_tag 'variant_id', Spree.t(:variant) %> + <%= select_tag :variant_id, options_from_collection_for_select(@variants, :id, :name_and_sku), class: 'select2 fullwidth' %> +
    +
    + +
    +
    + <%= label_tag 'quantity', Spree.t(:quantity) %> + <%= number_field_tag :quantity, 1, class: 'fullwidth' %> +
    +
    +
    + +
    + <%= button Spree.t(:transfer_stock), 'plus' %> + <%= Spree.t(:or) %> + <%= link_to_with_icon 'remove', Spree.t('actions.cancel'), collection_url, :class => 'button' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/stock_locations/edit.html.erb b/backend/app/views/spree/admin/stock_locations/edit.html.erb new file mode 100644 index 00000000000..fa7f85946d8 --- /dev/null +++ b/backend/app/views/spree/admin/stock_locations/edit.html.erb @@ -0,0 +1,19 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_stock_location) %> + <%= @stock_location.name %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_stock_locations_list), admin_stock_locations_path, :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_location } %> + +<%= form_for [:admin, @stock_location] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/stock_locations/index.html.erb b/backend/app/views/spree/admin/stock_locations/index.html.erb new file mode 100644 index 00000000000..b3d1c89f51f --- /dev/null +++ b/backend/app/views/spree/admin/stock_locations/index.html.erb @@ -0,0 +1,56 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:stock_locations) %> +<% end %> + +<% content_for :page_actions do %> + +<% end %> + +<% if @stock_locations.any? %> + + + + + + + + + + + + + + + + + <% @stock_locations.each do |stock_location| + @edit_url = edit_admin_stock_location_path(stock_location) + @delete_url = admin_stock_location_path(stock_location) + %> + + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:state) %><%= Spree.t(:stock_movements) %>
    <%= display_name(stock_location) %><%= Spree.t(state(stock_location)) %><%= link_to Spree.t(:stock_movements), admin_stock_location_stock_movements_path(stock_location.id) %> + <%= link_to_edit stock_location, :no_text => true %> + <%= link_to_delete stock_location, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/stock_location')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_stock_location_path %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/stock_locations/new.html.erb b/backend/app/views/spree/admin/stock_locations/new.html.erb new file mode 100644 index 00000000000..9afd6c02ac4 --- /dev/null +++ b/backend/app/views/spree/admin/stock_locations/new.html.erb @@ -0,0 +1,18 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_stock_location) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_stock_locations_list), admin_stock_locations_path, :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_locations } %> + +<%= form_for [:admin, @stock_location] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/stock_movements/_form.html.erb b/backend/app/views/spree/admin/stock_movements/_form.html.erb new file mode 100644 index 00000000000..ce006891d94 --- /dev/null +++ b/backend/app/views/spree/admin/stock_movements/_form.html.erb @@ -0,0 +1,14 @@ +
    +
    + <%= f.field_container :quantity do %> + <%= f.label :quantity, Spree.t(:quantity) %> + <%= f.text_field :quantity %> + <% end %> + <%= f.field_container :stock_item_id do %> + <%= f.label :stock_item_id, Spree.t(:stock_item_id) %> + <%= f.text_field 'stock_item_id', :class => 'fullwidth', :'data-stock-location-id' => params[:stock_location_id] %> + <% end %> +
    +
    + +<%= render :partial => "spree/admin/variants/autocomplete", :formats => :js %> \ No newline at end of file diff --git a/backend/app/views/spree/admin/stock_movements/edit.html.erb b/backend/app/views/spree/admin/stock_movements/edit.html.erb new file mode 100644 index 00000000000..fe5bdda2360 --- /dev/null +++ b/backend/app/views/spree/admin/stock_movements/edit.html.erb @@ -0,0 +1,16 @@ +<% content_for :page_title do %> + <%= Spree.t(:editing_stock_movement) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_stock_movements_list), admin_stock_location_stock_movements_path(stock_location), :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_movement } %> + +<%= form_for [:admin, stock_location, @stock_movement] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links', locals: { collection_url: admin_stock_location_stock_movements_path(stock_location) } %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/stock_movements/index.html.erb b/backend/app/views/spree/admin/stock_movements/index.html.erb new file mode 100644 index 00000000000..75a6a328a92 --- /dev/null +++ b/backend/app/views/spree/admin/stock_movements/index.html.erb @@ -0,0 +1,49 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:stock_movements_for_stock_location, stock_location_name: @stock_location.name) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_stock_movement), new_admin_stock_location_stock_movement_path(@stock_location), icon: 'plus', id: 'admin_new_stock_movement_link' %> +
  • +
  • + <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_stock_locations_list), admin_stock_locations_path, :class => 'button' %> +
  • +<% end %> + +<% if @stock_movements.any? %> + + + + + + + + + + + + + + <% @stock_movements.each do |stock_movement|%> + + + + + + <% end %> + +
    <%= Spree.t(:stock_item) %> + <%= Spree.t(:quantity) %><%= Spree.t(:action) %>
    + <%= display_variant(stock_movement) %> + <%= stock_movement.quantity %><%= pretty_originator(stock_movement) %>
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/stock_movement')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_stock_location_stock_movement_path(@stock_location) %>! +
    +<% end %> + +<%= paginate @stock_movements %> diff --git a/backend/app/views/spree/admin/stock_movements/new.html.erb b/backend/app/views/spree/admin/stock_movements/new.html.erb new file mode 100644 index 00000000000..96b1ece9c62 --- /dev/null +++ b/backend/app/views/spree/admin/stock_movements/new.html.erb @@ -0,0 +1,16 @@ +<% content_for :page_title do %> + <%= Spree.t(:new_stock_movement) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_stock_movements_list), admin_stock_location_stock_movements_path(stock_location), :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_movement } %> + +<%= form_for [:admin, stock_location, @stock_movement] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links', locals: { collection_url: admin_stock_location_stock_movements_path(stock_location) } %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/stock_transfers/_stock_movements.html.erb b/backend/app/views/spree/admin/stock_transfers/_stock_movements.html.erb new file mode 100644 index 00000000000..d66da7175c3 --- /dev/null +++ b/backend/app/views/spree/admin/stock_transfers/_stock_movements.html.erb @@ -0,0 +1,27 @@ +
    + + + + + + + + + + + + + + + + + <% stock_movements.each do |movement| %> + + + + + + <% end %> + +
    <%= Spree.t('variant') %><%= Spree.t('sku') %><%= Spree.t('quantity') %><%= Spree.t('count_on_hand') %>
    <%= movement.stock_item.variant.name %><%= movement.stock_item.variant.sku %><%= movement.quantity %><%= movement.stock_item.count_on_hand %>
    +
    diff --git a/backend/app/views/spree/admin/stock_transfers/index.html.erb b/backend/app/views/spree/admin/stock_transfers/index.html.erb new file mode 100644 index 00000000000..0789661142a --- /dev/null +++ b/backend/app/views/spree/admin/stock_transfers/index.html.erb @@ -0,0 +1,95 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:stock_transfers) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_stock_transfer), new_admin_stock_transfer_path, { :icon => 'forward' } %> +
  • +<% end %> + +
    +
    + <%= Spree.t(:search) %> + <%= search_form_for @q, :url => admin_stock_transfers_path do |f| %> + +
    +
    + <%= f.label :reference_cont, Spree.t(:reference_cont) %> + <%= f.text_field :reference_cont, class: 'fullwidth' %> +
    +
    + +
    +
    + <%= f.label :source_location, Spree.t(:source) %> + <%= f.select :source_location_id_eq, + options_from_collection_for_select(@stock_locations, :id, :name, @q.source_location_id_eq), + { include_blank: true }, class: 'select2 fullwidth' %> +
    +
    + +
    +
    + <%= f.label :destination_location, Spree.t(:destination) %> + <%= f.select :destination_location_id_eq, + options_from_collection_for_select(@stock_locations, :id, :name, @q.destination_location_id_eq), + { include_blank: true }, class: 'select2 fullwidth' %> +
    +
    + +
    + +
    +
    + <%= button Spree.t(:filter_results), 'search' %> +
    +
    + <% end %> +
    +
    + +<% if @stock_transfers.any? %> + + + + + + + + + + + + + + + + + + + <% @stock_transfers.each do |stock_transfer| %> + + + + + + + + <% end %> + +
    <%= Spree.t(:created_at) %><%= Spree.t(:reference) %><%= Spree.t(:source) %><%= Spree.t(:destination) %>
    <%= stock_transfer.created_at %><%= stock_transfer.reference %><%= stock_transfer.source_location.try(:name) %><%= stock_transfer.destination_location.try(:name) %> + <%= link_to '', admin_stock_transfer_path(stock_transfer), + title: 'view', class: 'view icon_link with-tip fa fa-eye-open no-text', + data: {action: 'view'} %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/stock_transfer')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_stock_transfer_path %>! +
    +<% end %> + +<%= paginate @stock_transfers %> diff --git a/backend/app/views/spree/admin/stock_transfers/new.html.erb b/backend/app/views/spree/admin/stock_transfers/new.html.erb new file mode 100644 index 00000000000..4ce750b7040 --- /dev/null +++ b/backend/app/views/spree/admin/stock_transfers/new.html.erb @@ -0,0 +1,110 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_stock_transfer) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_stock_transfers_list), admin_stock_transfers_path, :icon => 'arrow-left' %> +
  • +<% end %> + + + +<%= form_tag admin_stock_transfers_path, :method => :post do %> +
    + <%= Spree.t(:transfer_stock) %> + +
    +
    +
    + <%= label_tag 'reference', raw("#{Spree.t(:reference)} (#{Spree.t(:optional)})") %> + <%= text_field_tag :reference, '', class: 'fullwidth' %> +
    +
    +
    +
    + +
    +
    +
    +
    + <%= label_tag :transfer_source_location_id, Spree.t(:source) %> + <%= select_tag :transfer_source_location_id, {}, class: 'select2 fullwidth' %> +
    +
    +
    +
    + <%= label_tag :transfer_destination_location_id, Spree.t(:destination) %> + <%= select_tag :transfer_destination_location_id, {}, class: 'select2 fullwidth' %> +
    +
    +
    + +
    + <%= Spree.t(:add_variant) %> + +
    +
    + <%= label_tag 'variant_id', Spree.t(:variant) %> + <%= hidden_field_tag 'transfer_variant', {}, class: 'fullwidth' %> +
    +
    +
    +
    + <%= label_tag :transfer_variant_quantity, Spree.t(:quantity) %> + <%= number_field_tag :transfer_variant_quantity, 1, class: 'fullwidth', min: 0 %> +
    +
    +
    +
    + <%= button Spree.t(:add), 'plus button transfer_add_variant' %> +
    +
    + +
    + +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/variant')) %>. +
    + + + +
    + <%= button Spree.t(:transfer_stock), 'plus transfer_transfer' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/stock_transfers/show.html.erb b/backend/app/views/spree/admin/stock_transfers/show.html.erb new file mode 100644 index 00000000000..55145e2085a --- /dev/null +++ b/backend/app/views/spree/admin/stock_transfers/show.html.erb @@ -0,0 +1,51 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t('stock_transfer') %> (<%= @stock_transfer.number %>) +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_stock_transfers_list), admin_stock_transfers_path, :icon => 'arrow-left' %> +
  • +
  • + <%= button_link_to Spree.t(:new_stock_transfer), new_admin_stock_transfer_path, { :icon => 'forward' } %> +
  • +<% end %> + +
    + <%= Spree.t(:stock_transfer) %> + +
    +
    + +
    <%= @stock_transfer.reference %>
    +
    +
    + +
    +
    + +
    <%= @stock_transfer.created_at %>
    +
    +
    + + <% if @stock_transfer.source_movements.present? %> +
    + + <%= Spree.t(:source) %> <%= @stock_transfer.source_location.name %> + + <%= render :partial => 'stock_movements', :object => @stock_transfer.source_movements %> +
    + <% end %> + + <% if @stock_transfer.destination_movements.present? %> +
    + + <%= Spree.t(:destination) %> <%= @stock_transfer.destination_location.name %> + + <%= render :partial => 'stock_movements', :object => @stock_transfer.destination_movements %> +
    + <% end %> + +
    diff --git a/backend/app/views/spree/admin/tax_categories/_form.html.erb b/backend/app/views/spree/admin/tax_categories/_form.html.erb new file mode 100644 index 00000000000..0b765ec2718 --- /dev/null +++ b/backend/app/views/spree/admin/tax_categories/_form.html.erb @@ -0,0 +1,31 @@ +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> + <%= f.text_field :name, :class => 'fullwidth' %> + <% end %> +
    + +
    + <%= f.field_container :tax_code do %> + <%= f.label :tax_code, Spree.t(:tax_code) %> + <%= f.text_field :tax_code, :class => 'fullwidth' %> + <% end %> +
    + +
    + <%= f.field_container :description do %> + <%= f.label :description, Spree.t(:description) %>
    + <%= f.text_field :description, :class => 'fullwidth' %> + <% end %> +
    + +
    + <%= f.field_container :is_default, :class => ['checkbox'] do %> + + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/tax_categories/edit.html.erb b/backend/app/views/spree/admin/tax_categories/edit.html.erb new file mode 100644 index 00000000000..d9aa2fb5552 --- /dev/null +++ b/backend/app/views/spree/admin/tax_categories/edit.html.erb @@ -0,0 +1,18 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_tax_category) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_tax_categories_list), admin_tax_categories_path, :class => 'button' %>
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_category } %> + +<%= form_for [:admin, @tax_category] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/tax_categories/index.html.erb b/backend/app/views/spree/admin/tax_categories/index.html.erb new file mode 100644 index 00000000000..76615bbbd2c --- /dev/null +++ b/backend/app/views/spree/admin/tax_categories/index.html.erb @@ -0,0 +1,55 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:listing_tax_categories) %> +<% end %> + +<% content_for :page_actions do %> + +<% end %> + +<% if @tax_categories.any? %> + + + + + + + + + + + + + + + + + + + <% @tax_categories.each do |tax_category| + @edit_url = edit_admin_tax_category_path(tax_category) + @delete_url = admin_tax_category_path(tax_category) + %> + + + + + + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:tax_code) %><%= Spree.t(:description) %><%= Spree.t(:default) %>
    <%= tax_category.name %><%= tax_category.tax_code %><%= tax_category.description %><%= tax_category.is_default? ? Spree.t(:say_yes) : Spree.t(:say_no) %> + <%= link_to_edit tax_category, :no_text => true %> + <%= link_to_delete tax_category, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/tax_category')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_tax_category_path %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/tax_categories/new.html.erb b/backend/app/views/spree/admin/tax_categories/new.html.erb new file mode 100644 index 00000000000..85aad43c944 --- /dev/null +++ b/backend/app/views/spree/admin/tax_categories/new.html.erb @@ -0,0 +1,19 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_tax_category) %> +<% end %> + +<% content_for :page_actions do %> +
  • <%= link_to_with_icon 'arrow-left', Spree.t(:back_to_tax_categories_list), admin_tax_categories_path, :class => 'button' %>
  • +<% end %> + + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_category } %> + +<%= form_for [:admin, @tax_category] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/core/app/views/spree/admin/tax_categories/show.html.erb b/backend/app/views/spree/admin/tax_categories/show.html.erb similarity index 100% rename from core/app/views/spree/admin/tax_categories/show.html.erb rename to backend/app/views/spree/admin/tax_categories/show.html.erb diff --git a/backend/app/views/spree/admin/tax_rates/_form.html.erb b/backend/app/views/spree/admin/tax_rates/_form.html.erb new file mode 100644 index 00000000000..9fc8ab4934f --- /dev/null +++ b/backend/app/views/spree/admin/tax_rates/_form.html.erb @@ -0,0 +1,42 @@ +
    +
    +
    + <%= Spree.t(:general_settings) %> + +
    +
    + <%= f.label :name, Spree.t(:name) %> + <%= f.text_field :name, :class => 'fullwidth' %> +
    +
    + <%= f.label :amount, Spree.t(:rate) %> + <%= f.text_field :amount, :class => 'fullwidth' %> +
    <%= Spree.t(:tax_rate_amount_explanation) %> +
    +
    + <%= f.check_box :included_in_price %> + <%= f.label :included_in_price, Spree.t(:included_in_price) %> +
    +
    + +
    +
    + <%= f.label :zone, Spree.t(:zone) %> + <%= f.collection_select(:zone_id, @available_zones, :id, :name, {}, {:class => 'select2 fullwidth'}) %> +
    +
    + <%= f.label :tax_category_id, Spree.t(:tax_category) %> + <%= f.collection_select(:tax_category_id, @available_categories,:id, :name, {}, {:class => 'select2 fullwidth'}) %> +
    +
    + <%= f.check_box :show_rate_in_label %> + <%= f.label :show_rate_in_label, Spree.t(:show_rate_in_label) %> +
    +
    +
    +
    + +
    + + <%= render 'spree/admin/shared/calculator_fields', :f => f %> +
    diff --git a/backend/app/views/spree/admin/tax_rates/edit.html.erb b/backend/app/views/spree/admin/tax_rates/edit.html.erb new file mode 100644 index 00000000000..8867e2c113c --- /dev/null +++ b/backend/app/views/spree/admin/tax_rates/edit.html.erb @@ -0,0 +1,21 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_tax_rate) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_tax_rates_list), spree.admin_tax_rates_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_rate } %> + +<%= form_for [:admin, @tax_rate] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> +
    + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/tax_rates/index.html.erb b/backend/app/views/spree/admin/tax_rates/index.html.erb new file mode 100644 index 00000000000..98db455dd86 --- /dev/null +++ b/backend/app/views/spree/admin/tax_rates/index.html.erb @@ -0,0 +1,60 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:tax_rates) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_tax_rate), new_object_url, :icon => 'plus' %> +
  • +<% end %> + +<% if @tax_rates.any? %> + + + + + + + + + + + + + + + + + + + + + + + + + <% @tax_rates.each do |tax_rate|%> + + + + + + + + + + + <% end %> + +
    <%= Spree.t(:zone) %><%= Spree.t(:name) %><%= Spree.t(:category) %><%= Spree.t(:amount) %><%= Spree.t(:included_in_price) %><%= Spree.t(:show_rate_in_label) %><%= Spree.t(:calculator) %>
    <%=tax_rate.zone.try(:name) || Spree.t(:not_available) %><%=tax_rate.name %><%=tax_rate.tax_category.try(:name) || Spree.t(:not_available) %><%=tax_rate.amount %><%=tax_rate.included_in_price? ? Spree.t(:say_yes) : Spree.t(:say_no) %><%=tax_rate.show_rate_in_label? ? Spree.t(:say_yes) : Spree.t(:say_no) %><%=tax_rate.calculator.to_s %> + <%= link_to_edit tax_rate, :no_text => true %> + <%= link_to_delete tax_rate, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/tax_rate')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_tax_rate_path %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/tax_rates/new.html.erb b/backend/app/views/spree/admin/tax_rates/new.html.erb new file mode 100644 index 00000000000..095155c884e --- /dev/null +++ b/backend/app/views/spree/admin/tax_rates/new.html.erb @@ -0,0 +1,25 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_tax_rate) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_tax_rates_list), spree.admin_tax_rates_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_rate } %> + +<%= form_for [:admin, @tax_rate] do |f| %> +
    + + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + + <%= render :partial => 'spree/admin/shared/new_resource_links' %> + +
    +<% end %> diff --git a/backend/app/views/spree/admin/taxonomies/_form.html.erb b/backend/app/views/spree/admin/taxonomies/_form.html.erb new file mode 100644 index 00000000000..1166e84a2e9 --- /dev/null +++ b/backend/app/views/spree/admin/taxonomies/_form.html.erb @@ -0,0 +1,7 @@ +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= error_message_on :taxonomy, :name, :class => 'fullwidth title' %> + <%= text_field :taxonomy, :name %> + <% end %> +
    diff --git a/backend/app/views/spree/admin/taxonomies/_js_head.html.erb b/backend/app/views/spree/admin/taxonomies/_js_head.html.erb new file mode 100755 index 00000000000..0b73bbbfb04 --- /dev/null +++ b/backend/app/views/spree/admin/taxonomies/_js_head.html.erb @@ -0,0 +1,13 @@ +<% content_for :head do %> + <%= javascript_tag "var taxonomy_id = #{@taxonomy.id}; + var loading = '#{escape_javascript Spree.t(:loading)}'; + var new_taxon = '#{escape_javascript Spree.t(:new_taxon)}'; + var server_error = '#{escape_javascript Spree.t(:server_error)}'; + var taxonomy_tree_error = '#{escape_javascript Spree.t(:taxonomy_tree_error)}'; + + $(document).ready(function(){ + setup_taxonomy_tree(taxonomy_id); + }); + " + %> +<% end %> diff --git a/backend/app/views/spree/admin/taxonomies/_list.html.erb b/backend/app/views/spree/admin/taxonomies/_list.html.erb new file mode 100644 index 00000000000..2148fef4c61 --- /dev/null +++ b/backend/app/views/spree/admin/taxonomies/_list.html.erb @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + <% @taxonomies.each do |taxonomy| %> + + + + + + <% end %> + +
    <%= Spree.t(:name) %>
    <%= taxonomy.name %> + <%= link_to_edit taxonomy.id, :no_text => true %> + <%= link_to_delete taxonomy, :no_text => true %> +
    diff --git a/core/app/views/spree/admin/taxonomies/_taxon.html.erb b/backend/app/views/spree/admin/taxonomies/_taxon.html.erb similarity index 100% rename from core/app/views/spree/admin/taxonomies/_taxon.html.erb rename to backend/app/views/spree/admin/taxonomies/_taxon.html.erb diff --git a/backend/app/views/spree/admin/taxonomies/edit.erb b/backend/app/views/spree/admin/taxonomies/edit.erb new file mode 100755 index 00000000000..75d846e060d --- /dev/null +++ b/backend/app/views/spree/admin/taxonomies/edit.erb @@ -0,0 +1,43 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<%= render :partial => 'js_head' %> + +<% content_for :page_title do %> + <%= Spree.t(:taxonomy_edit) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_taxonomies_list), spree.admin_taxonomies_path, :icon => 'arrow-left' %> +
  • +<% end %> + + + +<%= form_for [:admin, @taxonomy] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> +
    + <%= label_tag nil, Spree.t(:tree) %>
    + +
    +
    + + +
    <%= Spree.t(:taxonomy_tree_instruction) %>
    + +
    + +
    + <%= button Spree.t('actions.update'), 'refresh' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), admin_taxonomies_path, :icon => 'remove' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/taxonomies/index.html.erb b/backend/app/views/spree/admin/taxonomies/index.html.erb new file mode 100644 index 00000000000..990bad0c5b5 --- /dev/null +++ b/backend/app/views/spree/admin/taxonomies/index.html.erb @@ -0,0 +1,24 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:taxonomies) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_taxonomy), spree.new_admin_taxonomy_url, :icon => 'plus', :id => 'admin_new_taxonomy_link' %> +
  • +<% end %> + +<%= render 'spree/admin/shared/product_sub_menu' %> + +<% if @taxonomies.any? %> +
    + <%= render 'list' %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/taxonomy')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_taxonomy_path %>! +
    +<% end %> \ No newline at end of file diff --git a/backend/app/views/spree/admin/taxonomies/new.html.erb b/backend/app/views/spree/admin/taxonomies/new.html.erb new file mode 100644 index 00000000000..1c0d2a4dcec --- /dev/null +++ b/backend/app/views/spree/admin/taxonomies/new.html.erb @@ -0,0 +1,24 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_taxonomy) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_taxonomies_list), spree.admin_taxonomies_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render 'spree/admin/shared/product_sub_menu' %> + +<%= form_for [:admin, @taxonomy] do |f| %> + + <%= render :partial => 'form', :locals => { :f => f } %> +
    +
    +
    + <%= button Spree.t(:create), 'ok' %> +
    +
    +<% end %> diff --git a/backend/app/views/spree/admin/taxons/_form.html.erb b/backend/app/views/spree/admin/taxons/_form.html.erb new file mode 100644 index 00000000000..23892d95bec --- /dev/null +++ b/backend/app/views/spree/admin/taxons/_form.html.erb @@ -0,0 +1,46 @@ +
    +
    + <%= f.field_container :name do %> + <%= f.label :name, Spree.t(:name) %> *
    + <%= text_field :taxon, :name, :class => 'fullwidth' %> + <%= error_message_on :taxon, :name, :class => 'fullwidth title' %> + <% end %> + + <%= f.field_container :permalink_part do %> + <%= f.label :permalink_part, Spree.t(:permalink) %> *
    + <%= text_field_tag :permalink_part, @permalink_part, :class => 'fullwidth' %>
    + + <%= @taxon.permalink.split('/')[0...-1].join('/') + '/' %> + + <% end %> + + <%= f.field_container :icon do %> + <%= f.label :icon, Spree.t(:icon) %>
    + <%= f.file_field :icon %> + <% end %> +
    + +
    + <%= f.field_container :description do %> + <%= f.label :description, Spree.t(:description) %>
    + <%= f.text_area :description, :class => 'fullwidth', :rows => 6 %> + <% end %> +
    + +
    + <%= f.field_container :meta_title do %> + <%= f.label :meta_title, Spree.t(:meta_title) %>
    + <%= f.text_field :meta_title, :class => 'fullwidth', :rows => 6 %> + <% end %> + + <%= f.field_container :meta_description do %> + <%= f.label :meta_description, Spree.t(:meta_description) %>
    + <%= f.text_field :meta_description, :class => 'fullwidth', :rows => 6 %> + <% end %> + + <%= f.field_container :meta_description do %> + <%= f.label :meta_keywords, Spree.t(:meta_keywords) %>
    + <%= f.text_field :meta_keywords, :class => 'fullwidth', :rows => 6 %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/taxons/_taxon_table.html.erb b/backend/app/views/spree/admin/taxons/_taxon_table.html.erb new file mode 100644 index 00000000000..1598bfb4702 --- /dev/null +++ b/backend/app/views/spree/admin/taxons/_taxon_table.html.erb @@ -0,0 +1,23 @@ + + + + + + + + + + <% taxons.each do |taxon| %> + + + + + + <% end %> + <% if taxons.empty? %> + + <% end %> + +
    <%= Spree.t(:name) %><%= Spree.t(:path) %>
    <%= taxon.name %><%= taxon_path taxon %> + <%= link_to_delete taxon, :url => remove_admin_product_taxon_url(@product, taxon), :name => icon('delete') + ' ' + Spree.t(:remove) %> +
    <%= Spree.t(:none) %>.
    diff --git a/backend/app/views/spree/admin/taxons/edit.html.erb b/backend/app/views/spree/admin/taxons/edit.html.erb new file mode 100644 index 00000000000..cec2364ae92 --- /dev/null +++ b/backend/app/views/spree/admin/taxons/edit.html.erb @@ -0,0 +1,38 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:taxon_edit) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_taxonomies_list), spree.admin_taxonomies_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%# Because otherwise the form would attempt to use to_param of @taxon %> +<% form_url = admin_taxonomy_taxon_path(@taxonomy.id, @taxon.id) %> +<%= form_for [:admin, @taxonomy, @taxon], :method => :put, :url => form_url, :html => { :multipart => true } do |f| %> + <%= render 'form', :f => f %> + +
    + <%= button Spree.t('actions.update'), 'refresh' %> + <%= Spree.t(:or) %> + <%= button_link_to Spree.t('actions.cancel'), edit_admin_taxonomy_url(@taxonomy), :icon => "remove" %> +
    +<% end %> + +<% content_for :head do %> + <%= javascript_tag do -%> + $(document).ready(function() { + var field = $('#permalink_part'), + target = $('#permalink_part_display'), + permalink_part_default = target.text().trim(); + + target.text(permalink_part_default + field.val()); + field.on('keyup blur', function () { + target.text(permalink_part_default + $(this).val()); + }); + }); + <% end -%> +<% end %> diff --git a/backend/app/views/spree/admin/taxons/index.html.erb b/backend/app/views/spree/admin/taxons/index.html.erb new file mode 100644 index 00000000000..be62df7f62a --- /dev/null +++ b/backend/app/views/spree/admin/taxons/index.html.erb @@ -0,0 +1,20 @@ +<% content_for :page_title do %> + <%= Spree.t(:taxons) %> +<% end %> + +<% content_for :table_filter_title do %> + <%= Spree.t(:choose_a_taxon_to_sort_products_for) %> +<% end %> + +<% content_for :table_filter do %> +
    + +
    +<% end %> + +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + + + +<%= render :partial => "spree/admin/products/autocomplete", :formats => :js %> + diff --git a/core/app/views/spree/admin/taxons/search.rabl b/backend/app/views/spree/admin/taxons/search.rabl similarity index 100% rename from core/app/views/spree/admin/taxons/search.rabl rename to backend/app/views/spree/admin/taxons/search.rabl diff --git a/backend/app/views/spree/admin/trackers/_form.html.erb b/backend/app/views/spree/admin/trackers/_form.html.erb new file mode 100644 index 00000000000..813133af96c --- /dev/null +++ b/backend/app/views/spree/admin/trackers/_form.html.erb @@ -0,0 +1,34 @@ +
    +
    +
    + <%= label_tag :tracker_analytics_id, Spree.t(:google_analytics_id) %> + <%= text_field :tracker, :analytics_id, :class => 'fullwidth' %> +
    +
    +
    +
    + <%= label_tag :tracker_environment, Spree.t(:environment) %> + <%= collection_select(:tracker, :environment, rails_environments, :to_s, :titleize, {}, {:id => 'tracker-env', :class => 'select2 fullwidth'}) %> +
    +
    +
    +
    + <%= label_tag nil, Spree.t(:active) %> +
      +
    • + <%= radio_button(:tracker, :active, true) %> + <%= label_tag :tracker_active_true, Spree.t(:say_yes) %> +
    • +
    • + <%= radio_button(:tracker, :active, false) %> + <%= label_tag :tracker_active_false, Spree.t(:say_no) %> +
    • +
    +
    +
    + +
    + +
    + +
    diff --git a/backend/app/views/spree/admin/trackers/edit.html.erb b/backend/app/views/spree/admin/trackers/edit.html.erb new file mode 100644 index 00000000000..848a2823580 --- /dev/null +++ b/backend/app/views/spree/admin/trackers/edit.html.erb @@ -0,0 +1,20 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_tracker) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_trackers_list), spree.admin_trackers_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tracker } %> + +<%= form_for [:admin, @tracker] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/trackers/index.html.erb b/backend/app/views/spree/admin/trackers/index.html.erb new file mode 100644 index 00000000000..83331d3950f --- /dev/null +++ b/backend/app/views/spree/admin/trackers/index.html.erb @@ -0,0 +1,48 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:analytics_trackers) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_tracker), new_object_url, :icon => 'plus', :id => 'admin_new_tracker_link' %> +
  • +<% end %> + +<% if @trackers.any? %> + + + + + + + + + + + + + + + + + <% @trackers.each do |tracker|%> + + + + + + + <% end %> + +
    <%= Spree.t(:google_analytics_id) %><%= Spree.t(:environment) %><%= Spree.t(:active) %>
    <%= tracker.analytics_id %><%= tracker.environment.to_s.titleize %><%= tracker.active ? Spree.t(:say_yes) : Spree.t(:say_no) %> + <%= link_to_edit tracker, :no_text => true %> + <%= link_to_delete tracker, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/tracker')) %>, + <%= link_to Spree.t(:add_one), new_object_url %>! +
    +<% end %> diff --git a/backend/app/views/spree/admin/trackers/new.html.erb b/backend/app/views/spree/admin/trackers/new.html.erb new file mode 100644 index 00000000000..9038ed1f610 --- /dev/null +++ b/backend/app/views/spree/admin/trackers/new.html.erb @@ -0,0 +1,20 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_tracker) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_trackers_list), spree.admin_trackers_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tracker } %> + +<%= form_for [:admin, @tracker] do |f| %> +
    + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/users/_addresses_form.html.erb b/backend/app/views/spree/admin/users/_addresses_form.html.erb new file mode 100644 index 00000000000..66e6c37c386 --- /dev/null +++ b/backend/app/views/spree/admin/users/_addresses_form.html.erb @@ -0,0 +1,19 @@ +
    +
    + <%= Spree.t(:billing_address) %> + <%= f.fields_for :bill_address, (@user.bill_address || Spree::Address.default(@user, "bill")) do |ba_form| %> + <% ba_form.object ||= Spree::Address.new(country: Spree::Country.new) %> + <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => ba_form, :type => "billing" } %> + <% end %> +
    +
    + +
    +
    + <%= Spree.t(:shipping_address) %> + <%= f.fields_for :ship_address, (@user.ship_address || Spree::Address.default(@user, "ship")) do |sa_form| %> + <% sa_form.object ||= Spree::Address.new(country: Spree::Country.new) %> + <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => sa_form, :type => "shipping" } %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/users/_form.html.erb b/backend/app/views/spree/admin/users/_form.html.erb new file mode 100644 index 00000000000..8817d4e19f2 --- /dev/null +++ b/backend/app/views/spree/admin/users/_form.html.erb @@ -0,0 +1,37 @@ +
    +
    + <%= f.field_container :email do %> + <%= f.label :email, Spree.t(:email) %> + <%= f.email_field :email, :class => 'fullwidth' %> + <%= error_message_on :user, :email %> + <% end %> + +
    + <%= label_tag nil, Spree.t(:roles) %> +
      + <% @roles.each do |role| %> +
    • + <%= check_box_tag 'user[spree_role_ids][]', role.id, @user.spree_roles.include?(role), :id => "user_spree_role_#{role.name}" %> + <%= label_tag "user_spree_role_#{role.name}", role.name %> +
    • + <% end %> +
    + <%= hidden_field_tag 'user[spree_role_ids][]', '' %> +
    + +
    + +
    + <%= f.field_container :password do %> + <%= f.label :password, Spree.t(:password) %> + <%= f.password_field :password, :class => 'fullwidth' %> + <%= f.error_message_on :password %> + <% end %> + + <%= f.field_container :password do %> + <%= f.label :password_confirmation, Spree.t(:confirm_password) %> + <%= f.password_field :password_confirmation, :class => 'fullwidth' %> + <%= f.error_message_on :password_confirmation %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/users/_sidebar.html.erb b/backend/app/views/spree/admin/users/_sidebar.html.erb new file mode 100644 index 00000000000..bc6de6c33f0 --- /dev/null +++ b/backend/app/views/spree/admin/users/_sidebar.html.erb @@ -0,0 +1,36 @@ +<% content_for :sidebar_title do %> + <%= Spree.t(:"admin.user.user_information") %> +<% end %> + +<% content_for :sidebar do %> + +<% end %> diff --git a/backend/app/views/spree/admin/users/_user_page_actions.html.erb b/backend/app/views/spree/admin/users/_user_page_actions.html.erb new file mode 100644 index 00000000000..4fcf44b4595 --- /dev/null +++ b/backend/app/views/spree/admin/users/_user_page_actions.html.erb @@ -0,0 +1,8 @@ +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_users_list), spree.admin_users_path, :icon => 'arrow-left' %> +
  • +
  • + <%= button_link_to Spree.t(:create_new_order), spree.new_admin_order_path(user_id: @user.id), :icon => 'plus' %> +
  • +<% end %> diff --git a/backend/app/views/spree/admin/users/addresses.html.erb b/backend/app/views/spree/admin/users/addresses.html.erb new file mode 100644 index 00000000000..ebb9e81381c --- /dev/null +++ b/backend/app/views/spree/admin/users/addresses.html.erb @@ -0,0 +1,24 @@ +<% content_for :page_title do %> + <%= link_to "#{Spree.t(:editing_user)} #{@user.email}", edit_admin_user_url(@user) %> +<% end %> + +<%= render :partial => 'spree/admin/users/sidebar', :locals => { :current => :address } %> +<%= render :partial => 'spree/admin/users/user_page_actions' %> + +
    + <%= Spree.t('addresses', :scope => 'admin.user') %> + +
    + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @user } %> +
    + +
    + <%= form_for [:admin, @user], as: :user, url: addresses_admin_user_url(@user), method: :put do |f| %> + <%= render :partial => 'addresses_form', :locals => { :f => f } %> + +
    + <%= render :partial => 'spree/admin/shared/edit_resource_links', :locals => { :collection_url => admin_users_url } %> +
    + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/users/edit.html.erb b/backend/app/views/spree/admin/users/edit.html.erb new file mode 100644 index 00000000000..54fda7223a6 --- /dev/null +++ b/backend/app/views/spree/admin/users/edit.html.erb @@ -0,0 +1,55 @@ +<% content_for :page_title do %> + <%= link_to "#{Spree.t(:editing_user)} #{@user.email}", edit_admin_user_url(@user) %> +<% end %> + +<%= render :partial => 'spree/admin/users/sidebar', :locals => { :current => :account } %> +<%= render :partial => 'spree/admin/users/user_page_actions' %> + +
    + <%= Spree.t(:general_settings) %> + +
    + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @user } %> +
    + +
    + <%= form_for [:admin, @user], as: :user, url: admin_user_url(@user), method: :put do |f| %> + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= render :partial => 'spree/admin/shared/edit_resource_links', :locals => { :collection_url => admin_users_url } %> +
    + <% end %> +
    +
    + +
    + <%= Spree.t('access', :scope => 'api') %> + + <% if @user.spree_api_key.present? %> +
    +
    <%= Spree.t('key', :scope => 'api') %>: <%= @user.spree_api_key %>
    +
    +
    + <%= form_tag spree.clear_api_key_admin_user_path(@user), :method => :put do %> + <%= button Spree.t('clear_key', :scope => 'api'), 'trash' %> + <% end %> + + <%= Spree.t(:or)%> + + <%= form_tag spree.generate_api_key_admin_user_path(@user), :method => :put do %> + <%= button Spree.t('regenerate_key', :scope => 'api'), 'refresh' %> + <% end %> +
    + + <% else %> + +
    <%= Spree.t('no_key', :scope => 'api') %>
    + +
    + <%= form_tag spree.generate_api_key_admin_user_path(@user), :method => :put do %> + <%= button Spree.t('generate_key', :scope => 'api'), 'key' %> + <% end %> +
    + <% end %> +
    diff --git a/backend/app/views/spree/admin/users/index.html.erb b/backend/app/views/spree/admin/users/index.html.erb new file mode 100644 index 00000000000..fe1e0e73650 --- /dev/null +++ b/backend/app/views/spree/admin/users/index.html.erb @@ -0,0 +1,53 @@ +<% content_for :page_title do %> + <%= Spree.t(:listing_users) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_user), new_admin_user_url, :icon => 'plus', :id => 'admin_new_user_link' %> +
  • +<% end %> + + + + + + + + + + + + + + <% @users.each do |user|%> + + + + + <% end %> + +
    <%= sort_link @search,:email, Spree.t(:user), {}, {:title => 'users_email_title'} %>
    <%=link_to user.email, edit_admin_user_url(user) %> + <%= link_to_edit user, :no_text => true %> + <%= link_to_delete user, :no_text => true %> +
    + +<%= paginate @users %> + +<% content_for :sidebar_title do %> + <%= Spree.t(:search) %> +<% end %> + +<% content_for :sidebar do %> +
    + <%= search_form_for [:admin, @search], url: admin_users_url do |f| %> +
    + <%= f.label :email_cont, Spree.t(:email) %>
    + <%= f.text_field :email_cont, :class => 'fullwidth' %> +
    +
    + <%= button Spree.t(:search), 'search' %> +
    + <% end %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/users/items.html.erb b/backend/app/views/spree/admin/users/items.html.erb new file mode 100644 index 00000000000..cfaeea82be1 --- /dev/null +++ b/backend/app/views/spree/admin/users/items.html.erb @@ -0,0 +1,74 @@ +<% content_for :page_title do %> + <%= link_to "#{Spree.t(:"admin.user.items_purchased")} #{@user.email}", edit_admin_user_url(@user) %> +<% end %> + +<%= render :partial => 'spree/admin/users/sidebar', :locals => { :current => :items } %> +<%= render :partial => 'spree/admin/users/user_page_actions' %> + +
    + <%= Spree.t(:"admin.user.items_purchased") %> + + <%= paginate @orders %> + + <% if @orders.any? %> + <%# TODO add search interface %> + + + + + + + + + + + + + + + + + + + + + + + <% @orders.each do |order| %> + <% order.line_items.each do |item| %> + + + + + + + + + + + <% end %> + <% end %> +
    <%= sort_link @search, :completed_at, I18n.t(:completed_at, :scope => 'activerecord.attributes.spree/order'), {}, {:title => 'orders_completed_at_title'} %><%= Spree.t(:description) %><%= I18n.t(:price, :scope => 'activerecord.attributes.spree/line_item') %><%= I18n.t(:quantity, :scope => 'activerecord.attributes.spree/line_item') %><%= Spree.t(:total) %><%= sort_link @search, :state, I18n.t(:state, :scope => 'activerecord.attributes.spree/order'), {}, {:title => 'orders_state_title'} %><%= sort_link @search, :number, Spree.t(:order_num, :scope => 'admin.user'), {}, {:title => 'orders_number_title'} %>
    <%= l(order.completed_at.to_date) if order.completed_at %> + <%= mini_image(item.variant) %> + + <%= item.product.name %>
    <%= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? %> + <% if item.variant.sku.present? %> + <%= Spree.t(:sku) %>: <%= item.variant.sku %> + <% end %> +
    <%= item.single_money.to_html %><%= item.quantity %><%= item.money.to_html %> +
    <%= Spree.t("order_state.#{order.state.downcase}") %>
    + <% if order.payment_state %> +
    <%= link_to Spree.t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) %>
    + <% end %> + <% if Spree::Order.checkout_step_names.include?(:delivery) && order.shipment_state %> +
    <%= Spree.t("shipment_states.#{order.shipment_state}") %>
    + <% end %> +
    <%= link_to order.number, edit_admin_order_url(order) %>
    + <% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/order')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_order_path %>! +
    + <% end %> + <%= paginate @orders %> +
    diff --git a/backend/app/views/spree/admin/users/new.html.erb b/backend/app/views/spree/admin/users/new.html.erb new file mode 100644 index 00000000000..a8d52ffcbc6 --- /dev/null +++ b/backend/app/views/spree/admin/users/new.html.erb @@ -0,0 +1,23 @@ +<% content_for :page_title do %> + <%= Spree.t(:new_user) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_users_list), spree.admin_users_path, :icon => 'arrow-left' %> +
  • +<% end %> + +
    + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @user } %> +
    + +
    + <%= form_for [:admin, @user], url: admin_users_url, method: :post do |f| %> + <%= render :partial => 'form', :locals => { :f => f } %> + +
    + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    + <% end %> +
    diff --git a/backend/app/views/spree/admin/users/orders.html.erb b/backend/app/views/spree/admin/users/orders.html.erb new file mode 100644 index 00000000000..136dc1286e1 --- /dev/null +++ b/backend/app/views/spree/admin/users/orders.html.erb @@ -0,0 +1,57 @@ +<% content_for :page_title do %> + <%= link_to "#{Spree.t(:"admin.user.order_history")} #{@user.email}", edit_admin_user_url(@user) %> +<% end %> + +<%= render :partial => 'spree/admin/users/sidebar', :locals => { :current => :orders } %> +<%= render :partial => 'spree/admin/users/user_page_actions' %> + +
    + <%= Spree.t(:"admin.user.order_history") %> + + <%= paginate @orders %> + + <% if @orders.any? %> + <%# TODO add search interface %> + + + + + + + + + + + + + + + + + <% @orders.each do |order| %> + + + + + + + <% end %> + +
    <%= sort_link @search, :completed_at, I18n.t(:completed_at, :scope => 'activerecord.attributes.spree/order'), {}, {:title => 'orders_completed_at_title'} %><%= sort_link @search, :number, I18n.t(:number, :scope => 'activerecord.attributes.spree/order'), {}, {:title => 'orders_number_title'} %><%= sort_link @search, :state, I18n.t(:state, :scope => 'activerecord.attributes.spree/order'), {}, {:title => 'orders_state_title'} %><%= sort_link @search, :total, I18n.t(:total, :scope => 'activerecord.attributes.spree/order'), {}, {:title => 'orders_total_title'} %>
    <%= l(order.completed_at.to_date) if order.completed_at %><%= link_to order.number, edit_admin_order_path(order) %> +
    <%= Spree.t("order_state.#{order.state.downcase}") %>
    + <% if order.payment_state %> +
    <%= link_to Spree.t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) %>
    + <% end %> + <% if Spree::Order.checkout_step_names.include?(:delivery) && order.shipment_state %> +
    <%= Spree.t("shipment_states.#{order.shipment_state}") %>
    + <% end %> +
    <%= order.display_total.to_html %>
    + <% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/order')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_order_path %>! +
    + <% end %> + + <%= paginate @orders %> +
    diff --git a/backend/app/views/spree/admin/variants/_autocomplete.js.erb b/backend/app/views/spree/admin/variants/_autocomplete.js.erb new file mode 100644 index 00000000000..3b29a8b2a44 --- /dev/null +++ b/backend/app/views/spree/admin/variants/_autocomplete.js.erb @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/backend/app/views/spree/admin/variants/_autocomplete_line_items_stock.js.erb b/backend/app/views/spree/admin/variants/_autocomplete_line_items_stock.js.erb new file mode 100644 index 00000000000..effa543d9c8 --- /dev/null +++ b/backend/app/views/spree/admin/variants/_autocomplete_line_items_stock.js.erb @@ -0,0 +1,49 @@ + diff --git a/backend/app/views/spree/admin/variants/_autocomplete_stock.js.erb b/backend/app/views/spree/admin/variants/_autocomplete_stock.js.erb new file mode 100644 index 00000000000..9acc576fe54 --- /dev/null +++ b/backend/app/views/spree/admin/variants/_autocomplete_stock.js.erb @@ -0,0 +1,53 @@ + diff --git a/backend/app/views/spree/admin/variants/_form.html.erb b/backend/app/views/spree/admin/variants/_form.html.erb new file mode 100644 index 00000000000..4cd53bb9e13 --- /dev/null +++ b/backend/app/views/spree/admin/variants/_form.html.erb @@ -0,0 +1,42 @@ +
    +
    + <% @product.option_types.each do |option_type| %> +
    + <%= label :new_variant, option_type.presentation %> + <%= f.collection_select 'option_value_ids', option_type.option_values, :id, :presentation, + { :prompt => true }, { :name => 'variant[option_value_ids][]', :class => 'select2 fullwidth' } %> +
    + <% end %> + +
    + <%= f.label :sku, Spree.t(:sku) %> + <%= f.text_field :sku, :class => 'fullwidth' %> +
    + +
    + <%= f.label :price, Spree.t(:price) %> + <%= f.text_field :price, :value => number_to_currency(@variant.price, :unit => ''), :class => 'fullwidth' %> +
    + +
    + <%= f.label :cost_price, Spree.t(:cost_price) %> + <%= f.text_field :cost_price, :value => number_to_currency(@variant.cost_price, :unit => ''), :class => 'fullwidth' %> +
    + +
    + <%= f.label :tax_category_id, Spree.t(:tax_category) %> + <%= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { :include_blank => Spree.t('match_choices.none') }, { :class => 'select2 fullwidth' }) %> +
    +
    +
    + +
    + <% [:weight, :height, :width, :depth].each do |field| %> +
    <%= f.label field, Spree.t(field) %> + <% value = number_with_precision(@variant.send(field), :precision => 2) %> + <%= f.text_field field, :value => value, :class => 'fullwidth' %> +
    + <% end %> +
    + +
    diff --git a/backend/app/views/spree/admin/variants/_split.js.erb b/backend/app/views/spree/admin/variants/_split.js.erb new file mode 100644 index 00000000000..b8567033b35 --- /dev/null +++ b/backend/app/views/spree/admin/variants/_split.js.erb @@ -0,0 +1,29 @@ + diff --git a/backend/app/views/spree/admin/variants/edit.html.erb b/backend/app/views/spree/admin/variants/edit.html.erb new file mode 100644 index 00000000000..b4466bc6c5c --- /dev/null +++ b/backend/app/views/spree/admin/variants/edit.html.erb @@ -0,0 +1,15 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Variants' } %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @variant } %> + +<%= form_for [:admin, @product, @variant] do |f| %> +
    +
    + <%= render :partial => 'form', :locals => { :f => f } %> +
    + + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +
    +<% end %> diff --git a/backend/app/views/spree/admin/variants/index.html.erb b/backend/app/views/spree/admin/variants/index.html.erb new file mode 100644 index 00000000000..9c3e5c16c07 --- /dev/null +++ b/backend/app/views/spree/admin/variants/index.html.erb @@ -0,0 +1,76 @@ +<%= render :partial => 'spree/admin/shared/product_sub_menu' %> + +<%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => 'Variants'} %> + +<%# Place for new variant form %> +
    + +<% if @variants.any? %> + + + + + + + + + + + + + + + + + + + <% @variants.each do |variant| %> + data-hook="variants_row" class="<%= cycle('odd', 'even')%>"> + + + + + + + <% end %> + <% unless @product.has_variants? %> + + <% end %> + +
    <%= Spree.t(:options) %><%= Spree.t(:price) %><%= Spree.t(:sku) %>
    + + <%= variant.options_text %><%= variant.display_price.to_html %><%= variant.sku %> + <%= link_to_edit(variant, :no_text => true) unless variant.deleted? %> +   + <%= link_to_delete(variant, :no_text => true) unless variant.deleted? %> +
    <%= Spree.t(:none) %>
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/variant')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_product_variant_path(@product) %>! +
    +<% end %> + +<% if @product.empty_option_values? %> +

    + <%= Spree.t(:to_add_variants_you_must_first_define) %> + <%= link_to Spree.t(:option_types), admin_product_url(@product) %> + <%= Spree.t(:and) %> + <%= link_to Spree.t(:option_values), admin_option_types_url %> +

    +<% else %> + <% content_for :page_actions do %> + + <% end %> +<% end %> diff --git a/backend/app/views/spree/admin/variants/new.html.erb b/backend/app/views/spree/admin/variants/new.html.erb new file mode 100644 index 00000000000..875fdfa0b1d --- /dev/null +++ b/backend/app/views/spree/admin/variants/new.html.erb @@ -0,0 +1,9 @@ +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @variant } %> + +<%= form_for [:admin, @product, @variant] do |f| %> +
    + <%= Spree.t(:new_variant) %> + <%= render :partial => 'form', :locals => { :f => f } %> + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +
    +<% end %> diff --git a/core/app/views/spree/admin/variants/new.js.erb b/backend/app/views/spree/admin/variants/new.js.erb similarity index 100% rename from core/app/views/spree/admin/variants/new.js.erb rename to backend/app/views/spree/admin/variants/new.js.erb diff --git a/backend/app/views/spree/admin/variants/update.js.erb b/backend/app/views/spree/admin/variants/update.js.erb new file mode 100644 index 00000000000..e4bc0cc8e04 --- /dev/null +++ b/backend/app/views/spree/admin/variants/update.js.erb @@ -0,0 +1 @@ +<%= @object.as_json %> diff --git a/backend/app/views/spree/admin/zones/_country_members.html.erb b/backend/app/views/spree/admin/zones/_country_members.html.erb new file mode 100644 index 00000000000..3a9487638cd --- /dev/null +++ b/backend/app/views/spree/admin/zones/_country_members.html.erb @@ -0,0 +1,10 @@ +
    +
    + <%= Spree.t(:countries) %> + + <%= zone_form.field_container :country_ids do %> + <%= zone_form.label :country_ids, Spree.t(:countries) %>
    + <%= zone_form.collection_select :country_ids, @countries, :id, :name, {}, { :multiple => true, :class => "select2 fullwidth" } %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/zones/_form.html.erb b/backend/app/views/spree/admin/zones/_form.html.erb new file mode 100644 index 00000000000..e2c667eb51a --- /dev/null +++ b/backend/app/views/spree/admin/zones/_form.html.erb @@ -0,0 +1,37 @@ +
    +
    + <%= Spree.t(:general_settings)%> + + <%= zone_form.field_container :name do %> + <%= zone_form.label :name, Spree.t(:name) %>
    + <%= zone_form.text_field :name, :class => 'fullwidth' %> + <% end %> + + <%= zone_form.field_container :description do %> + <%= zone_form.label :description, Spree.t(:description) %>
    + <%= zone_form.text_field :description, :class => 'fullwidth' %> + <% end %> + +
    + <%= zone_form.check_box :default_tax %> + <%= label_tag :zone_default_tax, Spree.t(:default_tax_zone) %> +
    + +
    + <%= label_tag Spree.t(:type) %> +
      +
    • + <%= zone_form.radio_button('kind', 'country', { :id => 'country_based' }) %> + <%= label_tag :country_based, Spree.t(:country_based) %> +
    • +
    • + <%= zone_form.radio_button('kind', 'state', { :id => 'state_based' }) %> + <%= label_tag :state_based, Spree.t(:state_based) %> +
    • +
    +
    +
    +
    + +<%= render :partial => 'state_members', :locals => { :zone_form => zone_form }%> +<%= render :partial => 'country_members', :locals => { :zone_form => zone_form } %> diff --git a/backend/app/views/spree/admin/zones/_state_members.html.erb b/backend/app/views/spree/admin/zones/_state_members.html.erb new file mode 100644 index 00000000000..3544a3b07ac --- /dev/null +++ b/backend/app/views/spree/admin/zones/_state_members.html.erb @@ -0,0 +1,10 @@ +
    +
    + <%= Spree.t(:states) %> + + <%= zone_form.field_container :state_ids do %> + <%= zone_form.label :state_ids, Spree.t(:states) %>
    + <%= zone_form.collection_select :state_ids, @states, :id, :name, {}, { :multiple => true, :class => "select2 fullwidth" } %> + <% end %> +
    +
    diff --git a/backend/app/views/spree/admin/zones/edit.html.erb b/backend/app/views/spree/admin/zones/edit.html.erb new file mode 100644 index 00000000000..98fda2b6f67 --- /dev/null +++ b/backend/app/views/spree/admin/zones/edit.html.erb @@ -0,0 +1,19 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:editing_zone) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_zones_list), admin_zones_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @zone } %> + +<%= form_for [:admin, @zone] do |zone_form| %> + <%= render :partial => 'form', :locals => { :zone_form => zone_form } %> +
    + <%= render :partial => 'spree/admin/shared/edit_resource_links' %> +<% end %> diff --git a/backend/app/views/spree/admin/zones/index.html.erb b/backend/app/views/spree/admin/zones/index.html.erb new file mode 100644 index 00000000000..79424543858 --- /dev/null +++ b/backend/app/views/spree/admin/zones/index.html.erb @@ -0,0 +1,54 @@ +<%= render 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:zones) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:new_zone), new_object_url, :icon => 'plus', :id => 'admin_new_zone_link' %> +
  • +<% end %> + +<%= paginate @zones %> + +<% if @zones.any? %> + + + + + + + + + + + + + + + + + <% @zones.each do |zone| %> + + + + + + + <% end %> + +
    <%= sort_link @search,:name, Spree.t(:name), :title => 'zones_order_by_name_title' %> + <%= sort_link @search,:description, Spree.t(:description), {}, {:title => 'zones_order_by_description_title'} %> + <%= Spree.t(:default_tax) %>
    <%= zone.name %><%= zone.description %><%= zone.default_tax? ? Spree.t(:say_yes) : Spree.t(:say_no) %> + <%=link_to_edit zone, :no_text => true %> + <%=link_to_delete zone, :no_text => true %> +
    +<% else %> +
    + <%= Spree.t(:no_resource_found, resource: I18n.t(:other, scope: 'activerecord.models.spree/zone')) %>, + <%= link_to Spree.t(:add_one), spree.new_admin_zone_path %>! +
    +<% end %> + +<%= paginate @zones %> diff --git a/backend/app/views/spree/admin/zones/new.html.erb b/backend/app/views/spree/admin/zones/new.html.erb new file mode 100644 index 00000000000..6322f770b33 --- /dev/null +++ b/backend/app/views/spree/admin/zones/new.html.erb @@ -0,0 +1,21 @@ +<%= render :partial => 'spree/admin/shared/configuration_menu' %> + +<% content_for :page_title do %> + <%= Spree.t(:new_zone) %> +<% end %> + +<% content_for :page_actions do %> +
  • + <%= button_link_to Spree.t(:back_to_zones_list), spree.admin_zones_path, :icon => 'arrow-left' %> +
  • +<% end %> + +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @zone } %> + +<%= form_for [:admin, @zone] do |zone_form| %> + <%= render :partial => 'form', :locals => { :zone_form => zone_form } %> + +
    + + <%= render :partial => 'spree/admin/shared/new_resource_links' %> +<% end %> diff --git a/backend/app/views/spree/layouts/admin.html.erb b/backend/app/views/spree/layouts/admin.html.erb new file mode 100644 index 00000000000..45c62a8ad99 --- /dev/null +++ b/backend/app/views/spree/layouts/admin.html.erb @@ -0,0 +1,53 @@ + + + + + + + + <%= render :partial => 'spree/admin/shared/head' %> + + + +
    + + <% if flash[:error] %> +
    <%= flash[:error] %>
    + <% end %> + <% if notice %> +
    <%= notice %>
    + <% end %> + <% if flash[:success] %> +
    <%= flash[:success] %>
    + <% end %> + +
    +
    +
    +
    <%= Spree.t(:loading) %>...
    +
    +
    + + <%= render :partial => 'spree/admin/shared/header' %> + <%= render :partial => 'spree/admin/shared/menu' %> + <%= render :partial => 'spree/admin/shared/sub_menu' %> + <%= render :partial => 'spree/admin/shared/content_header' %> + +
    +
    +
    + <%= render :partial => 'spree/admin/shared/table_filter' %> + +
    + <%= yield %> +
    + + <%= render :partial => 'spree/admin/shared/sidebar' %> +
    +
    +
    +
    + +
    + + diff --git a/backend/config/initializers/assets.rb b/backend/config/initializers/assets.rb new file mode 100644 index 00000000000..01939b89aef --- /dev/null +++ b/backend/config/initializers/assets.rb @@ -0,0 +1 @@ +Rails.application.config.assets.precompile += %w( jquery-ui/* logo/spree_50.png ) diff --git a/core/config/initializers/form_builder.rb b/backend/config/initializers/form_builder.rb similarity index 100% rename from core/config/initializers/form_builder.rb rename to backend/config/initializers/form_builder.rb diff --git a/backend/config/routes.rb b/backend/config/routes.rb new file mode 100644 index 00000000000..6126a240cf7 --- /dev/null +++ b/backend/config/routes.rb @@ -0,0 +1,175 @@ +Spree::Core::Engine.add_routes do + namespace :admin do + get '/search/users', to: "search#users", as: :search_users + + resources :promotions do + resources :promotion_rules + resources :promotion_actions + end + + resources :promotion_categories, except: [:show] + + resources :zones + + resources :countries do + resources :states + end + resources :states + resources :tax_categories + + resources :products do + resources :product_properties do + collection do + post :update_positions + end + end + resources :images do + collection do + post :update_positions + end + end + member do + get :clone + get :stock + end + resources :variants do + collection do + post :update_positions + end + end + resources :variants_including_master, only: [:update] + end + + get '/variants/search', to: "variants#search", as: :search_variants + + resources :option_types do + collection do + post :update_positions + post :update_values_positions + end + end + + delete '/option_values/:id', to: "option_values#destroy", as: :option_value + + resources :properties do + collection do + get :filtered + end + end + + delete '/product_properties/:id', to: "product_properties#destroy", as: :product_property + + resources :prototypes do + member do + get :select + end + + collection do + get :available + end + end + + resources :orders, except: [:show] do + member do + get :cart + post :resend + get :open_adjustments + get :close_adjustments + put :approve + put :cancel + put :resume + end + + resource :customer, controller: "orders/customer_details" + resources :customer_returns, only: [:index, :new, :edit, :create, :update] do + member do + put :refund + end + end + + resources :adjustments + resources :line_items + resources :return_authorizations do + member do + put :fire + end + end + resources :payments do + member do + put :fire + end + + resources :log_entries + resources :refunds, only: [:new, :create, :edit, :update] + end + + resources :reimbursements, only: [:create, :show, :edit, :update] do + member do + post :perform + end + end + end + + resource :general_settings do + collection do + post :clear_cache + end + end + + resources :return_items, only: [:update] + + resources :taxonomies do + collection do + post :update_positions + end + member do + get :get_children + end + resources :taxons + end + + resources :taxons, only: [:index, :show] do + collection do + get :search + end + end + + resources :reports, only: [:index] do + collection do + get :sales_total + post :sales_total + end + end + + resources :reimbursement_types, only: [:index] + resources :refund_reasons, except: [:show, :destroy] + resources :return_authorization_reasons, except: [:show, :destroy] + + resources :shipping_methods + resources :shipping_categories + resources :stock_transfers, only: [:index, :show, :new, :create] + resources :stock_locations do + resources :stock_movements, except: [:edit, :update, :destroy] + collection do + post :transfer_stock + end + end + + resources :stock_items, only: [:create, :update, :destroy] + resources :tax_rates + + resources :trackers + resources :payment_methods + + resources :users do + member do + get :orders + get :items + get :addresses + put :addresses + end + end + end + + get '/admin', to: 'admin/root#index', as: :admin +end diff --git a/backend/lib/spree/backend.rb b/backend/lib/spree/backend.rb new file mode 100644 index 00000000000..30187a076db --- /dev/null +++ b/backend/lib/spree/backend.rb @@ -0,0 +1,13 @@ +require 'rails/all' +require 'jquery-rails' +require 'jquery-ui-rails' +require 'deface' +require 'select2-rails' + +require 'spree_core' +require 'spree_api' + +require 'spree/responder' +require 'spree/backend/action_callbacks' +require 'spree/backend/callbacks' +require 'spree/backend/engine' diff --git a/core/lib/spree/core/action_callbacks.rb b/backend/lib/spree/backend/action_callbacks.rb similarity index 100% rename from core/lib/spree/core/action_callbacks.rb rename to backend/lib/spree/backend/action_callbacks.rb index 232daba8d3e..eb4dfb71d5f 100644 --- a/core/lib/spree/core/action_callbacks.rb +++ b/backend/lib/spree/backend/action_callbacks.rb @@ -21,6 +21,6 @@ def after(method) def fails(method) @fails_methods << method end - end + end end diff --git a/backend/lib/spree/backend/callbacks.rb b/backend/lib/spree/backend/callbacks.rb new file mode 100644 index 00000000000..34268a88678 --- /dev/null +++ b/backend/lib/spree/backend/callbacks.rb @@ -0,0 +1,52 @@ +module Spree + module Backend + module Callbacks + extend ActiveSupport::Concern + + module ClassMethods + + attr_accessor :callbacks + + protected + + def new_action + @callbacks ||= {} + @callbacks[:new_action] ||= Spree::ActionCallbacks.new + end + + def create + @callbacks ||= {} + @callbacks[:create] ||= Spree::ActionCallbacks.new + end + + def update + @callbacks ||= {} + @callbacks[:update] ||= Spree::ActionCallbacks.new + end + + def destroy + @callbacks ||= {} + @callbacks[:destroy] ||= Spree::ActionCallbacks.new + end + + def custom_callback(action) + @callbacks ||= {} + @callbacks[action] ||= Spree::ActionCallbacks.new + end + end + + protected + + def invoke_callbacks(action, callback_type) + callbacks = self.class.callbacks || {} + return if callbacks[action].nil? + case callback_type.to_sym + when :before then callbacks[action].before_methods.each {|method| send method } + when :after then callbacks[action].after_methods.each {|method| send method } + when :fails then callbacks[action].fails_methods.each {|method| send method } + end + end + + end + end +end diff --git a/backend/lib/spree/backend/engine.rb b/backend/lib/spree/backend/engine.rb new file mode 100644 index 00000000000..d627063d586 --- /dev/null +++ b/backend/lib/spree/backend/engine.rb @@ -0,0 +1,31 @@ +module Spree + module Backend + class Engine < ::Rails::Engine + config.middleware.use "Spree::Backend::Middleware::SeoAssist" + + initializer "spree.backend.environment", :before => :load_config_initializers do |app| + Spree::Backend::Config = Spree::BackendConfiguration.new + end + + # filter sensitive information during logging + initializer "spree.params.filter" do |app| + app.config.filter_parameters += [:password, :password_confirmation, :number] + end + + # sets the manifests / assets to be precompiled, even when initialize_on_precompile is false + initializer "spree.assets.precompile", :group => :all do |app| + app.config.assets.precompile += %w[ + spree/backend/all* + spree/backend/orders/edit_form.js + spree/backend/address_states.js + jqPlot/excanvas.min.js + spree/backend/images/new.js + jquery.jstree/themes/apple/* + fontawesome-webfont* + select2_locale* + jquery.alerts/images/* + ] + end + end + end +end diff --git a/backend/lib/spree_backend.rb b/backend/lib/spree_backend.rb new file mode 100644 index 00000000000..d095c2c556d --- /dev/null +++ b/backend/lib/spree_backend.rb @@ -0,0 +1 @@ +require 'spree/backend' diff --git a/backend/script/rails b/backend/script/rails new file mode 100755 index 00000000000..dbe7b350bf8 --- /dev/null +++ b/backend/script/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +ENGINE_ROOT = File.expand_path('../..', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/spree/backend/engine', __FILE__) + +require 'rails/all' +require 'rails/engine/commands' + diff --git a/backend/spec/controllers/spree/admin/base_controller_spec.rb b/backend/spec/controllers/spree/admin/base_controller_spec.rb new file mode 100644 index 00000000000..aa76fd859c2 --- /dev/null +++ b/backend/spec/controllers/spree/admin/base_controller_spec.rb @@ -0,0 +1,25 @@ +# Spree's rpsec controller tests get the Spree::ControllerHacks +# we don't need those for the anonymous controller here, so +# we call process directly instead of get +require 'spec_helper' + +describe Spree::Admin::BaseController, type: :controller do + controller(Spree::Admin::BaseController) do + def index + authorize! :update, Spree::Order + render text: 'test' + end + end + + context "unauthorized request" do + before do + allow_any_instance_of(Spree::Admin::BaseController).to receive(:spree_current_user).and_return(nil) + end + + it "redirects to root" do + allow(controller).to receive_message_chain(:spree, :root_path).and_return('/root') + get :index + expect(response).to redirect_to '/root' + end + end +end diff --git a/backend/spec/controllers/spree/admin/customer_returns_controller_spec.rb b/backend/spec/controllers/spree/admin/customer_returns_controller_spec.rb new file mode 100644 index 00000000000..3401c9bee4b --- /dev/null +++ b/backend/spec/controllers/spree/admin/customer_returns_controller_spec.rb @@ -0,0 +1,185 @@ +require 'spec_helper' + +module Spree + module Admin + describe CustomerReturnsController, :type => :controller do + stub_authorization! + + describe "#index" do + let(:order) { customer_return.order } + let(:customer_return) { create(:customer_return) } + + subject do + spree_get :index, { order_id: customer_return.order.to_param } + end + + before { subject } + + it "loads the order" do + expect(assigns(:order)).to eq order + end + + it "loads the customer return" do + expect(assigns(:customer_returns)).to include(customer_return) + end + end + + describe "#new" do + let(:order) { create(:shipped_order, line_items_count: 1) } + let!(:rma) { create :return_authorization, order: order, return_items: [create(:return_item, inventory_unit: order.inventory_units.first)] } + let!(:inactive_reimbursement_type) { create(:reimbursement_type, active: false) } + let!(:first_active_reimbursement_type) { create(:reimbursement_type) } + let!(:second_active_reimbursement_type) { create(:reimbursement_type) } + + subject do + spree_get :new, { order_id: order.to_param } + end + + it "loads the order" do + subject + expect(assigns(:order)).to eq order + end + + it "creates a new customer return" do + subject + expect(assigns(:customer_return)).to_not be_persisted + end + + context "with previous customer return" do + let(:order) { create(:shipped_order, line_items_count: 4) } + let(:rma) { create(:return_authorization, order: order) } + + let!(:rma_return_item) { create(:return_item, return_authorization: rma, inventory_unit: order.inventory_units.first) } + let!(:customer_return_return_item) { create(:return_item, return_authorization: rma, inventory_unit: order.inventory_units.last) } + + context "there is a return item associated with an rma but not a customer return" do + let!(:previous_customer_return) { create(:customer_return_without_return_items, return_items: [customer_return_return_item]) } + + before do + subject + end + + it "loads the persisted rma return items" do + expect(assigns(:rma_return_items).all? { |return_item| return_item.persisted? }).to eq true + end + + it "has one rma return item" do + expect(assigns(:rma_return_items)).to include(rma_return_item) + end + end + end + end + + describe "#edit" do + let(:order) { customer_return.order } + let(:customer_return) { create(:customer_return, line_items_count: 3) } + + let!(:accepted_return_item) { customer_return.return_items.order('id').first.tap(&:accept!) } + let!(:rejected_return_item) { customer_return.return_items.order('id').second.tap(&:reject!)} + let!(:manual_intervention_return_item) { customer_return.return_items.order('id').third.tap(&:require_manual_intervention!) } + + subject do + spree_get :edit, { order_id: order.to_param, id: customer_return.to_param } + end + + before do + subject + end + + it "loads the order" do + expect(assigns(:order)).to eq order + end + + it "loads the customer return" do + expect(assigns(:customer_return)).to eq customer_return + end + + it "loads the accepted return items" do + expect(assigns(:accepted_return_items)).to eq [accepted_return_item] + end + + it "loads the rejected return items" do + expect(assigns(:rejected_return_items)).to eq [rejected_return_item] + end + + it "loads the return items that require manual intervention" do + expect(assigns(:manual_intervention_return_items)).to eq [manual_intervention_return_item] + end + + it "loads the return items that are still pending" do + expect(assigns(:pending_return_items)).to eq [] + end + + it "loads the reimbursements that are still pending" do + expect(assigns(:pending_reimbursements)).to eq [] + end + end + + describe "#create" do + let(:order) { create(:shipped_order, line_items_count: 1) } + let!(:return_authorization) { create :return_authorization, order: order, return_items: [create(:return_item, inventory_unit: order.inventory_units.shipped.last)] } + + subject do + spree_post :create, customer_return_params + end + + context "valid customer return" do + let(:stock_location) { order.shipments.last.stock_location } + + let!(:customer_return_params) do + { + order_id: order.to_param, + customer_return: { + stock_location_id: stock_location.id, + return_items_attributes: { + "0" => { + id: return_authorization.return_items.first.id, + returned: "1", + "pre_tax_amount"=>"15.99", + inventory_unit_id: order.inventory_units.shipped.last.id + } + } + } + } + end + + it "creates a customer return" do + expect{ subject }.to change { Spree::CustomerReturn.count }.by(1) + end + + it "redirects to the index page" do + subject + expect(response).to redirect_to(spree.edit_admin_order_customer_return_path(order, assigns(:customer_return))) + end + end + + context "invalid customer return" do + let!(:customer_return_params) do + { + order_id: order.to_param, + customer_return: { + stock_location_id: "", + return_items_attributes: { + "0" => { + returned: "1", + "pre_tax_amount"=>"15.99", + inventory_unit_id: order.inventory_units.shipped.last.id + } + } + } + } + end + + it "doesn't create a customer return" do + expect{ subject }.to_not change { Spree::CustomerReturn.count } + end + + it "renders the new page" do + subject + expect(response).to render_template(:new) + end + end + end + end + end +end diff --git a/backend/spec/controllers/spree/admin/general_settings_controller_spec.rb b/backend/spec/controllers/spree/admin/general_settings_controller_spec.rb new file mode 100644 index 00000000000..158001db8f0 --- /dev/null +++ b/backend/spec/controllers/spree/admin/general_settings_controller_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Spree::Admin::GeneralSettingsController, type: :controller do + let(:user) { create(:user) } + + before do + allow(controller).to receive_messages :spree_current_user => user + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + end + + describe '#clear_cache' do + subject { spree_post :clear_cache } + + shared_examples 'a HTTP 204 response' do + it 'grant access to users with an admin role' do + subject + expect(response.status).to eq(204) + end + end + + context 'when no callback' do + it_behaves_like 'a HTTP 204 response' + end + + context 'when callback implemented' do + Spree::Admin::GeneralSettingsController.class_eval do + custom_callback(:clear_cache).after :foo + + def foo + # Make a call to Akamai, CloudFlare, etc invalidation.... + end + end + + before do + expect(controller).to receive(:foo).once + end + + it_behaves_like 'a HTTP 204 response' + end + end +end diff --git a/core/spec/controllers/spree/admin/missing_products_controller_spec.rb b/backend/spec/controllers/spree/admin/missing_products_controller_spec.rb similarity index 77% rename from core/spec/controllers/spree/admin/missing_products_controller_spec.rb rename to backend/spec/controllers/spree/admin/missing_products_controller_spec.rb index 29899e1d74f..f5feee219e7 100644 --- a/core/spec/controllers/spree/admin/missing_products_controller_spec.rb +++ b/backend/spec/controllers/spree/admin/missing_products_controller_spec.rb @@ -4,14 +4,14 @@ # the load_resource filter in Spree::Admin::ResourceController is prepended to the filter chain # this means this call is triggered before the authorize_admin call and in this case # the load_resource filter halts the request meaning authorize_admin is not called at all. -describe Spree::Admin::ProductsController do +describe Spree::Admin::ProductsController, :type => :controller do stub_authorization! # Regression test for GH #538 it "cannot find a non-existent product" do spree_get :edit, :id => "non-existent-product" - response.should redirect_to(spree.admin_products_path) - flash[:error].should eql("Product is not found") + expect(response).to redirect_to(spree.admin_products_path) + expect(flash[:error]).to eql("Product is not found") end end diff --git a/backend/spec/controllers/spree/admin/orders_controller_spec.rb b/backend/spec/controllers/spree/admin/orders_controller_spec.rb new file mode 100644 index 00000000000..cd6ce9abb25 --- /dev/null +++ b/backend/spec/controllers/spree/admin/orders_controller_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' +require 'cancan' +require 'spree/testing_support/bar_ability' + +# Ability to test access to specific model instances +class OrderSpecificAbility + include CanCan::Ability + + def initialize(user) + can [:admin, :manage], Spree::Order, :number => 'R987654321' + end +end + +describe Spree::Admin::OrdersController, :type => :controller do + + context "with authorization" do + stub_authorization! + + before do + request.env["HTTP_REFERER"] = "http://localhost:3000" + + # ensure no respond_overrides are in effect + if Spree::BaseController.spree_responders[:OrdersController].present? + Spree::BaseController.spree_responders[:OrdersController].clear + end + end + + let(:order) do + mock_model( + Spree::Order, + completed?: true, + total: 100, + number: 'R123456789', + all_adjustments: adjustments, + billing_address: mock_model(Spree::Address) + ) + end + + let(:adjustments) { double('adjustments') } + + before do + allow(Spree::Order).to receive_messages(find_by_number!: order) + end + + context "#approve" do + it "approves an order" do + expect(order).to receive(:approved_by).with(controller.try_spree_current_user) + spree_put :approve, id: order.number + expect(flash[:success]).to eq Spree.t(:order_approved) + end + end + + context "#cancel" do + it "cancels an order" do + expect(order).to receive(:canceled_by).with(controller.try_spree_current_user) + spree_put :cancel, id: order.number + expect(flash[:success]).to eq Spree.t(:order_canceled) + end + end + + context "#resume" do + it "resumes an order" do + expect(order).to receive(:resume!) + spree_put :resume, id: order.number + expect(flash[:success]).to eq Spree.t(:order_resumed) + end + end + + context "pagination" do + it "can page through the orders" do + spree_get :index, :page => 2, :per_page => 10 + expect(assigns[:orders].offset_value).to eq(10) + expect(assigns[:orders].limit_value).to eq(10) + end + end + + # Test for #3346 + context "#new" do + it "a new order has the current user assigned as a creator" do + spree_get :new + expect(assigns[:order].created_by).to eq(controller.try_spree_current_user) + end + end + + # Regression test for #3684 + context "#edit" do + it "does not refresh rates if the order is completed" do + allow(order).to receive_messages :completed? => true + expect(order).not_to receive :refresh_shipment_rates + spree_get :edit, :id => order.number + end + + it "does refresh the rates if the order is incomplete" do + allow(order).to receive_messages :completed? => false + expect(order).to receive :refresh_shipment_rates + spree_get :edit, :id => order.number + end + end + + # Test for #3919 + context "search" do + let(:user) { create(:user) } + + before do + allow(controller).to receive_messages :spree_current_user => user + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + + create(:completed_order_with_totals) + expect(Spree::Order.count).to eq 1 + end + + it "does not display duplicated results" do + spree_get :index, q: { + line_items_variant_id_in: Spree::Order.first.variants.map(&:id) + } + expect(assigns[:orders].map { |o| o.number }.count).to eq 1 + end + end + + context "#open_adjustments" do + let(:closed) { double('closed_adjustments') } + + before do + allow(adjustments).to receive(:where).and_return(closed) + allow(closed).to receive(:update_all) + end + + it "changes all the closed adjustments to open" do + expect(adjustments).to receive(:where).with(state: 'closed') + .and_return(closed) + expect(closed).to receive(:update_all).with(state: 'open') + spree_post :open_adjustments, id: order.number + end + + it "sets the flash success message" do + spree_post :open_adjustments, id: order.number + expect(flash[:success]).to eql('All adjustments successfully opened!') + end + + it "redirects back" do + spree_post :open_adjustments, id: order.number + expect(response).to redirect_to(:back) + end + end + + context "#close_adjustments" do + let(:open) { double('open_adjustments') } + + before do + allow(adjustments).to receive(:where).and_return(open) + allow(open).to receive(:update_all) + end + + it "changes all the open adjustments to closed" do + expect(adjustments).to receive(:where).with(state: 'open') + .and_return(open) + expect(open).to receive(:update_all).with(state: 'closed') + spree_post :close_adjustments, id: order.number + end + + it "sets the flash success message" do + spree_post :close_adjustments, id: order.number + expect(flash[:success]).to eql('All adjustments successfully closed!') + end + + it "redirects back" do + spree_post :close_adjustments, id: order.number + expect(response).to redirect_to(:back) + end + end + end + + context '#authorize_admin' do + let(:user) { create(:user) } + let(:order) { create(:completed_order_with_totals, :number => 'R987654321') } + + before do + allow(Spree::Order).to receive_messages :find_by_number! => order + allow(controller).to receive_messages :spree_current_user => user + end + + it 'should grant access to users with an admin role' do + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + spree_post :index + expect(response).to render_template :index + end + + it 'should grant access to users with an bar role' do + user.spree_roles << Spree::Role.find_or_create_by(name: 'bar') + Spree::Ability.register_ability(BarAbility) + spree_post :index + expect(response).to render_template :index + Spree::Ability.remove_ability(BarAbility) + end + + it 'should deny access to users with an bar role' do + allow(order).to receive(:update_attributes).and_return true + allow(order).to receive(:user).and_return Spree.user_class.new + allow(order).to receive(:token).and_return nil + user.spree_roles.clear + user.spree_roles << Spree::Role.find_or_create_by(name: 'bar') + Spree::Ability.register_ability(BarAbility) + spree_put :update, { :id => 'R123' } + expect(response).to redirect_to('/unauthorized') + Spree::Ability.remove_ability(BarAbility) + end + + it 'should deny access to users without an admin role' do + allow(user).to receive_messages :has_spree_role? => false + spree_post :index + expect(response).to redirect_to('/unauthorized') + end + + it 'should restrict returned order(s) on index when using OrderSpecificAbility' do + number = order.number + + 3.times { create(:completed_order_with_totals) } + expect(Spree::Order.complete.count).to eq 4 + Spree::Ability.register_ability(OrderSpecificAbility) + + allow(user).to receive_messages :has_spree_role? => false + spree_get :index + expect(response).to render_template :index + expect(assigns['orders'].size).to eq 1 + expect(assigns['orders'].first.number).to eq number + expect(Spree::Order.accessible_by(Spree::Ability.new(user), :index).pluck(:number)).to eq [number] + end + end + + context "order number not given" do + stub_authorization! + + it "raise active record not found" do + expect { + spree_get :edit, id: nil + }.to raise_error ActiveRecord::RecordNotFound + end + end +end diff --git a/backend/spec/controllers/spree/admin/payment_methods_controller_spec.rb b/backend/spec/controllers/spree/admin/payment_methods_controller_spec.rb new file mode 100644 index 00000000000..9d17207b74c --- /dev/null +++ b/backend/spec/controllers/spree/admin/payment_methods_controller_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +module Spree + class GatewayWithPassword < PaymentMethod + preference :password, :string, :default => "password" + end + + describe Admin::PaymentMethodsController, :type => :controller do + stub_authorization! + + let(:payment_method) { GatewayWithPassword.create!(:name => "Bogus", :preferred_password => "haxme") } + + # regression test for #2094 + it "does not clear password on update" do + expect(payment_method.preferred_password).to eq("haxme") + spree_put :update, :id => payment_method.id, :payment_method => { :type => payment_method.class.to_s, :preferred_password => "" } + expect(response).to redirect_to(spree.edit_admin_payment_method_path(payment_method)) + + payment_method.reload + expect(payment_method.preferred_password).to eq("haxme") + end + + context "tries to save invalid payment" do + it "doesn't break, responds nicely" do + expect { + spree_post :create, :payment_method => { :name => "", :type => "Spree::Gateway::Bogus" } + }.not_to raise_error + end + end + + it "can create a payment method of a valid type" do + expect { + spree_post :create, :payment_method => { :name => "Test Method", :type => "Spree::Gateway::Bogus" } + }.to change(Spree::PaymentMethod, :count).by(1) + + expect(response).to be_redirect + expect(response).to redirect_to spree.edit_admin_payment_method_path(assigns(:payment_method)) + end + + it "can not create a payment method of an invalid type" do + expect { + spree_post :create, :payment_method => { :name => "Invalid Payment Method", :type => "Spree::InvalidType" } + }.to change(Spree::PaymentMethod, :count).by(0) + + expect(response).to be_redirect + expect(response).to redirect_to spree.new_admin_payment_method_path + end + end +end diff --git a/backend/spec/controllers/spree/admin/payments_controller_spec.rb b/backend/spec/controllers/spree/admin/payments_controller_spec.rb new file mode 100644 index 00000000000..86b43e567af --- /dev/null +++ b/backend/spec/controllers/spree/admin/payments_controller_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +module Spree + module Admin + describe PaymentsController, :type => :controller do + stub_authorization! + + let(:order) { create(:order) } + + context "with a valid credit card" do + let(:order) { create(:order_with_line_items, :state => "payment") } + let(:payment_method) { create(:credit_card_payment_method, :display_on => "back_end") } + + before do + attributes = { + :order_id => order.number, + :card => "new", + :payment => { + :amount => order.total, + :payment_method_id => payment_method.id.to_s, + :source_attributes => { + :name => "Test User", + :number => "4111 1111 1111 1111", + :expiry => "09 / #{Time.now.year + 1}", + :verification_value => "123" + } + } + } + spree_post :create, attributes + end + + it "should process payment correctly" do + expect(order.payments.count).to eq(1) + expect(response).to redirect_to(spree.admin_order_payments_path(order)) + expect(order.reload.state).to eq('complete') + end + + # Regression for #4768 + it "doesnt process the same payment twice" do + expect(Spree::LogEntry.where(source: order.payments.first).count).to eq(1) + end + end + + # Regression test for #3233 + context "with a backend payment method" do + before do + @payment_method = create(:check_payment_method, :display_on => "back_end") + end + + it "loads backend payment methods" do + spree_get :new, :order_id => order.number + expect(response.status).to eq(200) + expect(assigns[:payment_methods]).to include(@payment_method) + end + end + + context "order has billing address" do + before do + order.bill_address = create(:address) + order.save! + end + + context "order does not have payments" do + it "redirect to new payments page" do + spree_get :index, { amount: 100, order_id: order.number } + expect(response).to redirect_to(spree.new_admin_order_payment_path(order)) + end + end + + context "order has payments" do + before do + order.payments << create(:payment, amount: order.total, order: order, state: 'completed') + end + + it "shows the payments page" do + spree_get :index, { amount: 100, order_id: order.number } + expect(response.code).to eq "200" + end + end + + end + + context "order does not have a billing address" do + before do + order.bill_address = nil + order.save + end + + it "should redirect to the customer details page" do + spree_get :index, { amount: 100, order_id: order.number } + expect(response).to redirect_to(spree.edit_admin_order_customer_path(order)) + end + end + + end + end +end diff --git a/backend/spec/controllers/spree/admin/products_controller_spec.rb b/backend/spec/controllers/spree/admin/products_controller_spec.rb new file mode 100644 index 00000000000..0f82b623194 --- /dev/null +++ b/backend/spec/controllers/spree/admin/products_controller_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Spree::Admin::ProductsController, :type => :controller do + stub_authorization! + + context "#index" do + let(:ability_user) { stub_model(Spree::LegacyUser, :has_spree_role? => true) } + + # Regression test for #1259 + it "can find a product by SKU" do + product = create(:product, :sku => "ABC123") + spree_get :index, :q => { :sku_start => "ABC123" } + expect(assigns[:collection]).not_to be_empty + expect(assigns[:collection]).to include(product) + end + end + + # regression test for #1370 + context "adding properties to a product" do + let!(:product) { create(:product) } + specify do + spree_put :update, :id => product.to_param, :product => { :product_properties_attributes => { "1" => { :property_name => "Foo", :value => "bar" } } } + expect(flash[:success]).to eq("Product #{product.name.inspect} has been successfully updated!") + end + + end + + + # regression test for #801 + context "destroying a product" do + let(:product) do + product = create(:product) + create(:variant, :product => product) + product + end + + it "deletes all the variants (including master) for the product" do + spree_delete :destroy, :id => product + expect(product.reload.deleted_at).not_to be_nil + product.variants_including_master.each do |variant| + expect(variant.reload.deleted_at).not_to be_nil + end + end + end + + context "stock" do + let(:product) { create(:product) } + it "restricts stock location based on accessible attributes" do + expect(Spree::StockLocation).to receive(:accessible_by).and_return([]) + spree_get :stock, :id => product + end + end +end diff --git a/backend/spec/controllers/spree/admin/promotion_actions_controller_spec.rb b/backend/spec/controllers/spree/admin/promotion_actions_controller_spec.rb new file mode 100644 index 00000000000..14a8102964d --- /dev/null +++ b/backend/spec/controllers/spree/admin/promotion_actions_controller_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Spree::Admin::PromotionActionsController, :type => :controller do + stub_authorization! + + let!(:promotion) { create(:promotion) } + + it "can create a promotion action of a valid type" do + spree_post :create, :promotion_id => promotion.id, :action_type => "Spree::Promotion::Actions::CreateAdjustment" + expect(response).to be_redirect + expect(response).to redirect_to spree.edit_admin_promotion_path(promotion) + expect(promotion.actions.count).to eq(1) + end + + it "can not create a promotion action of an invalid type" do + spree_post :create, :promotion_id => promotion.id, :action_type => "Spree::InvalidType" + expect(response).to be_redirect + expect(response).to redirect_to spree.edit_admin_promotion_path(promotion) + expect(promotion.rules.count).to eq(0) + end +end diff --git a/backend/spec/controllers/spree/admin/promotion_rules_controller_spec.rb b/backend/spec/controllers/spree/admin/promotion_rules_controller_spec.rb new file mode 100644 index 00000000000..4136fa0f530 --- /dev/null +++ b/backend/spec/controllers/spree/admin/promotion_rules_controller_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Spree::Admin::PromotionRulesController, :type => :controller do + stub_authorization! + + let!(:promotion) { create(:promotion) } + + it "can create a promotion rule of a valid type" do + spree_post :create, :promotion_id => promotion.id, :promotion_rule => { :type => "Spree::Promotion::Rules::Product" } + expect(response).to be_redirect + expect(response).to redirect_to spree.edit_admin_promotion_path(promotion) + expect(promotion.rules.count).to eq(1) + end + + it "can not create a promotion rule of an invalid type" do + spree_post :create, :promotion_id => promotion.id, :promotion_rule => { :type => "Spree::InvalidType" } + expect(response).to be_redirect + expect(response).to redirect_to spree.edit_admin_promotion_path(promotion) + expect(promotion.rules.count).to eq(0) + end +end diff --git a/backend/spec/controllers/spree/admin/promotions_controller_spec.rb b/backend/spec/controllers/spree/admin/promotions_controller_spec.rb new file mode 100644 index 00000000000..c851093acc8 --- /dev/null +++ b/backend/spec/controllers/spree/admin/promotions_controller_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Spree::Admin::PromotionsController, :type => :controller do + stub_authorization! + + let!(:promotion1) { create(:promotion, name: "name1", code: "code1", path: "path1") } + let!(:promotion2) { create(:promotion, name: "name2", code: "code2", path: "path2") } + let!(:category) { create :promotion_category } + + context "#index" do + it "succeeds" do + spree_get :index + expect(assigns[:promotions]).to match_array [promotion2, promotion1] + end + + it "assigns promotion categories" do + spree_get :index + expect(assigns[:promotion_categories]).to match_array [category] + end + + context "search" do + it "pages results" do + spree_get :index, per_page: '1' + expect(assigns[:promotions]).to eq [promotion2] + end + + it "filters by name" do + spree_get :index, q: {name_cont: promotion1.name} + expect(assigns[:promotions]).to eq [promotion1] + end + + it "filters by code" do + spree_get :index, q: {code_cont: promotion1.code} + expect(assigns[:promotions]).to eq [promotion1] + end + + it "filters by path" do + spree_get :index, q: {path_cont: promotion1.path} + expect(assigns[:promotions]).to eq [promotion1] + end + end + end + +end diff --git a/backend/spec/controllers/spree/admin/refunds_controller_spec.rb b/backend/spec/controllers/spree/admin/refunds_controller_spec.rb new file mode 100644 index 00000000000..9bdfe98b333 --- /dev/null +++ b/backend/spec/controllers/spree/admin/refunds_controller_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Spree::Admin::RefundsController do + stub_authorization! + + describe "POST create" do + context "a Spree::Core::GatewayError is raised" do + + let(:payment) { create(:payment) } + + subject do + spree_post :create, + refund: { amount: "50.0", refund_reason_id: "1" }, + payment_id: payment.id + end + + before(:each) do + def controller.create + raise Spree::Core::GatewayError.new('An error has occurred') + end + end + + it "sets an error message with the correct text" do + subject + expect(flash[:error]).to eq 'An error has occurred' + end + + it { should render_template(:new) } + end + end +end diff --git a/backend/spec/controllers/spree/admin/reimbursements_controller_spec.rb b/backend/spec/controllers/spree/admin/reimbursements_controller_spec.rb new file mode 100644 index 00000000000..2edc609f9a5 --- /dev/null +++ b/backend/spec/controllers/spree/admin/reimbursements_controller_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Spree::Admin::ReimbursementsController, :type => :controller do + stub_authorization! + + let!(:default_refund_reason) do + Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) + end + + describe '#create' do + let(:customer_return) { create(:customer_return, line_items_count: 1) } + let(:order) { customer_return.order } + let(:return_item) { customer_return.return_items.first } + let(:payment) { order.payments.first } + + subject do + spree_post :create, order_id: order.to_param, build_from_customer_return_id: customer_return.id + end + + it 'creates the reimbursement' do + expect { subject }.to change { order.reimbursements.count }.by(1) + expect(assigns(:reimbursement).return_items.to_a).to eq customer_return.return_items.to_a + end + + it 'redirects to the edit page' do + subject + expect(response).to redirect_to(spree.edit_admin_order_reimbursement_path(order, assigns(:reimbursement))) + end + end + + describe "#perform" do + let(:reimbursement) { create(:reimbursement) } + let(:customer_return) { reimbursement.customer_return } + let(:order) { reimbursement.order } + let(:return_items) { reimbursement.return_items } + let(:payment) { order.payments.first } + + subject do + spree_post :perform, order_id: order.to_param, id: reimbursement.to_param + end + + it 'redirects to customer return page' do + subject + expect(response).to redirect_to spree.admin_order_reimbursement_path(order, reimbursement) + end + + it 'performs the reimbursement' do + expect { + subject + }.to change { payment.refunds.count }.by(1) + expect(payment.refunds.last.amount).to be > 0 + expect(payment.refunds.last.amount).to eq return_items.to_a.sum(&:total) + end + + context "a Spree::Core::GatewayError is raised" do + before(:each) do + def controller.perform + raise Spree::Core::GatewayError.new('An error has occurred') + end + end + + it "sets an error message with the correct text" do + subject + expect(flash[:error]).to eq 'An error has occurred' + end + + it 'redirects to the edit page' do + subject + redirect_path = spree.edit_admin_order_reimbursement_path(order, assigns(:reimbursement)) + expect(response).to redirect_to(redirect_path) + end + end + end +end diff --git a/backend/spec/controllers/spree/admin/reports_controller_spec.rb b/backend/spec/controllers/spree/admin/reports_controller_spec.rb new file mode 100644 index 00000000000..e46a7f4c76a --- /dev/null +++ b/backend/spec/controllers/spree/admin/reports_controller_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Spree::Admin::ReportsController, :type => :controller do + stub_authorization! + + describe 'ReportsController.available_reports' do + it 'should contain sales_total' do + expect(Spree::Admin::ReportsController.available_reports.keys.include?(:sales_total)).to be true + end + + it 'should have the proper sales total report description' do + expect(Spree::Admin::ReportsController.available_reports[:sales_total][:description]).to eql('Sales Total For All Orders') + end + + end + + describe 'ReportsController.add_available_report!' do + context 'when adding the report name' do + it 'should contain the report' do + Spree::Admin::ReportsController.add_available_report!(:some_report) + expect(Spree::Admin::ReportsController.available_reports.keys.include?(:some_report)).to be true + end + end + end + + describe 'GET index' do + it 'should be ok' do + spree_get :index + expect(response).to be_ok + end + end + + it 'should respond to model_class as Spree::AdminReportsController' do + expect(controller.send(:model_class)).to eql(Spree::Admin::ReportsController) + end + + after(:each) do + Spree::Admin::ReportsController.available_reports.delete_if do |key, value| + key != :sales_total + end + end +end diff --git a/backend/spec/controllers/spree/admin/resource_controller_spec.rb b/backend/spec/controllers/spree/admin/resource_controller_spec.rb new file mode 100644 index 00000000000..f6979eb5837 --- /dev/null +++ b/backend/spec/controllers/spree/admin/resource_controller_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper' + +module Spree + module Admin + class WidgetsController < Spree::Admin::ResourceController + prepend_view_path('spec/test_views') + + def model_class + Widget + end + end + end +end + +describe Spree::Admin::WidgetsController, :type => :controller do + stub_authorization! + + after(:all) do + # Spree::Core::Engine.routes.reload_routes! + Rails.application.reload_routes! + end + + with_model 'Widget' do + table do |t| + t.string :name + t.integer :position + t.timestamps + end + + model do + acts_as_list + validates :name, presence: true + end + end + + before do + Spree::Core::Engine.routes.draw do + namespace :admin do + resources :widgets + end + end + end + + describe '#new' do + subject do + spree_get :new + end + + it 'succeeds' do + subject + expect(response).to be_success + end + end + + describe '#edit' do + let(:widget) { Widget.create!(name: 'a widget') } + + subject do + spree_get :edit, id: widget.to_param + end + + it 'succeeds' do + subject + expect(response).to be_success + end + end + + describe '#create' do + let(:params) do + {widget: {name: 'a widget'}} + end + + subject { spree_post :create, params } + + it 'creates the resource' do + expect { subject }.to change { Widget.count }.by(1) + end + + context 'failure' do + let(:params) do + {widget: {name: ''}} # blank name generates an error + end + + it 'sets a flash error' do + subject + expect(flash[:error]).to eq assigns(:widget).errors.full_messages.join(', ') + end + end + + context 'without any parameters' do + let(:params) { {} } + + before do + allow_any_instance_of(Widget).to receive(:name).and_return('some name') + end + + it 'creates the resource' do + expect { subject }.to change { Widget.count }.by(1) + end + end + end + + describe '#update' do + let(:widget) { Widget.create!(name: 'a widget') } + + let(:params) do + { + id: widget.to_param, + widget: {name: 'widget renamed'}, + } + end + + subject { spree_put :update, params } + + it 'updates the resource' do + expect { subject }.to change { widget.reload.name }.from('a widget').to('widget renamed') + end + + context 'failure' do + let(:params) do + { + id: widget.to_param, + widget: {name: ''}, # a blank name will trigger a validation error + } + end + + it 'sets a flash error' do + subject + expect(flash[:error]).to eq assigns(:widget).errors.full_messages.join(', ') + end + end + end + + describe '#destroy' do + let!(:widget) { Widget.create!(name: 'a widget') } + let(:params) { {id: widget.id} } + + subject { + spree_delete :destroy, params + } + + it 'destroys the resource' do + expect { subject }.to change { Widget.count }.from(1).to(0) + end + end + + describe '#update_positions' do + let(:widget_1) { Widget.create!(name: 'widget 1', position: 1) } + let(:widget_2) { Widget.create!(name: 'widget 2', position: 2) } + + subject { spree_post :update_positions, id: widget_1.to_param, positions: { widget_1.id => '2', widget_2.id => '1' }, format: 'js' } + + it 'updates the position of widget 1' do + expect { subject }.to change { widget_1.reload.position }.from(1).to(2) + end + + it 'updates the position of widget 2' do + expect { subject }.to change { widget_2.reload.position }.from(2).to(1) + end + + it 'touches updated_at' do + expect { subject }.to change { widget_1.reload.updated_at } + end + end +end diff --git a/backend/spec/controllers/spree/admin/return_authorizations_controller_spec.rb b/backend/spec/controllers/spree/admin/return_authorizations_controller_spec.rb new file mode 100644 index 00000000000..f2927006854 --- /dev/null +++ b/backend/spec/controllers/spree/admin/return_authorizations_controller_spec.rb @@ -0,0 +1,225 @@ +require 'spec_helper' + +describe Spree::Admin::ReturnAuthorizationsController, :type => :controller do + stub_authorization! + + # Regression test for #1370 #3 + let!(:order) { create(:shipped_order, line_items_count: 3) } + let!(:return_authorization_reason) { create(:return_authorization_reason) } + let(:inventory_unit_1) { order.inventory_units.order('id asc')[0] } + let(:inventory_unit_2) { order.inventory_units.order('id asc')[1] } + let(:inventory_unit_3) { order.inventory_units.order('id asc')[2] } + + describe "#load_return_authorization_reasons" do + let!(:inactive_rma_reason) { create(:return_authorization_reason, active: false) } + + context "return authorization has an associated inactive reason" do + let!(:other_inactive_rma_reason) { create(:return_authorization_reason, active: false) } + let(:return_authorization) { create(:return_authorization, reason: inactive_rma_reason) } + + it "loads all the active rma reasons" do + spree_get :edit, id: return_authorization.to_param, order_id: return_authorization.order.to_param + expect(assigns(:reasons)).to include(return_authorization_reason) + expect(assigns(:reasons)).to include(inactive_rma_reason) + expect(assigns(:reasons)).not_to include(other_inactive_rma_reason) + end + end + + context "return authorization has an associated active reason" do + let(:return_authorization) { create(:return_authorization, reason: return_authorization_reason) } + + it "loads all the active rma reasons" do + spree_get :edit, id: return_authorization.to_param, order_id: return_authorization.order.to_param + expect(assigns(:reasons)).to eq [return_authorization_reason] + end + end + + context "return authorization doesn't have an associated reason" do + it "loads all the active rma reasons" do + spree_get :new, order_id: order.to_param + expect(assigns(:reasons)).to eq [return_authorization_reason] + end + end + end + + describe "#load_return_items" do + shared_context 'without existing return items' do + context 'without existing return items' do + it 'has 3 new @form_return_items' do + subject + expect(assigns(:form_return_items).size).to eq 3 + expect(assigns(:form_return_items).select(&:new_record?).size).to eq 3 + end + end + end + + shared_context 'with existing return items' do + context 'with existing return items' do + let!(:return_item_1) { create(:return_item, inventory_unit: inventory_unit_1, return_authorization: return_authorization) } + + it 'has 1 existing return item and 2 new return items' do + subject + expect(assigns(:form_return_items).size).to eq 3 + expect(assigns(:form_return_items).select(&:persisted?)).to eq [return_item_1] + expect(assigns(:form_return_items).select(&:new_record?).size).to eq 2 + end + end + end + + context '#new' do + subject { spree_get :new, order_id: order.to_param } + + include_context 'without existing return items' + end + + context '#edit' do + subject do + spree_get :edit, { + id: return_authorization.to_param, + order_id: order.to_param, + } + end + + let(:return_authorization) { create(:return_authorization, order: order) } + + include_context 'without existing return items' + include_context 'with existing return items' + end + + context '#create failed' do + subject do + spree_post :create, { + return_authorization: {return_authorization_reason_id: -1}, # invalid reason_id + order_id: order.to_param, + } + end + + include_context 'without existing return items' + end + + context '#update failed' do + subject do + spree_put :update, { + return_authorization: {return_authorization_reason_id: -1}, # invalid reason_id + id: return_authorization.to_param, + order_id: order.to_param, + } + end + + let(:return_authorization) { create(:return_authorization, order: order) } + + include_context 'without existing return items' + include_context 'with existing return items' + end + end + + describe "#load_reimbursement_types" do + let(:order) { create(:order) } + let!(:inactive_reimbursement_type) { create(:reimbursement_type, active: false) } + let!(:first_active_reimbursement_type) { create(:reimbursement_type) } + let!(:second_active_reimbursement_type) { create(:reimbursement_type) } + + before do + spree_get :new, order_id: order.to_param + end + + it "loads all the active reimbursement types" do + expect(assigns(:reimbursement_types)).to include(first_active_reimbursement_type) + expect(assigns(:reimbursement_types)).to include(second_active_reimbursement_type) + expect(assigns(:reimbursement_types)).not_to include(inactive_reimbursement_type) + end + end + + context '#create' do + let(:stock_location) { create(:stock_location) } + + subject { spree_post :create, params } + + let(:params) do + { + order_id: order.to_param, + return_authorization: return_authorization_params, + } + end + + let(:return_authorization_params) do + { + memo: "", + stock_location_id: stock_location.id, + return_authorization_reason_id: return_authorization_reason.id, + } + end + + it "can create a return authorization" do + subject + expect(response).to redirect_to spree.admin_order_return_authorizations_path(order) + end + end + + context '#update' do + let(:return_authorization) { create(:return_authorization, order: order) } + + let(:params) do + { + id: return_authorization.to_param, + order_id: order.to_param, + return_authorization: return_authorization_params, + } + end + let(:return_authorization_params) do + { + memo: "", + return_items_attributes: return_items_params, + } + end + + subject { spree_put :update, params } + + context "adding an item" do + let(:return_items_params) do + { + '0' => {inventory_unit_id: inventory_unit_1.to_param} + } + end + + context 'without existing items' do + it 'creates a new item' do + expect { subject }.to change { Spree::ReturnItem.count }.by(1) + end + end + + context 'with existing completed items' do + let!(:completed_return_item) do + create(:return_item, { + return_authorization: return_authorization, + inventory_unit: inventory_unit_1, + reception_status: 'received', + }) + end + + it 'does not create new items' do + expect { subject }.to_not change { Spree::ReturnItem.count } + expect(assigns[:return_authorization].errors['return_items.inventory_unit']).to eq ["#{inventory_unit_1.id} has already been taken by return item #{completed_return_item.id}"] + end + end + end + + context "removing an item" do + let!(:return_item) do + create(:return_item, return_authorization: return_authorization, inventory_unit: inventory_unit_1) + end + + let(:return_items_params) do + { + '0' => {id: return_item.to_param, _destroy: '1'} + } + end + + context 'with existing items' do + it 'removes the item' do + expect { subject }.to change { Spree::ReturnItem.count }.by(-1) + end + end + end + end +end diff --git a/backend/spec/controllers/spree/admin/return_items_controller_spec.rb b/backend/spec/controllers/spree/admin/return_items_controller_spec.rb new file mode 100644 index 00000000000..009e22f0b47 --- /dev/null +++ b/backend/spec/controllers/spree/admin/return_items_controller_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Spree::Admin::ReturnItemsController, :type => :controller do + stub_authorization! + + describe '#update' do + let(:customer_return) { create(:customer_return) } + let(:return_item) { customer_return.return_items.first } + let(:old_acceptance_status) { 'accepted' } + let(:new_acceptance_status) { 'rejected' } + + subject do + spree_put :update, id: return_item.to_param, return_item: {acceptance_status: new_acceptance_status} + end + + it 'updates the return item' do + expect { + subject + }.to change { return_item.reload.acceptance_status }.from(old_acceptance_status).to(new_acceptance_status) + end + + it 'redirects to the custome return' do + subject + expect(response).to redirect_to spree.edit_admin_order_customer_return_path(customer_return.order, customer_return) + end + end +end diff --git a/backend/spec/controllers/spree/admin/root_controller_spec.rb b/backend/spec/controllers/spree/admin/root_controller_spec.rb new file mode 100644 index 00000000000..b3fe1047f5e --- /dev/null +++ b/backend/spec/controllers/spree/admin/root_controller_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Spree::Admin::RootController do + + context "unauthorized request" do + + before :each do + Spree::Admin::RootController.any_instance.stub(:spree_current_user).and_return(nil) + end + + it "redirects to orders path by default" do + get :index, use_route: 'admin' + + expect(response).to redirect_to '/admin/orders' + end + end + + context "authorized request" do + stub_authorization! + + it "redirects to orders path by default" do + get :index, use_route: 'admin' + + expect(response).to redirect_to '/admin/orders' + end + + it "redirects to wherever admin_root_redirects_path tells it to" do + Spree::Admin::RootController.any_instance.stub(:admin_root_redirect_path).and_return('/grooot') + + get :index, use_route: 'admin' + + expect(response).to redirect_to '/grooot' + end + end +end diff --git a/backend/spec/controllers/spree/admin/search_controller_spec.rb b/backend/spec/controllers/spree/admin/search_controller_spec.rb new file mode 100644 index 00000000000..52ac0f7e8d3 --- /dev/null +++ b/backend/spec/controllers/spree/admin/search_controller_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Spree::Admin::SearchController, :type => :controller do + stub_authorization! + # Regression test for ernie/ransack#176 + let(:user) { create(:user, :email => "spree_commerce@example.com") } + + before do + user.ship_address = create(:address) + user.bill_address = create(:address) + user.save + end + + it "can find a user by their email "do + spree_xhr_get :users, :q => user.email + expect(assigns[:users]).to include(user) + end + + it "can find a user by their ship address's first name" do + spree_xhr_get :users, :q => user.ship_address.firstname + expect(assigns[:users]).to include(user) + end + + it "can find a user by their ship address's last name" do + spree_xhr_get :users, :q => user.ship_address.lastname + expect(assigns[:users]).to include(user) + end + + it "can find a user by their bill address's first name" do + spree_xhr_get :users, :q => user.bill_address.firstname + expect(assigns[:users]).to include(user) + end + + it "can find a user by their bill address's last name" do + spree_xhr_get :users, :q => user.bill_address.lastname + expect(assigns[:users]).to include(user) + end + +end diff --git a/backend/spec/controllers/spree/admin/shipping_methods_controller_spec.rb b/backend/spec/controllers/spree/admin/shipping_methods_controller_spec.rb new file mode 100644 index 00000000000..fb25ec446de --- /dev/null +++ b/backend/spec/controllers/spree/admin/shipping_methods_controller_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Spree::Admin::ShippingMethodsController, :type => :controller do + stub_authorization! + + # Regression test for #1240 + it "should not hard-delete shipping methods" do + shipping_method = stub_model(Spree::ShippingMethod) + allow(Spree::ShippingMethod).to receive_messages :find => shipping_method + expect(shipping_method.deleted_at).to be_nil + spree_delete :destroy, :id => 1 + expect(shipping_method.reload.deleted_at).not_to be_nil + end +end diff --git a/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb b/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb new file mode 100644 index 00000000000..7c2063f6272 --- /dev/null +++ b/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +module Spree + module Admin + describe StockItemsController, :type => :controller do + stub_authorization! + + context "formats" do + let!(:stock_item) { create(:variant).stock_items.first } + + it "destroy stock item via js" do + expect { + spree_delete :destroy, format: :js, id: stock_item + }.to change{ StockItem.count }.by(-1) + end + end + end + end +end diff --git a/backend/spec/controllers/spree/admin/stock_locations_controller_spec.rb b/backend/spec/controllers/spree/admin/stock_locations_controller_spec.rb new file mode 100644 index 00000000000..626ddbf9e79 --- /dev/null +++ b/backend/spec/controllers/spree/admin/stock_locations_controller_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +module Spree + module Admin + describe StockLocationsController, :type => :controller do + stub_authorization! + + # Regression for #4272 + context "with no countries present" do + it "cannot create a new stock location" do + spree_get :new + expect(flash[:error]).to eq(Spree.t(:stock_locations_need_a_default_country)) + expect(response).to redirect_to(spree.admin_stock_locations_path) + end + end + + context "with a default country present" do + before do + country = FactoryGirl.create(:country) + Spree::Config[:default_country_id] = country.id + end + + it "can create a new stock location" do + spree_get :new + expect(response).to be_success + end + end + + context "with a country with the ISO code of 'US' existing" do + before do + FactoryGirl.create(:country, iso: 'US') + end + + it "can create a new stock location" do + spree_get :new + expect(response).to be_success + end + end + end + end +end \ No newline at end of file diff --git a/backend/spec/controllers/spree/admin/stock_transfers_controller_spec.rb b/backend/spec/controllers/spree/admin/stock_transfers_controller_spec.rb new file mode 100644 index 00000000000..23f372254e5 --- /dev/null +++ b/backend/spec/controllers/spree/admin/stock_transfers_controller_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +module Spree + describe Admin::StockTransfersController, :type => :controller do + stub_authorization! + + let!(:stock_transfer1) { + StockTransfer.create do |transfer| + transfer.source_location_id = 1 + transfer.destination_location_id = 2 + transfer.reference = 'PO 666' + end } + + let!(:stock_transfer2) { + StockTransfer.create do |transfer| + transfer.source_location_id = 3 + transfer.destination_location_id = 4 + transfer.reference = 'PO 666' + end } + + + context "#index" do + it "gets all transfers without search criteria" do + spree_get :index + expect(assigns[:stock_transfers].count).to eq 2 + end + + it "searches by source location" do + spree_get :index, :q => { :source_location_id_eq => 1 } + expect(assigns[:stock_transfers].count).to eq 1 + expect(assigns[:stock_transfers]).to include(stock_transfer1) + end + + it "searches by destination location" do + spree_get :index, :q => { :destination_location_id_eq => 4 } + expect(assigns[:stock_transfers].count).to eq 1 + expect(assigns[:stock_transfers]).to include(stock_transfer2) + end + end + end +end diff --git a/backend/spec/controllers/spree/admin/tax_categories_controller_spec.rb b/backend/spec/controllers/spree/admin/tax_categories_controller_spec.rb new file mode 100644 index 00000000000..4f6e1799634 --- /dev/null +++ b/backend/spec/controllers/spree/admin/tax_categories_controller_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +module Spree + module Admin + describe TaxCategoriesController, :type => :controller do + stub_authorization! + + describe 'GET #index' do + subject { spree_get :index } + + it 'should be successful' do + expect(subject).to be_success + end + end + + describe 'PUT #update' do + let(:tax_category) { create :tax_category } + + subject { spree_put :update, {id: tax_category.id, tax_category: { name: 'Foo', tax_code: 'Bar' }}} + + it 'should redirect' do + expect(subject).to be_redirect + end + + it 'should update' do + subject + tax_category.reload + expect(tax_category.name).to eq('Foo') + expect(tax_category.tax_code).to eq('Bar') + end + end + end + end +end diff --git a/backend/spec/controllers/spree/admin/users_controller_spec.rb b/backend/spec/controllers/spree/admin/users_controller_spec.rb new file mode 100644 index 00000000000..1ebff82a104 --- /dev/null +++ b/backend/spec/controllers/spree/admin/users_controller_spec.rb @@ -0,0 +1,155 @@ +require 'spec_helper' +require 'spree/testing_support/bar_ability' + +describe Spree::Admin::UsersController, :type => :controller do + let(:user) { create(:user) } + let(:mock_user) { mock_model Spree.user_class } + + before do + allow(controller).to receive_messages :spree_current_user => user + user.spree_roles.clear + end + + context "#show" do + before do + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + end + + it "redirects to edit" do + spree_get :show, id: user.id + expect(response).to redirect_to spree.edit_admin_user_path(user) + end + end + + context '#authorize_admin' do + before { use_mock_user } + + it 'grant access to users with an admin role' do + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + spree_post :index + expect(response).to render_template :index + end + + it "allows admins to update a user's API key" do + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + expect(mock_user).to receive(:generate_spree_api_key!).and_return(true) + spree_put :generate_api_key, id: mock_user.id + expect(response).to redirect_to(spree.edit_admin_user_path(mock_user)) + end + + it "allows admins to clear a user's API key" do + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + expect(mock_user).to receive(:clear_spree_api_key!).and_return(true) + spree_put :clear_api_key, id: mock_user.id + expect(response).to redirect_to(spree.edit_admin_user_path(mock_user)) + end + + it 'deny access to users with an bar role' do + user.spree_roles << Spree::Role.find_or_create_by(name: 'bar') + Spree::Ability.register_ability(BarAbility) + spree_post :index + expect(response).to redirect_to '/unauthorized' + end + + it 'deny access to users with an bar role' do + user.spree_roles << Spree::Role.find_or_create_by(name: 'bar') + Spree::Ability.register_ability(BarAbility) + spree_post :update, { id: '9' } + expect(response).to redirect_to '/unauthorized' + end + + it 'deny access to users without an admin role' do + allow(user).to receive_messages :has_spree_role? => false + spree_post :index + expect(response).to redirect_to '/unauthorized' + end + end + + describe "#create" do + before do + use_mock_user + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + end + + it "can create a shipping_address" do + expect(Spree.user_class).to receive(:new).with(hash_including( + "ship_address_attributes" => { "city" => "New York" } + )) + spree_post :create, { :user => { :ship_address_attributes => { :city => "New York" } } } + end + + it "can create a billing_address" do + expect(Spree.user_class).to receive(:new).with(hash_including( + "bill_address_attributes" => { "city" => "New York" } + )) + spree_post :create, { :user => { :bill_address_attributes => { :city => "New York" } } } + end + end + + describe "#update" do + before do + use_mock_user + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + end + + it "allows shipping address attributes through" do + expect(mock_user).to receive(:update_attributes).with(hash_including( + "ship_address_attributes" => { "city" => "New York" } + )) + spree_put :update, { :id => mock_user.id, :user => { :ship_address_attributes => { :city => "New York" } } } + end + + it "allows billing address attributes through" do + expect(mock_user).to receive(:update_attributes).with(hash_including( + "bill_address_attributes" => { "city" => "New York" } + )) + spree_put :update, { :id => mock_user.id, :user => { :bill_address_attributes => { :city => "New York" } } } + end + end + + describe "#orders" do + let(:order) { create(:order) } + before do + user.orders << order + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + end + + it "assigns a list of the users orders" do + spree_get :orders, { :id => user.id } + expect(assigns[:orders].count).to eq 1 + expect(assigns[:orders].first).to eq order + end + + it "assigns a ransack search for Spree::Order" do + spree_get :orders, { :id => user.id } + expect(assigns[:search]).to be_a Ransack::Search + expect(assigns[:search].klass).to eq Spree::Order + end + end + + describe "#items" do + let(:order) { create(:order) } + before do + user.orders << order + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + end + + it "assigns a list of the users orders" do + spree_get :items, { :id => user.id } + expect(assigns[:orders].count).to eq 1 + expect(assigns[:orders].first).to eq order + end + + it "assigns a ransack search for Spree::Order" do + spree_get :items, { :id => user.id } + expect(assigns[:search]).to be_a Ransack::Search + expect(assigns[:search].klass).to eq Spree::Order + end + end +end + +def use_mock_user + allow(mock_user).to receive(:save).and_return(true) + allow(Spree.user_class).to receive(:find).with(mock_user.id.to_s).and_return(mock_user) + allow(Spree.user_class).to receive(:new).and_return(mock_user) +end diff --git a/backend/spec/controllers/spree/admin/variants_controller_spec.rb b/backend/spec/controllers/spree/admin/variants_controller_spec.rb new file mode 100644 index 00000000000..00bfa6d4d93 --- /dev/null +++ b/backend/spec/controllers/spree/admin/variants_controller_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +module Spree + module Admin + describe VariantsController, :type => :controller do + stub_authorization! + + describe "#index" do + let(:product) { create(:product) } + let!(:variant_1) { create(:variant, product: product) } + let!(:variant_2) { create(:variant, product: product) } + + context "deleted is not requested" do + it "assigns the variants for a requested product" do + spree_get :index, product_id: product.slug + expect(assigns(:collection)).to include variant_1 + expect(assigns(:collection)).to include variant_2 + end + end + + context "deleted is requested" do + before { variant_2.destroy } + it "assigns only deleted variants for a requested product" do + spree_get :index, product_id: product.slug, deleted: "on" + expect(assigns(:collection)).not_to include variant_1 + expect(assigns(:collection)).to include variant_2 + end + end + end + end + end +end diff --git a/backend/spec/features/admin/configuration/analytics_tracker_spec.rb b/backend/spec/features/admin/configuration/analytics_tracker_spec.rb new file mode 100644 index 00000000000..1306833c3ec --- /dev/null +++ b/backend/spec/features/admin/configuration/analytics_tracker_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe "Analytics Tracker", :type => :feature do + stub_authorization! + + context "index" do + before(:each) do + 2.times { create(:tracker, :environment => "test") } + visit spree.admin_path + click_link "Configuration" + click_link "Analytics Tracker" + end + + it "should have the right content" do + expect(page).to have_content("Analytics Trackers") + end + + it "should have the right tabular values displayed" do + within_row(1) do + expect(column_text(1)).to eq("A100") + expect(column_text(2)).to eq("Test") + expect(column_text(3)).to eq("Yes") + end + + within_row(2) do + expect(column_text(1)).to eq("A100") + expect(column_text(2)).to eq("Test") + expect(column_text(3)).to eq("Yes") + end + end + end + + context "create" do + before(:each) do + visit spree.admin_path + click_link "Configuration" + click_link "Analytics Tracker" + end + + it "should be able to create a new analytics tracker" do + click_link "admin_new_tracker_link" + fill_in "tracker_analytics_id", :with => "A100" + select "Test", :from => "tracker-env" + click_button "Create" + + expect(page).to have_content("successfully created!") + within_row(1) do + expect(column_text(1)).to eq("A100") + expect(column_text(2)).to eq("Test") + expect(column_text(3)).to eq("Yes") + end + end + end +end diff --git a/backend/spec/features/admin/configuration/countries_spec.rb b/backend/spec/features/admin/configuration/countries_spec.rb new file mode 100644 index 00000000000..aaaed970e78 --- /dev/null +++ b/backend/spec/features/admin/configuration/countries_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +module Spree + describe "Countries", :type => :feature do + stub_authorization! + + it "deletes a state", js: true do + visit spree.admin_countries_path + click_link "New Country" + + fill_in "Name", with: "Brazil" + fill_in "Iso Name", with: "BRL" + click_button "Create" + + accept_alert do + click_icon :trash + end + wait_for_ajax + + expect { Country.find(country.id) }.to raise_error + end + end +end diff --git a/backend/spec/features/admin/configuration/general_settings_spec.rb b/backend/spec/features/admin/configuration/general_settings_spec.rb new file mode 100644 index 00000000000..53eed3807a8 --- /dev/null +++ b/backend/spec/features/admin/configuration/general_settings_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe "General Settings", type: :feature, js: true do + stub_authorization! + + before(:each) do + store = create(:store, name: 'Test Store', url: 'test.example.org', + mail_from_address: 'test@example.org') + visit spree.admin_path + click_link "Configuration" + click_link "General Settings" + end + + context "visiting general settings (admin)" do + it "should have the right content" do + expect(page).to have_content("General Settings") + expect(find("#store_name").value).to eq("Test Store") + expect(find("#store_url").value).to eq("test.example.org") + expect(find("#store_mail_from_address").value).to eq("test@example.org") + end + end + + context "editing general settings (admin)" do + it "should be able to update the site name" do + fill_in "store_name", with: "Spree Demo Site99" + fill_in "store_mail_from_address", with: "spree@example.org" + click_button "Update" + + assert_successful_update_message(:general_settings) + expect(find("#store_name").value).to eq("Spree Demo Site99") + expect(find("#store_mail_from_address").value).to eq("spree@example.org") + end + end + + context "clearing the cache" do + it "should clear the cache" do + expect(page).to_not have_content(Spree.t(:clear_cache_ok)) + expect(page).to have_content(Spree.t(:clear_cache_warning)) + + click_button "Clear Cache" + + expect(page).to have_content(Spree.t(:clear_cache_ok)) + end + end +end diff --git a/backend/spec/features/admin/configuration/payment_methods_spec.rb b/backend/spec/features/admin/configuration/payment_methods_spec.rb new file mode 100644 index 00000000000..5dbff0913b8 --- /dev/null +++ b/backend/spec/features/admin/configuration/payment_methods_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe "Payment Methods", :type => :feature do + stub_authorization! + + before(:each) do + visit spree.admin_path + click_link "Configuration" + end + + context "admin visiting payment methods listing page" do + it "should display existing payment methods" do + create(:check_payment_method) + click_link "Payment Methods" + + within("table#listing_payment_methods") do + expect(all("th")[0].text).to eq("Name") + expect(all("th")[1].text).to eq("Provider") + expect(all("th")[2].text).to eq("Environment") + expect(all("th")[3].text).to eq("Display") + expect(all("th")[4].text).to eq("Active") + end + + within('table#listing_payment_methods') do + expect(page).to have_content("Spree::PaymentMethod::Check") + end + end + end + + context "admin creating a new payment method" do + it "should be able to create a new payment method" do + click_link "Payment Methods" + click_link "admin_new_payment_methods_link" + expect(page).to have_content("New Payment Method") + fill_in "payment_method_name", :with => "check90" + fill_in "payment_method_description", :with => "check90 desc" + select "PaymentMethod::Check", :from => "gtwy-type" + click_button "Create" + expect(page).to have_content("successfully created!") + end + end + + context "admin editing a payment method" do + before(:each) do + create(:check_payment_method) + click_link "Payment Methods" + within("table#listing_payment_methods") do + click_icon(:edit) + end + end + + it "should be able to edit an existing payment method" do + fill_in "payment_method_name", :with => "Payment 99" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(find_field("payment_method_name").value).to eq("Payment 99") + end + + it "should display validation errors" do + fill_in "payment_method_name", :with => "" + click_button "Update" + expect(page).to have_content("Name can't be blank") + end + end +end diff --git a/backend/spec/features/admin/configuration/shipping_methods_spec.rb b/backend/spec/features/admin/configuration/shipping_methods_spec.rb new file mode 100644 index 00000000000..8058f8916f6 --- /dev/null +++ b/backend/spec/features/admin/configuration/shipping_methods_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe "Shipping Methods", :type => :feature do + stub_authorization! + let!(:zone) { create(:global_zone) } + let!(:shipping_method) { create(:shipping_method, :zones => [zone]) } + + after do + Capybara.ignore_hidden_elements = true + end + + before do + Capybara.ignore_hidden_elements = false + # HACK: To work around no email prompting on check out + allow_any_instance_of(Spree::Order).to receive_messages(:require_email => false) + create(:check_payment_method, :environment => 'test') + + visit spree.admin_path + click_link "Configuration" + click_link "Shipping Methods" + end + + context "show" do + it "should display existing shipping methods" do + within_row(1) do + expect(column_text(1)).to eq(shipping_method.name) + expect(column_text(2)).to eq(zone.name) + expect(column_text(3)).to eq("Flat rate") + expect(column_text(4)).to eq("Both") + end + end + end + + context "create" do + it "should be able to create a new shipping method" do + click_link "New Shipping Method" + + fill_in "shipping_method_name", :with => "bullock cart" + + within("#shipping_method_categories_field") do + check first("input[type='checkbox']")["name"] + end + + click_on "Create" + expect(current_path).to eql(spree.edit_admin_shipping_method_path(Spree::ShippingMethod.last)) + end + end + + # Regression test for #1331 + context "update" do + it "can change the calculator", :js => true do + within("#listing_shipping_methods") do + click_icon :edit + end + + expect(find(:css, ".calculator-settings-warning")).not_to be_visible + select2_search('Flexible Rate', :from => 'Calculator') + expect(find(:css, ".calculator-settings-warning")).to be_visible + + click_button "Update" + expect(page).not_to have_content("Shipping method is not found") + end + end +end diff --git a/backend/spec/features/admin/configuration/states_spec.rb b/backend/spec/features/admin/configuration/states_spec.rb new file mode 100755 index 00000000000..f2f6737b16e --- /dev/null +++ b/backend/spec/features/admin/configuration/states_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe "States", :type => :feature do + stub_authorization! + + let!(:country) { create(:country) } + + before(:each) do + @hungary = Spree::Country.create!(:name => "Hungary", :iso_name => "Hungary") + end + + # TODO: For whatever reason, rendering of the states page takes a non-trivial amount of time + # Therefore we navigate to it, and wait until what we see is visible + def go_to_states_page + visit spree.admin_country_states_path(country) + counter = 0 + until page.has_css?("#new_state_link") + if counter < 10 + sleep(2) + counter += 1 + else + raise "Could not see new state link!" + end + end + end + + context "admin visiting states listing" do + let!(:state) { create(:state, :country => country) } + + it "should correctly display the states" do + visit spree.admin_country_states_path(country) + expect(page).to have_content(state.name) + end + end + + context "creating and editing states" do + it "should allow an admin to edit existing states", :js => true do + go_to_states_page + set_select2_field("country", country.id) + + click_link "new_state_link" + fill_in "state_name", :with => "Calgary" + fill_in "Abbreviation", :with => "CL" + click_button "Create" + expect(page).to have_content("successfully created!") + expect(page).to have_content("Calgary") + end + + it "should allow an admin to create states for non default countries", :js => true do + go_to_states_page + set_select2_field "#country", @hungary.id + # Just so the change event actually gets triggered in this spec + # It is definitely triggered in the "real world" + page.execute_script("$('#country').trigger('change');") + + click_link "new_state_link" + fill_in "state_name", :with => "Pest megye" + fill_in "Abbreviation", :with => "PE" + click_button "Create" + expect(page).to have_content("successfully created!") + expect(page).to have_content("Pest megye") + expect(find("#s2id_country span").text).to eq("Hungary") + end + + it "should show validation errors", :js => true do + go_to_states_page + set_select2_field("country", country.id) + + click_link "new_state_link" + + fill_in "state_name", :with => "" + fill_in "Abbreviation", :with => "" + click_button "Create" + expect(page).to have_content("Name can't be blank") + end + end +end diff --git a/backend/spec/features/admin/configuration/stock_locations_spec.rb b/backend/spec/features/admin/configuration/stock_locations_spec.rb new file mode 100644 index 00000000000..0b9eb7a875a --- /dev/null +++ b/backend/spec/features/admin/configuration/stock_locations_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe "Stock Locations", :type => :feature do + stub_authorization! + + before(:each) do + country = create(:country) + visit spree.admin_path + click_link "Configuration" + click_link "Stock Locations" + end + + it "can create a new stock location" do + click_link "New Stock Location" + fill_in "Name", with: "London" + check "Active" + click_button "Create" + + expect(page).to have_content("successfully created") + expect(page).to have_content("London") + end + + it "can delete an existing stock location", js: true do + location = create(:stock_location) + visit current_path + + expect(find('#listing_stock_locations')).to have_content("NY Warehouse") + accept_alert do + click_icon :trash + end + # Wait for API request to complete. + wait_for_ajax + visit current_path + expect(page).to have_content("NO STOCK LOCATIONS FOUND") + end + + it "can update an existing stock location" do + create(:stock_location) + visit current_path + + expect(page).to have_content("NY Warehouse") + + click_icon :edit + fill_in "Name", with: "London" + click_button "Update" + + expect(page).to have_content("successfully updated") + expect(page).to have_content("London") + end +end diff --git a/backend/spec/features/admin/configuration/tax_categories_spec.rb b/backend/spec/features/admin/configuration/tax_categories_spec.rb new file mode 100644 index 00000000000..715715ea4b0 --- /dev/null +++ b/backend/spec/features/admin/configuration/tax_categories_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe "Tax Categories", :type => :feature do + stub_authorization! + + before(:each) do + visit spree.admin_path + click_link "Configuration" + end + + context "admin visiting tax categories list" do + it "should display the existing tax categories" do + create(:tax_category, :name => "Clothing", :tax_code => "CL001", :description => "For Clothing") + click_link "Tax Categories" + expect(page).to have_content("Listing Tax Categories") + within_row(1) do + expect(column_text(1)).to eq("Clothing") + expect(column_text(2)).to eq("CL001") + expect(column_text(3)).to eq("For Clothing") + expect(column_text(4)).to eq("No") + end + end + end + + context "admin creating new tax category" do + before(:each) do + click_link "Tax Categories" + click_link "admin_new_tax_categories_link" + end + + it "should be able to create new tax category" do + expect(page).to have_content("New Tax Category") + fill_in "tax_category_name", :with => "sports goods" + fill_in "tax_category_description", :with => "sports goods desc" + click_button "Create" + expect(page).to have_content("successfully created!") + end + + it "should show validation errors if there are any" do + click_button "Create" + expect(page).to have_content("Name can't be blank") + end + end + + context "admin editing a tax category" do + it "should be able to update an existing tax category" do + create(:tax_category) + click_link "Tax Categories" + within_row(1) { click_icon :edit } + fill_in "tax_category_description", :with => "desc 99" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(page).to have_content("desc 99") + end + end +end diff --git a/backend/spec/features/admin/configuration/tax_rates_spec.rb b/backend/spec/features/admin/configuration/tax_rates_spec.rb new file mode 100644 index 00000000000..700e4da0de4 --- /dev/null +++ b/backend/spec/features/admin/configuration/tax_rates_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe "Tax Rates", :type => :feature do + stub_authorization! + + let!(:tax_rate) { create(:tax_rate, :calculator => stub_model(Spree::Calculator)) } + + before do + visit spree.admin_path + click_link "Configuration" + end + + # Regression test for #535 + it "can see a tax rate in the list if the tax category has been deleted" do + tax_rate.tax_category.update_column(:deleted_at, Time.now) + expect { click_link "Tax Rates" }.not_to raise_error + within(:xpath, all("table tbody td")[2].path) do + expect(page).to have_content("N/A") + end + end + + # Regression test for #1422 + it "can create a new tax rate" do + click_link "Tax Rates" + click_link "New Tax Rate" + fill_in "Rate", :with => "0.05" + click_button "Create" + expect(page).to have_content("Tax Rate has been successfully created!") + end +end diff --git a/backend/spec/features/admin/configuration/taxonomies_spec.rb b/backend/spec/features/admin/configuration/taxonomies_spec.rb new file mode 100644 index 00000000000..0b2eff61851 --- /dev/null +++ b/backend/spec/features/admin/configuration/taxonomies_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe "Taxonomies", :type => :feature do + stub_authorization! + + before(:each) do + visit spree.admin_path + click_link "Configuration" + end + + context "show" do + it "should display existing taxonomies" do + create(:taxonomy, :name => 'Brand') + create(:taxonomy, :name => 'Categories') + click_link "Taxonomies" + within_row(1) { expect(page).to have_content("Brand") } + within_row(2) { expect(page).to have_content("Categories") } + end + end + + context "create" do + before(:each) do + click_link "Taxonomies" + click_link "admin_new_taxonomy_link" + end + + it "should allow an admin to create a new taxonomy" do + expect(page).to have_content("New Taxonomy") + fill_in "taxonomy_name", :with => "sports" + click_button "Create" + expect(page).to have_content("successfully created!") + end + + it "should display validation errors" do + fill_in "taxonomy_name", :with => "" + click_button "Create" + expect(page).to have_content("can't be blank") + end + end + + context "edit" do + it "should allow an admin to update an existing taxonomy" do + create(:taxonomy) + click_link "Taxonomies" + within_row(1) { click_icon :edit } + fill_in "taxonomy_name", :with => "sports 99" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(page).to have_content("sports 99") + end + end +end diff --git a/backend/spec/features/admin/configuration/zones_spec.rb b/backend/spec/features/admin/configuration/zones_spec.rb new file mode 100644 index 00000000000..b5a87d897d2 --- /dev/null +++ b/backend/spec/features/admin/configuration/zones_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe "Zones", :type => :feature do + stub_authorization! + + before(:each) do + Spree::Zone.delete_all + visit spree.admin_path + click_link "Configuration" + end + + context "show" do + it "should display existing zones" do + create(:zone, :name => "eastern", :description => "zone is eastern") + create(:zone, :name => "western", :description => "cool san fran") + click_link "Zones" + + within_row(1) { expect(page).to have_content("eastern") } + within_row(2) { expect(page).to have_content("western") } + + click_link "zones_order_by_description_title" + + within_row(1) { expect(page).to have_content("western") } + within_row(2) { expect(page).to have_content("eastern") } + end + end + + context "create" do + it "should allow an admin to create a new zone" do + click_link "Zones" + click_link "admin_new_zone_link" + expect(page).to have_content("New Zone") + fill_in "zone_name", :with => "japan" + fill_in "zone_description", :with => "japanese time zone" + click_button "Create" + expect(page).to have_content("successfully created!") + end + end +end diff --git a/backend/spec/features/admin/homepage_spec.rb b/backend/spec/features/admin/homepage_spec.rb new file mode 100644 index 00000000000..a2484ec95bb --- /dev/null +++ b/backend/spec/features/admin/homepage_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe "Homepage", :type => :feature do + + context 'as admin user' do + stub_authorization! + + context "visiting the homepage" do + before(:each) do + visit spree.admin_path + end + + it "should have the header text 'Listing Orders'" do + within('h1') { expect(page).to have_content("Listing Orders") } + end + + it "should have a link to overview" do + within(:xpath, ".//figure[@data-hook='logo-wrapper']") { page.find(:xpath, "a[@href='/admin']") } + end + + it "should have a link to orders" do + page.find_link("Orders")['/admin/orders'] + end + + it "should have a link to products" do + page.find_link("Products")['/admin/products'] + end + + it "should have a link to reports" do + page.find_link("Reports")['/admin/reports'] + end + + it "should have a link to configuration" do + page.find_link("Configuration")['/admin/configurations'] + end + end + + context "visiting the products tab" do + before(:each) do + visit spree.admin_products_path + end + + it "should have a link to products" do + within('#sub-menu') { page.find_link("Products")['/admin/products'] } + end + + it "should have a link to option types" do + within('#sub-menu') { page.find_link("Option Types")['/admin/option_types'] } + end + + it "should have a link to properties" do + within('#sub-menu') { page.find_link("Properties")['/admin/properties'] } + end + + it "should have a link to prototypes" do + within('#sub-menu') { page.find_link("Prototypes")['/admin/prototypes'] } + end + end + end + + context 'as fakedispatch user' do + + before do + allow_any_instance_of(Spree::Admin::BaseController).to receive(:spree_current_user).and_return(nil) + end + + custom_authorization! do |user| + can [:admin, :edit, :index, :read], Spree::Order + end + + it 'should only display tabs fakedispatch has access to' do + visit spree.admin_path + expect(page).to have_link('Orders') + expect(page).not_to have_link('Products') + expect(page).not_to have_link('Promotions') + expect(page).not_to have_link('Reports') + expect(page).not_to have_link('Configuration') + end + end + +end diff --git a/backend/spec/features/admin/locale_spec.rb b/backend/spec/features/admin/locale_spec.rb new file mode 100644 index 00000000000..9c0209f1a81 --- /dev/null +++ b/backend/spec/features/admin/locale_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe "setting locale", :type => :feature do + stub_authorization! + + before do + I18n.locale = I18n.default_locale + I18n.backend.store_translations(:fr, + :date => { + :month_names => [], + }, + :spree => { + :admin => { + :tab => { :orders => "Ordres" } + }, + :listing_orders => "Ordres", + }) + Spree::Backend::Config[:locale] = "fr" + end + + after do + I18n.locale = I18n.default_locale + Spree::Backend::Config[:locale] = "en" + end + + it "should be in french" do + visit spree.admin_path + click_link "Ordres" + expect(page).to have_content("Ordres") + end +end diff --git a/backend/spec/features/admin/orders/adjustments_promotions_spec.rb b/backend/spec/features/admin/orders/adjustments_promotions_spec.rb new file mode 100644 index 00000000000..1a15aec74c1 --- /dev/null +++ b/backend/spec/features/admin/orders/adjustments_promotions_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe "Adjustments Promotions", :type => :feature do + stub_authorization! + + before(:each) do + promotion = create(:promotion_with_item_adjustment, + :name => "$10 off", + :path => 'test', + :code => "10_off", + :starts_at => 1.day.ago, + :expires_at => 1.day.from_now, + :adjustment_rate => 10) + + order = create(:order_with_totals) + line_item = order.line_items.first + # so we can be sure of a determinate price in our assertions + line_item.update_column(:price, 10) + + visit spree.admin_order_adjustments_path(order) + end + + context "admin adding a promotion" do + context "successfully" do + it "should create a new adjustment", :js => true do + fill_in "coupon_code", :with => "10_off" + click_button "Add Coupon Code" + expect(page).to have_content("$10 off") + expect(page).to have_content("-$10.00") + end + end + + context "for non-existing promotion" do + it "should show an error message", :js => true do + fill_in "coupon_code", :with => "does_not_exist" + click_button "Add Coupon Code" + expect(page).to have_content("doesn't exist.") + end + end + + context "for already applied promotion" do + it "should show an error message", :js => true do + fill_in "coupon_code", :with => "10_off" + click_button "Add Coupon Code" + expect(page).to have_content('-$10.00') + + fill_in "coupon_code", :with => "10_off" + click_button "Add Coupon Code" + expect(page).to have_content("already been applied") + end + end + end +end diff --git a/backend/spec/features/admin/orders/adjustments_spec.rb b/backend/spec/features/admin/orders/adjustments_spec.rb new file mode 100644 index 00000000000..e3eb5f4afda --- /dev/null +++ b/backend/spec/features/admin/orders/adjustments_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe "Adjustments", :type => :feature do + stub_authorization! + + let!(:order) { create(:completed_order_with_totals, line_items_count: 5) } + let!(:line_item) do + line_item = order.line_items.first + # so we can be sure of a determinate price in our assertions + line_item.update_column(:price, 10) + line_item + end + + let!(:tax_adjustment) do + create(:tax_adjustment, + :adjustable => line_item, + :state => 'closed', + :order => order, + :label => "VAT 5%", + :amount => 10) + end + + let!(:adjustment) { order.adjustments.create!(order: order, label: 'Rebate', amount: 10) } + + before(:each) do + # To ensure the order totals are correct + order.update_totals + order.persist_totals + + visit spree.admin_path + click_link "Orders" + within_row(1) { click_icon :edit } + click_link "Adjustments" + end + + after :each do + order.reload.all_adjustments.each do |adjustment| + expect(adjustment.order_id).to equal(order.id) + end + end + + context "admin managing adjustments" do + it "should display the correct values for existing order adjustments" do + within_row(1) do + expect(column_text(2)).to eq("VAT 5%") + expect(column_text(3)).to eq("$10.00") + end + end + + it "only shows eligible adjustments" do + expect(page).not_to have_content("ineligible") + end + end + + context "admin creating a new adjustment" do + before(:each) do + click_link "New Adjustment" + end + + context "successfully" do + it "should create a new adjustment" do + fill_in "adjustment_amount", :with => "10" + fill_in "adjustment_label", :with => "rebate" + click_button "Continue" + expect(page).to have_content("successfully created!") + expect(page).to have_content("Total: $180.00") + end + end + + context "with validation errors" do + it "should not create a new adjustment" do + fill_in "adjustment_amount", :with => "" + fill_in "adjustment_label", :with => "" + click_button "Continue" + expect(page).to have_content("Label can't be blank") + expect(page).to have_content("Amount is not a number") + end + end + end + + context "admin editing an adjustment" do + + before(:each) do + within_row(2) { click_icon :edit } + end + + context "successfully" do + it "should update the adjustment" do + fill_in "adjustment_amount", :with => "99" + fill_in "adjustment_label", :with => "rebate 99" + click_button "Continue" + expect(page).to have_content("successfully updated!") + expect(page).to have_content("rebate 99") + within(".adjustments") do + expect(page).to have_content("$99.00") + end + + expect(page).to have_content("Total: $259.00") + end + end + + context "with validation errors" do + it "should not update the adjustment" do + fill_in "adjustment_amount", :with => "" + fill_in "adjustment_label", :with => "" + click_button "Continue" + expect(page).to have_content("Label can't be blank") + expect(page).to have_content("Amount is not a number") + end + end + end + + context "deleting an adjustment" do + it "should not be possible if adjustment is closed" do + within_row(1) do + expect(page).not_to have_css('.fa-trash') + end + end + + it "should update the total", :js => true do + accept_alert do + within_row(2) do + click_icon(:trash) + end + end + + expect(page).to have_content(/TOTAL: ?\$170\.00/) + end + end +end diff --git a/backend/spec/features/admin/orders/cancelling_and_resuming_spec.rb b/backend/spec/features/admin/orders/cancelling_and_resuming_spec.rb new file mode 100644 index 00000000000..429f4104b0e --- /dev/null +++ b/backend/spec/features/admin/orders/cancelling_and_resuming_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe "Cancelling + Resuming", :type => :feature do + + stub_authorization! + + let(:user) { double(id: 123, has_spree_role?: true, spree_api_key: 'fake') } + + before do + allow_any_instance_of(Spree::Admin::BaseController).to receive(:try_spree_current_user).and_return(user) + end + + let(:order) do + order = create(:order) + order.update_columns({ + :state => 'complete', + :completed_at => Time.now + }) + order + end + + it "can cancel an order" do + visit spree.edit_admin_order_path(order.number) + click_button 'cancel' + within(".additional-info") do + within(".state") do + expect(page).to have_content("canceled") + end + end + end + + context "with a cancelled order" do + before do + order.update_column(:state, 'canceled') + end + + it "can resume an order" do + visit spree.edit_admin_order_path(order.number) + click_button 'resume' + within(".additional-info") do + within(".state") do + expect(page).to have_content("resumed") + end + end + end + end +end diff --git a/backend/spec/features/admin/orders/customer_details_spec.rb b/backend/spec/features/admin/orders/customer_details_spec.rb new file mode 100644 index 00000000000..1ff8df2e5f3 --- /dev/null +++ b/backend/spec/features/admin/orders/customer_details_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' + +describe "Customer Details", type: :feature, js: true do + stub_authorization! + + let(:country) { create(:country, name: "Kangaland") } + let(:state) { create(:state, name: "Alabama", country: country) } + let!(:shipping_method) { create(:shipping_method, display_on: "front_end") } + let!(:order) { create(:order, state: 'complete', completed_at: "2011-02-01 12:36:15") } + let!(:product) { create(:product_in_stock) } + + # We need a unique name that will appear for the customer dropdown + let!(:ship_address) { create(:address, country: country, state: state, first_name: "Rumpelstiltskin") } + let!(:bill_address) { create(:address, country: country, state: state, first_name: "Rumpelstiltskin") } + + let!(:user) { create(:user, email: 'foobar@example.com', ship_address: ship_address, bill_address: bill_address) } + + context "brand new order" do + # Regression test for #3335 & #5317 + it "associates a user when not using guest checkout" do + visit spree.admin_path + click_link "Orders" + click_link "New Order" + select2_search product.name, from: Spree.t(:name_or_sku) + within("table.stock-levels") do + fill_in "variant_quantity", with: 1 + click_icon :plus + end + wait_for_ajax + click_link "Customer Details" + targetted_select2 "foobar@example.com", from: "#s2id_customer_search" + # 5317 - Address prefills using user's default. + expect(find('#order_bill_address_attributes_firstname').value).to eq user.bill_address.firstname + expect(find('#order_bill_address_attributes_lastname').value).to eq user.bill_address.lastname + expect(find('#order_bill_address_attributes_address1').value).to eq user.bill_address.address1 + expect(find('#order_bill_address_attributes_address2').value).to eq user.bill_address.address2 + expect(find('#order_bill_address_attributes_city').value).to eq user.bill_address.city + expect(find('#order_bill_address_attributes_zipcode').value).to eq user.bill_address.zipcode + expect(find('#order_bill_address_attributes_country_id').value).to eq user.bill_address.country_id.to_s + expect(find('#order_bill_address_attributes_state_id').value).to eq user.bill_address.state_id.to_s + expect(find('#order_bill_address_attributes_phone').value).to eq user.bill_address.phone + click_button "Update" + expect(Spree::Order.last.user).not_to be_nil + end + end + + context "editing an order" do + before do + configure_spree_preferences do |config| + config.default_country_id = country.id + config.company = true + end + + visit spree.admin_path + click_link "Orders" + within('table#listing_orders') { click_icon(:edit) } + end + + context "selected country has no state" do + before { create(:country, iso: "BRA", name: "Brazil") } + + it "changes state field to text input" do + click_link "Customer Details" + + within("#billing") do + targetted_select2 "Brazil", from: "#s2id_order_bill_address_attributes_country_id" + fill_in "order_bill_address_attributes_state_name", with: "Piaui" + end + + click_button "Update" + expect(find_field("order_bill_address_attributes_state_name").value).to eq("Piaui") + end + end + + it "should be able to update customer details for an existing order" do + order.ship_address = create(:address) + order.save! + + click_link "Customer Details" + within("#shipping") { fill_in_address "ship" } + within("#billing") { fill_in_address "bill" } + + click_button "Update" + click_link "Customer Details" + + # Regression test for #2950 + #2433 + # This act should transition the state of the order as far as it will go too + within("#order_tab_summary") do + expect(find(".state").text).to eq("COMPLETE") + end + end + + it "should show validation errors" do + click_link "Customer Details" + click_button "Update" + expect(page).to have_content("Shipping address first name can't be blank") + end + + it "updates order email for an existing order with a user" do + order.update_columns(ship_address_id: ship_address.id, bill_address_id: bill_address.id, state: "confirm", completed_at: nil) + previous_user = order.user + click_link "Customer Details" + fill_in "order_email", with: "newemail@example.com" + expect { click_button "Update" }.to change { order.reload.email }.to "newemail@example.com" + expect(order.user_id).to eq previous_user.id + expect(order.user.email).to eq previous_user.email + end + + context "country associated was removed" do + let(:brazil) { create(:country, iso: "BRA", name: "Brazil") } + + before do + order.bill_address.country.destroy + configure_spree_preferences do |config| + config.default_country_id = brazil.id + end + end + + it "sets default country when displaying form" do + click_link "Customer Details" + expect(find_field("order_bill_address_attributes_country_id").value.to_i).to eq brazil.id + end + end + + # Regression test for #942 + context "errors when no shipping methods are available" do + before do + Spree::ShippingMethod.delete_all + end + + specify do + click_link "Customer Details" + # Need to fill in valid information so it passes validations + fill_in "order_ship_address_attributes_firstname", with: "John 99" + fill_in "order_ship_address_attributes_lastname", with: "Doe" + fill_in "order_ship_address_attributes_lastname", with: "Company" + fill_in "order_ship_address_attributes_address1", with: "100 first lane" + fill_in "order_ship_address_attributes_address2", with: "#101" + fill_in "order_ship_address_attributes_city", with: "Bethesda" + fill_in "order_ship_address_attributes_zipcode", with: "20170" + + page.select('Alabama', from: 'order_ship_address_attributes_state_id') + fill_in "order_ship_address_attributes_phone", with: "123-456-7890" + expect { click_button "Update" }.not_to raise_error + end + end + end + + def fill_in_address(kind = "bill") + fill_in "First Name", with: "John 99" + fill_in "Last Name", with: "Doe" + fill_in "Company", with: "Company" + fill_in "Street Address", with: "100 first lane" + fill_in "Street Address (cont'd)", with: "#101" + fill_in "City", with: "Bethesda" + fill_in "Zip", with: "20170" + targetted_select2 "Alabama", from: "#s2id_order_#{kind}_address_attributes_state_id" + fill_in "Phone", with: "123-456-7890" + end +end diff --git a/backend/spec/features/admin/orders/line_items_spec.rb b/backend/spec/features/admin/orders/line_items_spec.rb new file mode 100644 index 00000000000..e4d3bf5f808 --- /dev/null +++ b/backend/spec/features/admin/orders/line_items_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +# Tests for #3958's features +describe "Order Line Items", type: :feature, js: true do + stub_authorization! + + before do + # Removing the delivery step causes the order page to render a different + # partial, called _line_items, which shows line items rather than shipments + allow(Spree::Order).to receive_messages :checkout_step_names => [:address, :payment, :confirm, :complete] + end + + let!(:order) do + order = create(:order_with_line_items, :line_items_count => 1) + order.shipments.destroy_all + order + end + + it "can edit a line item's quantity" do + visit spree.edit_admin_order_path(order) + within(".line-items") do + within_row(1) do + find(".edit-line-item").click + fill_in "quantity", :with => 10 + find(".save-line-item").click + within '.line-item-qty-show' do + expect(page).to have_content("10") + end + within '.line-item-total' do + expect(page).to have_content("$100.00") + end + end + end + end + + it "can delete a line item" do + visit spree.edit_admin_order_path(order) + + product_name = find(".line-items tr:nth-child(1) .line-item-name").text + + within(".line-items") do + within_row(1) do + accept_alert do + find(".delete-line-item").click + end + end + end + + expect(page).not_to have_content(product_name) + end +end diff --git a/backend/spec/features/admin/orders/listing_spec.rb b/backend/spec/features/admin/orders/listing_spec.rb new file mode 100644 index 00000000000..32c0b73b96b --- /dev/null +++ b/backend/spec/features/admin/orders/listing_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' + +describe "Orders Listing", type: :feature, js: true do + stub_authorization! + + let!(:promotion) { create(:promotion_with_item_adjustment) } + + before(:each) do + allow_any_instance_of(Spree::OrderInventory).to receive(:add_to_shipment) + @order1 = create(:order_with_line_items, created_at: 1.day.from_now, completed_at: 1.day.from_now, considered_risky: true, number: "R100") + @order2 = create(:order, created_at: 1.day.ago, completed_at: 1.day.ago, number: "R200") + visit spree.admin_orders_path + end + + context "listing orders" do + it "should list existing orders" do + within_row(1) do + expect(column_text(2)).to eq "R100" + expect(find("td:nth-child(3)")).to have_css '.considered_risky' + expect(column_text(4)).to eq "CART" + end + + within_row(2) do + expect(column_text(2)).to eq "R200" + expect(find("td:nth-child(3)")).to have_css '.considered_safe' + end + end + + it "should be able to sort the orders listing" do + # default is completed_at desc + within_row(1) { expect(page).to have_content("R100") } + within_row(2) { expect(page).to have_content("R200") } + + click_link "Completed At" + + # Completed at desc + within_row(1) { expect(page).to have_content("R200") } + within_row(2) { expect(page).to have_content("R100") } + + within('table#listing_orders thead') { click_link "Number" } + + # number asc + within_row(1) { expect(page).to have_content("R100") } + within_row(2) { expect(page).to have_content("R200") } + end + end + + context "searching orders" do + it "should be able to search orders" do + click_on 'Filter' + fill_in "q_number_cont", with: "R200" + click_on 'Filter Results' + within_row(1) do + expect(page).to have_content("R200") + end + + # Ensure that the other order doesn't show up + within("table#listing_orders") { expect(page).not_to have_content("R100") } + end + + it "should return both complete and incomplete orders when only complete orders is not checked" do + Spree::Order.create! email: "incomplete@example.com", completed_at: nil, state: 'cart' + click_on 'Filter' + uncheck "q_completed_at_not_null" + click_on 'Filter Results' + + expect(page).to have_content("R200") + expect(page).to have_content("incomplete@example.com") + end + + it "should be able to filter risky orders" do + click_on 'Filter' + # Check risky and filter + check "q_considered_risky_eq" + click_on 'Filter Results' + + click_on 'Filter' + # Insure checkbox still checked + expect(find("#q_considered_risky_eq")).to be_checked + # Insure we have the risky order, R100 + within_row(1) do + expect(page).to have_content("R100") + end + # Insure the non risky order is not present + expect(page).not_to have_content("R200") + end + + it "should be able to filter on variant_id" do + click_on 'Filter' + # Insure we have the SKU in the options + expect(find('#q_line_items_variant_id_in').all('option').collect(&:text)).to include(@order1.line_items.first.variant.sku) + + # Select and filter + find('#q_line_items_variant_id_in').find(:xpath, 'option[2]').select_option + click_on 'Filter Results' + + within_row(1) do + expect(page).to have_content(@order1.number) + end + + expect(page).not_to have_content(@order2.number) + end + + context "when pagination is really short" do + before do + @old_per_page = Spree::Config[:orders_per_page] + Spree::Config[:orders_per_page] = 1 + end + + after do + Spree::Config[:orders_per_page] = @old_per_page + end + + # Regression test for #4004 + it "should be able to go from page to page for incomplete orders" do + Spree::Order.destroy_all + 2.times { Spree::Order.create! email: "incomplete@example.com", completed_at: nil, state: 'cart' } + click_on 'Filter' + uncheck "q_completed_at_not_null" + click_on 'Filter Results' + within(".pagination") do + click_link "2" + end + expect(page).to have_content("incomplete@example.com") + click_on 'Filter' + expect(find("#q_completed_at_not_null")).not_to be_checked + end + end + + it "should be able to search orders using only completed at input" do + click_on 'Filter' + fill_in "q_created_at_gt", with: Date.current + + # Just so the datepicker gets out of poltergeists way. + page.execute_script("$('#q_created_at_gt').datepicker('widget').hide();") + + click_on 'Filter Results' + within_row(1) { expect(page).to have_content("R100") } + + # Ensure that the other order doesn't show up + within("table#listing_orders") { expect(page).not_to have_content("R200") } + end + + context "filter on promotions" do + before(:each) do + @order1.promotions << promotion + @order1.save + visit spree.admin_orders_path + end + + it "only shows the orders with the selected promotion" do + click_on 'Filter' + select2 promotion.name, from: "Promotion" + click_on 'Filter Results' + within_row(1) { expect(page).to have_content("R100") } + within("table#listing_orders") { expect(page).not_to have_content("R200") } + end + end + end +end diff --git a/backend/spec/features/admin/orders/log_entries_spec.rb b/backend/spec/features/admin/orders/log_entries_spec.rb new file mode 100644 index 00000000000..44bd6be89c9 --- /dev/null +++ b/backend/spec/features/admin/orders/log_entries_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe "Log entries", :type => :feature do + stub_authorization! + + let!(:payment) { create(:payment) } + + context "with a successful log entry" do + before do + response = ActiveMerchant::Billing::Response.new( + true, + "Transaction successful", + :transid => "ABCD1234" + ) + + payment.log_entries.create( + :source => payment.source, + :details => response.to_yaml + ) + end + + it "shows a successful attempt" do + visit spree.admin_order_payments_path(payment.order) + find("#payment_#{payment.id} a").click + click_link "Logs" + within("#listing_log_entries") do + expect(page).to have_content("Transaction successful") + end + end + end + + context "with a failed log entry" do + before do + response = ActiveMerchant::Billing::Response.new( + false, + "Transaction failed", + :transid => "ABCD1234" + ) + + payment.log_entries.create( + :source => payment.source, + :details => response.to_yaml + ) + end + + it "shows a failed attempt" do + visit spree.admin_order_payments_path(payment.order) + find("#payment_#{payment.id} a").click + click_link "Logs" + within("#listing_log_entries") do + expect(page).to have_content("Transaction failed") + end + end + end +end \ No newline at end of file diff --git a/backend/spec/features/admin/orders/new_order_spec.rb b/backend/spec/features/admin/orders/new_order_spec.rb new file mode 100644 index 00000000000..add2c9f7ad2 --- /dev/null +++ b/backend/spec/features/admin/orders/new_order_spec.rb @@ -0,0 +1,162 @@ +require 'spec_helper' + +describe "New Order", :type => :feature do + let!(:product) { create(:product_in_stock) } + let!(:state) { create(:state) } + let!(:user) { create(:user, ship_address: create(:address), bill_address: create(:address)) } + let!(:payment_method) { create(:check_payment_method) } + let!(:shipping_method) { create(:shipping_method) } + + stub_authorization! + + before do + visit spree.admin_path + click_on "Orders" + click_on "New Order" + end + + it "does check if you have a billing address before letting you add shipments" do + click_on "Shipments" + expect(page).to have_content 'Please fill in customer info' + expect(current_path).to eql(spree.edit_admin_order_customer_path(Spree::Order.last)) + end + + it "completes new order succesfully without using the cart", js: true do + select2_search product.name, from: Spree.t(:name_or_sku) + click_icon :plus + click_on "Customer Details" + + within "#select-customer" do + targetted_select2_search user.email, from: "#s2id_customer_search" + end + + check "order_use_billing" + fill_in_address + click_on "Update" + + click_on "Payments" + click_on "Update" + + expect(current_path).to eql(spree.admin_order_payments_path(Spree::Order.last)) + click_icon "capture" + + click_on "Shipments" + click_on "ship" + wait_for_ajax + + expect(page).to have_content("shipped") + end + + context "adding new item to the order", js: true do + it "inventory items show up just fine and are also registered as shipments" do + select2_search product.name, from: Spree.t(:name_or_sku) + + within("table.stock-levels") do + fill_in "variant_quantity", with: 2 + click_icon :plus + end + + within(".line-items") do + expect(page).to have_content(product.name) + end + + click_on "Customer Details" + + within "#select-customer" do + targetted_select2_search user.email, from: "#s2id_customer_search" + end + + check "order_use_billing" + fill_in_address + click_on "Update" + + click_on "Shipments" + + within(".stock-contents") do + expect(page).to have_content(product.name) + end + end + end + + # Regression test for #3958 + context "without a delivery step", js: true do + before do + allow(Spree::Order).to receive_messages checkout_step_names: [:address, :payment, :confirm, :complete] + end + + it "can still see line items" do + select2_search product.name, from: Spree.t(:name_or_sku) + click_icon :plus + within(".line-items") do + within(".line-item-name") do + expect(page).to have_content(product.name) + end + within(".line-item-qty-show") do + expect(page).to have_content("1") + end + within(".line-item-price") do + expect(page).to have_content(product.price) + end + end + end + end + + # Regression test for #3336 + context "start by customer address" do + it "completes order fine", js: true do + click_on "Customer Details" + + within "#select-customer" do + targetted_select2_search user.email, from: "#s2id_customer_search" + end + + check "order_use_billing" + fill_in_address + click_on "Update" + + click_on "Shipments" + select2_search product.name, from: Spree.t(:name_or_sku) + click_icon :plus + wait_for_ajax + + click_on "Payments" + click_on "Continue" + + within(".additional-info .state") do + expect(page).to have_content("COMPLETE") + end + end + end + + # Regression test for #5327 + context "customer with default credit card", js: true do + before do + create(:credit_card, default: true, user: user) + end + it "transitions to delivery not to complete" do + click_link "Orders" + click_link "New Order" + select2_search product.name, from: Spree.t(:name_or_sku) + within("table.stock-levels") do + fill_in "variant_quantity", with: 1 + click_icon :plus + end + wait_for_ajax + click_link "Customer Details" + targetted_select2 user.email, from: "#s2id_customer_search" + click_button "Update" + expect(Spree::Order.last.state).to eq 'delivery' + end + end + + def fill_in_address(kind = "bill") + fill_in "First Name", with: "John 99" + fill_in "Last Name", with: "Doe" + fill_in "Street Address", with: "100 first lane" + fill_in "Street Address (cont'd)", with: "#101" + fill_in "City", with: "Bethesda" + fill_in "Zip", with: "20170" + targetted_select2_search state.name, from: "#s2id_order_#{kind}_address_attributes_state_id" + fill_in "Phone", with: "123-456-7890" + end +end diff --git a/backend/spec/features/admin/orders/order_details_spec.rb b/backend/spec/features/admin/orders/order_details_spec.rb new file mode 100644 index 00000000000..63f4b5a29e9 --- /dev/null +++ b/backend/spec/features/admin/orders/order_details_spec.rb @@ -0,0 +1,550 @@ +# coding: utf-8 +require 'spec_helper' + +describe "Order Details", type: :feature, js: true do + let!(:stock_location) { create(:stock_location_with_items) } + let!(:product) { create(:product, :name => 'spree t-shirt', :price => 20.00) } + let!(:tote) { create(:product, :name => "Tote", :price => 15.00) } + let(:order) { create(:order, :state => 'complete', :completed_at => "2011-02-01 12:36:15", :number => "R100") } + let(:state) { create(:state) } + #let(:shipment) { create(:shipment, :order => order, :stock_location => stock_location) } + let!(:shipping_method) { create(:shipping_method, :name => "Default") } + + before do + order.shipments.create(stock_location_id: stock_location.id) + order.contents.add(product.master, 2) + end + + context 'as Admin' do + stub_authorization! + + + context "cart edit page" do + before do + product.master.stock_items.first.update_column(:count_on_hand, 100) + visit spree.cart_admin_order_path(order) + end + + + it "should allow me to edit order details" do + expect(page).to have_content("spree t-shirt") + expect(page).to have_content("$40.00") + + within_row(1) do + click_icon :edit + fill_in "quantity", :with => "1" + end + click_icon :ok + + within("#order_total") do + expect(page).to have_content("$20.00") + end + end + + it "can add an item to a shipment" do + select2_search "spree t-shirt", :from => Spree.t(:name_or_sku) + within("table.stock-levels") do + fill_in "variant_quantity", :with => 2 + click_icon :plus + end + + within("#order_total") do + expect(page).to have_content("$80.00") + end + end + + it "can remove an item from a shipment" do + expect(page).to have_content("spree t-shirt") + + within_row(1) do + accept_alert do + click_icon :trash + end + end + + # Click "ok" on confirmation dialog + expect(page).not_to have_content("spree t-shirt") + end + + # Regression test for #3862 + it "can cancel removing an item from a shipment" do + expect(page).to have_content("spree t-shirt") + + within_row(1) do + # Click "cancel" on confirmation dialog + dismiss_alert do + click_icon :trash + end + end + + expect(page).to have_content("spree t-shirt") + end + + it "can add tracking information" do + visit spree.edit_admin_order_path(order) + + within(".show-tracking") do + click_icon :edit + end + fill_in "tracking", :with => "FOOBAR" + click_icon :ok + + expect(page).not_to have_css("input[name=tracking]") + expect(page).to have_content("Tracking: FOOBAR") + end + + it "can change the shipping method" do + order = create(:completed_order_with_totals) + visit spree.edit_admin_order_path(order) + within("table.index tr.show-method") do + click_icon :edit + end + select2 "Default", :from => "Shipping Method" + click_icon :ok + + expect(page).not_to have_css('#selected_shipping_rate_id') + expect(page).to have_content("Default") + end + + it "will show the variant sku" do + order = create(:completed_order_with_totals) + visit spree.edit_admin_order_path(order) + sku = order.line_items.first.variant.sku + expect(page).to have_content("SKU: #{sku}") + end + + context "with special_instructions present" do + let(:order) { create(:order, :state => 'complete', :completed_at => "2011-02-01 12:36:15", :number => "R100", :special_instructions => "Very special instructions here") } + it "will show the special_instructions" do + visit spree.edit_admin_order_path(order) + expect(page).to have_content("Very special instructions here") + end + end + + context "variant doesn't track inventory" do + before do + tote.master.update_column :track_inventory, false + # make sure there's no stock level for any item + tote.master.stock_items.update_all count_on_hand: 0, backorderable: false + end + + it "adds variant to order just fine" do + select2_search tote.name, :from => Spree.t(:name_or_sku) + within("table.stock-levels") do + fill_in "variant_quantity", :with => 1 + click_icon :plus + end + + within(".line-items") do + expect(page).to have_content(tote.name) + end + end + end + + context "variant out of stock and not backorderable" do + before do + product.master.stock_items.first.update_column(:backorderable, false) + product.master.stock_items.first.update_column(:count_on_hand, 0) + end + + it "displays out of stock instead of add button" do + select2_search product.name, :from => Spree.t(:name_or_sku) + + within("table.stock-levels") do + expect(page).to have_content(Spree.t(:out_of_stock)) + end + end + end + end + + + context 'Shipment edit page' do + let!(:stock_location2) { create(:stock_location_with_items, name: 'Clarksville') } + + before do + product.master.stock_items.first.update_column(:backorderable, true) + product.master.stock_items.first.update_column(:count_on_hand, 100) + product.master.stock_items.last.update_column(:count_on_hand, 100) + end + + context 'splitting to location' do + before { visit spree.edit_admin_order_path(order) } + # can not properly implement until poltergeist supports checking alert text + # see https://github.com/teampoltergeist/poltergeist/pull/516 + it 'should warn you if you have not selected a location or shipment' + + context 'there is enough stock at the other location' do + it 'should allow me to make a split' do + expect(order.shipments.count).to eq(1) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + click_icon :ok + + wait_for_ajax + order.reload + + expect(order.shipments.count).to eq(2) + expect(order.shipments.last.backordered?).to eq(false) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(1) + expect(order.shipments.last.inventory_units_for(product.master).count).to eq(1) + end + + it 'should allow me to make a transfer via splitting off all stock' do + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 2 + click_icon :ok + + wait_for_ajax + order.reload + + expect(order.shipments.count).to eq(1) + expect(order.shipments.last.backordered?).to eq(false) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + expect(order.shipments.first.stock_location.id).to eq(stock_location2.id) + end + + it 'should allow me to split more than I have if available there' do + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 5 + click_icon :ok + + wait_for_ajax + order.reload + + expect(order.shipments.count).to eq(1) + expect(order.shipments.last.backordered?).to eq(false) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(5) + expect(order.shipments.first.stock_location.id).to eq(stock_location2.id) + end + + it 'should not split anything if the input quantity is garbage' do + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 'ff' + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(1) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + end + + it 'should not allow less than or equal to zero qty' do + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 0 + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(1) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + + + fill_in 'item_quantity', with: -1 + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(1) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + end + + context 'A shipment has shipped' do + + it 'should not show or let me back to the cart page, nor show the shipment edit buttons' do + order = create(:order, :state => 'payment', :number => "R100") + order.shipments.create!(stock_location_id: stock_location.id, state: 'shipped') + + visit spree.cart_admin_order_path(order) + + expect(page.current_path).to eq(spree.edit_admin_order_path(order)) + expect(page).not_to have_text 'Cart' + expect(page).not_to have_selector('.fa-arrows-h') + expect(page).not_to have_selector('.fa-trash') + end + + end + end + + context 'there is not enough stock at the other location' do + context 'and it cannot backorder' do + it 'should not allow me to split stock' do + product.master.stock_items.last.update_column(:backorderable, false) + product.master.stock_items.last.update_column(:count_on_hand, 0) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 2 + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(1) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + expect(order.shipments.first.stock_location.id).to eq(stock_location.id) + end + + end + + context 'but it can backorder' do + it 'should allow me to split and backorder the stock' do + product.master.stock_items.last.update_column(:count_on_hand, 0) + product.master.stock_items.last.update_column(:backorderable, true) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 2 + click_icon :ok + + wait_for_ajax + order.reload + + expect(order.shipments.count).to eq(1) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + expect(order.shipments.first.stock_location.id).to eq(stock_location2.id) + end + end + end + + context 'multiple items in cart' do + it 'should have no problem splitting if multiple items are in the from shipment' do + order.contents.add(create(:variant), 2) + expect(order.shipments.count).to eq(1) + expect(order.shipments.first.manifest.count).to eq(2) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 stock_location2.name, from: '#s2id_item_stock_location' + click_icon :ok + + wait_for_ajax + order.reload + + expect(order.shipments.count).to eq(2) + expect(order.shipments.last.backordered?).to eq(false) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(1) + expect(order.shipments.last.inventory_units_for(product.master).count).to eq(1) + end + end + end + + + context 'splitting to shipment' do + before do + @shipment2 = order.shipments.create(stock_location_id: stock_location2.id) + visit spree.edit_admin_order_path(order) + end + + it 'should delete the old shipment if enough are split off' do + expect(order.shipments.count).to eq(2) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 @shipment2.number, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 2 + click_icon :ok + + wait_for_ajax + order.reload + + expect(order.shipments.count).to eq(1) + expect(order.shipments.last.inventory_units_for(product.master).count).to eq(2) + end + + context 'receiving shipment can not backorder' do + before { product.master.stock_items.last.update_column(:backorderable, false) } + + it 'should not allow a split if the receiving shipment qty plus the incoming is greater than the count_on_hand' do + expect(order.shipments.count).to eq(2) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 @shipment2.number, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 1 + click_icon :ok + + wait_for_ajax + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 @shipment2.number, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 200 + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(2) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(1) + expect(order.shipments.last.inventory_units_for(product.master).count).to eq(1) + end + + it 'should not allow a shipment to split stock to itself' do + within_row(1) { click_icon 'arrows-h' } + targetted_select2 order.shipments.first.number, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 1 + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(2) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2) + end + + it 'should split fine if more than one line_item is in the receiving shipment' do + variant2 = create(:variant) + order.contents.add(variant2, 2, shipment: @shipment2) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 @shipment2.number, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 1 + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(2) + expect(order.shipments.first.inventory_units_for(product.master).count).to eq 1 + expect(order.shipments.last.inventory_units_for(product.master).count).to eq 1 + expect(order.shipments.first.inventory_units_for(variant2).count).to eq 0 + expect(order.shipments.last.inventory_units_for(variant2).count).to eq 2 + end + end + + context 'receiving shipment can backorder' do + it 'should add more to the backorder' do + product.master.stock_items.last.update_column(:backorderable, true) + product.master.stock_items.last.update_column(:count_on_hand, 0) + expect(@shipment2.reload.backordered?).to eq(false) + + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 @shipment2.number, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 1 + click_icon :ok + + wait_for_ajax + + expect(@shipment2.reload.backordered?).to eq(true) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 @shipment2.number, from: '#s2id_item_stock_location' + fill_in 'item_quantity', with: 1 + click_icon :ok + + wait_for_ajax + + expect(order.shipments.count).to eq(1) + expect(order.shipments.last.inventory_units_for(product.master).count).to eq(2) + expect(@shipment2.reload.backordered?).to eq(true) + end + end + end + end + end + + context 'with only read permissions' do + before do + allow_any_instance_of(Spree::Admin::BaseController).to receive(:spree_current_user).and_return(nil) + end + + custom_authorization! do |user| + can [:admin, :index, :read, :edit], Spree::Order + end + it "should not display forbidden links" do + visit spree.edit_admin_order_path(order) + + expect(page).not_to have_button('cancel') + expect(page).not_to have_button('Resend') + + # Order Tabs + expect(page).not_to have_link('Order Details') + expect(page).not_to have_link('Customer Details') + expect(page).not_to have_link('Adjustments') + expect(page).not_to have_link('Payments') + expect(page).not_to have_link('Return Authorizations') + + # Order item actions + expect(page).not_to have_css('.delete-item') + expect(page).not_to have_css('.split-item') + expect(page).not_to have_css('.edit-item') + expect(page).not_to have_css('.edit-tracking') + + expect(page).not_to have_css('#add-line-item') + end + end + + context 'as Fakedispatch' do + custom_authorization! do |user| + # allow dispatch to :admin, :index, and :edit on Spree::Order + can [:admin, :edit, :index, :read], Spree::Order + # allow dispatch to :index, :show, :create and :update shipments on the admin + can [:admin, :manage, :read, :ship], Spree::Shipment + end + + before do + allow(Spree.user_class).to receive(:find_by). + with(hash_including(:spree_api_key)). + and_return(Spree.user_class.new) + end + + it 'should not display order tabs or edit buttons without ability' do + visit spree.edit_admin_order_path(order) + + # Order Form + expect(page).not_to have_css('.edit-item') + # Order Tabs + expect(page).not_to have_link('Order Details') + expect(page).not_to have_link('Customer Details') + expect(page).not_to have_link('Adjustments') + expect(page).not_to have_link('Payments') + expect(page).not_to have_link('Return Authorizations') + end + + it "can add tracking information" do + visit spree.edit_admin_order_path(order) + within("table.index tr:nth-child(5)") do + click_icon :edit + end + fill_in "tracking", :with => "FOOBAR" + click_icon :ok + + expect(page).not_to have_css("input[name=tracking]") + expect(page).to have_content("Tracking: FOOBAR") + end + + it "can change the shipping method" do + order = create(:completed_order_with_totals) + visit spree.edit_admin_order_path(order) + within("table.index tr.show-method") do + click_icon :edit + end + select2 "Default", :from => "Shipping Method" + click_icon :ok + + expect(page).not_to have_css('#selected_shipping_rate_id') + expect(page).to have_content("Default") + end + + it 'can ship' do + order = create(:order_ready_to_ship) + order.refresh_shipment_rates + visit spree.edit_admin_order_path(order) + click_icon 'arrow-right' + wait_for_ajax + within '.shipment-state' do + expect(page).to have_content('SHIPPED') + end + end + end +end diff --git a/backend/spec/features/admin/orders/payments_spec.rb b/backend/spec/features/admin/orders/payments_spec.rb new file mode 100644 index 00000000000..d4ab01cbde3 --- /dev/null +++ b/backend/spec/features/admin/orders/payments_spec.rb @@ -0,0 +1,233 @@ +require 'spec_helper' + +describe 'Payments', :type => :feature do + stub_authorization! + + context "with a pre-existing payment" do + + let!(:payment) do + create(:payment, + order: order, + amount: order.outstanding_balance, + payment_method: create(:credit_card_payment_method), + state: state + ) + end + + let(:order) { create(:completed_order_with_totals, number: 'R100', line_items_count: 5) } + let(:state) { 'checkout' } + + before do + visit spree.admin_path + click_link 'Orders' + within_row(1) do + click_link order.number + end + click_link 'Payments' + end + + def refresh_page + visit current_path + end + + # Regression tests for #1453 + context 'with a check payment' do + let(:order) { create(:completed_order_with_totals, number: 'R100') } + let!(:payment) do + create(:payment, + order: order, + amount: order.outstanding_balance, + payment_method: create(:check_payment_method) # Check + ) + end + + it 'capturing a check payment from a new order' do + click_icon(:capture) + expect(page).not_to have_content('Cannot perform requested operation') + expect(page).to have_content('Payment Updated') + end + + it 'voids a check payment from a new order' do + click_icon(:void) + expect(page).to have_content('Payment Updated') + end + end + + it 'should list all captures for a payment' do + capture_amount = order.outstanding_balance/2 * 100 + payment.capture!(capture_amount) + + visit spree.admin_order_payment_path(order, payment) + expect(page).to have_content 'Capture events' + # within '#capture_events' do + within_row(1) do + expect(page).to have_content(capture_amount / 100) + end + # end + end + + it 'lists and create payments for an order', js: true do + within_row(1) do + expect(column_text(3)).to eq('$150.00') + expect(column_text(4)).to eq('Credit Card') + expect(column_text(6)).to eq('CHECKOUT') + end + + click_icon :void + expect(find('#payment_status').text).to eq('BALANCE DUE') + expect(page).to have_content('Payment Updated') + + within_row(1) do + expect(column_text(3)).to eq('$150.00') + expect(column_text(4)).to eq('Credit Card') + expect(column_text(6)).to eq('VOID') + end + + click_on 'New Payment' + expect(page).to have_content('New Payment') + click_button 'Update' + expect(page).to have_content('successfully created!') + + click_icon(:capture) + expect(find('#payment_status').text).to eq('PAID') + + expect(page).not_to have_selector('#new_payment_section') + end + + # Regression test for #1269 + it 'cannot create a payment for an order with no payment methods' do + Spree::PaymentMethod.delete_all + order.payments.delete_all + + click_on 'New Payment' + expect(page).to have_content('You cannot create a payment for an order without any payment methods defined.') + expect(page).to have_content('Please define some payment methods first.') + end + + %w[checkout pending].each do |state| + context "payment is #{state.inspect}", js: true do + let(:state) { state } + + it 'allows the amount to be edited by clicking on the edit button then saving' do + within_row(1) do + click_icon(:edit) + fill_in('amount', with: '$1') + click_icon(:save) + expect(page).to have_selector('td.amount span', text: '$1.00') + expect(payment.reload.amount).to eq(1.00) + end + end + + it 'allows the amount to be edited by clicking on the amount then saving' do + within_row(1) do + find('td.amount span').click + fill_in('amount', with: '$1.01') + click_icon(:save) + expect(page).to have_selector('td.amount span', text: '$1.01') + expect(payment.reload.amount).to eq(1.01) + end + end + + it 'allows the amount change to be cancelled by clicking on the cancel button' do + within_row(1) do + click_icon(:edit) + + # Can't use fill_in here, as under poltergeist that will unfocus (and + # thus submit) the field under poltergeist + find('td.amount input').click + page.execute_script("$('td.amount input').val('$1')") + + click_icon(:cancel) + expect(page).to have_selector('td.amount span', text: '$150.00') + expect(payment.reload.amount).to eq(150.00) + end + end + + it 'displays an error when the amount is invalid' do + within_row(1) do + click_icon(:edit) + fill_in('amount', with: 'invalid') + click_icon(:save) + expect(find('td.amount input').value).to eq('invalid') + expect(payment.reload.amount).to eq(150.00) + end + expect(page).to have_selector('.flash.error', text: 'Invalid resource. Please fix errors and try again.') + end + end + end + + context 'payment is completed', js: true do + let(:state) { 'completed' } + + it 'does not allow the amount to be edited' do + within_row(1) do + expect(page).not_to have_selector('.fa-edit') + expect(page).not_to have_selector('td.amount span') + end + end + end + end + + context "with no prior payments" do + let(:order) { create(:order_with_line_items, :line_items_count => 1) } + let!(:payment_method) { create(:credit_card_payment_method)} + + # Regression tests for #4129 + context "with a credit card payment method" do + before do + visit spree.admin_order_payments_path(order) + end + + it "is able to create a new credit card payment with valid information", :js => true do + fill_in "Card Number", :with => "4111 1111 1111 1111" + fill_in "Name", :with => "Test User" + fill_in "Expiration", :with => "09 / #{Time.now.year + 1}" + fill_in "Card Code", :with => "007" + # Regression test for #4277 + sleep(1) + expect(find('.ccType', :visible => false).value).to eq('visa') + click_button "Continue" + expect(page).to have_content("Payment has been successfully created!") + end + + it "is unable to create a new payment with invalid information" do + click_button "Continue" + expect(page).to have_content("Payment could not be created.") + expect(page).to have_content("Number can't be blank") + expect(page).to have_content("Name can't be blank") + expect(page).to have_content("Verification Value can't be blank") + expect(page).to have_content("Month is not a number") + expect(page).to have_content("Year is not a number") + end + end + + context "user existing card" do + let!(:cc) do + create(:credit_card, user_id: order.user_id, payment_method: payment_method, gateway_customer_profile_id: "BGS-RFRE") + end + + before { visit spree.admin_order_payments_path(order) } + + it "is able to reuse customer payment source" do + expect(find("#card_#{cc.id}")).to be_checked + click_button "Continue" + expect(page).to have_content("Payment has been successfully created!") + end + end + + context "with a check" do + let!(:payment_method) { create(:check_payment_method) } + + before do + visit spree.admin_order_payments_path(order.reload) + end + + it "can successfully be created and captured" do + click_on 'Continue' + expect(page).to have_content("Payment has been successfully created!") + click_icon(:capture) + expect(page).to have_content("Payment Updated") + end + end + end +end diff --git a/backend/spec/features/admin/orders/risk_analysis_spec.rb b/backend/spec/features/admin/orders/risk_analysis_spec.rb new file mode 100644 index 00000000000..9ec0ee162bd --- /dev/null +++ b/backend/spec/features/admin/orders/risk_analysis_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe 'Order Risk Analysis', :type => :feature do + stub_authorization! + + let!(:order) do + create(:completed_order_with_pending_payment) + end + + def visit_order + visit spree.admin_path + click_link 'Orders' + within_row(1) do + click_link order.number + end + end + + context "the order is considered risky" do + before do + allow_any_instance_of(Spree::Admin::BaseController).to receive_messages :try_spree_current_user => create(:user) + + order.payments.first.update_column(:avs_response, 'X') + order.considered_risky! + visit_order + end + + it "displays 'Risk Analysis' box" do + expect(page).to have_content 'Risk Analysis' + end + + it "can be approved" do + click_button('approve') + expect(page).to have_content 'Approver' + expect(page).to have_content 'Approved at' + expect(page).to have_content 'Status: complete' + end + end + + context "the order is not considered risky" do + before do + visit_order + end + + it "does not display 'Risk Analysis' box" do + expect(page).to_not have_content 'Risk Analysis' + end + end +end diff --git a/backend/spec/features/admin/orders/shipments_spec.rb b/backend/spec/features/admin/orders/shipments_spec.rb new file mode 100644 index 00000000000..7571cce5e74 --- /dev/null +++ b/backend/spec/features/admin/orders/shipments_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe "Shipments", :type => :feature do + stub_authorization! + + let!(:order) { create(:order_ready_to_ship, :number => "R100", :state => "complete", :line_items_count => 5) } + + # Regression test for #4025 + context "a shipment without a shipping method" do + before do + order.shipments.each do |s| + # Deleting the shipping rates causes there to be no shipping methods + s.shipping_rates.delete_all + end + end + + it "can still be displayed" do + expect { visit spree.edit_admin_order_path(order) }.not_to raise_error + end + end + + context "shipping an order", js: true do + before(:each) do + visit spree.admin_path + click_link "Orders" + within_row(1) do + click_link "R100" + end + end + + it "can ship a completed order" do + click_link "ship" + wait_for_ajax + + expect(page).to have_content("SHIPPED PACKAGE") + expect(order.reload.shipment_state).to eq("shipped") + end + end + + context "moving variants between shipments", js: true do + let!(:la) { create(:stock_location, name: "LA") } + before(:each) do + visit spree.admin_path + click_link "Orders" + within_row(1) do + click_link "R100" + end + end + + it "can move a variant to a new and to an existing shipment" do + expect(order.shipments.count).to eq(1) + + within_row(1) { click_icon 'arrows-h' } + targetted_select2 'LA', from: '#s2id_item_stock_location' + click_icon :ok + wait_for_ajax + expect(page.find("#shipment_#{order.shipments.first.id}")).to be_present + + within_row(2) { click_icon 'arrows-h' } + targetted_select2 "LA(#{order.reload.shipments.last.number})", from: '#s2id_item_stock_location' + click_icon :ok + wait_for_ajax + expect(page.find("#shipment_#{order.reload.shipments.last.id}")).to be_present + end + end +end diff --git a/backend/spec/features/admin/products/edit/images_spec.rb b/backend/spec/features/admin/products/edit/images_spec.rb new file mode 100644 index 00000000000..7351c49c212 --- /dev/null +++ b/backend/spec/features/admin/products/edit/images_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe "Product Images", :type => :feature do + stub_authorization! + + let(:file_path) { Rails.root + "../../spec/support/ror_ringer.jpeg" } + + before do + # Ensure attachment style keys are symbolized before running all tests + # Otherwise this would result in this error: + # undefined method `processors' for \"48x48>\ + Spree::Image.attachment_definitions[:attachment][:styles].symbolize_keys! + end + + context "uploading, editing, and deleting an image", :js => true do + it "should allow an admin to upload and edit an image for a product" do + Spree::Image.attachment_definitions[:attachment].delete :storage + + create(:product) + + visit spree.admin_path + click_link "Products" + click_icon(:edit) + click_link "Images" + click_link "new_image_link" + attach_file('image_attachment', file_path) + click_button "Update" + expect(page).to have_content("successfully created!") + + click_icon(:edit) + fill_in "image_alt", :with => "ruby on rails t-shirt" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(page).to have_content("ruby on rails t-shirt") + + accept_alert do + click_icon :trash + end + expect(page).not_to have_content("ruby on rails t-shirt") + end + end + + # Regression test for #2228 + it "should see variant images" do + variant = create(:variant) + variant.images.create!(:attachment => File.open(file_path)) + visit spree.admin_product_images_path(variant.product) + + expect(page).not_to have_content("No Images Found.") + within("table.index") do + expect(page).to have_content(variant.options_text) + + #ensure no duplicate images are displayed + expect(page).to have_css("tbody tr", :count => 1) + + #ensure variant header is displayed + within("thead") do + expect(page).to have_content("Variant") + end + + #ensure variant header is displayed + within("tbody") do + expect(page).to have_content("Size: S") + end + end + end + + it "should not see variant column when product has no variants" do + product = create(:product) + product.images.create!(:attachment => File.open(file_path)) + visit spree.admin_product_images_path(product) + + expect(page).not_to have_content("No Images Found.") + within("table.index") do + #ensure no duplicate images are displayed + expect(page).to have_css("tbody tr", :count => 1) + + #ensure variant header is not displayed + within("thead") do + expect(page).not_to have_content("Variant") + end + + #ensure correct cell count + expect(page).to have_css("thead th", :count => 3) + end + end +end diff --git a/backend/spec/features/admin/products/edit/products_spec.rb b/backend/spec/features/admin/products/edit/products_spec.rb new file mode 100644 index 00000000000..fa4bd3e9084 --- /dev/null +++ b/backend/spec/features/admin/products/edit/products_spec.rb @@ -0,0 +1,66 @@ +# encoding: UTF-8 +require 'spec_helper' + +describe 'Product Details', :type => :feature do + stub_authorization! + + context 'editing a product' do + it 'should list the product details' do + create(:product, :name => 'Bún thịt nướng', :sku => 'A100', + :description => 'lorem ipsum', :available_on => '2013-08-14 01:02:03') + + visit spree.admin_path + click_link 'Products' + within_row(1) { click_icon :edit } + + click_link 'Product Details' + + expect(find('.page-title').text.strip).to eq('Editing Product “Bún thịt nướng”') + expect(find('input#product_name').value).to eq('Bún thịt nướng') + expect(find('input#product_slug').value).to eq('bun-th-t-n-ng') + expect(find('textarea#product_description').text.strip).to eq('lorem ipsum') + expect(find('input#product_price').value).to eq('19.99') + expect(find('input#product_cost_price').value).to eq('17.00') + expect(find('input#product_available_on').value).to eq("2013/08/14") + expect(find('input#product_sku').value).to eq('A100') + end + + it "should handle slug changes" do + create(:product, :name => 'Bún thịt nướng', :sku => 'A100', + :description => 'lorem ipsum', :available_on => '2011-01-01 01:01:01') + + visit spree.admin_path + click_link 'Products' + within('table.index tbody tr:nth-child(1)') do + click_icon(:edit) + end + + fill_in "product_slug", :with => 'random-slug-value' + click_button "Update" + expect(page).to have_content("successfully updated!") + + fill_in "product_slug", :with => '' + click_button "Update" + within('#product_slug_field') { expect(page).to have_content("is too short") } + + fill_in "product_slug", :with => 'another-random-slug-value' + click_button "Update" + expect(page).to have_content("successfully updated!") + end + end + + # Regression test for #3385 + context "deleting a product", :js => true do + it "is still able to find the master variant" do + create(:product) + + visit spree.admin_products_path + within_row(1) do + accept_alert do + click_icon :trash + end + end + wait_for_ajax + end + end +end diff --git a/backend/spec/features/admin/products/edit/taxons_spec.rb b/backend/spec/features/admin/products/edit/taxons_spec.rb new file mode 100644 index 00000000000..254d52aee28 --- /dev/null +++ b/backend/spec/features/admin/products/edit/taxons_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe "Product Taxons", :type => :feature do + stub_authorization! + + after do + Capybara.ignore_hidden_elements = true + end + + before do + Capybara.ignore_hidden_elements = false + end + + context "managing taxons" do + def selected_taxons + find("#product_taxon_ids").value.split(',').map(&:to_i).uniq + end + + it "should allow an admin to manage taxons", :js => true do + taxon_1 = create(:taxon) + taxon_2 = create(:taxon, :name => 'Clothing') + product = create(:product) + product.taxons << taxon_1 + + visit spree.admin_path + click_link "Products" + within("table.index") do + click_icon :edit + end + + expect(find(".select2-search-choice").text).to eq(taxon_1.name) + expect(selected_taxons).to match_array([taxon_1.id]) + + select2_search "Clothing", :from => "Taxons" + click_button "Update" + expect(selected_taxons).to match_array([taxon_1.id, taxon_2.id]) + + # Regression test for #2139 + sleep(1) + expect(first(".select2-search-choice", text: taxon_1.name)).to be_present + expect(first(".select2-search-choice", text: taxon_2.name)).to be_present + end + end +end diff --git a/backend/spec/features/admin/products/edit/variants_spec.rb b/backend/spec/features/admin/products/edit/variants_spec.rb new file mode 100644 index 00000000000..38b190a45bc --- /dev/null +++ b/backend/spec/features/admin/products/edit/variants_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe "Product Variants", :type => :feature do + stub_authorization! + + before(:each) do + visit spree.admin_path + end + + context "editing variant option types", :js => true do + let!(:product) { create(:product) } + + it "should allow an admin to create option types for a variant" do + click_link "Products" + + within_row(1) { click_icon :edit } + + within('#sidebar') { click_link "Variants" } + expect(page).to have_content("TO ADD VARIANTS, YOU MUST FIRST DEFINE") + end + + it "allows admin to create a variant if there are option types" do + click_link "Products" + click_link "Option Types" + click_link "new_option_type_link" + fill_in "option_type_name", :with => "shirt colors" + fill_in "option_type_presentation", :with => "colors" + click_button "Create" + expect(page).to have_content("successfully created!") + + page.find('#option_type_option_values_attributes_0_name').set('color') + page.find('#option_type_option_values_attributes_0_presentation').set('black') + click_button "Update" + expect(page).to have_content("successfully updated!") + + visit spree.admin_path + click_link "Products" + within('table.index tbody tr:nth-child(1)') do + click_icon :edit + end + + select2_search "shirt", :from => "Option Types" + click_button "Update" + expect(page).to have_content("successfully updated!") + + within('#sidebar') { click_link "Variants" } + click_link "New Variant" + + targetted_select2 "black", :from => "#s2id_variant_option_value_ids" + fill_in "variant_sku", :with => "A100" + click_button "Create" + expect(page).to have_content("successfully created!") + + within(".index") do + expect(page).to have_content("19.99") + expect(page).to have_content("black") + expect(page).to have_content("A100") + end + end + end +end diff --git a/backend/spec/features/admin/products/option_types_spec.rb b/backend/spec/features/admin/products/option_types_spec.rb new file mode 100644 index 00000000000..a598396c9fd --- /dev/null +++ b/backend/spec/features/admin/products/option_types_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +describe "Option Types", :type => :feature do + stub_authorization! + + before(:each) do + visit spree.admin_path + click_link "Products" + end + + context "listing option types" do + it "should list existing option types" do + create(:option_type, :name => "tshirt-color", :presentation => "Color") + create(:option_type, :name => "tshirt-size", :presentation => "Size") + + click_link "Option Types" + within("table#listing_option_types") do + expect(page).to have_content("Color") + expect(page).to have_content("tshirt-color") + expect(page).to have_content("Size") + expect(page).to have_content("tshirt-size") + end + end + end + + context "creating a new option type" do + it "should allow an admin to create a new option type", :js => true do + click_link "Option Types" + click_link "new_option_type_link" + expect(page).to have_content("NEW OPTION TYPE") + fill_in "option_type_name", :with => "shirt colors" + fill_in "option_type_presentation", :with => "colors" + click_button "Create" + expect(page).to have_content("successfully created!") + + page.find('#option_type_option_values_attributes_0_name').set('color') + page.find('#option_type_option_values_attributes_0_presentation').set('black') + + click_button "Update" + expect(page).to have_content("successfully updated!") + end + end + + context "editing an existing option type" do + it "should allow an admin to update an existing option type" do + create(:option_type, :name => "tshirt-color", :presentation => "Color") + create(:option_type, :name => "tshirt-size", :presentation => "Size") + click_link "Option Types" + within('table#listing_option_types') { click_link "Edit" } + fill_in "option_type_name", :with => "foo-size 99" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(page).to have_content("foo-size 99") + end + end + + # Regression test for #2277 + it "can remove an option value from an option type", :js => true do + create(:option_value) + click_link "Option Types" + within('table#listing_option_types') { click_icon :edit } + expect(page).to have_content("Editing Option Type") + expect(all("tbody#option_values tr").count).to eq(1) + within("tbody#option_values") do + find('.spree_remove_fields').click + end + # Assert that the field is hidden automatically + expect(all("tbody#option_values tr").select(&:visible?).count).to eq(0) + + # Then assert that on a page refresh that it's still not visible + visit page.current_url + # What *is* visible is a new option value field, with blank values + # Sometimes the page doesn't load before the all check is done + # lazily finding the element gives the page 10 seconds + expect(page).to have_css("tbody#option_values") + all("tbody#option_values tr input").all? { |input| input.value.blank? } + end + + # Regression test for #3204 + it "can remove a non-persisted option value from an option type", :js => true do + create(:option_type) + click_link "Option Types" + within('table#listing_option_types') { click_icon :edit } + + wait_for_ajax + page.find("tbody#option_values", :visible => true) + + expect(all("tbody#option_values tr").select(&:visible?).count).to eq(1) + + # Add a new option type + click_link "Add Option Value" + expect(all("tbody#option_values tr").select(&:visible?).count).to eq(2) + + # Remove default option type + within("tbody#option_values") do + find('.fa-trash').click + end + # Check that there was no HTTP request + expect(all("div#progress[style]").count).to eq(0) + # Assert that the field is hidden automatically + expect(all("tbody#option_values tr").select(&:visible?).count).to eq(1) + + # Remove added option type + within("tbody#option_values") do + find('.fa-trash').click + end + # Check that there was no HTTP request + expect(all("div#progress[style]").count).to eq(0) + # Assert that the field is hidden automatically + expect(all("tbody#option_values tr").select(&:visible?).count).to eq(0) + + end + +end diff --git a/backend/spec/features/admin/products/products_spec.rb b/backend/spec/features/admin/products/products_spec.rb new file mode 100644 index 00000000000..fe6e65470f9 --- /dev/null +++ b/backend/spec/features/admin/products/products_spec.rb @@ -0,0 +1,389 @@ +# encoding: utf-8 +require 'spec_helper' + +describe "Products", :type => :feature do + context "as admin user" do + stub_authorization! + + before(:each) do + visit spree.admin_path + end + + def build_option_type_with_values(name, values) + ot = FactoryGirl.create(:option_type, :name => name) + values.each do |val| + ot.option_values.create(:name => val.downcase, :presentation => val) + end + ot + end + + context "listing products" do + context "sorting" do + before do + create(:product, :name => 'apache baseball cap', :price => 10) + create(:product, :name => 'zomg shirt', :price => 5) + end + + it "should list existing products with correct sorting by name" do + click_link "Products" + # Name ASC + within_row(1) { expect(page).to have_content('apache baseball cap') } + within_row(2) { expect(page).to have_content("zomg shirt") } + + # Name DESC + click_link "admin_products_listing_name_title" + within_row(1) { expect(page).to have_content("zomg shirt") } + within_row(2) { expect(page).to have_content('apache baseball cap') } + end + + it "should list existing products with correct sorting by price" do + click_link "Products" + + # Name ASC (default) + within_row(1) { expect(page).to have_content('apache baseball cap') } + within_row(2) { expect(page).to have_content("zomg shirt") } + + # Price DESC + click_link "admin_products_listing_price_title" + within_row(1) { expect(page).to have_content("zomg shirt") } + within_row(2) { expect(page).to have_content('apache baseball cap') } + end + end + + context "currency displaying" do + context "using Russian Rubles" do + before do + Spree::Config[:currency] = "RUB" + end + + let!(:product) do + create(:product, :name => "Just a product", :price => 19.99) + end + + # Regression test for #2737 + context "uses руб as the currency symbol" do + it "on the products listing page" do + visit spree.admin_products_path + within_row(1) { expect(page).to have_content("₽19.99") } + end + end + end + end + end + + context "searching products" do + it "should be able to search deleted products", :js => true do + create(:product, :name => 'apache baseball cap', :deleted_at => "2011-01-06 18:21:13") + create(:product, :name => 'zomg shirt') + + click_link "Products" + expect(page).to have_content("zomg shirt") + expect(page).not_to have_content("apache baseball cap") + check "Show Deleted" + click_icon :search + expect(page).to have_content("zomg shirt") + expect(page).to have_content("apache baseball cap") + uncheck "Show Deleted" + click_icon :search + expect(page).to have_content("zomg shirt") + expect(page).not_to have_content("apache baseball cap") + end + + it "should be able to search products by their properties" do + create(:product, :name => 'apache baseball cap', :sku => "A100") + create(:product, :name => 'apache baseball cap2', :sku => "B100") + create(:product, :name => 'zomg shirt') + + click_link "Products" + fill_in "q_name_cont", :with => "ap" + click_icon :search + expect(page).to have_content("apache baseball cap") + expect(page).to have_content("apache baseball cap2") + expect(page).not_to have_content("zomg shirt") + + fill_in "q_variants_including_master_sku_cont", :with => "A1" + click_icon :search + expect(page).to have_content("apache baseball cap") + expect(page).not_to have_content("apache baseball cap2") + expect(page).not_to have_content("zomg shirt") + end + end + + context "creating a new product from a prototype" do + def build_option_type_with_values(name, values) + ot = FactoryGirl.create(:option_type, :name => name) + values.each do |val| + ot.option_values.create(:name => val.downcase, :presentation => val) + end + ot + end + + let(:product_attributes) do + # FactoryGirl.attributes_for is un-deprecated! + # https://github.com/thoughtbot/factory_girl/issues/274#issuecomment-3592054 + FactoryGirl.attributes_for(:simple_product) + end + + let(:prototype) do + size = build_option_type_with_values("size", %w(Small Medium Large)) + FactoryGirl.create(:prototype, :name => "Size", :option_types => [ size ]) + end + + let(:option_values_hash) do + hash = {} + prototype.option_types.each do |i| + hash[i.id.to_s] = i.option_value_ids + end + hash + end + + before(:each) do + @option_type_prototype = prototype + @property_prototype = create(:prototype, :name => "Random") + @shipping_category = create(:shipping_category) + click_link "Products" + click_link "admin_new_product" + within('#new_product') do + expect(page).to have_content("SKU") + end + end + + it "should allow an admin to create a new product and variants from a prototype", :js => true do + fill_in "product_name", :with => "Baseball Cap" + fill_in "product_sku", :with => "B100" + fill_in "product_price", :with => "100" + fill_in "product_available_on", :with => "2012/01/24" + select "Size", :from => "Prototype" + check "Large" + select @shipping_category.name, from: "product_shipping_category_id" + click_button "Create" + expect(page).to have_content("successfully created!") + expect(Spree::Product.last.variants.length).to eq(1) + end + + it "should not display variants when prototype does not contain option types", :js => true do + select "Random", :from => "Prototype" + + fill_in "product_name", :with => "Baseball Cap" + + expect(page).not_to have_content("Variants") + end + + it "should keep option values selected if validation fails", :js => true do + fill_in "product_name", :with => "Baseball Cap" + fill_in "product_sku", :with => "B100" + fill_in "product_price", :with => "100" + select "Size", :from => "Prototype" + check "Large" + click_button "Create" + expect(page).to have_content("Shipping category can't be blank") + expect(field_labeled("Size")).to be_checked + expect(field_labeled("Large")).to be_checked + expect(field_labeled("Small")).not_to be_checked + end + end + + context "creating a new product" do + before(:each) do + @shipping_category = create(:shipping_category) + click_link "Products" + click_link "admin_new_product" + within('#new_product') do + expect(page).to have_content("SKU") + end + end + + it "should allow an admin to create a new product", :js => true do + fill_in "product_name", :with => "Baseball Cap" + fill_in "product_sku", :with => "B100" + fill_in "product_price", :with => "100" + fill_in "product_available_on", :with => "2012/01/24" + select @shipping_category.name, from: "product_shipping_category_id" + click_button "Create" + expect(page).to have_content("successfully created!") + click_button "Update" + expect(page).to have_content("successfully updated!") + end + + it "should show validation errors", :js => true do + fill_in "product_name", :with => "Baseball Cap" + fill_in "product_sku", :with => "B100" + fill_in "product_price", :with => "100" + click_button "Create" + expect(page).to have_content("Shipping category can't be blank") + end + + context "using a locale with a different decimal format " do + before do + # change English locale’s separator and delimiter to match 19,99 format + I18n.backend.store_translations(:en, + :number => { + :currency => { + :format => { + :separator => ",", + :delimiter => "." + } + } + }) + end + + after do + # revert changes to English locale + I18n.backend.store_translations(:en, + :number => { + :currency => { + :format => { + :separator => ".", + :delimiter => "," + } + } + }) + end + + it "should show localized price value on validation errors", :js => true do + fill_in "product_price", :with => "19,99" + click_button "Create" + expect(find('input#product_price').value).to eq('19,99') + end + end + + # Regression test for #2097 + it "can set the count on hand to a null value", :js => true do + fill_in "product_name", :with => "Baseball Cap" + fill_in "product_price", :with => "100" + select @shipping_category.name, from: "product_shipping_category_id" + click_button "Create" + expect(page).to have_content("successfully created!") + click_button "Update" + expect(page).to have_content("successfully updated!") + end + end + + + context "cloning a product", :js => true do + it "should allow an admin to clone a product" do + create(:product) + + click_link "Products" + within_row(1) do + click_icon :copy + end + + expect(page).to have_content("Product has been cloned") + end + + context "cloning a deleted product" do + it "should allow an admin to clone a deleted product" do + create(:product, :name => "apache baseball cap") + + click_link "Products" + check "Show Deleted" + click_button "Search" + + expect(page).to have_content("apache baseball cap") + + within_row(1) do + click_icon :copy + end + + expect(page).to have_content("Product has been cloned") + end + end + end + + context 'updating a product', :js => true do + let(:product) { create(:product) } + + let(:prototype) do + size = build_option_type_with_values("size", %w(Small Medium Large)) + FactoryGirl.create(:prototype, :name => "Size", :option_types => [ size ]) + end + + before(:each) do + @option_type_prototype = prototype + @property_prototype = create(:prototype, :name => "Random") + end + + it 'should parse correctly available_on' do + visit spree.admin_product_path(product) + fill_in "product_available_on", :with => "2012/12/25" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(Spree::Product.last.available_on).to eq('Tue, 25 Dec 2012 00:00:00 UTC +00:00') + end + + it 'should add option_types when selecting a prototype' do + visit spree.admin_product_path(product) + click_link 'Product Properties' + expect(page).to have_content("SELECT FROM PROTOTYPE") + click_link "Select From Prototype" + + within(:css, "#prototypes tr#row_1") do + click_link 'Select' + wait_for_ajax + end + + page.all('tr.product_property').size > 1 + within(:css, "tr.product_property:first-child") do + expect(first('input[type=text]').value).to eq('baseball_cap_color') + end + end + end + + context 'deleting a product', :js => true do + let!(:product) { create(:product) } + + it "is still viewable" do + visit spree.admin_products_path + accept_alert do + click_icon :trash + wait_for_ajax + end + # This will show our deleted product + check "Show Deleted" + click_icon :search + click_link product.name + expect(find("#product_price").value.to_f).to eq(product.price.to_f) + end + end + end + + context 'with only product permissions' do + + before do + allow_any_instance_of(Spree::Admin::BaseController).to receive(:spree_current_user).and_return(nil) + end + + custom_authorization! do |user| + can [:admin, :update, :index, :read], Spree::Product + end + let!(:product) { create(:product) } + + it "should only display accessible links on index" do + visit spree.admin_products_path + expect(page).to have_link('Products') + expect(page).not_to have_link('Option Types') + expect(page).not_to have_link('Properties') + expect(page).not_to have_link('Prototypes') + + expect(page).not_to have_link('New Product') + expect(page).not_to have_css('a.clone') + expect(page).to have_css('a.edit') + expect(page).not_to have_css('a.delete-resource') + end + + it "should only display accessible links on edit" do + visit spree.admin_product_path(product) + + # product tabs should be hidden + expect(page).to have_link('Product Details') + expect(page).not_to have_link('Images') + expect(page).not_to have_link('Variants') + expect(page).not_to have_link('Product Properties') + expect(page).not_to have_link('Stock Management') + + # no create permission + expect(page).not_to have_link('New Product') + end + end +end diff --git a/backend/spec/features/admin/products/properties_spec.rb b/backend/spec/features/admin/products/properties_spec.rb new file mode 100644 index 00000000000..237b8c575f5 --- /dev/null +++ b/backend/spec/features/admin/products/properties_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe "Properties", :type => :feature do + stub_authorization! + + before(:each) do + visit spree.admin_path + click_link "Products" + end + + context "Property index" do + before do + create(:property, :name => 'shirt size', :presentation => 'size') + create(:property, :name => 'shirt fit', :presentation => 'fit') + click_link "Properties" + end + + context "listing product properties" do + it "should list the existing product properties" do + within_row(1) do + expect(column_text(1)).to eq("shirt size") + expect(column_text(2)).to eq("size") + end + + within_row(2) do + expect(column_text(1)).to eq("shirt fit") + expect(column_text(2)).to eq("fit") + end + end + end + + context "searching properties" do + it 'should list properties matching search query', :js => true do + fill_in "q_name_cont", :with => "size" + click_icon :search + + expect(page).to have_content("shirt size") + expect(page).not_to have_content("shirt fit") + end + end + end + + context "creating a property" do + it "should allow an admin to create a new product property", :js => true do + click_link "Properties" + click_link "new_property_link" + within('#new_property') { expect(page).to have_content("NEW PROPERTY") } + + fill_in "property_name", :with => "color of band" + fill_in "property_presentation", :with => "color" + click_button "Create" + expect(page).to have_content("successfully created!") + end + end + + context "editing a property" do + before(:each) do + create(:property) + click_link "Properties" + within_row(1) { click_icon :edit } + end + + it "should allow an admin to edit an existing product property" do + fill_in "property_name", :with => "model 99" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(page).to have_content("model 99") + end + + it "should show validation errors" do + fill_in "property_name", :with => "" + click_button "Update" + expect(page).to have_content("Name can't be blank") + end + end + + context "linking a property to a product", :js => true do + before do + create(:product) + visit spree.admin_products_path + click_icon :edit + click_link "Product Properties" + end + + # Regression test for #2279 + it "successfully create and then remove product property" do + fill_in_property + # Sometimes the page doesn't load before the all check is done + # lazily finding the element gives the page 10 seconds + expect(page).to have_css("tbody#product_properties tr:nth-child(2)") + expect(all("tbody#product_properties tr").count).to eq(2) + + delete_product_property + + check_property_row_count(1) + end + + # Regression test for #4466 + it "successfully remove and create a product property at the same time" do + fill_in_property + + fill_in "product_product_properties_attributes_1_property_name", :with => "New Property" + fill_in "product_product_properties_attributes_1_value", :with => "New Value" + + delete_product_property + + # Give fadeOut time to complete + expect(page).not_to have_selector("#product_product_properties_attributes_0_property_name") + expect(page).not_to have_selector("#product_product_properties_attributes_0_value") + + click_button "Update" + + expect(page).not_to have_content("Product is not found") + + check_property_row_count(2) + end + + def fill_in_property + expect(page).to have_content('Editing Product') + fill_in "product_product_properties_attributes_0_property_name", :with => "A Property" + fill_in "product_product_properties_attributes_0_value", :with => "A Value" + click_button "Update" + click_link "Product Properties" + end + + def delete_product_property + page.evaluate_script('window.confirm = function() { return true; }') + click_icon :trash + wait_for_ajax # delete action must finish before reloading + end + + def check_property_row_count(expected_row_count) + click_link "Product Properties" + expect(page).to have_css("tbody#product_properties") + expect(all("tbody#product_properties tr").count).to eq(expected_row_count) + end + end +end diff --git a/backend/spec/features/admin/products/prototypes_spec.rb b/backend/spec/features/admin/products/prototypes_spec.rb new file mode 100644 index 00000000000..837bdf893f1 --- /dev/null +++ b/backend/spec/features/admin/products/prototypes_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe "Prototypes", :type => :feature do + stub_authorization! + + context "listing prototypes" do + it "should be able to list existing prototypes" do + create(:property, name: "model", presentation: "Model") + create(:property, name: "brand", presentation: "Brand") + create(:property, name: "shirt_fabric", presentation: "Fabric") + create(:property, name: "shirt_sleeve_length", presentation: "Sleeve") + create(:property, name: "mug_type", presentation: "Type") + create(:property, name: "bag_type", presentation: "Type") + create(:property, name: "manufacturer", presentation: "Manufacturer") + create(:property, name: "bag_size", presentation: "Size") + create(:property, name: "mug_size", presentation: "Size") + create(:property, name: "gender", presentation: "Gender") + create(:property, name: "shirt_fit", presentation: "Fit") + create(:property, name: "bag_material", presentation: "Material") + create(:property, name: "shirt_type", presentation: "Type") + p = create(:prototype, :name => "Shirt") + %w( brand gender manufacturer model shirt_fabric shirt_fit shirt_sleeve_length shirt_type ).each do |prop| + p.properties << Spree::Property.find_by_name(prop) + end + p = create(:prototype, name: "Mug") + %w( mug_size mug_type ).each do |prop| + p.properties << Spree::Property.find_by_name(prop) + end + p = create(:prototype, name: "Bag") + %w( bag_type bag_material ).each do |prop| + p.properties << Spree::Property.find_by_name(prop) + end + + visit spree.admin_path + click_link "Products" + click_link "Prototypes" + + within_row(1) { expect(column_text(1)).to eq "Shirt" } + within_row(2) { expect(column_text(1)).to eq "Mug" } + within_row(3) { expect(column_text(1)).to eq "Bag" } + end + end + + context "creating a prototype" do + it "should allow an admin to create a new product prototype", :js => true do + visit spree.admin_path + click_link "Products" + click_link "Prototypes" + click_link "new_prototype_link" + within('#new_prototype') do + expect(page).to have_content("NEW PROTOTYPE") + end + fill_in "prototype_name", with: "male shirts" + click_button "Create" + expect(page).to have_content("successfully created!") + click_link "Prototypes" + within_row(1) { click_icon :edit } + fill_in "prototype_name", with: "Shirt 99" + click_button "Update" + expect(page).to have_content("successfully updated!") + expect(page).to have_content("Shirt 99") + end + end + + context "editing a prototype" do + it "should allow to empty its properties" do + model_property = create(:property, name: "model", presentation: "Model") + brand_property = create(:property, name: "brand", presentation: "Brand") + + shirt_prototype = create(:prototype, name: "Shirt", properties: []) + %w( brand model ).each do |prop| + shirt_prototype.properties << Spree::Property.find_by_name(prop) + end + + visit spree.admin_path + click_link "Products" + click_link "Prototypes" + + click_on "Edit" + property_ids = find_field("prototype_property_ids").value.map(&:to_i) + expect(property_ids).to match_array [model_property.id, brand_property.id] + + unselect "Brand", from: "prototype_property_ids" + unselect "Model", from: "prototype_property_ids" + + click_button 'Update' + + click_on "Edit" + + expect(find_field("prototype_property_ids").value).to be_empty + end + end + + it 'should be deletable', js: true do + shirt_prototype = create(:prototype, name: "Shirt", properties: []) + shirt_prototype.taxons << create(:taxon) + + visit spree.admin_path + click_link "Products" + click_link "Prototypes" + + within("#spree_prototype_#{shirt_prototype.id}") do + page.find('.delete-resource').click + end + + page.evaluate_script('window.confirm = function() { return true; }') + + expect(page).to have_content("Prototype \"#{shirt_prototype.name}\" has been successfully removed!") + end +end diff --git a/backend/spec/features/admin/products/stock_management_spec.rb b/backend/spec/features/admin/products/stock_management_spec.rb new file mode 100644 index 00000000000..116d3443814 --- /dev/null +++ b/backend/spec/features/admin/products/stock_management_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +describe "Stock Management", :type => :feature do + stub_authorization! + + before(:each) do + visit spree.admin_path + end + + context "given a product with a variant and a stock location" do + let!(:stock_location) { create(:stock_location, name: 'Default') } + let!(:product) { create(:product, name: 'apache baseball cap', price: 10) } + let!(:variant) { product.master } + + before do + stock_location.stock_item(variant).update_column(:count_on_hand, 10) + + click_link "Products" + within_row(1) { click_icon :edit } + click_link "Stock Management" + end + + context "toggle backorderable for a variant's stock item" do + let(:backorderable) { find ".stock_item_backorderable" } + + before do + expect(backorderable).to be_checked + backorderable.set(false) + wait_for_ajax + end + + it "persists the value when page reload", js: true do + visit current_path + expect(backorderable).not_to be_checked + end + end + + # Regression test for #2896 + # The regression was that unchecking the last checkbox caused a redirect + # to happen. By ensuring that we're still on an /admin/products URL, we + # assert that the redirect is *not* happening. + it "can toggle backorderable for the second variant stock item", js: true do + new_location = create(:stock_location, name: "Another Location") + click_link "Stock Management" + + new_location_backorderable = find "#stock_item_backorderable_#{new_location.id}" + new_location_backorderable.set(false) + wait_for_ajax + + expect(page.current_url).to include("/admin/products") + end + + it "can create a new stock movement", js: true do + fill_in "stock_movement_quantity", with: 5 + select2 "default", from: "Stock Location" + click_button "Add Stock" + + expect(page).to have_content('successfully created') + + within(:css, '.stock_location_info table') do + expect(column_text(2)).to eq '15' + end + end + + it "can create a new negative stock movement", js: true do + fill_in "stock_movement_quantity", with: -5 + select2 "default", from: "Stock Location" + click_button "Add Stock" + + expect(page).to have_content('successfully created') + + within(:css, '.stock_location_info table') do + expect(column_text(2)).to eq '5' + end + end + + context "with multiple variants" do + before do + variant = product.variants.create!(sku: 'SPREEC') + variant.stock_items.first.update_column(:count_on_hand, 30) + click_link "Stock Management" + end + + it "can create a new stock movement for the specified variant", js: true do + fill_in "stock_movement_quantity", with: 10 + select2 "SPREEC", from: "Variant" + click_button "Add Stock" + + expect(page).to have_content('successfully created') + + within("#listing_product_stock tr", :text => "SPREEC") do + within("table") do + expect(column_text(2)).to eq '40' + end + end + end + end + + # Regression test for #3304 + context "with no stock location" do + before do + @product = create(:product, name: 'apache baseball cap', price: 10) + v = @product.variants.create!(sku: 'FOOBAR') + Spree::StockLocation.delete_all + click_link "Products" + within_row(1) do + click_icon :edit + end + click_link "Stock Management" + end + + it "redirects to stock locations page" do + expect(page).to have_content(Spree.t(:stock_management_requires_a_stock_location)) + expect(page.current_url).to include("admin/stock_locations") + end + end + end +end diff --git a/backend/spec/features/admin/products/variant_spec.rb b/backend/spec/features/admin/products/variant_spec.rb new file mode 100644 index 00000000000..a139f3d1322 --- /dev/null +++ b/backend/spec/features/admin/products/variant_spec.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 +require 'spec_helper' + +describe "Variants", :type => :feature do + stub_authorization! + + let(:product) { create(:product_with_option_types, :price => "1.99", :cost_price => "1.00", :weight => "2.5", :height => "3.0", :width => "1.0", :depth => "1.5") } + + context "creating a new variant" do + it "should allow an admin to create a new variant" do + product.options.each do |option| + create(:option_value, :option_type => option.option_type) + end + + visit spree.admin_path + click_link "Products" + within_row(1) { click_icon :edit } + click_link "Variants" + click_on "New Variant" + expect(find('input#variant_price').value).to eq("1.99") + expect(find('input#variant_cost_price').value).to eq("1.00") + expect(find('input#variant_weight').value).to eq("2.50") + expect(find('input#variant_height').value).to eq("3.00") + expect(find('input#variant_width').value).to eq("1.00") + expect(find('input#variant_depth').value).to eq("1.50") + expect(page).to have_select('variant[tax_category_id]') + end + end + + context "listing variants" do + context "currency displaying" do + context "using Russian Rubles" do + before do + Spree::Config[:currency] = "RUB" + end + + let!(:variant) do + create(:variant, :product => product, :price => 19.99) + end + + # Regression test for #2737 + context "uses руб as the currency symbol" do + it "on the products listing page" do + visit spree.admin_product_variants_path(product) + within_row(1) { expect(page).to have_content("₽19.99") } + end + end + end + end + end +end diff --git a/backend/spec/features/admin/promotion_adjustments_spec.rb b/backend/spec/features/admin/promotion_adjustments_spec.rb new file mode 100644 index 00000000000..6e07a05d461 --- /dev/null +++ b/backend/spec/features/admin/promotion_adjustments_spec.rb @@ -0,0 +1,256 @@ +require 'spec_helper' + +describe "Promotion Adjustments", :type => :feature do + stub_authorization! + + context "coupon promotions", :js => true do + before(:each) do + visit spree.admin_path + click_link "Promotions" + click_link "New Promotion" + end + + it "should allow an admin to create a flat rate discount coupon promo" do + fill_in "Name", :with => "Promotion" + fill_in "Code", :with => "order" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Item total", :from => "Add rule of type" + within('#rule_fields') { click_button "Add" } + + eventually_fill_in "promotion_promotion_rules_attributes_#{Spree::Promotion.count}_preferred_amount_min", :with => 30 + eventually_fill_in "promotion_promotion_rules_attributes_#{Spree::Promotion.count}_preferred_amount_max", :with => 60 + within('#rule_fields') { click_button "Update" } + + select2 "Create whole-order adjustment", :from => "Add action of type" + within('#action_fields') { click_button "Add" } + select2 "Flat Rate", :from => "Calculator" + within('#actions_container') { click_button "Update" } + + within('.calculator-fields') { fill_in "Amount", :with => 5 } + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + expect(promotion.code).to eq("order") + + first_rule = promotion.rules.first + expect(first_rule.class).to eq(Spree::Promotion::Rules::ItemTotal) + expect(first_rule.preferred_amount_min).to eq(30) + expect(first_rule.preferred_amount_max).to eq(60) + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateAdjustment) + first_action_calculator = first_action.calculator + expect(first_action_calculator.class).to eq(Spree::Calculator::FlatRate) + expect(first_action_calculator.preferred_amount).to eq(5) + end + + it "should allow an admin to create a single user coupon promo with flat rate discount" do + fill_in "Name", :with => "Promotion" + fill_in "Usage Limit", :with => "1" + fill_in "Code", :with => "single_use" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Create whole-order adjustment", :from => "Add action of type" + within('#action_fields') { click_button "Add" } + select2 "Flat Rate", :from => "Calculator" + within('#actions_container') { click_button "Update" } + within('#action_fields') { fill_in "Amount", :with => "5" } + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + expect(promotion.usage_limit).to eq(1) + expect(promotion.code).to eq("single_use") + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateAdjustment) + first_action_calculator = first_action.calculator + expect(first_action_calculator.class).to eq(Spree::Calculator::FlatRate) + expect(first_action_calculator.preferred_amount).to eq(5) + end + + it "should allow an admin to create an automatic promo with flat percent discount" do + fill_in "Name", :with => "Promotion" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Item total", :from => "Add rule of type" + within('#rule_fields') { click_button "Add" } + + eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount_min", :with => 30 + eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount_max", :with => 60 + within('#rule_fields') { click_button "Update" } + + select2 "Create whole-order adjustment", :from => "Add action of type" + within('#action_fields') { click_button "Add" } + select2 "Flat Percent", :from => "Calculator" + within('#actions_container') { click_button "Update" } + within('.calculator-fields') { fill_in "Flat Percent", :with => "10" } + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + expect(promotion.code).to be_blank + + first_rule = promotion.rules.first + expect(first_rule.class).to eq(Spree::Promotion::Rules::ItemTotal) + expect(first_rule.preferred_amount_min).to eq(30) + expect(first_rule.preferred_amount_max).to eq(60) + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateAdjustment) + first_action_calculator = first_action.calculator + expect(first_action_calculator.class).to eq(Spree::Calculator::FlatPercentItemTotal) + expect(first_action_calculator.preferred_flat_percent).to eq(10) + end + + it "should allow an admin to create an product promo with percent per item discount" do + create(:product, :name => "RoR Mug") + + fill_in "Name", :with => "Promotion" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Product(s)", :from => "Add rule of type" + within("#rule_fields") { click_button "Add" } + select2_search "RoR Mug", :from => "Choose products" + within('#rule_fields') { click_button "Update" } + + select2 "Create per-line-item adjustment", :from => "Add action of type" + within('#action_fields') { click_button "Add" } + select2 "Percent Per Item", :from => "Calculator" + within('#actions_container') { click_button "Update" } + within('.calculator-fields') { fill_in "Percent", :with => "10" } + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + expect(promotion.code).to be_blank + + first_rule = promotion.rules.first + expect(first_rule.class).to eq(Spree::Promotion::Rules::Product) + expect(first_rule.products.map(&:name)).to include("RoR Mug") + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateItemAdjustments) + first_action_calculator = first_action.calculator + expect(first_action_calculator.class).to eq(Spree::Calculator::PercentOnLineItem) + expect(first_action_calculator.preferred_percent).to eq(10) + end + + xit "should allow an admin to create an automatic promotion with free shipping (no code)" do + fill_in "Name", :with => "Promotion" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Item total", :from => "Add rule of type" + within('#rule_fields') { click_button "Add" } + eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount", :with => "30" + within('#rule_fields') { click_button "Update" } + + select2 "Create whole-order adjustment", :from => "Add action of type" + within('#action_fields') { click_button "Add" } + select2 "Free Shipping", :from => "Calculator" + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + expect(promotion.code).to be_blank + + first_rule = promotion.rules.first + expect(first_rule.class).to eq(Spree::Promotion::Rules::ItemTotal) + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateAdjustment) + first_action_calculator = first_action.calculator + expect(first_action_calculator.class).to eq(Spree::Calculator::FreeShipping) + end + + it "should allow an admin to create an automatic promo requiring a landing page to be visited" do + fill_in "Name", :with => "Promotion" + fill_in "Path", :with => "content/cvv" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Create whole-order adjustment", :from => "Add action of type" + within('#action_fields') { click_button "Add" } + select2 "Flat Rate", :from => "Calculator" + within('#actions_container') { click_button "Update" } + within('.calculator-fields') { fill_in "Amount", :with => "4" } + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + expect(promotion.path).to eq("content/cvv") + expect(promotion.code).to be_blank + expect(promotion.rules).to be_blank + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateAdjustment) + first_action_calculator = first_action.calculator + expect(first_action_calculator.class).to eq(Spree::Calculator::FlatRate) + expect(first_action_calculator.preferred_amount).to eq(4) + end + + it "should allow an admin to create a promotion that adds a 'free' item to the cart" do + create(:product, :name => "RoR Mug") + fill_in "Name", :with => "Promotion" + fill_in "Code", :with => "complex" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Create line items", :from => "Add action of type" + + within('#action_fields') { click_button "Add" } + + page.find('.create_line_items .select2-choice').click + page.find('.select2-input').set('RoR Mug') + page.find('.select2-highlighted').click + + within('#actions_container') { click_button "Update" } + + select2 "Create whole-order adjustment", :from => "Add action of type" + within('#new_promotion_action_form') { click_button "Add" } + select2 "Flat Rate", :from => "Calculator" + within('#actions_container') { click_button "Update" } + within('.calculator-fields') { fill_in "Amount", :with => "40.00" } + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + expect(promotion.code).to eq("complex") + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateLineItems) + line_item = expect(first_action.promotion_action_line_items).not_to be_empty + end + + it "ceasing to be eligible for a promotion with item total rule then becoming eligible again" do + fill_in "Name", :with => "Promotion" + click_button "Create" + expect(page).to have_content("Editing Promotion") + + select2 "Item total", :from => "Add rule of type" + within('#rule_fields') { click_button "Add" } + eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount_min", :with => "50" + eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount_max", :with => "150" + within('#rule_fields') { click_button "Update" } + + select2 "Create whole-order adjustment", :from => "Add action of type" + within('#action_fields') { click_button "Add" } + select2 "Flat Rate", :from => "Calculator" + within('#actions_container') { click_button "Update" } + within('.calculator-fields') { fill_in "Amount", :with => "5" } + within('#actions_container') { click_button "Update" } + + promotion = Spree::Promotion.find_by_name("Promotion") + + first_rule = promotion.rules.first + expect(first_rule.class).to eq(Spree::Promotion::Rules::ItemTotal) + expect(first_rule.preferred_amount_min).to eq(50) + expect(first_rule.preferred_amount_max).to eq(150) + + first_action = promotion.actions.first + expect(first_action.class).to eq(Spree::Promotion::Actions::CreateAdjustment) + expect(first_action.calculator.class).to eq(Spree::Calculator::FlatRate) + expect(first_action.calculator.preferred_amount).to eq(5) + end + end +end diff --git a/backend/spec/features/admin/promotions/tiered_calculator_spec.rb b/backend/spec/features/admin/promotions/tiered_calculator_spec.rb new file mode 100644 index 00000000000..2ecde18bbdb --- /dev/null +++ b/backend/spec/features/admin/promotions/tiered_calculator_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +feature "Tiered Calculator Promotions" do + stub_authorization! + + let(:promotion) { create :promotion } + + background do + visit spree.edit_admin_promotion_path(promotion) + end + + scenario "adding a tiered percent calculator", js: true do + select2 "Create whole-order adjustment", from: "Add action of type" + within('#action_fields') { click_button "Add" } + + select2 "Tiered Percent", from: "Calculator" + within('#actions_container') { click_button "Update" } + + within("#actions_container .settings") do + expect(page).to have_content("BASE PERCENT") + expect(page).to have_content("TIERS") + + click_button "Add" + end + + fill_in "Base Percent", with: 5 + + within(".tier") do + find("input:last-child").set(100) + find("input:first-child").set(10) + end + + within('#actions_container') { click_button "Update" } + + first_action = promotion.actions.first + expect(first_action.class).to eq Spree::Promotion::Actions::CreateAdjustment + + first_action_calculator = first_action.calculator + expect(first_action_calculator.class).to eq Spree::Calculator::TieredPercent + expect(first_action_calculator.preferred_base_percent).to eq 5 + expect(first_action_calculator.preferred_tiers).to eq Hash[100.0 => 10.0] + end + + context "with an existing tiered flat rate calculator" do + let(:promotion) { create :promotion, :with_order_adjustment } + + background do + action = promotion.actions.first + + action.calculator = Spree::Calculator::TieredFlatRate.new + action.calculator.preferred_base_amount = 5 + action.calculator.preferred_tiers = Hash[100 => 10, 200 => 15, 300 => 20] + action.calculator.save! + + visit spree.edit_admin_promotion_path(promotion) + end + + scenario "deleting a tier", js: true do + within(".tier:nth-child(2)") do + find(".remove").click + end + + within('#actions_container') { click_button "Update" } + + calculator = promotion.actions.first.calculator + expect(calculator.preferred_tiers).to eq Hash[100.0 => 10.0, 300.0 => 20.0] + end + end +end diff --git a/backend/spec/features/admin/reports_spec.rb b/backend/spec/features/admin/reports_spec.rb new file mode 100644 index 00000000000..8e8a0b27df2 --- /dev/null +++ b/backend/spec/features/admin/reports_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe "Reports", :type => :feature do + stub_authorization! + + context "visiting the admin reports page" do + it "should have the right content" do + visit spree.admin_path + click_link "Reports" + click_link "Sales Total" + + expect(page).to have_content("Sales Totals") + expect(page).to have_content("Item Total") + expect(page).to have_content("Adjustment Total") + expect(page).to have_content("Sales Total") + end + end + + context "searching the admin reports page" do + before do + order = create(:order) + order.update_columns({:adjustment_total => 100}) + order.completed_at = Time.now + order.save! + + order = create(:order) + order.update_columns({:adjustment_total => 200}) + order.completed_at = Time.now + order.save! + + #incomplete order + order = create(:order) + order.update_columns({:adjustment_total => 50}) + order.save! + + order = create(:order) + order.update_columns({:adjustment_total => 200}) + order.completed_at = 3.years.ago + order.created_at = 3.years.ago + order.save! + + order = create(:order) + order.update_columns({:adjustment_total => 200}) + order.completed_at = 3.years.from_now + order.created_at = 3.years.from_now + order.save! + end + + it "should allow me to search for reports" do + visit spree.admin_path + click_link "Reports" + click_link "Sales Total" + + fill_in "q_completed_at_gt", :with => 1.week.ago + fill_in "q_completed_at_lt", :with => 1.week.from_now + click_button "Search" + + expect(page).to have_content("$300.00") + end + end +end diff --git a/backend/spec/features/admin/stock_transfer_spec.rb b/backend/spec/features/admin/stock_transfer_spec.rb new file mode 100644 index 00000000000..fe9ad84a1e3 --- /dev/null +++ b/backend/spec/features/admin/stock_transfer_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe 'Stock Transfers', :type => :feature, :js => true do + stub_authorization! + + it 'transfer between 2 locations' do + source_location = create(:stock_location_with_items, :name => 'NY') + destination_location = create(:stock_location, :name => 'SF') + variant = Spree::Variant.last + + visit spree.new_admin_stock_transfer_path + + fill_in 'reference', :with => 'PO 666' + + select2_search variant.name, :from => 'Variant' + + click_button 'Add' + click_button 'Transfer Stock' + + expect(page).to have_content('STOCK TRANSFER REFERENCE PO 666') + expect(page).to have_content('NY') + expect(page).to have_content('SF') + expect(page).to have_content(variant.name) + + transfer = Spree::StockTransfer.last + expect(transfer.stock_movements.size).to eq 2 + end + + describe 'received stock transfer' do + def it_is_received_stock_transfer(page) + expect(page).to have_content('STOCK TRANSFER REFERENCE PO 666') + expect(page).not_to have_selector("#stock-location-source") + expect(page).to have_selector("#stock-location-destination") + + transfer = Spree::StockTransfer.last + expect(transfer.stock_movements.size).to eq 1 + expect(transfer.source_location).to be_nil + end + + it 'receive stock to a single location' do + source_location = create(:stock_location_with_items, :name => 'NY') + destination_location = create(:stock_location, :name => 'SF') + variant = Spree::Variant.last + + visit spree.new_admin_stock_transfer_path + + fill_in 'reference', :with => 'PO 666' + check 'transfer_receive_stock' + select('NY', :from => 'transfer_destination_location_id') + select2_search variant.name, :from => 'Variant' + + click_button 'Add' + click_button 'Transfer Stock' + + it_is_received_stock_transfer page + end + + it 'forced to only receive there is only one location' do + source_location = create(:stock_location_with_items, :name => 'NY') + variant = Spree::Variant.last + + visit spree.new_admin_stock_transfer_path + + fill_in 'reference', :with => 'PO 666' + + select('NY', :from => 'transfer_destination_location_id') + select2_search variant.name, :from => 'Variant' + + click_button 'Add' + click_button 'Transfer Stock' + + it_is_received_stock_transfer page + end + end +end diff --git a/backend/spec/features/admin/taxons_spec.rb b/backend/spec/features/admin/taxons_spec.rb new file mode 100644 index 00000000000..1ad3532add1 --- /dev/null +++ b/backend/spec/features/admin/taxons_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe "Taxonomies and taxons", :type => :feature do + stub_authorization! + + it "admin should be able to edit taxon" do + + visit spree.new_admin_taxonomy_path + + fill_in "Name", :with => "Hello" + click_button "Create" + + @taxonomy = Spree::Taxonomy.last + + visit spree.edit_admin_taxonomy_taxon_path(@taxonomy, @taxonomy.root.id) + + fill_in "taxon_name", :with => "Shirt" + fill_in "taxon_description", :with => "Discover our new rails shirts" + + fill_in "permalink_part", :with => "shirt-rails" + click_button "Update" + expect(page).to have_content("Taxon \"Shirt\" has been successfully updated!") + end +end diff --git a/backend/spec/features/admin/users_spec.rb b/backend/spec/features/admin/users_spec.rb new file mode 100644 index 00000000000..30c9238f2c1 --- /dev/null +++ b/backend/spec/features/admin/users_spec.rb @@ -0,0 +1,278 @@ +require 'spec_helper' + +describe 'Users', :type => :feature do + stub_authorization! + let!(:country) { create(:country) } + let!(:user_a) { create(:user_with_addreses, email: 'a@example.com') } + let!(:user_b) { create(:user_with_addreses, email: 'b@example.com') } + + let(:order) { create(:completed_order_with_totals, user: user_a, number: "R123") } + + let(:order_2) do + create(:completed_order_with_totals, user: user_a, number: "R456").tap do |o| + li = o.line_items.last + li.update_column(:price, li.price + 10) + end + end + + let(:orders) { [order, order_2] } + + shared_examples_for 'a user page' do + it 'has lifetime stats' do + orders + visit current_url # need to refresh after creating the orders for specs that did not require orders + within("#user-lifetime-stats") do + [:total_sales, :num_orders, :average_order_value, :member_since].each do |stat_name| + expect(page).to have_content Spree.t(stat_name) + end + expect(page).to have_content (order.total + order_2.total) + expect(page).to have_content orders.count + expect(page).to have_content (orders.sum(&:total) / orders.count) + expect(page).to have_content I18n.l(user_a.created_at.to_date) + end + end + + it 'can go back to the users list' do + expect(page).to have_link Spree.t(:back_to_users_list), href: spree.admin_users_path + end + + it 'can navigate to the account page' do + expect(page).to have_link Spree.t(:"admin.user.account"), href: spree.edit_admin_user_path(user_a) + end + + it 'can navigate to the order history' do + expect(page).to have_link Spree.t(:"admin.user.orders"), href: spree.orders_admin_user_path(user_a) + end + + it 'can navigate to the items purchased' do + expect(page).to have_link Spree.t(:"admin.user.items"), href: spree.items_admin_user_path(user_a) + end + end + + shared_examples_for 'a sortable attribute' do + before { click_link sort_link } + + it "can sort asc" do + within_table(table_id) do + expect(page).to have_text text_match_1 + expect(page).to have_text text_match_2 + expect(text_match_1).to appear_before text_match_2 + end + end + + it "can sort desc" do + within_table(table_id) do + click_link sort_link + + expect(page).to have_text text_match_1 + expect(page).to have_text text_match_2 + expect(text_match_2).to appear_before text_match_1 + end + end + end + + before do + visit spree.admin_path + click_link 'Users' + end + + context 'users index' do + + context "email" do + it_behaves_like "a sortable attribute" do + let(:text_match_1) { user_a.email } + let(:text_match_2) { user_b.email } + let(:table_id) { "listing_users" } + let(:sort_link) { "users_email_title" } + end + end + + it 'displays the correct results for a user search' do + fill_in 'q_email_cont', with: user_a.email + click_button 'Search' + within_table('listing_users') do + expect(page).to have_text user_a.email + expect(page).not_to have_text user_b.email + end + end + end + + context 'editing users' do + before { click_link user_a.email } + + it_behaves_like 'a user page' + + it 'can edit the user email' do + fill_in 'user_email', with: 'a@example.com99' + click_button 'Update' + + expect(user_a.reload.email).to eq 'a@example.com99' + expect(page).to have_text 'Account updated' + expect(find_field('user_email').value).to eq 'a@example.com99' + end + + it 'can edit the user password' do + fill_in 'user_password', with: 'welcome' + fill_in 'user_password_confirmation', with: 'welcome' + click_button 'Update' + + expect(page).to have_text 'Account updated' + end + + it 'can edit user roles' do + Spree::Role.create name: "admin" + click_link user_a.email + + check 'user_spree_role_admin' + click_button 'Update' + expect(page).to have_text 'Account updated' + expect(find_field('user_spree_role_admin')['checked']).to be true + end + + it 'can edit user shipping address' do + click_link "Addresses" + + within("#admin_user_edit_addresses") do + fill_in "user_ship_address_attributes_address1", with: "1313 Mockingbird Ln" + click_button 'Update' + expect(find_field('user_ship_address_attributes_address1').value).to eq "1313 Mockingbird Ln" + end + + expect(user_a.reload.ship_address.address1).to eq "1313 Mockingbird Ln" + end + + it 'can edit user billing address' do + click_link "Addresses" + + within("#admin_user_edit_addresses") do + fill_in "user_bill_address_attributes_address1", with: "1313 Mockingbird Ln" + click_button 'Update' + expect(find_field('user_bill_address_attributes_address1').value).to eq "1313 Mockingbird Ln" + end + + expect(user_a.reload.bill_address.address1).to eq "1313 Mockingbird Ln" + end + + context 'no api key exists' do + it 'can generate a new api key' do + within("#admin_user_edit_api_key") do + expect(user_a.spree_api_key).to be_blank + click_button Spree.t('generate_key', :scope => 'api') + end + + expect(user_a.reload.spree_api_key).to be_present + + within("#admin_user_edit_api_key") do + expect(find("#current-api-key").text).to match /Key: #{user_a.spree_api_key}/ + end + end + end + + context 'an api key exists' do + before do + user_a.generate_spree_api_key! + expect(user_a.reload.spree_api_key).to be_present + visit current_path + end + + it 'can clear an api key' do + within("#admin_user_edit_api_key") do + click_button Spree.t('clear_key', :scope => 'api') + end + + expect(user_a.reload.spree_api_key).to be_blank + expect { find("#current-api-key") }.to raise_error Capybara::ElementNotFound + end + + it 'can regenerate an api key' do + old_key = user_a.spree_api_key + + within("#admin_user_edit_api_key") do + click_button Spree.t('regenerate_key', :scope => 'api') + end + + expect(user_a.reload.spree_api_key).to be_present + expect(user_a.reload.spree_api_key).not_to eq old_key + + within("#admin_user_edit_api_key") do + expect(find("#current-api-key").text).to match /Key: #{user_a.spree_api_key}/ + end + end + end + end + + context 'order history with sorting' do + + before do + orders + click_link user_a.email + within("#sidebar") { click_link Spree.t(:"admin.user.orders") } + end + + it_behaves_like 'a user page' + + context "completed_at" do + it_behaves_like "a sortable attribute" do + let(:text_match_1) { I18n.l(order.completed_at.to_date) } + let(:text_match_2) { I18n.l(order_2.completed_at.to_date) } + let(:table_id) { "listing_orders" } + let(:sort_link) { "orders_completed_at_title" } + end + end + + [:number, :state, :total].each do |attr| + context attr do + it_behaves_like "a sortable attribute" do + let(:text_match_1) { order.send(attr).to_s } + let(:text_match_2) { order_2.send(attr).to_s } + let(:table_id) { "listing_orders" } + let(:sort_link) { "orders_#{attr}_title" } + end + end + end + end + + context 'items purchased with sorting' do + + before do + orders + click_link user_a.email + within("#sidebar") { click_link Spree.t(:"admin.user.items") } + end + + it_behaves_like 'a user page' + + context "completed_at" do + it_behaves_like "a sortable attribute" do + let(:text_match_1) { I18n.l(order.completed_at.to_date) } + let(:text_match_2) { I18n.l(order_2.completed_at.to_date) } + let(:table_id) { "listing_items" } + let(:sort_link) { "orders_completed_at_title" } + end + end + + [:number, :state].each do |attr| + context attr do + it_behaves_like "a sortable attribute" do + let(:text_match_1) { order.send(attr).to_s } + let(:text_match_2) { order_2.send(attr).to_s } + let(:table_id) { "listing_items" } + let(:sort_link) { "orders_#{attr}_title" } + end + end + end + + it "has item attributes" do + items = order.line_items | order_2.line_items + expect(page).to have_table 'listing_items' + within_table('listing_items') do + items.each do |item| + expect(page).to have_selector(".item-name", text: item.product.name) + expect(page).to have_selector(".item-price", text: item.single_money.to_html) + expect(page).to have_selector(".item-quantity", text: item.quantity) + expect(page).to have_selector(".item-total", text: item.money.to_html) + end + end + end + end +end diff --git a/backend/spec/helpers/admin/base_helper_spec.rb b/backend/spec/helpers/admin/base_helper_spec.rb new file mode 100644 index 00000000000..ff1901d12a5 --- /dev/null +++ b/backend/spec/helpers/admin/base_helper_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Spree::Admin::BaseHelper, :type => :helper do + include Spree::Admin::BaseHelper + + context "#datepicker_field_value" do + it "should return nil when date is empty" do + date = nil + expect(datepicker_field_value(date)).to be_nil + end + + it "should return a formatted date when date is present" do + date = "2013-08-14".to_time + expect(datepicker_field_value(date)).to eq("2013/08/14") + end + end + + context "rails environments" do + it "returns the existing environments" do + expect(rails_environments).to eql ["development","production", "test"] + end + end + +end diff --git a/backend/spec/helpers/admin/navigation_helper_spec.rb b/backend/spec/helpers/admin/navigation_helper_spec.rb new file mode 100644 index 00000000000..034256c2c83 --- /dev/null +++ b/backend/spec/helpers/admin/navigation_helper_spec.rb @@ -0,0 +1,107 @@ +# coding: UTF-8 +require 'spec_helper' + +describe Spree::Admin::NavigationHelper, :type => :helper do + + describe "#tab" do + before do + allow(helper).to receive(:cannot?).and_return false + end + + context "creating an admin tab" do + it "should capitalize the first letter of each word in the tab's label" do + admin_tab = helper.tab(:orders) + expect(admin_tab).to include("Orders") + end + end + + it "should accept options with label and capitalize each word of it" do + admin_tab = helper.tab(:orders, :label => "delivered orders") + expect(admin_tab).to include("Delivered Orders") + end + + it "should capitalize words with unicode characters" do + # overview + admin_tab = helper.tab(:orders, :label => "přehled") + expect(admin_tab).to include("Přehled") + end + + describe "selection" do + context "when match_path option is not supplied" do + subject(:tab) { helper.tab(:orders) } + + it "should be selected if the controller matches" do + allow(controller).to receive(:controller_name).and_return("orders") + expect(subject).to include('class="selected"') + end + + it "should not be selected if the controller does not match" do + allow(controller).to receive(:controller_name).and_return("bonobos") + expect(subject).not_to include('class="selected"') + end + + end + + context "when match_path option is supplied" do + before do + allow(helper).to receive(:admin_path).and_return("/somepath") + allow(helper).to receive(:request).and_return(double(ActionDispatch::Request, :fullpath => "/somepath/orders/edit/1")) + end + + it "should be selected if the fullpath matches" do + allow(controller).to receive(:controller_name).and_return("bonobos") + tab = helper.tab(:orders, :label => "delivered orders", :match_path => '/orders') + expect(tab).to include('class="selected"') + end + + it "should be selected if the fullpath matches a regular expression" do + allow(controller).to receive(:controller_name).and_return("bonobos") + tab = helper.tab(:orders, :label => "delivered orders", :match_path => /orders$|orders\//) + expect(tab).to include('class="selected"') + end + + it "should not be selected if the fullpath does not match" do + allow(controller).to receive(:controller_name).and_return("bonobos") + tab = helper.tab(:orders, :label => "delivered orders", :match_path => '/shady') + expect(tab).not_to include('class="selected"') + end + + it "should not be selected if the fullpath does not match a regular expression" do + allow(controller).to receive(:controller_name).and_return("bonobos") + tab = helper.tab(:orders, :label => "delivered orders", :match_path => /shady$|shady\//) + expect(tab).not_to include('class="selected"') + end + end + end + end + + describe '#klass_for' do + + it 'returns correct klass for Spree model' do + expect(klass_for(:products)).to eq(Spree::Product) + expect(klass_for(:product_properties)).to eq(Spree::ProductProperty) + end + + it 'returns correct klass for non-spree model' do + class MyUser + end + expect(klass_for(:my_users)).to eq(MyUser) + + Object.send(:remove_const, 'MyUser') + end + + it 'returns correct namespaced klass for non-spree model' do + module My + class User + end + end + + expect(klass_for(:my_users)).to eq(My::User) + + My.send(:remove_const, 'User') + Object.send(:remove_const, 'My') + end + + end + +end \ No newline at end of file diff --git a/backend/spec/helpers/admin/stock_movements_helper_spec.rb b/backend/spec/helpers/admin/stock_movements_helper_spec.rb new file mode 100644 index 00000000000..7f9a7f48cc1 --- /dev/null +++ b/backend/spec/helpers/admin/stock_movements_helper_spec.rb @@ -0,0 +1,30 @@ +# coding: UTF-8 +require 'spec_helper' + +describe Spree::Admin::StockMovementsHelper, :type => :helper do + + describe "#pretty_originator" do + + context "transfering between two locations" do + let(:destination_location) { create(:stock_location_with_items) } + let(:source_location) { create(:stock_location_with_items) } + let(:stock_item) { source_location.stock_items.order(:id).first } + let(:variant) { stock_item.variant } + + before do + @stock_transfer = Spree::StockTransfer.create(reference: 'PO123') + variants = { variant => 5 } + @stock_transfer.transfer(source_location, + destination_location, + variants) + helper.pretty_originator(@stock_transfer.stock_movements.last) + end + + + it "returns link to stock transfer" do + expect(helper.pretty_originator(@stock_transfer.stock_movements.last)).to eq @stock_transfer.number + end + end + end + +end \ No newline at end of file diff --git a/backend/spec/helpers/promotion_rules_helper_spec.rb b/backend/spec/helpers/promotion_rules_helper_spec.rb new file mode 100644 index 00000000000..9834b5ac108 --- /dev/null +++ b/backend/spec/helpers/promotion_rules_helper_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +module Spree + describe Spree::PromotionRulesHelper, :type => :helper do + it "does not include existing rules in options" do + promotion = Spree::Promotion.new + promotion.promotion_rules << Spree::Promotion::Rules::ItemTotal.new + + options = helper.options_for_promotion_rule_types(promotion) + expect(options).not_to match(/ItemTotal/) + end + end +end diff --git a/backend/spec/spec_helper.rb b/backend/spec/spec_helper.rb new file mode 100644 index 00000000000..e17b6fcb1ef --- /dev/null +++ b/backend/spec/spec_helper.rb @@ -0,0 +1,118 @@ +if ENV["COVERAGE"] + # Run Coverage report + require 'simplecov' + SimpleCov.start do + add_group 'Controllers', 'app/controllers' + add_group 'Helpers', 'app/helpers' + add_group 'Mailers', 'app/mailers' + add_group 'Models', 'app/models' + add_group 'Views', 'app/views' + add_group 'Libraries', 'lib' + end +end + +# This file is copied to ~/spec when you run 'ruby script/generate rspec' +# from the project root directory. +ENV["RAILS_ENV"] ||= 'test' + +begin + require File.expand_path("../dummy/config/environment", __FILE__) +rescue LoadError + puts "Could not load dummy application. Please ensure you have run `bundle exec rake test_app`" + exit +end + +require 'rspec/rails' + +# Requires supporting files with custom matchers and macros, etc, +# in ./support/ and its subdirectories. +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} + +require 'database_cleaner' +require 'ffaker' + +require 'spree/testing_support/authorization_helpers' +require 'spree/testing_support/factories' +require 'spree/testing_support/preferences' +require 'spree/testing_support/controller_requests' +require 'spree/testing_support/flash' +require 'spree/testing_support/url_helpers' +require 'spree/testing_support/order_walkthrough' +require 'spree/testing_support/capybara_ext' + +require 'paperclip/matchers' + +require 'capybara/poltergeist' +Capybara.javascript_driver = :poltergeist + +RSpec.configure do |config| + config.color = true + config.infer_spec_type_from_file_location! + config.mock_with :rspec + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, comment the following line or assign false + # instead of true. + config.use_transactional_fixtures = false + + # A workaround to deal with random failure caused by phantomjs. Turn it on + # by setting ENV['RSPEC_RETRY_COUNT']. Limit it to features tests where + # phantomjs is used. + config.before(:all, :type => :feature) do + if ENV['RSPEC_RETRY_COUNT'] + config.verbose_retry = true # show retry status in spec process + config.default_retry_count = ENV['RSPEC_RETRY_COUNT'].to_i + end + end + + config.before :suite do + Capybara.match = :prefer_exact + DatabaseCleaner.clean_with :truncation + end + + config.before(:each) do + Rails.cache.clear + WebMock.disable! + if RSpec.current_example.metadata[:js] + DatabaseCleaner.strategy = :truncation + else + DatabaseCleaner.strategy = :transaction + end + # TODO: Find out why open_transactions ever gets below 0 + # See issue #3428 + if ActiveRecord::Base.connection.open_transactions < 0 + ActiveRecord::Base.connection.increment_open_transactions + end + + DatabaseCleaner.start + reset_spree_preferences + end + + config.after(:each) do + # Ensure js requests finish processing before advancing to the next test + wait_for_ajax if RSpec.current_example.metadata[:js] + + DatabaseCleaner.clean + end + + config.after(:each, :type => :feature) do |example| + missing_translations = page.body.scan(/translation missing: #{I18n.locale}\.(.*?)[\s<\"&]/) + if missing_translations.any? + puts "Found missing translations: #{missing_translations.inspect}" + puts "In spec: #{example.location}" + end + end + + config.include FactoryGirl::Syntax::Methods + + config.include Spree::TestingSupport::Preferences + config.include Spree::TestingSupport::UrlHelpers + config.include Spree::TestingSupport::ControllerRequests + config.include Spree::TestingSupport::Flash + + config.include Paperclip::Shoulda::Matchers + + config.extend WithModel + + config.fail_fast = ENV['FAIL_FAST'] || false +end diff --git a/backend/spec/support/appear_before_matcher.rb b/backend/spec/support/appear_before_matcher.rb new file mode 100644 index 00000000000..dbc08743f95 --- /dev/null +++ b/backend/spec/support/appear_before_matcher.rb @@ -0,0 +1,8 @@ +require 'rspec/expectations' + +RSpec::Matchers.define :appear_before do |expected| + match do |actual| + raise "Page instance required to use the appear_before matcher" unless page + page.body.index(actual) <= page.body.index(expected) + end +end diff --git a/backend/spec/support/ror_ringer.jpeg b/backend/spec/support/ror_ringer.jpeg new file mode 100644 index 00000000000..68009b1b7a1 Binary files /dev/null and b/backend/spec/support/ror_ringer.jpeg differ diff --git a/backend/spec/test_views/spree/admin/widgets/edit.html.erb b/backend/spec/test_views/spree/admin/widgets/edit.html.erb new file mode 100644 index 00000000000..366226fb970 --- /dev/null +++ b/backend/spec/test_views/spree/admin/widgets/edit.html.erb @@ -0,0 +1 @@ +edit diff --git a/backend/spec/test_views/spree/admin/widgets/new.html.erb b/backend/spec/test_views/spree/admin/widgets/new.html.erb new file mode 100644 index 00000000000..3e757656cf3 --- /dev/null +++ b/backend/spec/test_views/spree/admin/widgets/new.html.erb @@ -0,0 +1 @@ +new diff --git a/backend/spree_backend.gemspec b/backend/spree_backend.gemspec new file mode 100644 index 00000000000..ae2d1c48c69 --- /dev/null +++ b/backend/spree_backend.gemspec @@ -0,0 +1,27 @@ +# encoding: UTF-8 +version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip + +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'spree_backend' + s.version = version + s.summary = 'backend e-commerce functionality for the Spree project.' + s.description = 'Required dependency for Spree' + + s.required_ruby_version = '>= 1.9.3' + s.author = 'Sean Schofield' + s.email = 'sean@spreecommerce.com' + s.homepage = 'http://spreecommerce.com' + s.rubyforge_project = 'spree_backend' + + s.files = Dir['LICENSE', 'README.md', 'app/**/*', 'config/**/*', 'lib/**/*', 'db/**/*', 'vendor/**/*'] + s.require_path = 'lib' + s.requirements << 'none' + + s.add_dependency 'spree_api', version + s.add_dependency 'spree_core', version + + s.add_dependency 'jquery-rails', '~> 3.1.2' + s.add_dependency 'jquery-ui-rails', '~> 5.0.0' + s.add_dependency 'select2-rails', '3.5.9.1' # 3.5.9.2 breaks forms +end diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100755 index 00000000000..b3fe56f2ae9 Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png new file mode 100755 index 00000000000..4c6db33f17f Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_flat_55_ffffff_40x100.png b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_55_ffffff_40x100.png new file mode 100755 index 00000000000..6ebfa5026e2 Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_55_ffffff_40x100.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_flat_75_ffffff_40x100.png b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_75_ffffff_40x100.png new file mode 100755 index 00000000000..6ebfa5026e2 Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_flat_75_ffffff_40x100.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png b/backend/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png new file mode 100755 index 00000000000..6faa4263e84 Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png b/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png new file mode 100755 index 00000000000..e3135425734 Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png b/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png new file mode 100755 index 00000000000..100899629bd Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png b/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png new file mode 100755 index 00000000000..13e3cc6badf Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-icons_0073ea_256x240.png b/backend/vendor/assets/images/jquery-ui/ui-icons_0073ea_256x240.png new file mode 100755 index 00000000000..56402482b6d Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-icons_0073ea_256x240.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-icons_454545_256x240.png b/backend/vendor/assets/images/jquery-ui/ui-icons_454545_256x240.png new file mode 100755 index 00000000000..1486b0cfc2e Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-icons_454545_256x240.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-icons_666666_256x240.png b/backend/vendor/assets/images/jquery-ui/ui-icons_666666_256x240.png new file mode 100755 index 00000000000..a32c57d80be Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-icons_666666_256x240.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-icons_ff0084_256x240.png b/backend/vendor/assets/images/jquery-ui/ui-icons_ff0084_256x240.png new file mode 100755 index 00000000000..4648bebd32b Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-icons_ff0084_256x240.png differ diff --git a/backend/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png b/backend/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png new file mode 100755 index 00000000000..29ba7d28fcb Binary files /dev/null and b/backend/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png differ diff --git a/core/vendor/assets/javascripts/jquery.alerts/images/help.gif b/backend/vendor/assets/images/jquery.alerts/images/help.gif similarity index 75% rename from core/vendor/assets/javascripts/jquery.alerts/images/help.gif rename to backend/vendor/assets/images/jquery.alerts/images/help.gif index 3b514253179..fbb5b329d4c 100755 Binary files a/core/vendor/assets/javascripts/jquery.alerts/images/help.gif and b/backend/vendor/assets/images/jquery.alerts/images/help.gif differ diff --git a/backend/vendor/assets/images/jquery.alerts/images/important.gif b/backend/vendor/assets/images/jquery.alerts/images/important.gif new file mode 100755 index 00000000000..654942362fc Binary files /dev/null and b/backend/vendor/assets/images/jquery.alerts/images/important.gif differ diff --git a/backend/vendor/assets/images/jquery.alerts/images/info.gif b/backend/vendor/assets/images/jquery.alerts/images/info.gif new file mode 100755 index 00000000000..35e1dfa6c2c Binary files /dev/null and b/backend/vendor/assets/images/jquery.alerts/images/info.gif differ diff --git a/backend/vendor/assets/images/jquery.alerts/images/title.gif b/backend/vendor/assets/images/jquery.alerts/images/title.gif new file mode 100755 index 00000000000..12253c7a44f Binary files /dev/null and b/backend/vendor/assets/images/jquery.alerts/images/title.gif differ diff --git a/backend/vendor/assets/javascripts/css_browser_selector_dev.js b/backend/vendor/assets/javascripts/css_browser_selector_dev.js new file mode 100644 index 00000000000..559f1dde47d --- /dev/null +++ b/backend/vendor/assets/javascripts/css_browser_selector_dev.js @@ -0,0 +1,129 @@ +/* +CSS Browser Selector 0.6.2 +Originally written by Rafael Lima (http://rafael.adm.br) +http://rafael.adm.br/css_browser_selector +License: http://creativecommons.org/licenses/by/2.5/ + +Co-maintained by: +https://github.com/verbatim/css_browser_selector +*/ + +/*jshint laxcomma:true*/ + +function css_browser_selector(u) { + 'use strict'; + + var uaInfo = {}, + screens = [320, 480, 640, 768, 1024, 1152, 1280, 1440, 1680, 1920, 2560], + allScreens = screens.length, + ua = u.toLowerCase(), + is = function (t) { + return new RegExp(t, "i").test(ua); + }, + version = function (p, n) { + n = n.replace(".", "_"); + var i = n.indexOf('_'), + ver = ""; + while (i > 0) { + ver += " " + p + n.substring(0, i); + i = n.indexOf('_', i + 1); + } + ver += " " + p + n; + return ver; + }, + g = 'gecko', + w = 'webkit', + c = 'chrome', + f = 'firefox', + s = 'safari', + o = 'opera', + m = 'mobile', + a = 'android', + bb = 'blackberry', + lang = 'lang_', + dv = 'device_', + html = document.documentElement, + b = [ + + (!(/opera|webtv/i.test(ua)) && /msie\s(\d+)/.test(ua)) ? ('ie ie' + (/trident\/4\.0/.test(ua) ? '8' : RegExp.$1)) : is('firefox/') ? g + " " + f + (/firefox\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua) ? ' ' + f + RegExp.$2 + ' ' + f + RegExp.$2 + "_" + RegExp.$4 : '') : is('gecko/') ? g : is('opera') ? o + (/version\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua) ? ' ' + o + RegExp.$2 + ' ' + o + RegExp.$2 + "_" + RegExp.$4 : (/opera(\s|\/)(\d+)\.(\d+)/.test(ua) ? ' ' + o + RegExp.$2 + " " + o + RegExp.$2 + "_" + RegExp.$3 : '')) : is('konqueror') ? 'konqueror' + + : is('blackberry') ? (bb + (/Version\/(\d+)(\.(\d+)+)/i.test(ua) ? " " + bb + RegExp.$1 + " " + bb + RegExp.$1 + RegExp.$2.replace('.', '_') : (/Blackberry ?(([0-9]+)([a-z]?))[\/|;]/gi.test(ua) ? ' ' + bb + RegExp.$2 + (RegExp.$3 ? ' ' + bb + RegExp.$2 + RegExp.$3 : '') : ''))) + : is('android') ? (a + (/Version\/(\d+)(\.(\d+))+/i.test(ua) ? " " + a + RegExp.$1 + " " + a + RegExp.$1 + RegExp.$2.replace('.', '_') : '') + (/Android (.+); (.+) Build/i.test(ua) ? ' ' + dv + ((RegExp.$2).replace(/ /g, "_")).replace(/-/g, "_") : '')) + : is('chrome') ? w + ' ' + c + (/chrome\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua) ? ' ' + c + RegExp.$2 + ((RegExp.$4 > 0) ? ' ' + c + RegExp.$2 + "_" + RegExp.$4 : '') : '') + : is('iron') ? w + ' iron' + : is('applewebkit/') ? (w + ' ' + s + (/version\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua) ? ' ' + s + RegExp.$2 + " " + s + RegExp.$2 + RegExp.$3.replace('.', '_') : (/ Safari\/(\d+)/i.test(ua) ? ((RegExp.$1 === "419" || RegExp.$1 === "417" || RegExp.$1 === "416" || RegExp.$1 === "412") ? ' ' + s + '2_0' : RegExp.$1 === "312" ? ' ' + s + '1_3' : RegExp.$1 === "125" ? ' ' + s + '1_2' : RegExp.$1 === "85" ? ' ' + s + '1_0' : '') : ''))) + : is('mozilla/') ? g : '' + + , + is("android|mobi|mobile|j2me|iphone|ipod|ipad|blackberry|playbook|kindle|silk") ? m : '' + + , + is('j2me') ? 'j2me' : is('ipad|ipod|iphone') ? ( + ( + /CPU( iPhone)? OS (\d+[_|\.]\d+([_|\.]\d+)*)/i.test(ua) ? + 'ios' + version('ios', RegExp.$2) : '') + ' ' + (/(ip(ad|od|hone))/gi.test(ua) ? RegExp.$1 : "") + ) + + : is('playbook') ? 'playbook' + : is('kindle|silk') ? 'kindle' + : is('playbook') ? 'playbook' + : is('mac') ? 'mac' + (/mac os x ((\d+)[.|_](\d+))/.test(ua) ? (' mac' + (RegExp.$2) + ' mac' + (RegExp.$1).replace('.', "_")) : '') + : is('win') ? 'win' + (is('windows nt 6.2') ? ' win8' + : is('windows nt 6.1') ? ' win7' + : is('windows nt 6.0') ? ' vista' + : is('windows nt 5.2') || is('windows nt 5.1') ? ' win_xp' + : is('windows nt 5.0') ? ' win_2k' + : is('windows nt 4.0') || is('WinNT4.0') ? ' win_nt' : '') + : is('freebsd') ? 'freebsd' + : (is('x11|linux')) ? 'linux' : '' + + , + (/[; |\[](([a-z]{2})(\-[a-z]{2})?)[)|;|\]]/i.test(ua)) ? (lang + RegExp.$2).replace("-", "_") + (RegExp.$3 !== '' ? (' ' + lang + RegExp.$1).replace("-", "_") : '') : '' + + , + (is('ipad|iphone|ipod') && !is('safari')) ? 'ipad_app' : '' + + ]; // b + + function screenSize() { + var w = window.outerWidth || html.clientWidth; + var h = window.outerHeight || html.clientHeight; + uaInfo.orientation = ((w < h) ? "portrait" : "landscape"); + // remove previous min-width, max-width, client-width, client-height, and orientation + html.className = html.className.replace(/ ?orientation_\w+/g, "").replace(/ [min|max|cl]+[w|h]_\d+/g, ""); + for (var i = (allScreens - 1); i >= 0; i--) { + if (w >= screens[i]) { + uaInfo.maxw = screens[i]; + break; + } + } + var widthClasses = ""; + for (var info in uaInfo) { + if (uaInfo.hasOwnProperty(info)) { + widthClasses += " " + info + "_" + uaInfo[info]; + } + } + html.className = (html.className + widthClasses); + return widthClasses; + } // screenSize + + window.onresize = screenSize; + screenSize(); + + function retina() { + var r = window.devicePixelRatio > 1; + if (r) { + html.className += ' retina'; + } else { + html.className += ' non-retina'; + } + } + retina(); + + var cssbs = (b.join(' ')) + " js "; + html.className = (cssbs + html.className.replace(/\b(no[-|_]?)?js\b/g, "")).replace(/^ /, "").replace(/ +/g, " "); + + return cssbs; +} + +css_browser_selector(navigator.userAgent); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/equalize.js b/backend/vendor/assets/javascripts/equalize.js similarity index 100% rename from core/vendor/assets/javascripts/equalize.js rename to backend/vendor/assets/javascripts/equalize.js diff --git a/backend/vendor/assets/javascripts/handlebars.js b/backend/vendor/assets/javascripts/handlebars.js new file mode 100644 index 00000000000..bec7085c510 --- /dev/null +++ b/backend/vendor/assets/javascripts/handlebars.js @@ -0,0 +1,2746 @@ +/*! + + handlebars v1.3.0 + +Copyright (C) 2011 by Yehuda Katz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +@license +*/ +/* exported Handlebars */ +var Handlebars = (function() { +// handlebars/safe-string.js +var __module4__ = (function() { + "use strict"; + var __exports__; + // Build out our basic SafeString type + function SafeString(string) { + this.string = string; + } + + SafeString.prototype.toString = function() { + return "" + this.string; + }; + + __exports__ = SafeString; + return __exports__; +})(); + +// handlebars/utils.js +var __module3__ = (function(__dependency1__) { + "use strict"; + var __exports__ = {}; + /*jshint -W004 */ + var SafeString = __dependency1__; + + var escape = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }; + + var badChars = /[&<>"'`]/g; + var possible = /[&<>"'`]/; + + function escapeChar(chr) { + return escape[chr] || "&"; + } + + function extend(obj, value) { + for(var key in value) { + if(Object.prototype.hasOwnProperty.call(value, key)) { + obj[key] = value[key]; + } + } + } + + __exports__.extend = extend;var toString = Object.prototype.toString; + __exports__.toString = toString; + // Sourced from lodash + // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt + var isFunction = function(value) { + return typeof value === 'function'; + }; + // fallback for older versions of Chrome and Safari + if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value === 'function' && toString.call(value) === '[object Function]'; + }; + } + var isFunction; + __exports__.isFunction = isFunction; + var isArray = Array.isArray || function(value) { + return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; + }; + __exports__.isArray = isArray; + + function escapeExpression(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof SafeString) { + return string.toString(); + } else if (!string && string !== 0) { + return ""; + } + + // Force a string conversion as this will be done by the append regardless and + // the regex test will do this transparently behind the scenes, causing issues if + // an object's to string has escaped characters in it. + string = "" + string; + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); + } + + __exports__.escapeExpression = escapeExpression;function isEmpty(value) { + if (!value && value !== 0) { + return true; + } else if (isArray(value) && value.length === 0) { + return true; + } else { + return false; + } + } + + __exports__.isEmpty = isEmpty; + return __exports__; +})(__module4__); + +// handlebars/exception.js +var __module5__ = (function() { + "use strict"; + var __exports__; + + var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + + function Exception(message, node) { + var line; + if (node && node.firstLine) { + line = node.firstLine; + + message += ' - ' + line + ':' + node.firstColumn; + } + + var tmp = Error.prototype.constructor.call(this, message); + + // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. + for (var idx = 0; idx < errorProps.length; idx++) { + this[errorProps[idx]] = tmp[errorProps[idx]]; + } + + if (line) { + this.lineNumber = line; + this.column = node.firstColumn; + } + } + + Exception.prototype = new Error(); + + __exports__ = Exception; + return __exports__; +})(); + +// handlebars/base.js +var __module2__ = (function(__dependency1__, __dependency2__) { + "use strict"; + var __exports__ = {}; + var Utils = __dependency1__; + var Exception = __dependency2__; + + var VERSION = "1.3.0"; + __exports__.VERSION = VERSION;var COMPILER_REVISION = 4; + __exports__.COMPILER_REVISION = COMPILER_REVISION; + var REVISION_CHANGES = { + 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it + 2: '== 1.0.0-rc.3', + 3: '== 1.0.0-rc.4', + 4: '>= 1.0.0' + }; + __exports__.REVISION_CHANGES = REVISION_CHANGES; + var isArray = Utils.isArray, + isFunction = Utils.isFunction, + toString = Utils.toString, + objectType = '[object Object]'; + + function HandlebarsEnvironment(helpers, partials) { + this.helpers = helpers || {}; + this.partials = partials || {}; + + registerDefaultHelpers(this); + } + + __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { + constructor: HandlebarsEnvironment, + + logger: logger, + log: log, + + registerHelper: function(name, fn, inverse) { + if (toString.call(name) === objectType) { + if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); } + Utils.extend(this.helpers, name); + } else { + if (inverse) { fn.not = inverse; } + this.helpers[name] = fn; + } + }, + + registerPartial: function(name, str) { + if (toString.call(name) === objectType) { + Utils.extend(this.partials, name); + } else { + this.partials[name] = str; + } + } + }; + + function registerDefaultHelpers(instance) { + instance.registerHelper('helperMissing', function(arg) { + if(arguments.length === 2) { + return undefined; + } else { + throw new Exception("Missing helper: '" + arg + "'"); + } + }); + + instance.registerHelper('blockHelperMissing', function(context, options) { + var inverse = options.inverse || function() {}, fn = options.fn; + + if (isFunction(context)) { context = context.call(this); } + + if(context === true) { + return fn(this); + } else if(context === false || context == null) { + return inverse(this); + } else if (isArray(context)) { + if(context.length > 0) { + return instance.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + return fn(context); + } + }); + + instance.registerHelper('each', function(context, options) { + var fn = options.fn, inverse = options.inverse; + var i = 0, ret = "", data; + + if (isFunction(context)) { context = context.call(this); } + + if (options.data) { + data = createFrame(options.data); + } + + if(context && typeof context === 'object') { + if (isArray(context)) { + for(var j = context.length; i 0) { + throw new Exception("Invalid path: " + original, this); + } else if (part === "..") { + depth++; + } else { + this.isScoped = true; + } + } else { + dig.push(part); + } + } + + this.original = original; + this.parts = dig; + this.string = dig.join('.'); + this.depth = depth; + + // an ID is simple if it only has one part, and that part is not + // `..` or `this`. + this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; + + this.stringModeValue = this.string; + }, + + PartialNameNode: function(name, locInfo) { + LocationInfo.call(this, locInfo); + this.type = "PARTIAL_NAME"; + this.name = name.original; + }, + + DataNode: function(id, locInfo) { + LocationInfo.call(this, locInfo); + this.type = "DATA"; + this.id = id; + }, + + StringNode: function(string, locInfo) { + LocationInfo.call(this, locInfo); + this.type = "STRING"; + this.original = + this.string = + this.stringModeValue = string; + }, + + IntegerNode: function(integer, locInfo) { + LocationInfo.call(this, locInfo); + this.type = "INTEGER"; + this.original = + this.integer = integer; + this.stringModeValue = Number(integer); + }, + + BooleanNode: function(bool, locInfo) { + LocationInfo.call(this, locInfo); + this.type = "BOOLEAN"; + this.bool = bool; + this.stringModeValue = bool === "true"; + }, + + CommentNode: function(comment, locInfo) { + LocationInfo.call(this, locInfo); + this.type = "comment"; + this.comment = comment; + } + }; + + // Must be exported as an object rather than the root of the module as the jison lexer + // most modify the object to operate properly. + __exports__ = AST; + return __exports__; +})(__module5__); + +// handlebars/compiler/parser.js +var __module9__ = (function() { + "use strict"; + var __exports__; + /* jshint ignore:start */ + /* Jison generated parser */ + var handlebars = (function(){ + var parser = {trace: function trace() { }, + yy: {}, + symbols_: {"error":2,"root":3,"statements":4,"EOF":5,"program":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"sexpr":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"CLOSE_UNESCAPED":24,"OPEN_PARTIAL":25,"partialName":26,"partial_option0":27,"sexpr_repetition0":28,"sexpr_option0":29,"dataName":30,"param":31,"STRING":32,"INTEGER":33,"BOOLEAN":34,"OPEN_SEXPR":35,"CLOSE_SEXPR":36,"hash":37,"hash_repetition_plus0":38,"hashSegment":39,"ID":40,"EQUALS":41,"DATA":42,"pathSegments":43,"SEP":44,"$accept":0,"$end":1}, + terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",32:"STRING",33:"INTEGER",34:"BOOLEAN",35:"OPEN_SEXPR",36:"CLOSE_SEXPR",40:"ID",41:"EQUALS",42:"DATA",44:"SEP"}, + productions_: [0,[3,2],[3,1],[6,2],[6,3],[6,2],[6,1],[6,1],[6,0],[4,1],[4,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,4],[7,2],[17,3],[17,1],[31,1],[31,1],[31,1],[31,1],[31,1],[31,3],[37,1],[39,3],[26,1],[26,1],[26,1],[30,2],[21,1],[43,3],[43,1],[27,0],[27,1],[28,0],[28,2],[29,0],[29,1],[38,1],[38,2]], + performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { + + var $0 = $$.length - 1; + switch (yystate) { + case 1: return new yy.ProgramNode($$[$0-1], this._$); + break; + case 2: return new yy.ProgramNode([], this._$); + break; + case 3:this.$ = new yy.ProgramNode([], $$[$0-1], $$[$0], this._$); + break; + case 4:this.$ = new yy.ProgramNode($$[$0-2], $$[$0-1], $$[$0], this._$); + break; + case 5:this.$ = new yy.ProgramNode($$[$0-1], $$[$0], [], this._$); + break; + case 6:this.$ = new yy.ProgramNode($$[$0], this._$); + break; + case 7:this.$ = new yy.ProgramNode([], this._$); + break; + case 8:this.$ = new yy.ProgramNode([], this._$); + break; + case 9:this.$ = [$$[$0]]; + break; + case 10: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; + break; + case 11:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0], this._$); + break; + case 12:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0], this._$); + break; + case 13:this.$ = $$[$0]; + break; + case 14:this.$ = $$[$0]; + break; + case 15:this.$ = new yy.ContentNode($$[$0], this._$); + break; + case 16:this.$ = new yy.CommentNode($$[$0], this._$); + break; + case 17:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], stripFlags($$[$0-2], $$[$0]), this._$); + break; + case 18:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], stripFlags($$[$0-2], $$[$0]), this._$); + break; + case 19:this.$ = {path: $$[$0-1], strip: stripFlags($$[$0-2], $$[$0])}; + break; + case 20:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], stripFlags($$[$0-2], $$[$0]), this._$); + break; + case 21:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], stripFlags($$[$0-2], $$[$0]), this._$); + break; + case 22:this.$ = new yy.PartialNode($$[$0-2], $$[$0-1], stripFlags($$[$0-3], $$[$0]), this._$); + break; + case 23:this.$ = stripFlags($$[$0-1], $$[$0]); + break; + case 24:this.$ = new yy.SexprNode([$$[$0-2]].concat($$[$0-1]), $$[$0], this._$); + break; + case 25:this.$ = new yy.SexprNode([$$[$0]], null, this._$); + break; + case 26:this.$ = $$[$0]; + break; + case 27:this.$ = new yy.StringNode($$[$0], this._$); + break; + case 28:this.$ = new yy.IntegerNode($$[$0], this._$); + break; + case 29:this.$ = new yy.BooleanNode($$[$0], this._$); + break; + case 30:this.$ = $$[$0]; + break; + case 31:$$[$0-1].isHelper = true; this.$ = $$[$0-1]; + break; + case 32:this.$ = new yy.HashNode($$[$0], this._$); + break; + case 33:this.$ = [$$[$0-2], $$[$0]]; + break; + case 34:this.$ = new yy.PartialNameNode($$[$0], this._$); + break; + case 35:this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0], this._$), this._$); + break; + case 36:this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0], this._$)); + break; + case 37:this.$ = new yy.DataNode($$[$0], this._$); + break; + case 38:this.$ = new yy.IdNode($$[$0], this._$); + break; + case 39: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2]; + break; + case 40:this.$ = [{part: $$[$0]}]; + break; + case 43:this.$ = []; + break; + case 44:$$[$0-1].push($$[$0]); + break; + case 47:this.$ = [$$[$0]]; + break; + case 48:$$[$0-1].push($$[$0]); + break; + } + }, + table: [{3:1,4:2,5:[1,3],8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[3]},{5:[1,16],8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[2,2]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{4:20,6:18,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{4:20,6:22,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{17:23,21:24,30:25,40:[1,28],42:[1,27],43:26},{17:29,21:24,30:25,40:[1,28],42:[1,27],43:26},{17:30,21:24,30:25,40:[1,28],42:[1,27],43:26},{17:31,21:24,30:25,40:[1,28],42:[1,27],43:26},{21:33,26:32,32:[1,34],33:[1,35],40:[1,28],43:26},{1:[2,1]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{10:36,20:[1,37]},{4:38,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,7],22:[1,13],23:[1,14],25:[1,15]},{7:39,8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,6],22:[1,13],23:[1,14],25:[1,15]},{17:23,18:[1,40],21:24,30:25,40:[1,28],42:[1,27],43:26},{10:41,20:[1,37]},{18:[1,42]},{18:[2,43],24:[2,43],28:43,32:[2,43],33:[2,43],34:[2,43],35:[2,43],36:[2,43],40:[2,43],42:[2,43]},{18:[2,25],24:[2,25],36:[2,25]},{18:[2,38],24:[2,38],32:[2,38],33:[2,38],34:[2,38],35:[2,38],36:[2,38],40:[2,38],42:[2,38],44:[1,44]},{21:45,40:[1,28],43:26},{18:[2,40],24:[2,40],32:[2,40],33:[2,40],34:[2,40],35:[2,40],36:[2,40],40:[2,40],42:[2,40],44:[2,40]},{18:[1,46]},{18:[1,47]},{24:[1,48]},{18:[2,41],21:50,27:49,40:[1,28],43:26},{18:[2,34],40:[2,34]},{18:[2,35],40:[2,35]},{18:[2,36],40:[2,36]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{21:51,40:[1,28],43:26},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,3],22:[1,13],23:[1,14],25:[1,15]},{4:52,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,5],22:[1,13],23:[1,14],25:[1,15]},{14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]},{18:[2,45],21:56,24:[2,45],29:53,30:60,31:54,32:[1,57],33:[1,58],34:[1,59],35:[1,61],36:[2,45],37:55,38:62,39:63,40:[1,64],42:[1,27],43:26},{40:[1,65]},{18:[2,37],24:[2,37],32:[2,37],33:[2,37],34:[2,37],35:[2,37],36:[2,37],40:[2,37],42:[2,37]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,66]},{18:[2,42]},{18:[1,67]},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],25:[1,15]},{18:[2,24],24:[2,24],36:[2,24]},{18:[2,44],24:[2,44],32:[2,44],33:[2,44],34:[2,44],35:[2,44],36:[2,44],40:[2,44],42:[2,44]},{18:[2,46],24:[2,46],36:[2,46]},{18:[2,26],24:[2,26],32:[2,26],33:[2,26],34:[2,26],35:[2,26],36:[2,26],40:[2,26],42:[2,26]},{18:[2,27],24:[2,27],32:[2,27],33:[2,27],34:[2,27],35:[2,27],36:[2,27],40:[2,27],42:[2,27]},{18:[2,28],24:[2,28],32:[2,28],33:[2,28],34:[2,28],35:[2,28],36:[2,28],40:[2,28],42:[2,28]},{18:[2,29],24:[2,29],32:[2,29],33:[2,29],34:[2,29],35:[2,29],36:[2,29],40:[2,29],42:[2,29]},{18:[2,30],24:[2,30],32:[2,30],33:[2,30],34:[2,30],35:[2,30],36:[2,30],40:[2,30],42:[2,30]},{17:68,21:24,30:25,40:[1,28],42:[1,27],43:26},{18:[2,32],24:[2,32],36:[2,32],39:69,40:[1,70]},{18:[2,47],24:[2,47],36:[2,47],40:[2,47]},{18:[2,40],24:[2,40],32:[2,40],33:[2,40],34:[2,40],35:[2,40],36:[2,40],40:[2,40],41:[1,71],42:[2,40],44:[2,40]},{18:[2,39],24:[2,39],32:[2,39],33:[2,39],34:[2,39],35:[2,39],36:[2,39],40:[2,39],42:[2,39],44:[2,39]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{36:[1,72]},{18:[2,48],24:[2,48],36:[2,48],40:[2,48]},{41:[1,71]},{21:56,30:60,31:73,32:[1,57],33:[1,58],34:[1,59],35:[1,61],40:[1,28],42:[1,27],43:26},{18:[2,31],24:[2,31],32:[2,31],33:[2,31],34:[2,31],35:[2,31],36:[2,31],40:[2,31],42:[2,31]},{18:[2,33],24:[2,33],36:[2,33],40:[2,33]}], + defaultActions: {3:[2,2],16:[2,1],50:[2,42]}, + parseError: function parseError(str, hash) { + throw new Error(str); + }, + parse: function parse(input) { + var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + this.lexer.setInput(input); + this.lexer.yy = this.yy; + this.yy.lexer = this.lexer; + this.yy.parser = this; + if (typeof this.lexer.yylloc == "undefined") + this.lexer.yylloc = {}; + var yyloc = this.lexer.yylloc; + lstack.push(yyloc); + var ranges = this.lexer.options && this.lexer.options.ranges; + if (typeof this.yy.parseError === "function") + this.parseError = this.yy.parseError; + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + function lex() { + var token; + token = self.lexer.lex() || 1; + if (typeof token !== "number") { + token = self.symbols_[token] || token; + } + return token; + } + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == "undefined") { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === "undefined" || !action.length || !action[0]) { + var errStr = ""; + if (!recovering) { + expected = []; + for (p in table[state]) + if (this.terminals_[p] && p > 2) { + expected.push("'" + this.terminals_[p] + "'"); + } + if (this.lexer.showPosition) { + errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; + } else { + errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); + } + this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); + } + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(this.lexer.yytext); + lstack.push(this.lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = this.lexer.yyleng; + yytext = this.lexer.yytext; + yylineno = this.lexer.yylineno; + yyloc = this.lexer.yylloc; + if (recovering > 0) + recovering--; + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; + if (ranges) { + yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; + } + r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); + if (typeof r !== "undefined") { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; + } + }; + + + function stripFlags(open, close) { + return { + left: open.charAt(2) === '~', + right: close.charAt(0) === '~' || close.charAt(1) === '~' + }; + } + + /* Jison generated lexer */ + var lexer = (function(){ + var lexer = ({EOF:1, + parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + setInput:function (input) { + this._input = input; + this._more = this._less = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; + if (this.options.ranges) this.yylloc.range = [0,0]; + this.offset = 0; + return this; + }, + input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) this.yylloc.range[1]++; + + this._input = this._input.slice(1); + return ch; + }, + unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length-len-1); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length-1); + this.matched = this.matched.substr(0, this.matched.length-1); + + if (lines.length-1) this.yylineno -= lines.length-1; + var r = this.yylloc.range; + + this.yylloc = {first_line: this.yylloc.first_line, + last_line: this.yylineno+1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + return this; + }, + more:function () { + this._more = true; + return this; + }, + less:function (n) { + this.unput(this.match.slice(n)); + }, + pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); + }, + showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c+"^"; + }, + next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) this.done = true; + + var token, + match, + tempMatch, + index, + col, + lines; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i=0;i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (!this.options.flex) break; + } + } + if (match) { + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) this.yylineno += lines.length; + this.yylloc = {first_line: this.yylloc.last_line, + last_line: this.yylineno+1, + first_column: this.yylloc.last_column, + last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); + if (this.done && this._input) this.done = false; + if (token) return token; + else return; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), + {text: "", token: null, line: this.yylineno}); + } + }, + lex:function lex() { + var r = this.next(); + if (typeof r !== 'undefined') { + return r; + } else { + return this.lex(); + } + }, + begin:function begin(condition) { + this.conditionStack.push(condition); + }, + popState:function popState() { + return this.conditionStack.pop(); + }, + _currentRules:function _currentRules() { + return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; + }, + topState:function () { + return this.conditionStack[this.conditionStack.length-2]; + }, + pushState:function begin(condition) { + this.begin(condition); + }}); + lexer.options = {}; + lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { + + + function strip(start, end) { + return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng-end); + } + + + var YYSTATE=YY_START + switch($avoiding_name_collisions) { + case 0: + if(yy_.yytext.slice(-2) === "\\\\") { + strip(0,1); + this.begin("mu"); + } else if(yy_.yytext.slice(-1) === "\\") { + strip(0,1); + this.begin("emu"); + } else { + this.begin("mu"); + } + if(yy_.yytext) return 14; + + break; + case 1:return 14; + break; + case 2: + this.popState(); + return 14; + + break; + case 3:strip(0,4); this.popState(); return 15; + break; + case 4:return 35; + break; + case 5:return 36; + break; + case 6:return 25; + break; + case 7:return 16; + break; + case 8:return 20; + break; + case 9:return 19; + break; + case 10:return 19; + break; + case 11:return 23; + break; + case 12:return 22; + break; + case 13:this.popState(); this.begin('com'); + break; + case 14:strip(3,5); this.popState(); return 15; + break; + case 15:return 22; + break; + case 16:return 41; + break; + case 17:return 40; + break; + case 18:return 40; + break; + case 19:return 44; + break; + case 20:// ignore whitespace + break; + case 21:this.popState(); return 24; + break; + case 22:this.popState(); return 18; + break; + case 23:yy_.yytext = strip(1,2).replace(/\\"/g,'"'); return 32; + break; + case 24:yy_.yytext = strip(1,2).replace(/\\'/g,"'"); return 32; + break; + case 25:return 42; + break; + case 26:return 34; + break; + case 27:return 34; + break; + case 28:return 33; + break; + case 29:return 40; + break; + case 30:yy_.yytext = strip(1,2); return 40; + break; + case 31:return 'INVALID'; + break; + case 32:return 5; + break; + } + }; + lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\()/,/^(?:\))/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.)])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s)])))/,/^(?:false(?=([~}\s)])))/,/^(?:-?[0-9]+(?=([~}\s)])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; + lexer.conditions = {"mu":{"rules":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[3],"inclusive":false},"INITIAL":{"rules":[0,1,32],"inclusive":true}}; + return lexer;})() + parser.lexer = lexer; + function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; + return new Parser; + })();__exports__ = handlebars; + /* jshint ignore:end */ + return __exports__; +})(); + +// handlebars/compiler/base.js +var __module8__ = (function(__dependency1__, __dependency2__) { + "use strict"; + var __exports__ = {}; + var parser = __dependency1__; + var AST = __dependency2__; + + __exports__.parser = parser; + + function parse(input) { + // Just return if an already-compile AST was passed in. + if(input.constructor === AST.ProgramNode) { return input; } + + parser.yy = AST; + return parser.parse(input); + } + + __exports__.parse = parse; + return __exports__; +})(__module9__, __module7__); + +// handlebars/compiler/compiler.js +var __module10__ = (function(__dependency1__) { + "use strict"; + var __exports__ = {}; + var Exception = __dependency1__; + + function Compiler() {} + + __exports__.Compiler = Compiler;// the foundHelper register will disambiguate helper lookup from finding a + // function in a context. This is necessary for mustache compatibility, which + // requires that context functions in blocks are evaluated by blockHelperMissing, + // and then proceed as if the resulting value was provided to blockHelperMissing. + + Compiler.prototype = { + compiler: Compiler, + + disassemble: function() { + var opcodes = this.opcodes, opcode, out = [], params, param; + + for (var i=0, l=opcodes.length; i 0) { + this.source[1] = this.source[1] + ", " + locals.join(", "); + } + + // Generate minimizer alias mappings + if (!this.isChild) { + for (var alias in this.context.aliases) { + if (this.context.aliases.hasOwnProperty(alias)) { + this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; + } + } + } + + if (this.source[1]) { + this.source[1] = "var " + this.source[1].substring(2) + ";"; + } + + // Merge children + if (!this.isChild) { + this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; + } + + if (!this.environment.isSimple) { + this.pushSource("return buffer;"); + } + + var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; + + for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } + return this.topStackName(); + }, + topStackName: function() { + return "stack" + this.stackSlot; + }, + flushInline: function() { + var inlineStack = this.inlineStack; + if (inlineStack.length) { + this.inlineStack = []; + for (var i = 0, len = inlineStack.length; i < len; i++) { + var entry = inlineStack[i]; + if (entry instanceof Literal) { + this.compileStack.push(entry); + } else { + this.pushStack(entry); + } + } + } + }, + isInline: function() { + return this.inlineStack.length; + }, + + popStack: function(wrapped) { + var inline = this.isInline(), + item = (inline ? this.inlineStack : this.compileStack).pop(); + + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + if (!inline) { + if (!this.stackSlot) { + throw new Exception('Invalid stack pop'); + } + this.stackSlot--; + } + return item; + } + }, + + topStack: function(wrapped) { + var stack = (this.isInline() ? this.inlineStack : this.compileStack), + item = stack[stack.length - 1]; + + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + return item; + } + }, + + quotedString: function(str) { + return '"' + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 + .replace(/\u2029/g, '\\u2029') + '"'; + }, + + setupHelper: function(paramSize, name, missingParams) { + var params = [], + paramsInit = this.setupParams(paramSize, params, missingParams); + var foundHelper = this.nameLookup('helpers', name, 'helper'); + + return { + params: params, + paramsInit: paramsInit, + name: foundHelper, + callParams: ["depth0"].concat(params).join(", "), + helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") + }; + }, + + setupOptions: function(paramSize, params) { + var options = [], contexts = [], types = [], param, inverse, program; + + options.push("hash:" + this.popStack()); + + if (this.options.stringParams) { + options.push("hashTypes:" + this.popStack()); + options.push("hashContexts:" + this.popStack()); + } + + inverse = this.popStack(); + program = this.popStack(); + + // Avoid setting fn and inverse if neither are set. This allows + // helpers to do a check for `if (options.fn)` + if (program || inverse) { + if (!program) { + this.context.aliases.self = "this"; + program = "self.noop"; + } + + if (!inverse) { + this.context.aliases.self = "this"; + inverse = "self.noop"; + } + + options.push("inverse:" + inverse); + options.push("fn:" + program); + } + + for(var i=0; i'); if(!js.length) { return false; } for(i = 0, j = js.length; i < j; i++) { tmp = this._parse_json(js[i], obj, true); - if(tmp.length) { d = d.add(tmp); } + if(tmp.length) { d = d.append(tmp); } } + d = d.children(); } else { if(typeof js == "string") { js = { data : js }; } @@ -2685,14 +2686,14 @@ $(function() { var css_string = '' + '#vakata-dragged ins { display:block; text-decoration:none; width:16px; height:16px; margin:0 0 0 0; padding:0; position:absolute; top:4px; left:4px; ' + - ' -moz-border-radius:4px; border-radius:4px; -webkit-border-radius:4px; ' + + ' border-radius:4px; -webkit-border-radius:4px; ' + '} ' + '#vakata-dragged .jstree-ok { background:green; } ' + '#vakata-dragged .jstree-invalid { background:red; } ' + '#jstree-marker { padding:0; margin:0; font-size:12px; overflow:hidden; height:12px; width:8px; position:absolute; top:-30px; z-index:10001; background-repeat:no-repeat; display:none; background-color:transparent; text-shadow:1px 1px 1px white; color:black; line-height:10px; } ' + '#jstree-marker-line { padding:0; margin:0; line-height:0%; font-size:1px; overflow:hidden; height:1px; width:100px; position:absolute; top:-30px; z-index:10000; background-repeat:no-repeat; display:none; background-color:#456c43; ' + ' cursor:pointer; border:1px solid #eeeeee; border-left:0; -moz-box-shadow: 0px 0px 2px #666; -webkit-box-shadow: 0px 0px 2px #666; box-shadow: 0px 0px 2px #666; ' + - ' -moz-border-radius:1px; border-radius:1px; -webkit-border-radius:1px; ' + + ' border-radius:1px; -webkit-border-radius:1px; ' + '}' + ''; $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); @@ -3685,11 +3686,6 @@ this.show_contextmenu(e.currentTarget, e.pageX, e.pageY); } }, this)) - .delegate("a", "click.jstree", $.proxy(function (e) { - if(this.data.contextmenu) { - $.vakata.context.hide(); - } - }, this)) .bind("destroy.jstree", $.proxy(function () { // TODO: move this to descruct method if(this.data.contextmenu) { diff --git a/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/bg.jpg b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/bg.jpg new file mode 100755 index 00000000000..ac38d8c27f0 Binary files /dev/null and b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/bg.jpg differ diff --git a/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/d.png b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/d.png new file mode 100755 index 00000000000..050becf0cdf Binary files /dev/null and b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/d.png differ diff --git a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/dot_for_ie.gif b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/dot_for_ie.gif similarity index 100% rename from core/vendor/assets/javascripts/jquery.jstree/themes/apple/dot_for_ie.gif rename to backend/vendor/assets/javascripts/jquery.jstree/themes/apple/dot_for_ie.gif diff --git a/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/style.scss b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/style.scss new file mode 100755 index 00000000000..d19c4bf9593 --- /dev/null +++ b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/style.scss @@ -0,0 +1,61 @@ +/* + * jsTree apple theme 1.0 + * Supported features: dots/no-dots, icons/no-icons, focused, loading + * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search + */ + +.jstree-apple > ul { background:image-url("jquery.jstree/themes/apple/bg.jpg") left top repeat; } +.jstree-apple li, +.jstree-apple ins { background-image:image-url("jquery.jstree/themes/apple/d.png"); background-repeat:no-repeat; background-color:transparent; } +.jstree-apple li { background-position:-90px 0; background-repeat:repeat-y; } +.jstree-apple li.jstree-last { background:transparent; } +.jstree-apple .jstree-open > ins { background-position:-72px 0; } +.jstree-apple .jstree-closed > ins { background-position:-54px 0; } +.jstree-apple .jstree-leaf > ins { background-position:-36px 0; } + +.jstree-apple a { border-radius:4px; -webkit-border-radius:4px; text-shadow:1px 1px 1px white; } +.jstree-apple .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 3px 0 1px; text-shadow:1px 1px 1px silver; } +.jstree-apple .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 3px 0 1px; } +.jstree-apple a .jstree-icon { background-position:-56px -20px; } +.jstree-apple a.jstree-loading .jstree-icon { background:image-url("jquery.jstree/themes/apple/throbber.gif") center center no-repeat !important; } + +.jstree-apple.jstree-focused { background:white; } + +.jstree-apple .jstree-no-dots li, +.jstree-apple .jstree-no-dots .jstree-leaf > ins { background:transparent; } +.jstree-apple .jstree-no-dots .jstree-open > ins { background-position:-18px 0; } +.jstree-apple .jstree-no-dots .jstree-closed > ins { background-position:0 0; } + +.jstree-apple .jstree-no-icons a .jstree-icon { display:none; } + +.jstree-apple .jstree-search { font-style:italic; } + +.jstree-apple .jstree-no-icons .jstree-checkbox { display:inline-block; } +.jstree-apple .jstree-no-checkboxes .jstree-checkbox { display:none !important; } +.jstree-apple .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; } +.jstree-apple .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; } +.jstree-apple .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; } +.jstree-apple .jstree-checked > a > .checkbox:hover { background-position:-38px -37px; } +.jstree-apple .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; } +.jstree-apple .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; } + +#vakata-dragged.jstree-apple ins { background:transparent !important; } +/*#vakata-dragged.jstree-apple .jstree-ok { background:image-url("jquery.jstree/themes/apple/d.png") -2px -53px no-repeat !important; }*/ +/*#vakata-dragged.jstree-apple .jstree-invalid { background:image-url("jquery.jstree/themes/apple/d.png") -18px -53px no-repeat !important; }*/ +/*#jstree-marker.jstree-apple { background:image-url("jquery.jstree/themes/apple/d.png") -41px -57px no-repeat !important; text-indent:-100px; }*/ + +.jstree-apple a.jstree-search { color:aqua; } +.jstree-apple .jstree-locked a { color:silver; cursor:default; } + +#vakata-contextmenu.jstree-apple-context, +#vakata-contextmenu.jstree-apple-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; } +#vakata-contextmenu.jstree-apple-context li { } +#vakata-contextmenu.jstree-apple-context a { color:black; } +#vakata-contextmenu.jstree-apple-context a:hover, +#vakata-contextmenu.jstree-apple-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -webkit-border-radius:2px; border-radius:2px; } +#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a, +#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; } +#vakata-contextmenu.jstree-apple-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; } +#vakata-contextmenu.jstree-apple-context li ul { margin-left:-4px; } + +/* TODO: IE6 support - the `>` selectors */ diff --git a/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/throbber.gif b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/throbber.gif new file mode 100755 index 00000000000..08eb9a0bab9 Binary files /dev/null and b/backend/vendor/assets/javascripts/jquery.jstree/themes/apple/throbber.gif differ diff --git a/core/vendor/assets/javascripts/jquery.powertip.js b/backend/vendor/assets/javascripts/jquery.powertip.js similarity index 100% rename from core/vendor/assets/javascripts/jquery.powertip.js rename to backend/vendor/assets/javascripts/jquery.powertip.js diff --git a/core/vendor/assets/javascripts/jquery.vAlign.js b/backend/vendor/assets/javascripts/jquery.vAlign.js similarity index 100% rename from core/vendor/assets/javascripts/jquery.vAlign.js rename to backend/vendor/assets/javascripts/jquery.vAlign.js diff --git a/core/vendor/assets/javascripts/modernizr.js b/backend/vendor/assets/javascripts/modernizr.js similarity index 100% rename from core/vendor/assets/javascripts/modernizr.js rename to backend/vendor/assets/javascripts/modernizr.js diff --git a/core/vendor/assets/javascripts/responsive-tables.js b/backend/vendor/assets/javascripts/responsive-tables.js similarity index 100% rename from core/vendor/assets/javascripts/responsive-tables.js rename to backend/vendor/assets/javascripts/responsive-tables.js diff --git a/backend/vendor/assets/javascripts/spin.js b/backend/vendor/assets/javascripts/spin.js new file mode 100644 index 00000000000..1f7f6dcc967 --- /dev/null +++ b/backend/vendor/assets/javascripts/spin.js @@ -0,0 +1,379 @@ + +//fgnass.github.com/spin.js#v1.3.2 + +/** + * Copyright (c) 2011-2013 Felix Gnass + * Licensed under the MIT license + */ (function(root, factory) { + + /* CommonJS */ + if (typeof exports == 'object') module.exports = factory() + + /* AMD module */ + else if (typeof define == 'function' && define.amd) define(factory) + + /* Browser global */ + else root.Spinner = factory() +} +(this, function() { + "use strict"; + + var prefixes = ['webkit', 'Moz', 'ms', 'O'] /* Vendor prefixes */ + , + animations = {} /* Animation rules keyed by their name */ + , + useCssAnimations /* Whether to use CSS animations or setTimeout */ + + /** + * Utility function to create elements. If no tag name is given, + * a DIV is created. Optionally properties can be passed. + */ + function createEl(tag, prop) { + var el = document.createElement(tag || 'div'), + n + + for (n in prop) el[n] = prop[n] + return el + } + + /** + * Appends children and returns the parent. + */ + function ins(parent /* child1, child2, ...*/ ) { + for (var i = 1, n = arguments.length; i < n; i++) + parent.appendChild(arguments[i]) + + return parent + } + + /** + * Insert a new stylesheet to hold the @keyframe or VML rules. + */ + var sheet = (function() { + var el = createEl('style', { + type: 'text/css' + }) + ins(document.getElementsByTagName('head')[0], el) + return el.sheet || el.styleSheet + }()) + + /** + * Creates an opacity keyframe animation rule and returns its name. + * Since most mobile Webkits have timing issues with animation-delay, + * we create separate rules for each line/segment. + */ + function addAnimation(alpha, trail, i, lines) { + var name = ['opacity', trail, ~~ (alpha * 100), i, lines].join('-'), + start = 0.01 + i / lines * 100, + z = Math.max(1 - (1 - alpha) / trail * (100 - start), alpha), + prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase(), + pre = prefix && '-' + prefix + '-' || '' + + if (!animations[name]) { + sheet.insertRule('@' + pre + 'keyframes ' + name + '{' + '0%{opacity:' + z + '}' + start + '%{opacity:' + alpha + '}' + (start + 0.01) + '%{opacity:1}' + (start + trail) % 100 + '%{opacity:' + alpha + '}' + '100%{opacity:' + z + '}' + '}', sheet.cssRules.length) + + animations[name] = 1 + } + + return name + } + + /** + * Tries various vendor prefixes and returns the first supported property. + */ + function vendor(el, prop) { + var s = el.style, + pp, i + + prop = prop.charAt(0).toUpperCase() + prop.slice(1) + for (i = 0; i < prefixes.length; i++) { + pp = prefixes[i] + prop + if (s[pp] !== undefined) return pp + } + if (s[prop] !== undefined) return prop + } + + /** + * Sets multiple style properties at once. + */ + function css(el, prop) { + for (var n in prop) + el.style[vendor(el, n) || n] = prop[n] + + return el + } + + /** + * Fills in default values. + */ + function merge(obj) { + for (var i = 1; i < arguments.length; i++) { + var def = arguments[i] + for (var n in def) + if (obj[n] === undefined) obj[n] = def[n] + } + return obj + } + + /** + * Returns the absolute page-offset of the given element. + */ + function pos(el) { + var o = { + x: el.offsetLeft, + y: el.offsetTop + } + while ((el = el.offsetParent)) + o.x += el.offsetLeft, o.y += el.offsetTop + + return o + } + + /** + * Returns the line color from the given string or array. + */ + function getColor(color, idx) { + return typeof color == 'string' ? color : color[idx % color.length] + } + + // Built-in defaults + + var defaults = { + lines: 12, // The number of lines to draw + length: 7, // The length of each line + width: 5, // The line thickness + radius: 10, // The radius of the inner circle + rotate: 0, // Rotation offset + corners: 1, // Roundness (0..1) + color: '#000', // #rgb or #rrggbb + direction: 1, // 1: clockwise, -1: counterclockwise + speed: 1, // Rounds per second + trail: 100, // Afterglow percentage + opacity: 1 / 4, // Opacity of the lines + fps: 20, // Frames per second when using setTimeout() + zIndex: 2e9, // Use a high z-index by default + className: 'spinner', // CSS class to assign to the element + top: 'auto', // center vertically + left: 'auto', // center horizontally + position: 'relative' // element position + } + + /** The constructor */ + function Spinner(o) { + if (typeof this == 'undefined') return new Spinner(o) + this.opts = merge(o || {}, Spinner.defaults, defaults) + } + + // Global defaults that override the built-ins: + Spinner.defaults = {} + + merge(Spinner.prototype, { + + /** + * Adds the spinner to the given target element. If this instance is already + * spinning, it is automatically removed from its previous target b calling + * stop() internally. + */ + spin: function(target) { + this.stop() + + var self = this, + o = self.opts, + el = self.el = css(createEl(0, { + className: o.className + }), { + position: o.position, + width: 0, + zIndex: o.zIndex + }), + mid = o.radius + o.length + o.width, + ep // element position + , + tp // target position + + if (target) { + target.insertBefore(el, target.firstChild || null) + tp = pos(target) + ep = pos(el) + css(el, { + left: (o.left == 'auto' ? tp.x - ep.x + (target.offsetWidth >> 1) : parseInt(o.left, 10) + mid) + 'px', + top: (o.top == 'auto' ? tp.y - ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + 'px' + }) + } + + el.setAttribute('role', 'progressbar') + self.lines(el, self.opts) + + if (!useCssAnimations) { + // No CSS animation support, use setTimeout() instead + var i = 0, + start = (o.lines - 1) * (1 - o.direction) / 2, + alpha, fps = o.fps, + f = fps / o.speed, + ostep = (1 - o.opacity) / (f * o.trail / 100), + astep = f / o.lines + + ; + (function anim() { + i++; + for (var j = 0; j < o.lines; j++) { + alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity) + + self.opacity(el, j * o.direction + start, alpha, o) + } + self.timeout = self.el && setTimeout(anim, ~~ (1000 / fps)) + })() + } + return self + }, + + /** + * Stops and removes the Spinner. + */ + stop: function() { + var el = this.el + if (el) { + clearTimeout(this.timeout) + if (el.parentNode) el.parentNode.removeChild(el) + this.el = undefined + } + return this + }, + + /** + * Internal method that draws the individual lines. Will be overwritten + * in VML fallback mode below. + */ + lines: function(el, o) { + var i = 0, + start = (o.lines - 1) * (1 - o.direction) / 2, + seg + + function fill(color, shadow) { + return css(createEl(), { + position: 'absolute', + width: (o.length + o.width) + 'px', + height: o.width + 'px', + background: color, + boxShadow: shadow, + transformOrigin: 'left', + transform: 'rotate(' + ~~ (360 / o.lines * i + o.rotate) + 'deg) translate(' + o.radius + 'px' + ',0)', + borderRadius: (o.corners * o.width >> 1) + 'px' + }) + } + + for (; i < o.lines; i++) { + seg = css(createEl(), { + position: 'absolute', + top: 1 + ~ (o.width / 2) + 'px', + transform: o.hwaccel ? 'translate3d(0,0,0)' : '', + opacity: o.opacity, + animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + ' ' + 1 / o.speed + 's linear infinite' + }) + + if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), { + top: 2 + 'px' + })) + ins(el, ins(seg, fill(getColor(o.color, i), '0 0 1px rgba(0,0,0,.1)'))) + } + return el + }, + + /** + * Internal method that adjusts the opacity of a single line. + * Will be overwritten in VML fallback mode below. + */ + opacity: function(el, i, val) { + if (i < el.childNodes.length) el.childNodes[i].style.opacity = val + } + + }) + + + function initVML() { + + /* Utility function to create a VML tag */ + function vml(tag, attr) { + return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr) + } + + // No CSS transforms but VML support, add a CSS rule for VML elements: + sheet.addRule('.spin-vml', 'behavior:url(#default#VML)') + + Spinner.prototype.lines = function(el, o) { + var r = o.length + o.width, + s = 2 * r + + function grp() { + return css( + vml('group', { + coordsize: s + ' ' + s, + coordorigin: -r + ' ' + -r + }), { + width: s, + height: s + }) + } + + var margin = -(o.width + o.length) * 2 + 'px', + g = css(grp(), { + position: 'absolute', + top: margin, + left: margin + }), + i + + function seg(i, dx, filter) { + ins(g, + ins(css(grp(), { + rotation: 360 / o.lines * i + 'deg', + left: ~~dx + }), + ins(css(vml('roundrect', { + arcsize: o.corners + }), { + width: r, + height: o.width, + left: o.radius, + top: -o.width >> 1, + filter: filter + }), + vml('fill', { + color: getColor(o.color, i), + opacity: o.opacity + }), + vml('stroke', { + opacity: 0 + }) // transparent stroke to fix color bleeding upon opacity change + ))) + } + + if (o.shadow) for (i = 1; i <= o.lines; i++) + seg(i, - 2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)') + + for (i = 1; i <= o.lines; i++) seg(i) + return ins(el, g) + } + + Spinner.prototype.opacity = function(el, i, val, o) { + var c = el.firstChild + o = o.shadow && o.lines || 0 + if (c && i + o < c.childNodes.length) { + c = c.childNodes[i + o]; + c = c && c.firstChild; + c = c && c.firstChild + if (c) c.opacity = val + } + } + } + + var probe = css(createEl('group'), { + behavior: 'url(#default#VML)' + }) + + if (!vendor(probe, 'transform') && probe.adj) initVML() + else useCssAnimations = vendor(probe, 'animation') + + return Spinner + +})); \ No newline at end of file diff --git a/backend/vendor/assets/javascripts/trunk8.js b/backend/vendor/assets/javascripts/trunk8.js new file mode 100644 index 00000000000..22c648892b9 --- /dev/null +++ b/backend/vendor/assets/javascripts/trunk8.js @@ -0,0 +1,369 @@ +/**! + * trunk8 v1.3.1 + * https://github.com/rviscomi/trunk8 + * + * Copyright 2012 Rick Viscomi + * Released under the MIT License. + * + * Date: September 26, 2012 + */ +(function ($) { + var methods, + utils, + SIDES = { + /* cen...ter */ + center: 'center', + /* ...left */ + left: 'left', + /* right... */ + right: 'right' + }, + WIDTH = { + auto: 'auto' + }; + + function trunk8(element) { + this.$element = $(element); + this.original_text = this.$element.html(); + this.settings = $.extend({}, $.fn.trunk8.defaults); + } + + trunk8.prototype.updateSettings = function (options) { + this.settings = $.extend(this.settings, options); + }; + + function stripHTML(html) { + var tmp = document.createElement("DIV"); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText; + } + + function getHtmlArr(str) { + /* Builds an array of strings and designated */ + /* HTML tags around them. */ + if (stripHTML(str) === str) { + return str.split(/\s/g); + } + var allResults = [], + reg = /<([a-z]+)([^<]*)(?:>(.*?(?!<\1>)*)<\/\1>|\s+\/>)(['.?!,]*)|((?:[^<>\s])+['.?!,]*\w?|)/ig, + outArr = reg.exec(str), + lastI, + ind; + while (outArr && lastI !== reg.lastIndex) { + lastI = reg.lastIndex; + if (outArr[5]) { + allResults.push(outArr[5]); + } else if (outArr[1]) { + allResults.push({ + tag: outArr[1], + attribs: outArr[2], + content: outArr[3], + after: outArr[4] + }); + } + outArr = reg.exec(str); + } + for (ind = 0; ind < allResults.length; ind++) { + if (typeof allResults[ind] !== 'string' && allResults[ind].content) { + allResults[ind].content = getHtmlArr(allResults[ind].content); + } + } + return allResults; + } + + function rebuildHtmlFromBite(bite, htmlObject, fill) { + // Take the processed bite after binary-search + // truncated and re-build the original HTML + // tags around the processed string. + bite = bite.replace(fill, ''); + + var biteHelper = function (contentArr, tagInfo) { + var retStr = '', + content, + biteContent, + biteLength, + i; + for (i = 0; i < contentArr.length; i++) { + content = contentArr[i]; + biteLength = $.trim(bite).split(' ').length; + if ($.trim(bite).length) { + if (typeof content === 'string') { + if (!//.test(content)) { + if (biteLength === 1 && $.trim(bite).length <= content.length) { + content = bite; + // We want the fill to go inside of the last HTML + // element if the element is a container. + if (tagInfo === 'p' || tagInfo === 'div') { + content += fill; + } + bite = ''; + } else { + bite = bite.replace(content, ''); + } + } + retStr += $.trim(content) + ((i === contentArr.length - 1 || biteLength <= 1) ? '' : ' '); + } else { + biteContent = biteHelper(content.content, content.tag); + if (content.after) { + bite = bite.replace(content.after, ''); + } + if (biteContent) { + if (!content.after) { + content.after = ' '; + } + retStr += '<' + content.tag + content.attribs + '>' + biteContent + '' + content.after; + } + } + } + } + return retStr; + }, + htmlResults = biteHelper(htmlObject); + + // Add fill if doesn't exist. This will place it outside the HTML elements. + if (htmlResults.slice(htmlResults.length - fill.length) === fill) { + htmlResults += fill; + } + + return htmlResults; + } + + function truncate() { + var data = this.data('trunk8'), + settings = data.settings, + width = settings.width, + side = settings.side, + fill = settings.fill, + parseHTML = settings.parseHTML, + line_height = utils.getLineHeight(this) * settings.lines, + str = data.original_text, + length = str.length, + max_bite = '', + lower, upper, + bite_size, + bite, + text, + htmlObject; + + /* Reset the field to the original string. */ + this.html(str); + text = this.text(); + + /* If string has HTML and parse HTML is set, build */ + /* the data struct to house the tags */ + if (parseHTML && stripHTML(str) !== str) { + htmlObject = getHtmlArr(str); + str = stripHTML(str); + length = str.length; + } + + if (width === WIDTH.auto) { + /* Assuming there is no "overflow: hidden". */ + if (this.height() <= line_height) { + /* Text is already at the optimal trunkage. */ + return; + } + + /* Binary search technique for finding the optimal trunkage. */ + /* Find the maximum bite without overflowing. */ + lower = 0; + upper = length - 1; + + while (lower <= upper) { + bite_size = lower + ((upper - lower) >> 1); + + bite = utils.eatStr(str, side, length - bite_size, fill); + + if (parseHTML && htmlObject) { + bite = rebuildHtmlFromBite(bite, htmlObject, fill); + } + + this.html(bite); + + /* Check for overflow. */ + if (this.height() > line_height) { + upper = bite_size - 1; + } else { + lower = bite_size + 1; + + /* Save the bigger bite. */ + max_bite = (max_bite.length > bite.length) ? max_bite : bite; + } + } + + /* Reset the content to eliminate possible existing scroll bars. */ + this.html(''); + + /* Display the biggest bite. */ + this.html(max_bite); + + if (settings.tooltip) { + this.attr('title', text); + } + } else if (!isNaN(width)) { + bite_size = length - width; + + bite = utils.eatStr(str, side, bite_size, fill); + + this.html(bite); + + if (settings.tooltip) { + this.attr('title', str); + } + } else { + $.error('Invalid width "' + width + '".'); + } + } + + methods = { + init: function (options) { + return this.each(function () { + var $this = $(this), + data = $this.data('trunk8'); + + if (!data) { + $this.data('trunk8', (data = new trunk8(this))); + } + + data.updateSettings(options); + + truncate.call($this); + }); + }, + + /** Updates the text value of the elements while maintaining truncation. */ + update: function (new_string) { + return this.each(function () { + var $this = $(this); + + /* Update text. */ + if (new_string) { + $this.data('trunk8').original_text = new_string; + } + + /* Truncate accordingly. */ + truncate.call($this); + }); + }, + + revert: function () { + return this.each(function () { + /* Get original text. */ + var text = $(this).data('trunk8').original_text; + + /* Revert element to original text. */ + $(this).html(text); + }); + }, + + /** Returns this instance's settings object. NOT CHAINABLE. */ + getSettings: function () { + return $(this.get(0)).data('trunk8').settings; + } + }; + + utils = { + /** Replaces [bite_size] [side]-most chars in [str] with [fill]. */ + eatStr: function (str, side, bite_size, fill) { + var length = str.length, + key = utils.eatStr.generateKey.apply(null, arguments), + half_length, + half_bite_size; + + /* If the result is already in the cache, return it. */ + if (utils.eatStr.cache[key]) { + return utils.eatStr.cache[key]; + } + + /* Common error handling. */ + if ((typeof str !== 'string') || (length === 0)) { + $.error('Invalid source string "' + str + '".'); + } + if ((bite_size < 0) || (bite_size > length)) { + $.error('Invalid bite size "' + bite_size + '".'); + } else if (bite_size === 0) { + /* No bite should show no truncation. */ + return str; + } + if (typeof (fill + '') !== 'string') { + $.error('Fill unable to be converted to a string.'); + } + + /* Compute the result, store it in the cache, and return it. */ + switch (side) { + case SIDES.right: + /* str... */ + return utils.eatStr.cache[key] = $.trim(str.substr(0, length - bite_size)) + fill; + + case SIDES.left: + /* ...str */ + return utils.eatStr.cache[key] = fill + $.trim(str.substr(bite_size)); + + case SIDES.center: + /* Bit-shift to the right by one === Math.floor(x / 2) */ + half_length = length >> 1; // halve the length + half_bite_size = bite_size >> 1; // halve the bite_size + + /* st...r */ + return utils.eatStr.cache[key] = $.trim(utils.eatStr(str.substr(0, length - half_length), SIDES.right, bite_size - half_bite_size, '')) + fill + $.trim(utils.eatStr(str.substr(length - half_length), SIDES.left, half_bite_size, '')); + + default: + $.error('Invalid side "' + side + '".'); + } + }, + + getLineHeight: function (elem) { + var floats = $(elem).css('float'); + if (floats !== 'none') { + $(elem).css('float', 'none'); + } + var pos = $(elem).css('position'); + if (pos === 'absolute') { + $(elem).css('position', 'static'); + } + + var html = $(elem).html(), + wrapper_id = 'line-height-test', + line_height; + + /* Set the content to a small single character and wrap. */ + $(elem).html('i').wrap('
    '); + + /* Calculate the line height by measuring the wrapper.*/ + line_height = $('#' + wrapper_id).innerHeight(); + + /* Remove the wrapper and reset the content. */ + $(elem).html(html).css({ + 'float': floats, + 'position': pos + }).unwrap(); + + return line_height; + } + }; + + utils.eatStr.cache = {}; + utils.eatStr.generateKey = function () { + return Array.prototype.join.call(arguments, ''); + }; + + $.fn.trunk8 = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.trunk8'); + } + }; + + /* Default trunk8 settings. */ + $.fn.trunk8.defaults = { + fill: '…', + lines: 1, + side: SIDES.right, + tooltip: true, + width: WIDTH.auto, + parseHTML: false + }; +})(jQuery); \ No newline at end of file diff --git a/core/vendor/assets/stylesheets/jquery-ui.datepicker.css.erb b/backend/vendor/assets/stylesheets/jquery-ui.datepicker.css.erb similarity index 96% rename from core/vendor/assets/stylesheets/jquery-ui.datepicker.css.erb rename to backend/vendor/assets/stylesheets/jquery-ui.datepicker.css.erb index 7e1f05352f2..33b69f7253c 100644 --- a/core/vendor/assets/stylesheets/jquery-ui.datepicker.css.erb +++ b/backend/vendor/assets/stylesheets/jquery-ui.datepicker.css.erb @@ -280,14 +280,14 @@ ----------------------------------*/ /* Corner radius */ -.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 2px; -webkit-border-top-left-radius: 2px; -khtml-border-top-left-radius: 2px; border-top-left-radius: 2px; } -.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 2px; -webkit-border-top-right-radius: 2px; -khtml-border-top-right-radius: 2px; border-top-right-radius: 2px; } -.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 2px; -webkit-border-bottom-left-radius: 2px; -khtml-border-bottom-left-radius: 2px; border-bottom-left-radius: 2px; } -.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 2px; -webkit-border-bottom-right-radius: 2px; -khtml-border-bottom-right-radius: 2px; border-bottom-right-radius: 2px; } +.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -webkit-border-top-left-radius: 2px; -khtml-border-top-left-radius: 2px; border-top-left-radius: 2px; } +.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -webkit-border-top-right-radius: 2px; -khtml-border-top-right-radius: 2px; border-top-right-radius: 2px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -webkit-border-bottom-left-radius: 2px; -khtml-border-bottom-left-radius: 2px; border-bottom-left-radius: 2px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -webkit-border-bottom-right-radius: 2px; -khtml-border-bottom-right-radius: 2px; border-bottom-right-radius: 2px; } /* Overlays */ .ui-widget-overlay { background: #eeeeee url(<%= asset_path("jquery-ui/ui-bg_flat_0_eeeeee_40x100.png") %>) 50% 50% repeat-x; opacity: .80;filter:Alpha(Opacity=80); } -.ui-widget-shadow { margin: -4px 0 0 -4px; padding: 4px; background: #aaaaaa url(<%= asset_path("jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png") %>) 50% 50% repeat-x; opacity: .60;filter:Alpha(Opacity=60); -moz-border-radius: 0px; -khtml-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; }/* +.ui-widget-shadow { margin: -4px 0 0 -4px; padding: 4px; background: #aaaaaa url(<%= asset_path("jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png") %>) 50% 50% repeat-x; opacity: .60;filter:Alpha(Opacity=60); -khtml-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; }/* * jQuery UI Datepicker 1.8.14 * * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) diff --git a/core/vendor/assets/javascripts/jquery.alerts/jquery.alerts.css.erb b/backend/vendor/assets/stylesheets/jquery.alerts/jquery.alerts.css.erb similarity index 100% rename from core/vendor/assets/javascripts/jquery.alerts/jquery.alerts.css.erb rename to backend/vendor/assets/stylesheets/jquery.alerts/jquery.alerts.css.erb diff --git a/core/vendor/assets/javascripts/jquery.alerts/jquery.alerts.spree.css b/backend/vendor/assets/stylesheets/jquery.alerts/jquery.alerts.spree.css similarity index 100% rename from core/vendor/assets/javascripts/jquery.alerts/jquery.alerts.spree.css rename to backend/vendor/assets/stylesheets/jquery.alerts/jquery.alerts.spree.css diff --git a/core/vendor/assets/stylesheets/jquery.powertip.css b/backend/vendor/assets/stylesheets/jquery.powertip.css similarity index 100% rename from core/vendor/assets/stylesheets/jquery.powertip.css rename to backend/vendor/assets/stylesheets/jquery.powertip.css diff --git a/core/vendor/assets/stylesheets/responsive-tables.css b/backend/vendor/assets/stylesheets/responsive-tables.css similarity index 100% rename from core/vendor/assets/stylesheets/responsive-tables.css rename to backend/vendor/assets/stylesheets/responsive-tables.css diff --git a/build-ci.rb b/build-ci.rb new file mode 100755 index 00000000000..cef699ebacb --- /dev/null +++ b/build-ci.rb @@ -0,0 +1,184 @@ +#!/usr/bin/env ruby + +require 'pathname' + +class Project + attr_reader :name + + NODE_TOTAL = Integer(ENV.fetch('CIRCLE_NODE_TOTAL', 1)) + NODE_INDEX = Integer(ENV.fetch('CIRCLE_NODE_INDEX', 0)) + + ROOT = Pathname.pwd.freeze + VENDOR_BUNDLE = ROOT.join('vendor', 'bundle').freeze + + BUNDLER_JOBS = 4 + BUNDLER_RETRIES = 3 + + DEFAULT_MODE = 'test'.freeze + + def initialize(name) + @name = name + end + + ALL = %w[api backend core frontend sample].map(&method(:new)).freeze + + # Install subproject + # + # @raise [RuntimeError] + # in case of failure + # + # @return [self] + # otherwise + def install + chdir do + bundle_check or bundle_install or fail 'Cannot finish gem installation' + end + self + end + + # Test subproject for passing its tests + # + # @return [Boolean] + # the success of the build + def pass? + chdir do + setup_test_app + run_tests + end + end + +private + + # Check if current bundle is already usable + # + # @return [Boolean] + def bundle_check + system(%W[bundle check --path=#{VENDOR_BUNDLE}]) + end + + # Install the current bundle + # + # @return [Boolean] + # the success of the installation + def bundle_install + system(%W[ + bundle + install + --path=#{VENDOR_BUNDLE} + --jobs=#{BUNDLER_JOBS} + --retry=#{BUNDLER_RETRIES} + ]) + end + + # Setup the test app + # + # @return [undefined] + def setup_test_app + system(%w[bundle exec rake test_app]) or fail 'Failed to setup the test app' + end + + # Run tests for subproject + # + # @return [Boolean] + # the success of the tests + def run_tests + system(%w[bundle exec rspec spec]) + end + + # Execute system command via execve + # + # No shell interpolation gets done this way. No escapes needed. + # + # @return [Boolean] + # the success of the system command + def system(arguments) + Kernel.system(*arguments) + end + + # Change to subproject directory and execute block + # + # @return [undefined] + def chdir(&block) + Dir.chdir(ROOT.join(name), &block) + end + + # Install subprojects + # + # @return [self] + def self.install + current_projects.each do |project| + log("Installing project: #{project.name}") + project.install + end + self + end + private_class_method :install + + # Execute tests on subprojects + # + # @return [Boolean] + # the success of ALL subprojects + def self.test + projects = current_projects + suffix = "#{projects.length} projects(s) on node #{NODE_INDEX.succ} / #{NODE_TOTAL}" + + log("Running #{suffix}") + projects.each do |project| + log("- #{project.name}") + end + + builds = projects.map do |project| + log("Building: #{project.name}") + project.pass? + end + log("Finished running #{suffix}") + + projects.zip(builds).each do |project, build| + log("- #{project.name} #{build ? 'SUCCESS' : 'FAILURE'}") + end + + builds.all? + end + private_class_method :test + + # Return the projects active on current node + # + # @return [Array] + def self.current_projects + NODE_INDEX.step(ALL.length - 1, NODE_TOTAL).map(&ALL.method(:fetch)) + end + private_class_method :current_projects + + # Log a progress message to stderr + # + # @param [String] message + # + # @return [undefined] + def self.log(message) + $stderr.puts(message) + end + private_class_method :log + + # Process CLI arguments + # + # @param [Array] arguments + # + # @return [Boolean] + # the success of the CLI run + def self.run_cli(arguments) + fail ArgumentError if arguments.length > 1 + mode = arguments.fetch(0, DEFAULT_MODE) + + case mode + when 'install' + install + true + when 'test' + test + else + fail "Unknown mode: #{mode.inspect}" + end + end +end # Project + +exit Project.run_cli(ARGV) diff --git a/build.sh b/build.sh index e94e064a0c3..d7ee83894c2 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,50 @@ - alias set_gemfile='export BUNDLE_GEMFILE="`pwd`/Gemfile"' - bundle exec rake test_app - cd api; set_gemfile; bundle install; bundle exec rspec spec - cd ../core; set_gemfile; bundle install; bundle exec rspec spec - cd ../dash; set_gemfile; bundle install; bundle exec rspec spec - cd ../promo; set_gemfile; bundle install; bundle exec rspec spec +#!/bin/sh + +set -e + +# Switching Gemfile +set_gemfile(){ + echo "Switching Gemfile..." + export BUNDLE_GEMFILE="`pwd`/Gemfile" +} + +# Target postgres. Override with: `DB=sqlite bash build.sh` +export DB=${DB:-postgres} + +# Set retry count at 2 for flakey poltergeist specs. +export RSPEC_RETRY_COUNT=2 + +# Spree defaults +echo "Setup Spree defaults and creating test application..." +bundle check || bundle update --quiet +bundle exec rake test_app + +# Spree API +echo "**************************************" +echo "* Setup Spree API and running RSpec..." +echo "**************************************" +cd api; set_gemfile; bundle update --quiet; bundle exec rspec spec + +# Spree Backend +echo "******************************************" +echo "* Setup Spree Backend and running RSpec..." +echo "******************************************" +cd ../backend; set_gemfile; bundle update --quiet; bundle exec rspec spec + +# Spree Core +echo "***************************************" +echo "* Setup Spree Core and running RSpec..." +echo "***************************************" +cd ../core; set_gemfile; bundle update --quiet; bundle exec rspec spec + +# Spree Frontend +echo "*******************************************" +echo "* Setup Spree Frontend and running RSpec..." +echo "*******************************************" +cd ../frontend; set_gemfile; bundle update --quiet; bundle exec rspec spec + +# Spree Sample +echo "*****************************************" +echo "* Setup Spree Sample and running RSpec..." +echo "*****************************************" +cd ../sample; set_gemfile; bundle update --quiet; bundle exec rspec spec diff --git a/ci/travis.sh b/ci/travis.sh deleted file mode 100755 index e68e3c43622..00000000000 --- a/ci/travis.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -vars=(${GEM//:/ }) -ENGINE=${vars[0]} -export DB=${vars[1]} -cd ${ENGINE} -bundle exec rake test_app -export BUNDLE_GEMFILE="`pwd`/Gemfile" -bundle install --quiet -bundle exec rspec spec diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000000..1ba5bfeef55 --- /dev/null +++ b/circle.yml @@ -0,0 +1,15 @@ +--- +machine: + environment: + DB: postgres + services: + - postgresql + ruby: + version: 2.1.5 +dependencies: + override: + - ./build-ci.rb install +test: + override: + - './build-ci.rb test': + parallel: true diff --git a/cmd/LICENSE b/cmd/LICENSE index 74f73e35ac3..bef97d82cc6 100644 --- a/cmd/LICENSE +++ b/cmd/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2012, Spree Commerce, Inc. and other contributors +Copyright (c) 2007-2014, Spree Commerce, Inc. and other contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/cmd/README.md b/cmd/README.md index 2ea6eed0cc3..061137fe338 100644 --- a/cmd/README.md +++ b/cmd/README.md @@ -42,14 +42,16 @@ to use a local clone of Spree, pass the --path option spree install my_store --path=../spree -options +Options ------- -* --auto_accept - answer yes to all questions -* --edge - to use the edge version of Spree -* --path=../spree - to use a local version of spree -* --git=git@github.com:cmar/spree.git -* --branch=my_changes or --ref=23423423423423 or --tag=my_tag +* `--auto_accept` to answer yes to all questions +* `--edge` to use the edge version of Spree +* `--path=../spree` to use a local version of spree +* `--branch=n-n-stable` to use git branch +* `--git=git@github.com:cmar/spree.git` to use git version of spree + * `--ref=23423423423423` to use git reference + * `--tag=my_tag` to use git tag Older Versions of Spree ----------------------- diff --git a/cmd/bin/spree b/cmd/bin/spree index 0d9e86f7159..d6f6567701a 100755 --- a/cmd/bin/spree +++ b/cmd/bin/spree @@ -1,2 +1,2 @@ #!/usr/bin/env ruby -require 'spree_cmd' \ No newline at end of file +require 'spree_cmd' diff --git a/cmd/bin/spree_cmd b/cmd/bin/spree_cmd index 463a044343e..d6f6567701a 100755 --- a/cmd/bin/spree_cmd +++ b/cmd/bin/spree_cmd @@ -1,3 +1,2 @@ #!/usr/bin/env ruby - -require 'spree_cmd' \ No newline at end of file +require 'spree_cmd' diff --git a/cmd/lib/spree_cmd.rb b/cmd/lib/spree_cmd.rb index 21a76ef9cc2..ef3ea40140b 100644 --- a/cmd/lib/spree_cmd.rb +++ b/cmd/lib/spree_cmd.rb @@ -3,7 +3,6 @@ case ARGV.first when 'version', '-v', '--version' - puts "outputting version" puts Gem.loaded_specs['spree_cmd'].version when 'extension' ARGV.shift diff --git a/cmd/lib/spree_cmd/extension.rb b/cmd/lib/spree_cmd/extension.rb index 590f0f7aea2..4e63a0d5cdb 100644 --- a/cmd/lib/spree_cmd/extension.rb +++ b/cmd/lib/spree_cmd/extension.rb @@ -15,7 +15,7 @@ def generate directory 'app', "#{file_name}/app" directory 'lib', "#{file_name}/lib" - directory 'script', "#{file_name}/script" + directory 'bin', "#{file_name}/bin" template 'extension.gemspec', "#{file_name}/#{file_name}.gemspec" template 'Gemfile', "#{file_name}/Gemfile" @@ -27,7 +27,6 @@ def generate template 'config/locales/en.yml', "#{file_name}/config/locales/en.yml" template 'rspec', "#{file_name}/.rspec" template 'spec/spec_helper.rb.tt', "#{file_name}/spec/spec_helper.rb" - template 'Versionfile', "#{file_name}/Versionfile" end def final_banner @@ -39,7 +38,7 @@ def final_banner Please update the Versionfile to designate compatibility with different versions of Spree. See http://spreecommerce.com/documentation/extensions.html#versionfile - Consider listing your extension in the official extension registry http://spreecommerce.com/extensions" + Consider listing your extension in the official extension registry http://spreecommerce.com/extensions #{'*' * 80} } @@ -51,7 +50,7 @@ def class_name end def spree_version - '2.0.0.beta' + '2.4.11.beta' end def use_prefix(prefix) diff --git a/cmd/lib/spree_cmd/installer.rb b/cmd/lib/spree_cmd/installer.rb index 3b530ed3990..5cf10143803 100644 --- a/cmd/lib/spree_cmd/installer.rb +++ b/cmd/lib/spree_cmd/installer.rb @@ -42,14 +42,13 @@ def verify_image_magick def prepare_options @spree_gem_options = {} - if options[:edge] - @spree_gem_options[:git] = 'git://github.com/spree/spree.git' + if options[:edge] || options[:branch] + @spree_gem_options[:git] = 'https://github.com/spree/spree.git' elsif options[:path] @spree_gem_options[:path] = options[:path] elsif options[:git] @spree_gem_options[:git] = options[:git] @spree_gem_options[:ref] = options[:ref] if options[:ref] - @spree_gem_options[:branch] = options[:branch] if options[:branch] @spree_gem_options[:tag] = options[:tag] if options[:tag] elsif options[:version] @spree_gem_options[:version] = options[:version] @@ -57,12 +56,14 @@ def prepare_options version = Gem.loaded_specs['spree_cmd'].version @spree_gem_options[:version] = version.to_s end + + @spree_gem_options[:branch] = options[:branch] if options[:branch] end def ask_questions @install_default_gateways = ask_with_default('Would you like to install the default gateways? (Recommended)') @install_default_auth = ask_with_default('Would you like to install the default authentication system?') - + if @install_default_auth @user_class = "Spree::User" else @@ -70,7 +71,7 @@ def ask_questions if @user_class.blank? @user_class = "User" end - end + end if options[:skip_install_data] @run_migrations = false @@ -93,12 +94,16 @@ def add_gems gem :spree, @spree_gem_options - if @install_default_gateways - gem :spree_gateway + if @install_default_gateways && @spree_gem_options[:branch] + gem :spree_gateway, github: 'spree/spree_gateway', branch: @spree_gem_options[:branch] + elsif @install_default_gateways + gem :spree_gateway, github: 'spree/spree_gateway', branch: '2-4-stable' end - if @install_default_auth - gem :spree_auth_devise, :github => "spree/spree_auth_devise", :branch => "edge" + if @install_default_auth && @spree_gem_options[:branch] + gem :spree_auth_devise, github: 'spree/spree_auth_devise', branch: @spree_gem_options[:branch] + elsif @install_default_auth + gem :spree_auth_devise, github: 'spree/spree_auth_devise', branch: '2-4-stable' end run 'bundle install', :capture => true @@ -124,8 +129,8 @@ def gem(name, gem_options={}) say_status :gemfile, name parts = ["'#{name}'"] parts << ["'#{gem_options.delete(:version)}'"] if gem_options[:version] - gem_options.each { |key, value| parts << ":#{key} => '#{value}'" } - append_file 'Gemfile', "gem #{parts.join(', ')}\n", :verbose => false + gem_options.each { |key, value| parts << "#{key}: '#{value}'" } + append_file 'Gemfile', "\ngem #{parts.join(', ')}", :verbose => false end def ask_with_default(message, default = 'yes') @@ -161,7 +166,7 @@ def create_rails_app end def rails_project? - File.exists? File.join(@app_path, 'script', 'rails') + File.exists? File.join(@app_path, 'bin', 'rails') end def linux? diff --git a/cmd/lib/spree_cmd/templates/extension/CONTRIBUTING.md b/cmd/lib/spree_cmd/templates/extension/CONTRIBUTING.md new file mode 100644 index 00000000000..d2a4581713a --- /dev/null +++ b/cmd/lib/spree_cmd/templates/extension/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# How to contribute + +Third-party patches are essential to any great open source project. We want +to keep it as easy as possible to contribute changes that get things working +in your environment. There are a few guidelines that we need contributors to +follow so that we can have a chance of keeping on top of things. + +## Getting Started + +* Make sure you have a [GitHub account](https://github.com/signup/free) +* Submit a ticket for your issue, assuming one does not already exist. + * Clearly describe the issue including steps to reproduce when it is a bug. + * Make sure you fill in the earliest version that you know has the issue. +* Fork the repository on GitHub + +## Making Changes + +* Create a topic branch from where you want to base your work. + * This is usually the master branch. + * Only target release branches if you are certain your fix must be on that + branch. + * To quickly create a topic branch based on master; `git branch + fix/master/my_contribution master` then checkout the new branch with `git + checkout fix/master/my_contribution`. Please avoid working directly on the + `master` branch. +* Make commits of logical units. +* Check for unnecessary whitespace with `git diff --check` before committing. +* Make sure your commit messages are in the proper format. + +```` + (#99999) Make the example in CONTRIBUTING imperative and concrete + + Without this patch applied the example commit message in the CONTRIBUTING + document is not a concrete example. This is a problem because the + contributor is left to imagine what the commit message should look like + based on a description rather than an example. This patch fixes the + problem by making the example concrete and imperative. + + The first line is a real life imperative statement with a ticket number + from our issue tracker. The body describes the behavior without the patch, + why this is a problem, and how the patch fixes the problem when applied. +```` + +* Make sure you have added the necessary tests for your changes. +* Run _all_ the tests to assure nothing else was accidentally broken. + +## Submitting Changes + +* Push your changes to a topic branch in your fork of the repository. +* Submit a pull request to the extensions repository. +* Update any Github issues to mark that you have submitted code and are ready for it to be reviewed. + * Include a link to the pull request in the ticket + +# Additional Resources + +* [General GitHub documentation](http://help.github.com/) +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) diff --git a/cmd/lib/spree_cmd/templates/extension/Gemfile b/cmd/lib/spree_cmd/templates/extension/Gemfile index 17dbc22025e..bb0b8546ad0 100644 --- a/cmd/lib/spree_cmd/templates/extension/Gemfile +++ b/cmd/lib/spree_cmd/templates/extension/Gemfile @@ -1,6 +1,7 @@ -source 'http://rubygems.org' +source 'https://rubygems.org' +gem 'spree', github: 'spree/spree', branch: '2-4-stable' # Provides basic authentication functionality for testing parts of your engine -gem 'spree_auth_devise', :git => "git://github.com/spree/spree_auth_devise" +gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-4-stable' gemspec diff --git a/cmd/lib/spree_cmd/templates/extension/README.md b/cmd/lib/spree_cmd/templates/extension/README.md index 597e00202fd..c70a31058f5 100644 --- a/cmd/lib/spree_cmd/templates/extension/README.md +++ b/cmd/lib/spree_cmd/templates/extension/README.md @@ -3,19 +3,37 @@ Introduction goes here. +Installation +------------ -Example -======= +Add <%= file_name %> to your Gemfile: -Example goes here. +```ruby +gem '<%= file_name %>' +``` + +Bundle your dependencies and run the installation generator: + +```shell +bundle +bundle exec rails g <%= file_name %>:install +``` Testing ------- -Be sure to bundle your dependencies and then create a dummy test app for the specs to run against. +First bundle your dependencies, then run `rake`. `rake` will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using `rake test_app`. + +```shell +bundle +bundle exec rake +``` + +When testing your applications integration with this extension you may use it's factories. +Simply add this require statement to your spec_helper: - $ bundle - $ bundle exec rake test_app - $ bundle exec rspec spec +```ruby +require '<%= file_name %>/factories' +``` Copyright (c) <%= Time.now.year %> [name of extension creator], released under the New BSD License diff --git a/cmd/lib/spree_cmd/templates/extension/Rakefile b/cmd/lib/spree_cmd/templates/extension/Rakefile index 5cff331f5c7..abf5d3cfc84 100644 --- a/cmd/lib/spree_cmd/templates/extension/Rakefile +++ b/cmd/lib/spree_cmd/templates/extension/Rakefile @@ -2,14 +2,20 @@ require 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' -require 'spree/core/testing_support/common_rake' +require 'spree/testing_support/extension_rake' RSpec::Core::RakeTask.new -task :default => [:spec] +task :default do + if Dir["spec/dummy"].empty? + Rake::Task[:test_app].invoke + Dir.chdir("../../") + end + Rake::Task[:spec].invoke +end desc 'Generates a dummy app for testing' task :test_app do ENV['LIB_NAME'] = '<%=file_name%>' - Rake::Task['common:test_app'].invoke + Rake::Task['extension:test_app'].invoke end diff --git a/cmd/lib/spree_cmd/templates/extension/Versionfile b/cmd/lib/spree_cmd/templates/extension/Versionfile deleted file mode 100644 index 5db83aa583e..00000000000 --- a/cmd/lib/spree_cmd/templates/extension/Versionfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file is used to designate compatibilty with different versions of Spree -# Please see http://spreecommerce.com/documentation/extensions.html#versionfile for details - -# Examples -# -# '1.2.x' => { :branch => 'master' } -# '1.1.x' => { :branch => '1-1-stable' } -# '1.0.x' => { :branch => '1-0-stable' } -# '0.70.x' => { :branch => '0-70-stable' } -# '0.40.x' => { :tag => 'v1.0.0', :version => '1.0.0' } - diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/admin/%file_name%.js b/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/admin/%file_name%.js deleted file mode 100644 index a3b2c53284c..00000000000 --- a/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/admin/%file_name%.js +++ /dev/null @@ -1 +0,0 @@ -//= require admin/spree_core diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/spree/backend/%file_name%.js b/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/spree/backend/%file_name%.js new file mode 100644 index 00000000000..8aa3b014409 --- /dev/null +++ b/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/spree/backend/%file_name%.js @@ -0,0 +1,2 @@ +// Placeholder manifest file. +// the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/backend/all.js' \ No newline at end of file diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/spree/frontend/%file_name%.js b/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/spree/frontend/%file_name%.js new file mode 100644 index 00000000000..a79f2e948dd --- /dev/null +++ b/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/spree/frontend/%file_name%.js @@ -0,0 +1,2 @@ +// Placeholder manifest file. +// the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js' \ No newline at end of file diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/store/%file_name%.js b/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/store/%file_name%.js deleted file mode 100644 index d5cb5c754f8..00000000000 --- a/cmd/lib/spree_cmd/templates/extension/app/assets/javascripts/store/%file_name%.js +++ /dev/null @@ -1 +0,0 @@ -//= require store/spree_core diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/admin/%file_name%.css b/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/admin/%file_name%.css deleted file mode 100644 index 21ef02a685b..00000000000 --- a/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/admin/%file_name%.css +++ /dev/null @@ -1,3 +0,0 @@ -/* - *= require admin/spree_core -*/ diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/spree/backend/%file_name%.css b/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/spree/backend/%file_name%.css new file mode 100644 index 00000000000..e3c236629e3 --- /dev/null +++ b/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/spree/backend/%file_name%.css @@ -0,0 +1,4 @@ +/* +Placeholder manifest file. +the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css' +*/ diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/spree/frontend/%file_name%.css b/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/spree/frontend/%file_name%.css new file mode 100644 index 00000000000..da236237c52 --- /dev/null +++ b/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/spree/frontend/%file_name%.css @@ -0,0 +1,4 @@ +/* +Placeholder manifest file. +the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css' +*/ diff --git a/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/store/%file_name%.css b/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/store/%file_name%.css deleted file mode 100644 index 94dbe33abd5..00000000000 --- a/cmd/lib/spree_cmd/templates/extension/app/assets/stylesheets/store/%file_name%.css +++ /dev/null @@ -1,3 +0,0 @@ -/* - *= require store/spree_core -*/ diff --git a/cmd/lib/spree_cmd/templates/extension/script/rails.tt b/cmd/lib/spree_cmd/templates/extension/bin/rails.tt similarity index 100% rename from cmd/lib/spree_cmd/templates/extension/script/rails.tt rename to cmd/lib/spree_cmd/templates/extension/bin/rails.tt diff --git a/cmd/lib/spree_cmd/templates/extension/extension.gemspec b/cmd/lib/spree_cmd/templates/extension/extension.gemspec index dc57ddc8680..751b5a26eb7 100644 --- a/cmd/lib/spree_cmd/templates/extension/extension.gemspec +++ b/cmd/lib/spree_cmd/templates/extension/extension.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.version = '<%= spree_version %>' s.summary = 'TODO: Add gem summary here' s.description = 'TODO: Add (optional) gem description here' - s.required_ruby_version = '>= 1.8.7' + s.required_ruby_version = '>= 1.9.3' # s.author = 'You' # s.email = 'you@example.com' @@ -18,11 +18,14 @@ Gem::Specification.new do |s| s.add_dependency 'spree_core', '~> <%= spree_version %>' - s.add_development_dependency 'capybara', '~> 1.1.2' + s.add_development_dependency 'capybara', '~> 2.4' s.add_development_dependency 'coffee-rails' - s.add_development_dependency 'factory_girl', '~> 2.6.4' + s.add_development_dependency 'database_cleaner' + s.add_development_dependency 'factory_girl', '~> 4.5' s.add_development_dependency 'ffaker' - s.add_development_dependency 'rspec-rails', '~> 2.9' - s.add_development_dependency 'sass-rails' + s.add_development_dependency 'rspec-rails', '~> 3.1' + s.add_development_dependency 'sass-rails', '~> 4.0.2' + s.add_development_dependency 'selenium-webdriver' + s.add_development_dependency 'simplecov' s.add_development_dependency 'sqlite3' end diff --git a/cmd/lib/spree_cmd/templates/extension/lib/%file_name%/engine.rb.tt b/cmd/lib/spree_cmd/templates/extension/lib/%file_name%/engine.rb.tt index 0e1f3615df0..ea0578d611f 100644 --- a/cmd/lib/spree_cmd/templates/extension/lib/%file_name%/engine.rb.tt +++ b/cmd/lib/spree_cmd/templates/extension/lib/%file_name%/engine.rb.tt @@ -4,8 +4,6 @@ module <%= class_name %> isolate_namespace Spree engine_name '<%= file_name %>' - config.autoload_paths += %W(#{config.root}/lib) - # use rspec for tests config.generators do |g| g.test_framework :rspec diff --git a/cmd/lib/spree_cmd/templates/extension/lib/%file_name%/factories.rb.tt b/cmd/lib/spree_cmd/templates/extension/lib/%file_name%/factories.rb.tt new file mode 100644 index 00000000000..0ff6d9043c1 --- /dev/null +++ b/cmd/lib/spree_cmd/templates/extension/lib/%file_name%/factories.rb.tt @@ -0,0 +1,6 @@ +FactoryGirl.define do + # Define your Spree extensions Factories within this file to enable applications, and other extensions to use and override them. + # + # Example adding this to your spec_helper will load these Factories for use: + # require '<%= file_name %>/factories' +end diff --git a/cmd/lib/spree_cmd/templates/extension/lib/generators/%file_name%/install/install_generator.rb.tt b/cmd/lib/spree_cmd/templates/extension/lib/generators/%file_name%/install/install_generator.rb.tt index f3609a95d21..3be016fc1c8 100644 --- a/cmd/lib/spree_cmd/templates/extension/lib/generators/%file_name%/install/install_generator.rb.tt +++ b/cmd/lib/spree_cmd/templates/extension/lib/generators/%file_name%/install/install_generator.rb.tt @@ -2,14 +2,16 @@ module <%= class_name %> module Generators class InstallGenerator < Rails::Generators::Base + class_option :auto_run_migrations, :type => :boolean, :default => false + def add_javascripts - append_file 'app/assets/javascripts/store/all.js', "//= require store/<%= file_name %>\n" - append_file 'app/assets/javascripts/admin/all.js', "//= require admin/<%= file_name %>\n" + append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/<%= file_name %>\n" + append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/<%= file_name %>\n" end def add_stylesheets - inject_into_file 'app/assets/stylesheets/store/all.css', " *= require store/<%= file_name %>\n", :before => /\*\//, :verbose => true - inject_into_file 'app/assets/stylesheets/admin/all.css', " *= require admin/<%= file_name %>\n", :before => /\*\//, :verbose => true + inject_into_file 'vendor/assets/stylesheets/spree/frontend/all.css', " *= require spree/frontend/<%= file_name %>\n", :before => /\*\//, :verbose => true + inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/<%= file_name %>\n", :before => /\*\//, :verbose => true end def add_migrations @@ -17,12 +19,12 @@ module <%= class_name %> end def run_migrations - res = ask 'Would you like to run the migrations now? [Y/n]' - if res == '' || res.downcase == 'y' - run 'bundle exec rake db:migrate' - else - puts 'Skipping rake db:migrate, don\'t forget to run it!' - end + run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]') + if run_migrations + run 'bundle exec rake db:migrate' + else + puts 'Skipping rake db:migrate, don\'t forget to run it!' + end end end end diff --git a/cmd/lib/spree_cmd/templates/extension/rspec b/cmd/lib/spree_cmd/templates/extension/rspec index 53607ea52b7..5052887a0fb 100644 --- a/cmd/lib/spree_cmd/templates/extension/rspec +++ b/cmd/lib/spree_cmd/templates/extension/rspec @@ -1 +1 @@ ---colour +--color \ No newline at end of file diff --git a/cmd/lib/spree_cmd/templates/extension/spec/spec_helper.rb.tt b/cmd/lib/spree_cmd/templates/extension/spec/spec_helper.rb.tt index 51c395699d3..479f0940655 100644 --- a/cmd/lib/spree_cmd/templates/extension/spec/spec_helper.rb.tt +++ b/cmd/lib/spree_cmd/templates/extension/spec/spec_helper.rb.tt @@ -1,31 +1,51 @@ +# Run Coverage report +require 'simplecov' +SimpleCov.start do + add_filter 'spec/dummy' + add_group 'Controllers', 'app/controllers' + add_group 'Helpers', 'app/helpers' + add_group 'Mailers', 'app/mailers' + add_group 'Models', 'app/models' + add_group 'Views', 'app/views' + add_group 'Libraries', 'lib' +end + # Configure Rails Environment ENV['RAILS_ENV'] = 'test' require File.expand_path('../dummy/config/environment.rb', __FILE__) require 'rspec/rails' +require 'database_cleaner' require 'ffaker' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f } -# Requires factories defined in spree_core -require 'spree/core/testing_support/factories' -require 'spree/core/testing_support/controller_requests' -require 'spree/core/testing_support/authorization_helpers' -require 'spree/core/url_helpers' +# Requires factories and other useful helpers defined in spree_core. +require 'spree/testing_support/authorization_helpers' +require 'spree/testing_support/capybara_ext' +require 'spree/testing_support/controller_requests' +require 'spree/testing_support/factories' +require 'spree/testing_support/url_helpers' + +# Requires factories defined in lib/<%= file_name %>/factories.rb +require '<%= file_name %>/factories' RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods + # Infer an example group's spec type from the file location. + config.infer_spec_type_from_file_location! + # == URL Helpers # # Allows access to Spree's routes in specs: # # visit spree.admin_path # current_path.should eql(spree.products_path) - config.include Spree::Core::UrlHelpers + config.include Spree::TestingSupport::UrlHelpers # == Mock Framework # @@ -35,12 +55,33 @@ RSpec.configure do |config| # config.mock_with :flexmock # config.mock_with :rr config.mock_with :rspec + config.color = true # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. - config.use_transactional_fixtures = true + # Capybara javascript drivers require transactional fixtures set to false, and we use DatabaseCleaner + # to cleanup after each test instead. Without transactional fixtures set to false the records created + # to setup a test will be unavailable to the browser, which runs under a separate server instance. + config.use_transactional_fixtures = false + + # Ensure Suite is set to use transactions for speed. + config.before :suite do + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with :truncation + end + + # Before each spec check if it is a Javascript test and switch between using database transactions or not where necessary. + config.before :each do + DatabaseCleaner.strategy = RSpec.current_example.metadata[:js] ? :truncation : :transaction + DatabaseCleaner.start + end + + # After each spec clean the database. + config.after :each do + DatabaseCleaner.clean + end + + config.fail_fast = ENV['FAIL_FAST'] || false + config.order = "random" end diff --git a/cmd/spec/spec_helper.rb b/cmd/spec/spec_helper.rb index 116d1b03800..f939216345e 100644 --- a/cmd/spec/spec_helper.rb +++ b/cmd/spec/spec_helper.rb @@ -1,12 +1,27 @@ +if ENV["COVERAGE"] + # Run Coverage report + require 'simplecov' + SimpleCov.start do + add_group 'Controllers', 'app/controllers' + add_group 'Helpers', 'app/helpers' + add_group 'Mailers', 'app/mailers' + add_group 'Models', 'app/models' + add_group 'Views', 'app/views' + add_group 'Libraries', 'lib' + end +end + # This file is copied to ~/spec when you run 'ruby script/generate rspec' # from the project root directory. ENV["RAILS_ENV"] ||= 'test' RSpec.configure do |config| + config.color = true config.mock_with :rspec config.fixture_path = "#{::Rails.root}/spec/fixtures" config.use_transactional_fixtures = false + config.fail_fast = ENV['FAIL_FAST'] || false end diff --git a/cmd/spree_cmd.gemspec b/cmd/spree_cmd.gemspec index 1bd565cc851..4384a9a09c1 100644 --- a/cmd/spree_cmd.gemspec +++ b/cmd/spree_cmd.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.authors = ["Chris Mar"] s.email = ["chris@spreecommerce.com"] s.homepage = "http://spreecommerce.com" + s.license = %q{BSD-3} s.summary = %q{Spree Commerce command line utility} s.description = %q{tools to create new Spree stores and extensions} @@ -21,5 +22,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'rspec' # Temporary hack until https://github.com/wycats/thor/issues/234 is fixed - s.add_dependency 'thor', '>= 0.14.6' + s.add_dependency 'thor', '~> 0.14' end diff --git a/common_spree_dependencies.rb b/common_spree_dependencies.rb index eb97ab1dc4b..e684276a173 100644 --- a/common_spree_dependencies.rb +++ b/common_spree_dependencies.rb @@ -1,36 +1,45 @@ # By placing all of Spree's shared dependencies in this file and then loading # it for each component's Gemfile, we can be sure that we're only testing just # the one component of Spree. -source 'http://rubygems.org' +source 'https://rubygems.org' -gem 'json' -gem 'sqlite3' -gem 'mysql2' -gem 'pg' -gem 'multi_json', "1.2.0" -# Gems used only for assets and not required -# in production environments by default. -group :assets do - gem 'sass-rails', "~> 3.2" - gem 'coffee-rails', "~> 3.2" +platforms :ruby do + gem 'mysql2' + gem 'pg' + gem 'sqlite3' end -group :test do - gem 'guard' - gem 'guard-rspec', '~> 0.5.0' - gem 'rspec-rails', '~> 2.9.0' - gem 'factory_girl_rails', '~> 1.7.0' - gem 'email_spec', '~> 1.2.1' - - gem 'ffaker' - gem 'shoulda-matchers', '~> 1.0.0' - gem 'capybara', '1.1.3' - gem 'selenium-webdriver', '2.25.0' - gem 'database_cleaner', '0.7.1' - gem 'launchy' - # gem 'debugger' +platforms :jruby do + gem 'jruby-openssl' + gem 'activerecord-jdbcsqlite3-adapter' end -gemspec +gem 'coffee-rails', '~> 4.0.0' +gem 'sass-rails', '~> 4.0.2' +group :test do + gem 'capybara', '~> 2.4' + gem 'database_cleaner', '~> 1.3' + gem 'email_spec' + gem 'factory_girl_rails', '~> 4.5.0' + gem 'launchy' + gem 'rspec-activemodel-mocks' + gem 'rspec-collection_matchers' + gem 'rspec-its' + gem 'rspec-rails', '~> 3.1.0' + gem 'simplecov' + gem 'webmock', '1.8.11' + gem 'poltergeist', '1.5.0' + gem 'timecop' + gem 'with_model' +end +group :test, :development do + platforms :ruby_19 do + gem 'pry-debugger' + end + platforms :ruby_20, :ruby_21 do + gem 'pry-byebug' + end + gem 'rspec-retry' +end diff --git a/core/.rspec b/core/.rspec deleted file mode 100644 index 53607ea52b7..00000000000 --- a/core/.rspec +++ /dev/null @@ -1 +0,0 @@ ---colour diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md new file mode 100644 index 00000000000..f4a0249631c --- /dev/null +++ b/core/CHANGELOG.md @@ -0,0 +1 @@ +## Spree 2.4.0 (unreleased) ## diff --git a/core/Guardfile b/core/Guardfile deleted file mode 100644 index 4f2814fecbb..00000000000 --- a/core/Guardfile +++ /dev/null @@ -1,13 +0,0 @@ -guard 'rspec', :version => 2, :spec_paths => %w(spec), - :cli => (File.read('.rspec').split("\n").join(' ') if File.exists?('.rspec')) do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/requests/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch("spec/spec_helper.rb") { "spec" } - watch("config/routes.rb") - watch("app/controllers/application_controller.rb") { "spec/controllers" } - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } -end diff --git a/core/LICENSE b/core/LICENSE index 74f73e35ac3..bef97d82cc6 100644 --- a/core/LICENSE +++ b/core/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2012, Spree Commerce, Inc. and other contributors +Copyright (c) 2007-2014, Spree Commerce, Inc. and other contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/core/Rakefile b/core/Rakefile index 66ce502b22b..d28e3ed8fae 100644 --- a/core/Rakefile +++ b/core/Rakefile @@ -4,7 +4,8 @@ require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' require 'rspec/core/rake_task' -require 'spree/core/testing_support/common_rake' +require 'spree/testing_support/common_rake' +load 'lib/tasks/exchanges.rake' Bundler::GemHelper.install_tasks RSpec::Core::RakeTask.new diff --git a/core/app/assets/images/admin/bg/spree_50.png b/core/app/assets/images/admin/bg/spree_50.png deleted file mode 100644 index 8d1ade475b2..00000000000 Binary files a/core/app/assets/images/admin/bg/spree_50.png and /dev/null differ diff --git a/core/app/assets/images/admin/payment_banner.png b/core/app/assets/images/admin/payment_banner.png deleted file mode 100644 index 8178bdf56c2..00000000000 Binary files a/core/app/assets/images/admin/payment_banner.png and /dev/null differ diff --git a/core/app/assets/images/admin/progress.gif b/core/app/assets/images/admin/progress.gif deleted file mode 100644 index e0deaaaf35b..00000000000 Binary files a/core/app/assets/images/admin/progress.gif and /dev/null differ diff --git a/core/app/assets/images/credit_cards/amex_cid.gif b/core/app/assets/images/credit_cards/amex_cid.gif deleted file mode 100644 index 13d91deeca0..00000000000 Binary files a/core/app/assets/images/credit_cards/amex_cid.gif and /dev/null differ diff --git a/core/app/assets/images/credit_cards/credit_card.gif b/core/app/assets/images/credit_cards/credit_card.gif deleted file mode 100644 index 5c0ec44fe02..00000000000 Binary files a/core/app/assets/images/credit_cards/credit_card.gif and /dev/null differ diff --git a/core/app/assets/images/credit_cards/discover_cid.gif b/core/app/assets/images/credit_cards/discover_cid.gif deleted file mode 100644 index 097c75bfc8a..00000000000 Binary files a/core/app/assets/images/credit_cards/discover_cid.gif and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/american_express.png b/core/app/assets/images/credit_cards/icons/american_express.png deleted file mode 100644 index 25a6f302fbe..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/american_express.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/cirrus.png b/core/app/assets/images/credit_cards/icons/cirrus.png deleted file mode 100644 index 08feeffacad..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/cirrus.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/delta.png b/core/app/assets/images/credit_cards/icons/delta.png deleted file mode 100644 index 9282bb5b6a9..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/delta.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/dinersclub.png b/core/app/assets/images/credit_cards/icons/dinersclub.png deleted file mode 100644 index 38d9359b07e..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/dinersclub.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/directdebit.png b/core/app/assets/images/credit_cards/icons/directdebit.png deleted file mode 100644 index 14a2486b108..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/directdebit.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/discover.png b/core/app/assets/images/credit_cards/icons/discover.png deleted file mode 100644 index 163a1eb641e..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/discover.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/egold.png b/core/app/assets/images/credit_cards/icons/egold.png deleted file mode 100644 index f2811f68e6e..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/egold.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/maestro.png b/core/app/assets/images/credit_cards/icons/maestro.png deleted file mode 100644 index 3ee99ad42ee..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/maestro.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/master.png b/core/app/assets/images/credit_cards/icons/master.png deleted file mode 100644 index 0674308812c..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/master.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/paypal.png b/core/app/assets/images/credit_cards/icons/paypal.png deleted file mode 100644 index e067afee8ae..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/paypal.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/solo.png b/core/app/assets/images/credit_cards/icons/solo.png deleted file mode 100644 index 1e78b4aa0b2..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/solo.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/switch.png b/core/app/assets/images/credit_cards/icons/switch.png deleted file mode 100644 index 9dad21a87f9..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/switch.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/visa.png b/core/app/assets/images/credit_cards/icons/visa.png deleted file mode 100644 index 02cde006020..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/visa.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/visaelectron.png b/core/app/assets/images/credit_cards/icons/visaelectron.png deleted file mode 100644 index b387c7f103d..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/visaelectron.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/westernunion.png b/core/app/assets/images/credit_cards/icons/westernunion.png deleted file mode 100644 index d884f694e6c..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/westernunion.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/wirecard.png b/core/app/assets/images/credit_cards/icons/wirecard.png deleted file mode 100644 index 27b55585f23..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/wirecard.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/icons/worldpay.png b/core/app/assets/images/credit_cards/icons/worldpay.png deleted file mode 100644 index f26ecfc8143..00000000000 Binary files a/core/app/assets/images/credit_cards/icons/worldpay.png and /dev/null differ diff --git a/core/app/assets/images/credit_cards/master_cid.jpg b/core/app/assets/images/credit_cards/master_cid.jpg deleted file mode 100644 index b6b77e7776e..00000000000 Binary files a/core/app/assets/images/credit_cards/master_cid.jpg and /dev/null differ diff --git a/core/app/assets/images/credit_cards/visa_cid.gif b/core/app/assets/images/credit_cards/visa_cid.gif deleted file mode 100644 index 2dc54032462..00000000000 Binary files a/core/app/assets/images/credit_cards/visa_cid.gif and /dev/null differ diff --git a/core/app/assets/images/icons/add-to-cart.png b/core/app/assets/images/icons/add-to-cart.png deleted file mode 100644 index 4cccc773791..00000000000 Binary files a/core/app/assets/images/icons/add-to-cart.png and /dev/null differ diff --git a/core/app/assets/images/icons/checkout.png b/core/app/assets/images/icons/checkout.png deleted file mode 100644 index 41dcb703bee..00000000000 Binary files a/core/app/assets/images/icons/checkout.png and /dev/null differ diff --git a/core/app/assets/images/icons/delete.png b/core/app/assets/images/icons/delete.png deleted file mode 100755 index 1514d51a3cf..00000000000 Binary files a/core/app/assets/images/icons/delete.png and /dev/null differ diff --git a/core/app/assets/images/icons/update.png b/core/app/assets/images/icons/update.png deleted file mode 100644 index 4c5a58d45c6..00000000000 Binary files a/core/app/assets/images/icons/update.png and /dev/null differ diff --git a/core/app/assets/images/logo/spree_50.png b/core/app/assets/images/logo/spree_50.png new file mode 100644 index 00000000000..07c00ee4347 Binary files /dev/null and b/core/app/assets/images/logo/spree_50.png differ diff --git a/core/app/assets/images/noimage/large.png b/core/app/assets/images/noimage/large.png index 1caf12fea9f..fd0e9b6bee0 100644 Binary files a/core/app/assets/images/noimage/large.png and b/core/app/assets/images/noimage/large.png differ diff --git a/core/app/assets/images/noimage/mini.png b/core/app/assets/images/noimage/mini.png index 049f3df0596..ae0e527a85f 100644 Binary files a/core/app/assets/images/noimage/mini.png and b/core/app/assets/images/noimage/mini.png differ diff --git a/core/app/assets/images/noimage/product.png b/core/app/assets/images/noimage/product.png index 1caf12fea9f..fd0e9b6bee0 100644 Binary files a/core/app/assets/images/noimage/product.png and b/core/app/assets/images/noimage/product.png differ diff --git a/core/app/assets/images/noimage/small.png b/core/app/assets/images/noimage/small.png index 1e588434d2c..0b5b8cd2a11 100644 Binary files a/core/app/assets/images/noimage/small.png and b/core/app/assets/images/noimage/small.png differ diff --git a/core/app/assets/images/spinner.gif b/core/app/assets/images/spinner.gif deleted file mode 100644 index 274b4fdb1e8..00000000000 Binary files a/core/app/assets/images/spinner.gif and /dev/null differ diff --git a/core/app/assets/images/store/cart.png b/core/app/assets/images/store/cart.png deleted file mode 100755 index 9490140c544..00000000000 Binary files a/core/app/assets/images/store/cart.png and /dev/null differ diff --git a/core/app/assets/javascripts/admin/address_states.js b/core/app/assets/javascripts/admin/address_states.js deleted file mode 100644 index 9c224ca8888..00000000000 --- a/core/app/assets/javascripts/admin/address_states.js +++ /dev/null @@ -1,26 +0,0 @@ -var update_state = function(region) { - var country = $('span#' + region + 'country .select2').select2('val'); - var states = state_mapper[country]; - - var state_select = $('span#' + region + 'state .select2'); - var state_input = $('span#' + region + 'state input'); - - if(states) { - state_select.html(''); - var states_with_blank = [["",""]].concat(states); - $.each(states_with_blank, function(pos,id_nm) { - var opt = $(document.createElement('option')) - .attr('value', id_nm[0]) - .html(id_nm[1]); - state_select.append(opt); - }); - state_select.prop("disabled", false).show(); - state_select.select2(); - state_input.hide().prop("disabled", true); - - } else { - state_input.prop("disabled", false).show(); - state_select.select2('destroy').hide(); - } - -}; diff --git a/core/app/assets/javascripts/admin/admin.js.erb b/core/app/assets/javascripts/admin/admin.js.erb deleted file mode 100644 index da33c941ef6..00000000000 --- a/core/app/assets/javascripts/admin/admin.js.erb +++ /dev/null @@ -1,254 +0,0 @@ -//= require_self -//= require admin/variant_autocomplete -//= require admin/taxon_autocomplete -//= require admin/spree-select2 - -/** -This is a collection of javascript functions and whatnot -under the spree namespace that do stuff we find helpful. -Hopefully, this will evolve into a propper class. -**/ - -jQuery(function($) { - // Make main menu use full width - $('.fullwidth-menu').horizontalNav({ - tableDisplay: false, - responsiveDelay: 0 - }); - - // Vertical align of checkbox fields - $('.field.checkbox label').vAlign() - - <% # Re-adjusting admin menu during test causes tests to fail, - # like states_spec and shipping_methods_spec. Let's not do this. %> - <% unless Rails.env.test? %> - $('.main-menu-wrapper ul').AdaptiveMenu({ - text: " " + Spree.translations.more + "", - klass: "dropdown" - }); - <% end %> - - // Add some tips - $('.with-tip').powerTip({ - smartPlacement: true, - fadeInTime: 50, - fadeOutTime: 50, - intentPollInterval: 300 - }); - - $('.with-tip').on({ - powerTipPreRender: function(){ - $('#powerTip').addClass($(this).attr("data-action")); - $('#powerTip').addClass($(this).attr("data-tip-color")); - }, - powerTipClose: function(){ - $('#powerTip').removeClass($(this).attr("data-action")) - } - }); - - // Make flash messages dissapear - setTimeout('$(".flash").fadeOut()', 5000); - - // Highlight hovered table column - $('table tbody tr td.actions a').hover(function(){ - var tr = $(this).closest('tr'); - var klass = 'highlight action-' + $(this).attr('data-action') - tr.addClass(klass) - tr.prev().addClass('before-' + klass); - }, function(){ - var tr = $(this).closest('tr'); - var klass = 'highlight action-' + $(this).attr('data-action') - tr.removeClass(klass) - tr.prev().removeClass('before-' + klass); - }); - - // Trunkate text in page_title that didn't fit - var truncate_elements = $('.truncate'); - - truncate_elements.each(function(){ - $(this).trunk8(); - }); - $(window).resize(function (event) { - truncate_elements.each(function(){ - $(this).trunk8(); - }) - }); - - // Make height of dt/dd elements the same - $("dl").equalize('outerHeight'); - -}); - - -$.fn.visible = function(cond) { this[cond ? 'show' : 'hide' ]() }; - -show_flash_error = function(message) { - error_div = $('.flash.error'); - if (error_div.length > 0) { - error_div.html(message); - error_div.show(); - } else { - if ($("#content .toolbar").length > 0) { - $("#content .toolbar").before('
    ' + message + '
    '); - } else { - $("#content h1").before('
    ' + message + '
    '); - } - } -} - -// Apply to individual radio button that makes another element visible when checked -$.fn.radioControlsVisibilityOfElement = function(dependentElementSelector){ - if(!this.get(0)){ return } - showValue = this.get(0).value; - radioGroup = $("input[name='" + this.get(0).name + "']"); - radioGroup.each(function(){ - $(this).click(function(){ - $(dependentElementSelector).visible(this.checked && this.value == showValue) - }); - if(this.checked){ this.click() } - }); -} - -handle_date_picker_fields = function(){ - $('.datepicker').datepicker({ - dateFormat: Spree.translations.date_picker, - dayNames: Spree.translations.abbr_day_names, - dayNamesMin: Spree.translations.abbr_day_names, - monthNames: Spree.translations.month_names, - prevText: Spree.translations.previous, - nextText: Spree.translations.next, - showOn: "focus" - }); - - // Correctly display range dates - $('.date-range-filter .datepicker-from').datepicker('option', 'onSelect', function(selectedDate) { - $(".date-range-filter .datepicker-to" ).datepicker( "option", "minDate", selectedDate ); - }); - $('.date-range-filter .datepicker-to').datepicker('option', 'onSelect', function(selectedDate) { - $(".date-range-filter .datepicker-from" ).datepicker( "option", "maxDate", selectedDate ); - }); -} - -$(document).ready(function(){ - handle_date_picker_fields(); - $(".observe_field").on('change', function() { - target = $(this).attr("data-update"); - ajax_indicator = $(this).attr("data-ajax-indicator") || '#busy_indicator'; - $(target).hide(); - $(ajax_indicator).show(); - $.ajax({ dataType: 'html', - url: $(this).attr("data-base-url")+encodeURIComponent($(this).val()), - type: 'get', - success: function(data){ - $(target).html(data); - $(ajax_indicator).hide(); - $(target).show(); - } - }); - }); - - $('.add_fields').click(function() { - var target = $(this).data("target"); - var new_table_row = $(target + ' tr:visible:last').clone(); - var new_id = new Date().getTime(); - new_table_row.find("input").each(function () { - var el = $(this); - el.val(""); - el.attr("id", el.attr("id").replace(/\d+/, new_id)) - el.attr("name", el.attr("name").replace(/\d+/, new_id)) - }) - $(target).prepend(new_table_row); - }) - - $('body').on('click', '.delete-resource', function() { - var el = $(this); - if (confirm(el.data("confirm"))) { - $.ajax({ - type: 'POST', - url: $(this).attr("href"), - data: { - _method: 'delete', - authenticity_token: AUTH_TOKEN - }, - dataType: 'script', - success: function(response) { - el.parents("tr").fadeOut('hide'); - }, - error: function(response, textStatus, errorThrown) { - show_flash_error(response.responseText); - } - }); - } - return false; - }); - - $('body').on('click', 'a.remove_fields', function() { - $(this).prev("input[type=hidden]").val("1"); - $(this).closest(".fields").hide(); - return false; - }); - - $('body').on('click', '.select_properties_from_prototype', function(){ - $("#busy_indicator").show(); - var clicked_link = $(this); - $.ajax({ dataType: 'script', url: clicked_link.attr("href"), type: 'get', - success: function(data){ - clicked_link.parent("td").parent("tr").hide(); - $("#busy_indicator").hide(); - } - }); - return false; - }); - - // Fix sortable helper - var fixHelper = function(e, ui) { - ui.children().each(function() { - $(this).width($(this).width()); - }); - return ui; - }; - - $('table.sortable').ready(function(){ - var td_count = $(this).find('tbody tr:first-child td').length - $('table.sortable tbody').sortable( - { - handle: '.handle', - helper: fixHelper, - placeholder: 'ui-sortable-placeholder', - update: function(event, ui) { - $("#progress").show(); - positions = {}; - $.each($('table.sortable tbody tr'), function(position, obj){ - reg = /spree_(\w+_?)+_(\d+)/; - parts = reg.exec($(obj).attr('id')); - if (parts) { - positions['positions['+parts[2]+']'] = position; - } - }); - $.ajax({ - type: 'POST', - dataType: 'script', - url: $(ui.item).closest("table.sortable").data("sortable-link"), - data: positions, - success: function(data){ $("#progress").hide(); } - }); - }, - start: function (event, ui) { - // Set correct height for placehoder (from dragged tr) - ui.placeholder.height(ui.item.height()) - // Fix placeholder content to make it correct width - ui.placeholder.html("") - }, - stop: function (event, ui) { - // Fix odd/even classes after reorder - $("table.sortable tr:even").removeClass("odd even").addClass("even"); - $("table.sortable tr:odd").removeClass("odd even").addClass("odd"); - } - - }).disableSelection(); - }); - - $('a.dismiss').click(function() { - $(this).parent().fadeOut(); - }); -}); diff --git a/core/app/assets/javascripts/admin/calculator.js b/core/app/assets/javascripts/admin/calculator.js deleted file mode 100644 index 5c02e5bc9b8..00000000000 --- a/core/app/assets/javascripts/admin/calculator.js +++ /dev/null @@ -1,16 +0,0 @@ -$(function() { - var calculator_select = $('select#calc_type') - var original_calc_type = calculator_select.attr('value'); - $('div#calculator-settings-warning').hide(); - calculator_select.change(function() { - if (calculator_select.attr('value') == original_calc_type) { - $('div.calculator-settings').show(); - $('div#calculator-settings-warning').hide(); - $('.calculator-settings input').prop("disabled", false); - } else { - $('div.calculator-settings').hide(); - $('div#calculator-settings-warning').show(); - $('.calculator-settings input').prop("disabled", true); - } - }); -}) diff --git a/core/app/assets/javascripts/admin/checkouts/edit.js b/core/app/assets/javascripts/admin/checkouts/edit.js deleted file mode 100644 index 5f50ecce508..00000000000 --- a/core/app/assets/javascripts/admin/checkouts/edit.js +++ /dev/null @@ -1,134 +0,0 @@ -$(document).ready(function(){ - - add_address = function(addr){ - var html = ""; - if(addr!=undefined){ - html += addr['firstname'] + " " + addr['lastname'] + ", "; - html += addr['address1'] + ", " + addr['address2'] + ", "; - html += addr['city'] + ", "; - - if(addr['state_id']!=null){ - html += addr['state']['name'] + ", "; - }else{ - html += addr['state_name'] + ", "; - } - - html += addr['country']['name']; - } - return html; - } - - format_user_autocomplete = function(item){ - var data = item.data - var html = "

    " + data['email'] +"

    "; - html += "Billing: "; - html += add_address(data['bill_address']); - html += ""; - - html += "Shipping: "; - html += add_address(data['ship_address']); - html += ""; - - return html - } - - prep_user_autocomplete_data = function(data){ - return $.map(eval(data['users']), function(row) { - return { - data: row, - value: row['email'], - result: row['email'] - } - }); - } - - if ($("#customer_search").length > 0) { - $("#customer_search").autocomplete({ - minChars: 5, - delay: 500, - source: function(request, response) { - var params = { q: $('#customer_search').val(), - authenticity_token: AUTH_TOKEN } - $.get(Spree.routes.user_search + '&' + jQuery.param(params), function(data) { - result = prep_user_autocomplete_data(data) - response(result); - }); - }, - focus: function(event, ui) { - $('#customer_search').val(ui.item.label); - $(ui).addClass('ac_over'); - return false; - }, - select: function(event, ui) { - $('#customer_search').val(ui.item.label); - _.each(['bill', 'ship'], function(addr_name){ - var addr = ui.item.data[addr_name + '_address']; - if(addr!=undefined){ - $('#order_' + addr_name + '_address_attributes_firstname').val(addr['firstname']); - $('#order_' + addr_name + '_address_attributes_lastname').val(addr['lastname']); - $('#order_' + addr_name + '_address_attributes_company').val(addr['company']); - $('#order_' + addr_name + '_address_attributes_address1').val(addr['address1']); - $('#order_' + addr_name + '_address_attributes_address2').val(addr['address2']); - $('#order_' + addr_name + '_address_attributes_city').val(addr['city']); - $('#order_' + addr_name + '_address_attributes_zipcode').val(addr['zipcode']); - $('#order_' + addr_name + '_address_attributes_state_id').val(addr['state_id']); - $('#order_' + addr_name + '_address_attributes_country_id').val(addr['country_id']); - $('#order_' + addr_name + '_address_attributes_phone').val(addr['phone']); - } - }); - - $('#order_email').val(ui.item.data['email']); - $('#user_id').val(ui.item.data['id']); - $('#guest_checkout_true').prop("checked", false); - $('#guest_checkout_false').prop("checked", true); - $('#guest_checkout_false').prop("disabled", false); - return true; - } - }).data("autocomplete")._renderItem = function(ul, item) { - $(ul).addClass('ac_results'); - html = format_user_autocomplete(item); - return $("
  • ") - .data("item.autocomplete", item) - .append("" + html + "") - .appendTo(ul); - } - - $("#customer_search").data("autocomplete")._resizeMenu = function() { - var ul = this.menu.element; - ul.outerWidth(this.element.outerWidth()); - } - - - } - - var show_billing = function(show) { - if(show) { - $('#shipping').show(); - $('#shipping input').prop("disabled", false); - $('#shipping select').prop("disabled", false); - } else { - $('#shipping').hide(); - $('#shipping input').prop("disabled", true); - $('#shipping select').prop("disabled", true); - } - } - - $('input#order_use_billing').click(function() { - show_billing(!$(this).is(':checked')); - }); - - $('#guest_checkout_true').change(function() { - $('#customer_search').val(""); - $('#user_id').val(""); - $('#checkout_email').val(""); - - var fields = ["firstname", "lastname", "company", "address1", "address2", - "city", "zipcode", "state_id", "country_id", "phone"] - $.each(fields, function(i, field) { - $('#order_bill_address_attributes' + field).val(""); - $('#order_ship_address_attributes' + field).val(""); - }) - }); -}); - - diff --git a/core/app/assets/javascripts/admin/gateway.js b/core/app/assets/javascripts/admin/gateway.js deleted file mode 100644 index f42c33fc7b1..00000000000 --- a/core/app/assets/javascripts/admin/gateway.js +++ /dev/null @@ -1,13 +0,0 @@ -$(function() { - var original_gtwy_type = $('#gtwy-type').attr('value'); - $('div#gateway-settings-warning').hide(); - $('#gtwy-type').change(function() { - if ($('#gtwy-type').attr('value') == original_gtwy_type) { - $('div.gateway-settings').show(); - $('div#gateway-settings-warning').hide(); - } else { - $('div.gateway-settings').hide(); - $('div#gateway-settings-warning').show(); - } - }); -}) diff --git a/core/app/assets/javascripts/admin/image_settings.js.erb b/core/app/assets/javascripts/admin/image_settings.js.erb deleted file mode 100644 index 04244c86461..00000000000 --- a/core/app/assets/javascripts/admin/image_settings.js.erb +++ /dev/null @@ -1,62 +0,0 @@ -$(document).ready(function() { - - if ($('input#preferences_use_s3[type="checkbox"]:checked').length > 0) { - $('#s3_settings, #s3_headers').show(); - } - - // Toggle display of S3 settings based on value of use_s3 checkbox - $('input#preferences_use_s3[type="checkbox"]').click(function() { - $('#s3_settings, #s3_headers').toggle(); - }); - - $('.destroy_style').live("click", function(e) { - e.preventDefault(); - $(this).parent().remove(); - }); - - $('.destroy_new_attachment_styles').live("click", function(e) { - e.preventDefault(); - $(this).closest('.new_attachment_styles').remove(); - }); - - $('.destroy_new_s3_headers').live("click", function(e) { - e.preventDefault(); - $(this).closest('.new_s3_headers').remove(); - }); - - // Handle adding new styles - var styles_hash_index = 1; - $('.add_new_style').live("click", function(e) { - e.preventDefault(); - $('#new-styles').append(generate_html_for_hash("new_attachment_styles", styles_hash_index)); - }); - - // Handle adding new headers - var headers_hash_index = 1; - $('.add_header').live("click", function(e) { - e.preventDefault(); - $('#headers_list').append(generate_html_for_hash("new_s3_headers", headers_hash_index)); - }); - - // Generates html for new paperclip styles form fields - generate_html_for_hash = function(hash_name, index) { - var html = '
    '; - html += '
    '; - html += ''; - html += '
    '; - html += '
    ' - html += ''; - html += ''; - html += '
    ' - html += '   ' + Spree.translations.destroy + ''; - html += '
    '; - - index += 1; - return html; - }; - - - -}); diff --git a/core/app/assets/javascripts/admin/nested-attribute.js b/core/app/assets/javascripts/admin/nested-attribute.js deleted file mode 100644 index 0282992ac94..00000000000 --- a/core/app/assets/javascripts/admin/nested-attribute.js +++ /dev/null @@ -1,23 +0,0 @@ -//On page load -replace_ids = function(s){ - var new_id = new Date().getTime(); - return s.replace(/NEW_RECORD/g, new_id); -} - -$(function() { - $('a[id*=nested]').click(function() { - var template = $(this).attr('href').replace(/.*#/, ''); - html = replace_ids(eval(template)); - $('#ul-' + $(this).attr('id')).append(html); - update_remove_links(); - }); - update_remove_links(); -}) - -var update_remove_links = function() { - $('.remove').click(function() { - $(this).prevAll(':first').val(1); - $(this).parent().hide(); - return false; - }); -}; diff --git a/core/app/assets/javascripts/admin/orders/edit.js b/core/app/assets/javascripts/admin/orders/edit.js deleted file mode 100644 index 357195a7c7f..00000000000 --- a/core/app/assets/javascripts/admin/orders/edit.js +++ /dev/null @@ -1,15 +0,0 @@ -$(document).ready(function(){ - - $("#add_line_item_to_order").on("click", function(){ - if($('#add_variant_id').val() == ''){ return false; } - update_target = $(this).attr("data-update"); - $.ajax({ dataType: 'script', url: this.href, type: "POST", - data: {"line_item[variant_id]": $('#add_variant_id').val(), - "line_item[quantity]": $('#add_quantity').val()} - }); - return false; - }); - - $(".variant_autocomplete").variantAutocomplete(); - -}); diff --git a/core/app/assets/javascripts/admin/orders/edit_form.js b/core/app/assets/javascripts/admin/orders/edit_form.js deleted file mode 100644 index 2610d3df9c8..00000000000 --- a/core/app/assets/javascripts/admin/orders/edit_form.js +++ /dev/null @@ -1,16 +0,0 @@ -$(document).ready(function() { - $.each($('td.qty input'), function(i, input) { - - $(input).on('change', function() { - - var id = "#" + $(this).attr('id').replace("_quantity", "_id"); - - jQuery.post("/admin/orders/" + $('input#order_number').val() + "/line_items/" + $(id).val(), - { _method: "put", "line_item[quantity]": $(this).val()}, - function(resp) { - $('#order-form-wrapper').html(resp.responseText); - }) - }) - }) -}); - diff --git a/core/app/assets/javascripts/admin/payments/new.js b/core/app/assets/javascripts/admin/payments/new.js deleted file mode 100644 index bf164dd38aa..00000000000 --- a/core/app/assets/javascripts/admin/payments/new.js +++ /dev/null @@ -1,9 +0,0 @@ -$(document).ready(function(){ - - $("#card_new").radioControlsVisibilityOfElement('#card_form'); - - $('select.jump_menu').change(function(){ - window.location = this.options[this.selectedIndex].value; - }); - -}); \ No newline at end of file diff --git a/core/app/assets/javascripts/admin/progress.coffee b/core/app/assets/javascripts/admin/progress.coffee deleted file mode 100644 index edc20a541b8..00000000000 --- a/core/app/assets/javascripts/admin/progress.coffee +++ /dev/null @@ -1,27 +0,0 @@ -$(document).ready -> - opts = - lines: 11 - length: 2 - width: 3 - radius: 9 - corners: 1 - rotate: 0 - color: '#fff' - speed: 0.8 - trail: 48 - shadow: false - hwaccel: true - className: 'spinner' - zIndex: 2e9 - top: 'auto' - left: 'auto' - - target = document.getElementById("spinner") - - $(document).ajaxStart -> - $("#progress").fadeIn() - spinner = new Spinner(opts).spin(target) - - $(document).ajaxStop -> - $("#progress").fadeOut() - diff --git a/core/app/assets/javascripts/admin/shipping_methods.js.coffee b/core/app/assets/javascripts/admin/shipping_methods.js.coffee deleted file mode 100644 index c6b63332087..00000000000 --- a/core/app/assets/javascripts/admin/shipping_methods.js.coffee +++ /dev/null @@ -1,10 +0,0 @@ -$ -> - ($ 'input[type=checkbox]:not(:checked)').attr 'disabled', true if ($ '.categories input:checked').length > 0 - categoryCheckboxes = '.categories input[type=checkbox]' - $(categoryCheckboxes).change -> - if ($ this).is(':checked') - ($ categoryCheckboxes + ':not(:checked)').attr 'disabled', true - ($ this).removeAttr 'disabled' - else - ($ 'input[type=checkbox]').removeAttr 'disabled' - diff --git a/core/app/assets/javascripts/admin/spree-select2.js.erb b/core/app/assets/javascripts/admin/spree-select2.js.erb deleted file mode 100644 index 0abca9b36ff..00000000000 --- a/core/app/assets/javascripts/admin/spree-select2.js.erb +++ /dev/null @@ -1,24 +0,0 @@ -//= require select2 -jQuery(function($) { - <% unless Rails.env.test? %> - // Make select beautiful - $('select.select2').select2({ - allowClear: true - }); - - function format_taxons(taxon) { - new_taxon = taxon.text.replace('->', '') - return new_taxon; - } - - $("#product_taxon_ids").on({ - change: function(e){ - $('.select2-search-choice .with-tip').powerTip({ - smartPlacement: true, - fadeInTime: 50, - fadeOutTime: 50 - }) - } - }) - <% end %> -}) diff --git a/core/app/assets/javascripts/admin/spree_core.js b/core/app/assets/javascripts/admin/spree_core.js deleted file mode 100644 index f815c443fb1..00000000000 --- a/core/app/assets/javascripts/admin/spree_core.js +++ /dev/null @@ -1,19 +0,0 @@ -//= require jquery-ui -//= require modernizr -//= require jquery.cookie -//= require jquery.tokeninput -//= require jquery.delayedobserver -//= require jquery.jstree/jquery.jstree -//= require jquery.alerts/jquery.alerts -//= require jquery.powertip -//= require jquery.vAlign -//= require css_browser_selector_dev -//= require spin -//= require trunk8 -//= require jquery.adaptivemenu -//= require equalize -//= require responsive-tables -//= require jquery.horizontalNav -//= require_tree . - -var Spree = {}; diff --git a/core/app/assets/javascripts/admin/taxon_autocomplete.js.erb b/core/app/assets/javascripts/admin/taxon_autocomplete.js.erb deleted file mode 100644 index 4127838f20d..00000000000 --- a/core/app/assets/javascripts/admin/taxon_autocomplete.js.erb +++ /dev/null @@ -1,36 +0,0 @@ -function cleanTaxons(data) { - var taxons = $.map(data['taxons'], function(result) { - return result - }) - return taxons; -} - -$(document).ready(function() { - if ($("#product_taxon_ids").length > 0) { - $("#product_taxon_ids").select2({ - placeholder: "Add a taxon", - multiple: true, - initSelection: function(element, callback) { - return $.getJSON(Spree.routes.taxon_search + "?ids=" + (element.val()), null, function(data) { - return callback(self.cleanTaxons(data)); - }) - }, - ajax: { - url: Spree.routes.taxon_search, - datatype: 'json', - data: function(term, page) { - return { q: term } - }, - results: function (data, page) { - return { results: self.cleanTaxons(data) } - } - }, - formatResult: function(taxon) { - return taxon.pretty_name - }, - formatSelection: function(taxon) { - return taxon.pretty_name - } - }) - } -}) diff --git a/core/app/assets/javascripts/admin/taxonomy.js b/core/app/assets/javascripts/admin/taxonomy.js deleted file mode 100644 index 0de7e4bca4e..00000000000 --- a/core/app/assets/javascripts/admin/taxonomy.js +++ /dev/null @@ -1,205 +0,0 @@ -var handle_ajax_error = function(XMLHttpRequest, textStatus, errorThrown){ - $.jstree.rollback(last_rollback); - $("#ajax_error").show().html("" + server_error + "
    " + taxonomy_tree_error); -}; - -//var handle_move = function(li, target, droppped, tree, rb) { -var handle_move = function(e, data) { - last_rollback = data.rlbk; - var position = data.rslt.cp; - var node = data.rslt.o; - var new_parent = data.rslt.np; - - $.ajax({ - type: "POST", - dataType: "json", - url: base_url + node.attr("id"), - data: ({_method: "put", "taxon[parent_id]": new_parent.attr("id"), "taxon[position]": position, authenticity_token: AUTH_TOKEN}), - error: handle_ajax_error - }); - - return true -}; - -var handle_create = function(e, data) { - last_rollback = data.rlbk; - var node = data.rslt.obj; - var name = data.rslt.name; - var position = data.rslt.position; - var new_parent = data.rslt.parent; - - $.ajax({ - type: "POST", - dataType: "json", - url: base_url, - data: ({"taxon[name]": name, "taxon[parent_id]": new_parent.attr("id"), "taxon[position]": position, authenticity_token: AUTH_TOKEN}), - error: handle_ajax_error, - success: function(data,result) { - node.attr('id', data.taxon.id); - } - }); - -}; - -var handle_rename = function(e, data) { - last_rollback = data.rlbk; - var node = data.rslt.obj; - var name = data.rslt.new_name; - - $.ajax({ - type: "POST", - dataType: "json", - url: base_url + node.attr("id"), - data: ({_method: "put", "taxon[name]": name, authenticity_token: AUTH_TOKEN}), - error: handle_ajax_error - }); - }; - -var handle_delete = function(e, data){ - last_rollback = data.rlbk; - var node = data.rslt.obj; - - jConfirm(Spree.translations.are_you_sure_delete, Spree.translations.confirm_delete, function(r) { - if(r){ - $.ajax({ - type: "POST", - dataType: "json", - url: base_url + node.attr("id"), - data: ({_method: "delete", authenticity_token: AUTH_TOKEN}), - error: handle_ajax_error - }); - }else{ - $.jstree.rollback(last_rollback); - last_rollback = null; - } - }); - -}; - -var taxonomy_id; - -$(document).ready(function(){ - if(taxonomy_id!=undefined){ - - base_url = $("#taxonomy_tree").data("url").split("?")[0] + "/" ; - child_url = base_url.replace("/taxons", "/get_children.json"); - - is_cut = false; - last_rollback = null; - - var conf = { - json_data : { - "data" : initial, - "ajax" : { - "url" : child_url, - "data" : function (n) { - return { parent_id : n.attr ? n.attr("id") : 0 }; - } - } - }, - "themes" : { - "theme" : "apple", - "url" : "/assets/jquery.jstree/themes/apple/style.css" - }, - "strings" : { - "new_node" : new_taxon, - "loading" : Spree.translations.loading + "..." - }, - "crrm" : { - "move" : { - "check_move" : function (m) { - var position = m.cp; - var node = m.o; - var new_parent = m.np; - - if(!new_parent) return false; //no parent - - if(node.attr("rel")=="root") return false; //can't drag root - - if(new_parent.attr("id")=="taxonomy_tree" && position==0) return false; // can't drop before root - - return true; - - } - } - }, - "contextmenu" : { - "items" : function(obj) { - var id_of_node = obj.attr("id"); - var type_of_node = obj.attr("rel"); - var menu = {}; - if(type_of_node == "root") { - menu = { - "create" : { - "label" : " " + Spree.translations.add, - "action" : function (obj) { this.create(obj); } - }, - "paste" : { - "separator_before" : true, - "label" : " " + Spree.translations.paste, - "action" : function (obj) { is_cut = false; this.paste(obj); }, - "_disabled" : is_cut == false - }, - "edit" : { - "separator_before" : true, - "label" : " " + Spree.translations.edit, - "action" : function (obj) { window.location = base_url + obj.attr("id") + "/edit/"; } - } - } - } else { - menu = { - "create" : { - "label" : " " + Spree.translations.add, - "action" : function (obj) { this.create(obj); } - }, - "rename" : { - "label" : " " + Spree.translations.rename, - "action" : function (obj) { this.rename(obj); } - }, - "remove" : { - "label" : " " + Spree.translations.remove, - "action" : function (obj) { this.remove(obj); } - }, - "cut" : { - "separator_before" : true, - "label" : " " + Spree.translations.cut, - "action" : function (obj) { is_cut = true; this.cut(obj); } - }, - "paste" : { - "label" : " " + Spree.translations.paste, - "action" : function (obj) { is_cut = false; this.paste(obj); }, - "_disabled" : is_cut == false - }, - "edit" : { - "separator_before" : true, - "label" : " " + Spree.translations.edit, - "action" : function (obj) { window.location = base_url + obj.attr("id") + "/edit/"; } - } - } - } - return menu; - } - }, - - "plugins" : [ "themes", "json_data", "dnd", "crrm", "contextmenu"] - } - - $("#taxonomy_tree").jstree(conf) - .bind("move_node.jstree", handle_move) - .bind("remove.jstree", handle_delete) - .bind("create.jstree", handle_create) - .bind("rename.jstree", handle_rename); - - $("#taxonomy_tree a").on("dblclick", function (e) { - $("#taxonomy_tree").jstree("rename", this) - }); - - - $(document).keypress(function(e){ - //surpress form submit on enter/return - if (e.keyCode == 13){ - e.preventDefault(); - } - }); - } -}); diff --git a/core/app/assets/javascripts/admin/underscore-min.js b/core/app/assets/javascripts/admin/underscore-min.js deleted file mode 100644 index f502cf9f6a0..00000000000 --- a/core/app/assets/javascripts/admin/underscore-min.js +++ /dev/null @@ -1,26 +0,0 @@ -// Underscore.js 1.1.6 -// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore is freely distributable under the MIT license. -// Portions of Underscore are inspired or borrowed from Prototype, -// Oliver Steele's Functional, and John Resig's Micro-Templating. -// For all details and documentation: -// http://documentcloud.github.com/underscore -(function(){var p=this,C=p._,m={},i=Array.prototype,n=Object.prototype,f=i.slice,D=i.unshift,E=n.toString,l=n.hasOwnProperty,s=i.forEach,t=i.map,u=i.reduce,v=i.reduceRight,w=i.filter,x=i.every,y=i.some,o=i.indexOf,z=i.lastIndexOf;n=Array.isArray;var F=Object.keys,q=Function.prototype.bind,b=function(a){return new j(a)};typeof module!=="undefined"&&module.exports?(module.exports=b,b._=b):p._=b;b.VERSION="1.1.6";var h=b.each=b.forEach=function(a,c,d){if(a!=null)if(s&&a.forEach===s)a.forEach(c,d);else if(b.isNumber(a.length))for(var e= -0,k=a.length;e=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a, -c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;bd?1:0}),"value")};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.zip=function(){for(var a=f.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c), -e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}};b.keys=F||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)l.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a, -b.identity)};b.functions=b.methods=function(a){return b.filter(b.keys(a),function(c){return b.isFunction(a[c])}).sort()};b.extend=function(a){h(f.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){h(f.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,c){if(a===c)return!0;var d=typeof a;if(d!= -typeof c)return!1;if(a==c)return!0;if(!a&&c||a&&!c)return!1;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual)return a.isEqual(c);if(b.isDate(a)&&b.isDate(c))return a.getTime()===c.getTime();if(b.isNaN(a)&&b.isNaN(c))return!1;if(b.isRegExp(a)&&b.isRegExp(c))return a.source===c.source&&a.global===c.global&&a.ignoreCase===c.ignoreCase&&a.multiline===c.multiline;if(d!=="object")return!1;if(a.length&&a.length!==c.length)return!1;d=b.keys(a);var e=b.keys(c);if(d.length!=e.length)return!1; -for(var f in a)if(!(f in c)||!b.isEqual(a[f],c[f]))return!1;return!0};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(l.call(a,c))return!1;return!0};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=n||function(a){return E.call(a)==="[object Array]"};b.isArguments=function(a){return!(!a||!l.call(a,"callee"))};b.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};b.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)}; -b.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===!0||a===!1};b.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};b.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||a.ignoreCase===!1))};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){p._=C;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e= -0;e/g,interpolate:/<%=([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate|| -null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return c?d(c):d};var j=function(a){this._wrapped=a};b.prototype=j.prototype;var r=function(a,c){return c?b(a).chain():a},H=function(a,c){j.prototype[a]=function(){var a=f.call(arguments);D.call(a,this._wrapped);return r(c.apply(b,a),this._chain)}};b.mixin(b);h(["pop","push","reverse","shift","sort", -"splice","unshift"],function(a){var b=i[a];j.prototype[a]=function(){b.apply(this._wrapped,arguments);return r(this._wrapped,this._chain)}});h(["concat","join","slice"],function(a){var b=i[a];j.prototype[a]=function(){return r(b.apply(this._wrapped,arguments),this._chain)}});j.prototype.chain=function(){this._chain=!0;return this};j.prototype.value=function(){return this._wrapped}})(); diff --git a/core/app/assets/javascripts/admin/variant_autocomplete.js.erb b/core/app/assets/javascripts/admin/variant_autocomplete.js.erb deleted file mode 100644 index 3574d8a9424..00000000000 --- a/core/app/assets/javascripts/admin/variant_autocomplete.js.erb +++ /dev/null @@ -1,40 +0,0 @@ -//= require handlebars -<%#encoding: UTF-8%> -// variant autocompletion - -$(document).ready(function() { - window.variantTemplate = Handlebars.compile($('#variant_autocomplete_template').text()); -}) - -formatVariantResult = function(variant) { - if (variant["images"][0] != undefined) { - variant.image = variant.images[0].image.mini_url - } - return variantTemplate({ variant: variant, translations: Spree.translations }) -} - -$.fn.variantAutocomplete = function() { - this.parent().children(".options_placeholder").attr('id', this.parent().data('index')) - this.select2({ - placeholder: "Select a variant", - minimumInputLength: 4, - ajax: { - url: Spree.routes.variants_search, - datatype: 'json', - data: function(term, page) { - return { q: term } - }, - results: function (data, page) { - var variants = $.map(data['variants'], function(result) { - return result['variant'] - }) - return { results: variants } - } - }, - formatResult: formatVariantResult, - formatSelection: function (variant) { - $(this.element).parent().children('.options_placeholder').html(variant.options_text) - return variant.name; - } - }) -} diff --git a/core/app/assets/javascripts/spree.js.coffee.erb b/core/app/assets/javascripts/spree.js.coffee.erb new file mode 100644 index 00000000000..ce68770eb3f --- /dev/null +++ b/core/app/assets/javascripts/spree.js.coffee.erb @@ -0,0 +1,46 @@ +#= require jsuri +class window.Spree + @ready: (callback) -> + jQuery(document).ready(callback) + + @mountedAt: -> + "<%= Rails.application.routes.url_helpers.spree_path %>" + + @pathFor: (path) -> + locationOrigin = "#{window.location.protocol}//#{window.location.hostname}" + (if window.location.port then ":#{window.location.port}" else "") + "#{locationOrigin}#{@mountedAt()}#{path}" + + # Helper function to take a URL and add query parameters to it + # Uses the JSUri library from here: https://github.com/derek-watson/jsUri + # Thanks to Jake Moffat for the suggestion: https://twitter.com/jakeonrails/statuses/321776992221544449 + @url: (uri, query) -> + if uri.path == undefined + uri = new Uri(uri) + if query + $.each query, (key, value) -> + uri.addQueryParam(key, value) + return uri + + # This function automatically appends the API token + # for the user to the end of any URL. + # Immediately after, this string is then passed to jQuery.ajax. + # + # ajax works in two ways in jQuery: + # + # $.ajax("url", {settings: 'go here'}) + # or: + # $.ajax({url: "url", settings: 'go here'}) + # + # This function will support both of these calls. + @ajax: (url_or_settings, settings) -> + if (typeof(url_or_settings) == "string") + $.ajax(Spree.url(url_or_settings).toString(), settings) + else + url = url_or_settings['url'] + delete url_or_settings['url'] + $.ajax(Spree.url(url).toString(), url_or_settings) + + @routes: + states_search: @pathFor('api/states') + apply_coupon_code: (order_id) -> + Spree.pathFor("api/orders/#{order_id}/apply_coupon_code") diff --git a/core/app/assets/javascripts/store/cart.js.coffee b/core/app/assets/javascripts/store/cart.js.coffee deleted file mode 100644 index e220f7d362d..00000000000 --- a/core/app/assets/javascripts/store/cart.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -$ -> - if ($ 'form#update-cart').is('*') - ($ 'form#update-cart a.delete').show().one 'click', -> - ($ this).parents('.line-item').first().find('input.line_item_quantity').val 0 - ($ this).parents('form').first().submit() - false - - ($ 'form#update-cart').submit -> - ($ 'form#update-cart #update-button').attr('disabled', true) \ No newline at end of file diff --git a/core/app/assets/javascripts/store/checkout.js.coffee b/core/app/assets/javascripts/store/checkout.js.coffee deleted file mode 100644 index 29464065930..00000000000 --- a/core/app/assets/javascripts/store/checkout.js.coffee +++ /dev/null @@ -1,79 +0,0 @@ -@disableSaveOnClick = -> - ($ 'form.edit_order').submit -> - ($ this).find(':submit, :image').attr('disabled', true).removeClass('primary').addClass 'disabled' - -$ -> - if ($ '#checkout_form_address').is('*') - ($ '#checkout_form_address').validate() - - country_from_region = (region) -> - ($ 'p#' + region + 'country' + ' span#' + region + 'country-selection :only-child').val() - - get_states = (region) -> - state_mapper[country_from_region(region)] - - get_states_required = (region) -> - states_required_mapper[country_from_region(region)] - - update_state = (region) -> - states = get_states(region) - states_required = get_states_required(region) - - state_para = ($ 'p#' + region + 'state') - state_select = state_para.find('select') - state_input = state_para.find('input') - state_span_required = state_para.find('state-required') - if states - selected = parseInt state_select.val() - state_select.html '' - states_with_blank = [ [ '', '' ] ].concat(states) - $.each states_with_blank, (pos, id_nm) -> - opt = ($ document.createElement('option')).attr('value', id_nm[0]).html(id_nm[1]) - opt.prop 'selected', true if selected is id_nm[0] - state_select.append opt - - state_select.prop('disabled', false).show() - state_input.hide().prop 'disabled', true - state_span_required.show() - else - state_select.hide().prop 'disabled', true - state_input.show() - if states_required - state_span_required.show() - else - state_input.val '' - state_span_required.hide() - state_para.toggle(!!states_required) - state_input.prop('disabled', !states_required) - - ($ 'p#bcountry select').change -> - update_state 'b' - - ($ 'p#scountry select').change -> - update_state 's' - - update_state 'b' - update_state 's' - - ($ 'input#order_use_billing').click(-> - if ($ this).is(':checked') - ($ '#shipping .inner').hide() - ($ '#shipping .inner input, #shipping .inner select').prop 'disabled', true - else - ($ '#shipping .inner').show() - ($ '#shipping .inner input, #shipping .inner select').prop 'disabled', false - if get_states('s') - ($ 'span#sstate input').hide().prop 'disabled', true - else - ($ 'span#sstate select').hide().prop 'disabled', true - ).triggerHandler 'click' - - if ($ '#checkout_form_payment').is('*') - # Activate already checked payment method if form is re-rendered - # i.e. if user enters invalid data - ($ 'input[type="radio"]:checked').click() - - ($ 'input[type="radio"][name="order[payments_attributes][][payment_method_id]"]').click(-> - ($ '#payment-methods li').hide() - ($ '#payment_method_' + @value).show() if @checked - ) diff --git a/core/app/assets/javascripts/store/product.js.coffee b/core/app/assets/javascripts/store/product.js.coffee deleted file mode 100644 index ec55b95c262..00000000000 --- a/core/app/assets/javascripts/store/product.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -add_image_handlers = -> - thumbnails = ($ '#product-images ul.thumbnails') - ($ '#main-image').data 'selectedThumb', ($ '#main-image img').attr('src') - thumbnails.find('li').eq(0).addClass 'selected' - thumbnails.find('a').on 'click', (event) -> - ($ '#main-image').data 'selectedThumb', ($ event.currentTarget).attr('href') - ($ '#main-image').data 'selectedThumbId', ($ event.currentTarget).parent().attr('id') - ($ this).mouseout -> - thumbnails.find('li').removeClass 'selected' - ($ event.currentTarget).parent('li').addClass 'selected' - false - - thumbnails.find('li').on 'mouseenter', (event) -> - ($ '#main-image img').attr 'src', ($ event.currentTarget).find('a').attr('href') - - thumbnails.find('li').on 'mouseleave', (event) -> - ($ '#main-image img').attr 'src', ($ '#main-image').data('selectedThumb') - -show_variant_images = (variant_id) -> - ($ 'li.vtmb').hide() - ($ 'li.vtmb-' + variant_id).show() - currentThumb = ($ '#' + ($ '#main-image').data('selectedThumbId')) - if not currentThumb.hasClass('vtmb-' + variant_id) - thumb = ($ ($ 'ul.thumbnails li:visible.vtmb').eq(0)) - thumb = ($ ($ 'ul.thumbnails li:visible').eq(0)) unless thumb.length > 0 - newImg = thumb.find('a').attr('href') - ($ 'ul.thumbnails li').removeClass 'selected' - thumb.addClass 'selected' - ($ '#main-image img').attr 'src', newImg - ($ '#main-image').data 'selectedThumb', newImg - ($ '#main-image').data 'selectedThumbId', thumb.attr('id') - -update_variant_price = (variant) -> - variant_price = variant.data('price') - ($ '.price.selling').text(variant_price) if variant_price - -$ -> - add_image_handlers() - show_variant_images ($ '#product-variants input[type="radio"]').eq(0).attr('value') if ($ '#product-variants input[type="radio"]').length > 0 - ($ '#product-variants input[type="radio"]').click (event) -> - show_variant_images @value - update_variant_price ($ this) diff --git a/core/app/assets/javascripts/store/spree_core.js b/core/app/assets/javascripts/store/spree_core.js deleted file mode 100644 index 72cb9b1ebd2..00000000000 --- a/core/app/assets/javascripts/store/spree_core.js +++ /dev/null @@ -1,4 +0,0 @@ -//= require jquery.validate/jquery.validate.min -//= require store/checkout -//= require store/product -//= require store/cart diff --git a/core/app/assets/stylesheets/admin/components/_navigation.scss b/core/app/assets/stylesheets/admin/components/_navigation.scss deleted file mode 100644 index 79969884bd6..00000000000 --- a/core/app/assets/stylesheets/admin/components/_navigation.scss +++ /dev/null @@ -1,150 +0,0 @@ -// Navigation -//--------------------------------------------------- -.inline-menu { - margin: 0; - -webkit-margin-before: 0; - -webkit-padding-start: 0; -} - -nav.menu { - ul { - list-style: none; - - li { - a { - padding: 10px 0; - display: block; - position: relative; - text-align: left; - border: 1px solid transparent; - text-transform: uppercase; - font-weight: 600; - font-size: 90%; - } - - &.active a { - color: $color-2; - border-left-width: 0; - border-bottom-color: $color-2; - } - - &:hover a { - color: $color-2; - } - } - } -} - -[data-hook="admin_login_navigation_bar"] { - ul { - text-align: right; - - li { - padding: 5px 0 5px 10px; - text-align: right; - font-size: 90%; - color: $color-link; - margin-top: 8px; - - &[data-hook="user-logged-in-as"] { - width: 50%; - color: $color-body-text; - } - - &:hover { - i { - color: $color-2; - } - } - } - } -} - -#admin-menu { - background-color: $color-3; - - li { - min-width: 90px; - - a { - display: block; - padding: 25px 20px; - color: $color-1 !important; - text-transform: uppercase; - position: relative; - text-align: center; - - i { - display: inline; - } - - &:hover { - background-color: $color-2; - - &:after { - content: ''; - position: absolute; - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 5px solid $color-2; - bottom: 0px; - margin-bottom: -5px; - left: 50%; - margin-left: -10px; - z-index: 1; - } - } - } - - .dropdown { - width: 300px; - background-color: $color-3; - width: 200px; - z-index: 100000; - - > li { - width: 200px !important; - - a:after { - display: none; - } - } - } - - &.selected a { - @extend a:hover; - } - } -} - -#sub-menu { - background-color: $color-2; - padding-bottom: 0; - - li { - a { - display: block; - padding: 12px 20px; - color: $color-1; - text-align: center; - text-transform: uppercase; - position: relative; - font-size: 85%; - } - - &.selected a, a:hover { - &:after { - content: ''; - position: absolute; - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 5px solid $color-2; - bottom: 0px; - margin-bottom: -5px; - left: 50%; - margin-left: -10px; - z-index: 1; - } - } - } -} \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/components/_sidebar.scss b/core/app/assets/stylesheets/admin/components/_sidebar.scss deleted file mode 100644 index ef0375a9610..00000000000 --- a/core/app/assets/stylesheets/admin/components/_sidebar.scss +++ /dev/null @@ -1,23 +0,0 @@ -// Sidebar -//--------------------------------------------------- -#sidebar { - overflow: visible; - border-top: 1px solid $color-border; - margin-top: 17px; - - .sidebar-title { - color: $color-2; - text-transform: uppercase; - text-align: center; - font-size: 14px; - font-weight: 600; - - > span { - display: inline; - background: #fff; - padding: 5px 10px; - position: relative; - top: -14px; - } - } -} diff --git a/core/app/assets/stylesheets/admin/components/_states.scss b/core/app/assets/stylesheets/admin/components/_states.scss deleted file mode 100644 index bc1fefabbcc..00000000000 --- a/core/app/assets/stylesheets/admin/components/_states.scss +++ /dev/null @@ -1,30 +0,0 @@ -.state { - @include border-radius($border-radius); - text-transform: uppercase; - font-size: 80%; - font-weight: 600; - border: 1px solid transparent; - padding: 2px 5px; - width: 100%; - display: inline-block; - text-align: center; - - @each $state in $states { - &.#{$state} { - background-color: get-value($states, $states-bg-colors, $state); - - &, a { - color: get-value($states, $states-text-colors, $state); - } - } - } -} - -table tbody tr { - &[class*="state"] td:first-child { - border-left-width: 3px; - } - &.state-complete td:first-child { border-left-color: $color-success } - &.state-cart td:first-child { border-left-color: very-light($color-notice, 6) } - &.state-canceled td:first-child { border-left-color: $color-error } -} \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/globals/_variables.scss b/core/app/assets/stylesheets/admin/globals/_variables.scss deleted file mode 100644 index c53767fd6a9..00000000000 --- a/core/app/assets/stylesheets/admin/globals/_variables.scss +++ /dev/null @@ -1,134 +0,0 @@ -// ------------------------------------------------------------- -// Variables used in all other files -//-------------------------------------------------------------- - -// Fonts -//-------------------------------------------------------------- -$base-font-family: "Open Sans", "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; - -// Colors -//-------------------------------------------------------------- - -// Basic color palette for admin -$color-1: #FFFFFF !default; // White -$color-2: #9FC820 !default; // Green -$color-3: #5FA5E8 !default; // Light Blue -$color-4: #6788A2 !default; // Dark Blue -$color-5: #C60F13 !default; // Red -$color-6: #FF9300 !default; // Yellow - -// Body base colors -$color-body-bg: $color-1 !default; -$color-body-text: $color-4 !default; -$color-headers: $color-4 !default; -$color-link: $color-3 !default; -$color-link-hover: $color-2 !default; -$color-link-active: $color-2 !default; -$color-link-focus: $color-2 !default; -$color-link-visited: $color-3 !default; -$color-border: very-light($color-3, 8) !default; - -// Basic flash colors -$color-success: $color-2 !default; -$color-notice: $color-6 !default; -$color-error: $color-5 !default; - -// Table colors -$color-tbl-odd: $color-1 !default; -$color-tbl-even: very-light($color-3) !default; -$color-tbl-thead: very-light($color-3) !default; - -// Button colors -$color-btn-bg: $color-3 !default; -$color-btn-text: $color-1 !default; -$color-btn-hover-bg: $color-2 !default; -$color-btn-hover-text: $color-1 !default; - -// Actions colors -$color-action-edit-bg: very-light($color-success, 5 ) !default; -$color-action-edit-brd: very-light($color-success, 20 ) !default; -$color-action-clone-bg: very-light($color-notice, 5 ) !default; -$color-action-clone-brd: very-light($color-notice, 15 ) !default; -$color-action-remove-bg: very-light($color-error, 5 ) !default; -$color-action-remove-brd: very-light($color-error, 10 ) !default; -$color-action-void-bg: very-light($color-error, 10 ) !default; -$color-action-void-brd: very-light($color-error, 20 ) !default; -$color-action-capture-bg: very-light($color-success, 5 ) !default; -$color-action-capture-brd: very-light($color-success, 20 ) !default; -$color-action-mail-bg: very-light($color-success, 5 ) !default; -$color-action-mail-brd: very-light($color-success, 20 ) !default; - -// Select2 select field colors -$color-sel-bg: $color-3 !default; -$color-sel-text: $color-1 !default; -$color-sel-hover-bg: $color-2 !default; -$color-sel-hover-text: $color-1 !default; - -// Text inputs colors -$color-txt-brd: $color-border !default; -$color-txt-text: $color-4 !default; -$color-txt-hover-brd: very-light($color-2, 20) !default; - -// States label colors -$color-ste-complete-bg: $color-success !default; -$color-ste-complete-text: $color-1 !default; -$color-ste-completed-bg: $color-success !default; -$color-ste-completed-text: $color-1 !default; -$color-ste-sold-bg: $color-success !default; -$color-ste-sold-text: $color-1 !default; -$color-ste-pending-bg: $color-notice !default; -$color-ste-pending-text: $color-1 !default; -$color-ste-paid-bg: $color-success !default; -$color-ste-paid-text: $color-1 !default; -$color-ste-shipped-bg: $color-success !default; -$color-ste-shipped-text: $color-1 !default; -$color-ste-balance_due-bg: $color-notice !default; -$color-ste-balance_due-text: $color-1 !default; -$color-ste-backorder-bg: $color-notice !default; -$color-ste-backorder-text: $color-1 !default; -$color-ste-none-bg: $color-error !default; -$color-ste-none-text: $color-1 !default; -$color-ste-ready-bg: $color-success !default; -$color-ste-ready-text: $color-1 !default; -$color-ste-void-bg: $color-error !default; -$color-ste-void-text: $color-1 !default; -$color-ste-canceled-bg: $color-error !default; -$color-ste-canceled-text: $color-1 !default; -$color-ste-address-bg: $color-error !default; -$color-ste-address-text: $color-1 !default; -$color-ste-checkout-bg: $color-notice !default; -$color-ste-checkout-text: $color-1 !default; -$color-ste-cart-bg: $color-notice !default; -$color-ste-cart-text: $color-1 !default; -$color-ste-payment-bg: $color-error !default; -$color-ste-payment-text: $color-1 !default; -$color-ste-delivery-bg: $color-success !default; -$color-ste-delivery-text: $color-1 !default; -$color-ste-confirm-bg: $color-error !default; -$color-ste-confirm-text: $color-1 !default; - -// Avaliable states -$states: completed, complete, sold, pending, paid, shipped, balance_due, backorder, checkout, cart, address, delivery, payment, confirm, canceled, ready, void !default; -$states-bg-colors: $color-ste-completed-bg, $color-ste-complete-bg, $color-ste-sold-bg, $color-ste-pending-bg, $color-ste-paid-bg, $color-ste-shipped-bg, $color-ste-balance_due-bg, $color-ste-backorder-bg, $color-ste-checkout-bg, $color-ste-cart-bg, $color-ste-address-bg, $color-ste-delivery-bg, $color-ste-payment-bg, $color-ste-confirm-bg, $color-ste-canceled-bg, $color-ste-ready-bg, $color-ste-void-bg !default; -$states-text-colors: $color-ste-completed-text, $color-ste-complete-text, $color-ste-sold-text, $color-ste-pending-text, $color-ste-paid-text, $color-ste-shipped-text, $color-ste-balance_due-text, $color-ste-backorder-text, $color-ste-checkout-text, $color-ste-cart-text, $color-ste-address-text, $color-ste-delivery-text, $color-ste-payment-text, $color-ste-confirm-text, $color-ste-canceled-text, $color-ste-ready-text, $color-ste-void-text !default; - -// Avaliable actions -$actions: edit, clone, remove, void, capture, mail !default; -$actions-bg-colors: $color-action-edit-bg, $color-action-clone-bg, $color-action-remove-bg, $color-action-void-bg, $color-action-capture-bg, $color-action-mail-bg !default; -$actions-brd-colors: $color-action-edit-brd, $color-action-clone-brd, $color-action-remove-brd, $color-action-void-brd, $color-action-capture-brd, $color-action-mail-brd !default; - -// Sizes -//-------------------------------------------------------------- -$body-font-size: 13px !default; - -$h6-size: $body-font-size + 2 !default; -$h5-size: $h6-size + 2 !default; -$h4-size: $h5-size + 2 !default; -$h3-size: $h4-size + 2 !default; -$h2-size: $h3-size + 2 !default; -$h1-size: $h2-size + 2 !default; - -$border-radius: 3px !default; - -$font-weight-bold: 600 !default; -$font-weight-normal: 400 !default; \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/globals/_variables_override.scss b/core/app/assets/stylesheets/admin/globals/_variables_override.scss deleted file mode 100644 index 821b6e55c85..00000000000 --- a/core/app/assets/stylesheets/admin/globals/_variables_override.scss +++ /dev/null @@ -1,7 +0,0 @@ -/*--------------------------------------------------------- - Empty file to override variables in user applications. - - To set your own colors, sizes or fonts just override this - file in you're application and set valiables according to - globals/_variables.scss file. - --------------------------------------------------------- */ \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/plugins/_select2.scss b/core/app/assets/stylesheets/admin/plugins/_select2.scss deleted file mode 100644 index 6bb14e7e7d3..00000000000 --- a/core/app/assets/stylesheets/admin/plugins/_select2.scss +++ /dev/null @@ -1,176 +0,0 @@ -.select2-container { - &:hover .select2-choice, &.select2-container-active .select2-choice { - background-color: $color-sel-hover-bg !important; - border-color: $color-sel-hover-bg !important; - } - .select2-choice { - background-image: none !important; - background-color: $color-sel-bg; - border: none !important; - box-shadow: none !important; - @include border-radius($border-radius); - color: $color-1 !important; - font-size: 90%; - height: 31px; - line-height: inherit !important; - padding: 5px 15px 7px; - - span { - display: block; - padding: 2px; - } - - .select2-search-choice-close { - background-image: none !important; - font-size: 100% !important; - @extend .icon-remove; - @extend [class^="icon-"]:before; - margin-top: 2px; - } - - div { - background-image: none !important; - background-color: transparent !important; - border-radius: 0 !important; - border: none !important; - - b { - background-image: none !important; - display: inherit !important; - width: auto !important; - height: auto !important; - margin-top: 10px; - @extend .icon-caret-down; - @extend [class^="icon-"]:before; - } - } - } - - &.select2-container-active { - .select2-choice { - box-shadow: none !important; - } - - &.select2-dropdown-open .select2-choice div b { - @extend .icon-caret-up - } - } -} - -.select2-drop { - border-color: $color-sel-hover-bg; - box-shadow: none !important; - z-index: 1000000; - - &.select2-drop-above { - border-color: $color-sel-hover-bg; - } -} - -.select2-search { - @extend [class^="icon-"]:before; - @extend .icon-search; - font-size: 100%; - padding-left: 15px; - color: darken($color-border, 15); - - input { - @extend input[type="text"]; - margin-top: 5px; - margin-left: -23px; - padding-left: 25px; - padding-top: 6px; - padding-bottom: 6px; - font-family: $base-font-family; - font-size: 90%; - box-shadow: none; - background-image: none; - } -} - -.select2-results { - padding-left: 0 !important; - - li { - font-size: 85% !important; - - &.select2-highlighted { - .select2-result-label { - &, h6 { - color: $color-1 !important; - } - } - } - - .select2-result-label { - color: $color-body-text; - min-height: 22px; - clear: both; - overflow: auto; - } - - &.select2-no-results, &.select2-searching { - padding: 5px; - background-color: transparent; - color: $color-body-text; - text-align: center; - font-weight: $font-weight-bold; - text-transform: uppercase; - } - } - - .select2-highlighted { - background-color: $color-sel-bg; - } -} - -.select2-container-multi { - &.select2-container-active, &.select2-dropdown-open { - .select2-choices { - border-color: $color-sel-hover-bg !important; - box-shadow: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - } - .select2-choices { - @extend input[type="text"]; - padding: 6px 3px 3px 3px; - box-shadow: none; - background-image: none; - - .select2-search-choice { - @include border-radius($border-radius); - margin: 0 0 3px 3px; - background-image: none; - background-color: $color-sel-bg; - border: none; - box-shadow: none; - color: $color-1 !important; - font-size: 85%; - - &:hover { - background-color: $color-sel-hover-bg; - } - - .select2-search-choice-close { - background-image: none !important; - font-size: 85% !important; - @extend .icon-remove; - @extend [class^="icon-"]:before; - margin-left: 2px; - color: $color-1; - } - } - } -} - -label .select2-container { - margin-top: -6px; - .select2-choice { - span { - text-transform: none; - font-weight: normal; - } - } -} \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/plugins/font-awesome.scss b/core/app/assets/stylesheets/admin/plugins/font-awesome.scss deleted file mode 100755 index c642925b0fe..00000000000 --- a/core/app/assets/stylesheets/admin/plugins/font-awesome.scss +++ /dev/null @@ -1,303 +0,0 @@ -/* Font Awesome - the iconic font designed for use with Twitter Bootstrap - ------------------------------------------------------- - The full suite of pictographic icons, examples, and documentation - can be found at: http://fortawesome.github.com/Font-Awesome/ - - License - ------------------------------------------------------- - The Font Awesome webfont, CSS, and LESS files are licensed under CC BY 3.0: - http://creativecommons.org/licenses/by/3.0/ A mention of - 'Font Awesome - http://fortawesome.github.com/Font-Awesome' in human-readable - source code is considered acceptable attribution (most common on the web). - If human readable source code is not available to the end user, a mention in - an 'About' or 'Credits' screen is considered acceptable (most common in desktop - or mobile software). - - Contact - ------------------------------------------------------- - Email: dave@davegandy.com - Twitter: http://twitter.com/fortaweso_me - Work: http://lemonwi.se co-founder - - */ -@font-face { - font-family: "FontAwesome"; - src: url('/assets/fontawesome-webfont.eot'); - src: url('/assets/fontawesome-webfont.eot?#iefix') format('eot'), url('/assets/fontawesome-webfont.woff') format('woff'), url('/assets/fontawesome-webfont.ttf') format('truetype'), url('/assets/fontawesome-webfont.svg#FontAwesome') format('svg'); - font-weight: normal; - font-style: normal; -} - -/* Font Awesome styles - ------------------------------------------------------- */ -[class^="icon-"]:before, [class*=" icon-"]:before { - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - display: inline-block; - text-decoration: inherit; -} -a [class^="icon-"], a [class*=" icon-"] { - display: inline-block; - text-decoration: inherit; -} -/* makes the font 33% larger relative to the icon container */ -.icon-large:before { - vertical-align: top; - font-size: 1.3333333333333333em; -} -.btn [class^="icon-"], .btn [class*=" icon-"] { - /* keeps button heights with and without icons the same */ - - line-height: .9em; -} -li [class^="icon-"], li [class*=" icon-"] { - display: inline-block; - width: 1.25em; - text-align: center; -} -li .icon-large[class^="icon-"], li .icon-large[class*=" icon-"] { - /* 1.5 increased font size for icon-large * 1.25 width */ - - width: 1.875em; -} -li[class^="icon-"], li[class*=" icon-"] { - margin-left: 0; - list-style-type: none; -} -li[class^="icon-"]:before, li[class*=" icon-"]:before { - text-indent: -2em; - text-align: center; -} -li[class^="icon-"].icon-large:before, li[class*=" icon-"].icon-large:before { - text-indent: -1.3333333333333333em; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.icon-glass:before { content: "\f000"; } -.icon-music:before { content: "\f001"; } -.icon-search:before { content: "\f002"; } -.icon-envelope:before { content: "\f003"; } -.icon-heart:before { content: "\f004"; } -.icon-star:before { content: "\f005"; } -.icon-star-empty:before { content: "\f006"; } -.icon-user:before { content: "\f007"; } -.icon-film:before { content: "\f008"; } -.icon-th-large:before { content: "\f009"; } -.icon-th:before { content: "\f00a"; } -.icon-th-list:before { content: "\f00b"; } -.icon-ok:before { content: "\f00c"; } -.icon-remove:before { content: "\f00d"; } -.icon-zoom-in:before { content: "\f00e"; } - -.icon-zoom-out:before { content: "\f010"; } -.icon-off:before { content: "\f011"; } -.icon-signal:before { content: "\f012"; } -.icon-cog:before { content: "\f013"; } -.icon-trash:before { content: "\f014"; } -.icon-home:before { content: "\f015"; } -.icon-file:before { content: "\f016"; } -.icon-time:before { content: "\f017"; } -.icon-road:before { content: "\f018"; } -.icon-download-alt:before { content: "\f019"; } -.icon-download:before { content: "\f01a"; } -.icon-upload:before { content: "\f01b"; } -.icon-inbox:before { content: "\f01c"; } -.icon-play-circle:before { content: "\f01d"; } -.icon-repeat:before { content: "\f01e"; } - -/* \f020 doesn't work in Safari. all shifted one down */ -.icon-refresh:before { content: "\f021"; } -.icon-list-alt:before { content: "\f022"; } -.icon-lock:before { content: "\f023"; } -.icon-flag:before { content: "\f024"; } -.icon-headphones:before { content: "\f025"; } -.icon-volume-off:before { content: "\f026"; } -.icon-volume-down:before { content: "\f027"; } -.icon-volume-up:before { content: "\f028"; } -.icon-qrcode:before { content: "\f029"; } -.icon-barcode:before { content: "\f02a"; } -.icon-tag:before { content: "\f02b"; } -.icon-tags:before { content: "\f02c"; } -.icon-book:before { content: "\f02d"; } -.icon-bookmark:before { content: "\f02e"; } -.icon-print:before { content: "\f02f"; } - -.icon-camera:before { content: "\f030"; } -.icon-font:before { content: "\f031"; } -.icon-bold:before { content: "\f032"; } -.icon-italic:before { content: "\f033"; } -.icon-text-height:before { content: "\f034"; } -.icon-text-width:before { content: "\f035"; } -.icon-align-left:before { content: "\f036"; } -.icon-align-center:before { content: "\f037"; } -.icon-align-right:before { content: "\f038"; } -.icon-align-justify:before { content: "\f039"; } -.icon-list:before { content: "\f03a"; } -.icon-indent-left:before { content: "\f03b"; } -.icon-indent-right:before { content: "\f03c"; } -.icon-facetime-video:before { content: "\f03d"; } -.icon-picture:before { content: "\f03e"; } - -.icon-pencil:before { content: "\f040"; } -.icon-map-marker:before { content: "\f041"; } -.icon-adjust:before { content: "\f042"; } -.icon-tint:before { content: "\f043"; } -.icon-edit:before { content: "\f044"; } -.icon-share:before { content: "\f045"; } -.icon-check:before { content: "\f046"; } -.icon-move:before { content: "\f047"; } -.icon-step-backward:before { content: "\f048"; } -.icon-fast-backward:before { content: "\f049"; } -.icon-backward:before { content: "\f04a"; } -.icon-play:before { content: "\f04b"; } -.icon-pause:before { content: "\f04c"; } -.icon-stop:before { content: "\f04d"; } -.icon-forward:before { content: "\f04e"; } - -.icon-fast-forward:before { content: "\f050"; } -.icon-step-forward:before { content: "\f051"; } -.icon-eject:before { content: "\f052"; } -.icon-chevron-left:before { content: "\f053"; } -.icon-chevron-right:before { content: "\f054"; } -.icon-plus-sign:before { content: "\f055"; } -.icon-minus-sign:before { content: "\f056"; } -.icon-remove-sign:before { content: "\f057"; } -.icon-ok-sign:before { content: "\f058"; } -.icon-question-sign:before { content: "\f059"; } -.icon-info-sign:before { content: "\f05a"; } -.icon-screenshot:before { content: "\f05b"; } -.icon-remove-circle:before { content: "\f05c"; } -.icon-ok-circle:before { content: "\f05d"; } -.icon-ban-circle:before { content: "\f05e"; } - -.icon-arrow-left:before { content: "\f060"; } -.icon-arrow-right:before { content: "\f061"; } -.icon-arrow-up:before { content: "\f062"; } -.icon-arrow-down:before { content: "\f063"; } -.icon-share-alt:before { content: "\f064"; } -.icon-resize-full:before { content: "\f065"; } -.icon-resize-small:before { content: "\f066"; } -.icon-plus:before { content: "\f067"; } -.icon-minus:before { content: "\f068"; } -.icon-asterisk:before { content: "\f069"; } -.icon-exclamation-sign:before { content: "\f06a"; } -.icon-gift:before { content: "\f06b"; } -.icon-leaf:before { content: "\f06c"; } -.icon-fire:before { content: "\f06d"; } -.icon-eye-open:before { content: "\f06e"; } - -.icon-eye-close:before { content: "\f070"; } -.icon-warning-sign:before { content: "\f071"; } -.icon-plane:before { content: "\f072"; } -.icon-calendar:before { content: "\f073"; } -.icon-random:before { content: "\f074"; } -.icon-comment:before { content: "\f075"; } -.icon-magnet:before { content: "\f076"; } -.icon-chevron-up:before { content: "\f077"; } -.icon-chevron-down:before { content: "\f078"; } -.icon-retweet:before { content: "\f079"; } -.icon-shopping-cart:before { content: "\f07a"; } -.icon-folder-close:before { content: "\f07b"; } -.icon-folder-open:before { content: "\f07c"; } -.icon-resize-vertical:before { content: "\f07d"; } -.icon-resize-horizontal:before { content: "\f07e"; } - -.icon-bar-chart:before { content: "\f080"; } -.icon-twitter-sign:before { content: "\f081"; } -.icon-facebook-sign:before { content: "\f082"; } -.icon-camera-retro:before { content: "\f083"; } -.icon-key:before { content: "\f084"; } -.icon-cogs:before { content: "\f085"; } -.icon-comments:before { content: "\f086"; } -.icon-thumbs-up:before { content: "\f087"; } -.icon-thumbs-down:before { content: "\f088"; } -.icon-star-half:before { content: "\f089"; } -.icon-heart-empty:before { content: "\f08a"; } -.icon-signout:before { content: "\f08b"; } -.icon-linkedin-sign:before { content: "\f08c"; } -.icon-pushpin:before { content: "\f08d"; } -.icon-external-link:before { content: "\f08e"; } - -.icon-signin:before { content: "\f090"; } -.icon-trophy:before { content: "\f091"; } -.icon-github-sign:before { content: "\f092"; } -.icon-upload-alt:before { content: "\f093"; } -.icon-lemon:before { content: "\f094"; } -.icon-phone:before { content: "\f095"; } -.icon-check-empty:before { content: "\f096"; } -.icon-bookmark-empty:before { content: "\f097"; } -.icon-phone-sign:before { content: "\f098"; } -.icon-twitter:before { content: "\f099"; } -.icon-facebook:before { content: "\f09a"; } -.icon-github:before { content: "\f09b"; } -.icon-unlock:before { content: "\f09c"; } -.icon-credit-card:before { content: "\f09d"; } -.icon-rss:before { content: "\f09e"; } - -.icon-hdd:before { content: "\f0a0"; } -.icon-bullhorn:before { content: "\f0a1"; } -.icon-bell:before { content: "\f0a2"; } -.icon-certificate:before { content: "\f0a3"; } -.icon-hand-right:before { content: "\f0a4"; } -.icon-hand-left:before { content: "\f0a5"; } -.icon-hand-up:before { content: "\f0a6"; } -.icon-hand-down:before { content: "\f0a7"; } -.icon-circle-arrow-left:before { content: "\f0a8"; } -.icon-circle-arrow-right:before { content: "\f0a9"; } -.icon-circle-arrow-up:before { content: "\f0aa"; } -.icon-circle-arrow-down:before { content: "\f0ab"; } -.icon-globe:before { content: "\f0ac"; } -.icon-wrench:before { content: "\f0ad"; } -.icon-tasks:before { content: "\f0ae"; } - -.icon-filter:before { content: "\f0b0"; } -.icon-briefcase:before { content: "\f0b1"; } -.icon-fullscreen:before { content: "\f0b2"; } - -.icon-group:before { content: "\f0c0"; } -.icon-link:before { content: "\f0c1"; } -.icon-cloud:before { content: "\f0c2"; } -.icon-beaker:before { content: "\f0c3"; } -.icon-cut:before { content: "\f0c4"; } -.icon-copy:before { content: "\f0c5"; } -.icon-paper-clip:before { content: "\f0c6"; } -.icon-save:before { content: "\f0c7"; } -.icon-sign-blank:before { content: "\f0c8"; } -.icon-reorder:before { content: "\f0c9"; } -.icon-list-ul:before { content: "\f0ca"; } -.icon-list-ol:before { content: "\f0cb"; } -.icon-strikethrough:before { content: "\f0cc"; } -.icon-underline:before { content: "\f0cd"; } -.icon-table:before { content: "\f0ce"; } - -.icon-magic:before { content: "\f0d0"; } -.icon-truck:before { content: "\f0d1"; } -.icon-pinterest:before { content: "\f0d2"; } -.icon-pinterest-sign:before { content: "\f0d3"; } -.icon-google-plus-sign:before { content: "\f0d4"; } -.icon-google-plus:before { content: "\f0d5"; } -.icon-money:before { content: "\f0d6"; } -.icon-caret-down:before { content: "\f0d7"; } -.icon-caret-up:before { content: "\f0d8"; } -.icon-caret-left:before { content: "\f0d9"; } -.icon-caret-right:before { content: "\f0da"; } -.icon-columns:before { content: "\f0db"; } -.icon-sort:before { content: "\f0dc"; } -.icon-sort-down:before { content: "\f0dd"; } -.icon-sort-up:before { content: "\f0de"; } - -.icon-envelope-alt:before { content: "\f0e0"; } -.icon-linkedin:before { content: "\f0e1"; } -.icon-undo:before { content: "\f0e2"; } -.icon-legal:before { content: "\f0e3"; } -.icon-dashboard:before { content: "\f0e4"; } -.icon-comment-alt:before { content: "\f0e5"; } -.icon-comments-alt:before { content: "\f0e6"; } -.icon-bolt:before { content: "\f0e7"; } -.icon-sitemap:before { content: "\f0e8"; } -.icon-umbrella:before { content: "\f0e9"; } -.icon-paste:before { content: "\f0ea"; } - -.icon-user-md:before { content: "\f200"; } diff --git a/core/app/assets/stylesheets/admin/sections/_orders.scss b/core/app/assets/stylesheets/admin/sections/_orders.scss deleted file mode 100644 index 4187aa8138a..00000000000 --- a/core/app/assets/stylesheets/admin/sections/_orders.scss +++ /dev/null @@ -1,38 +0,0 @@ -// Customize orders filter -[data-hook="admin_orders_index_search"] { - select[data-placeholder="Status"] { - width: 100%; - } - - .select2-container { - width: 100% !important; - } - - // .field.checkbox { - // margin-top: 35px; - - // &:first-child { - // margin-bottom: 60px !important; - // } - // } -} - -// Customize orduct add fieldset -#add-line-item { - fieldset { - padding: 10px 0; - - .field { - margin-bottom: 0; - - input[type="text"], input[type="number"] { - width: 100%; - } - } - .actions { - .button { - margin-top: 28px; - } - } - } -} \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/sections/_products.scss b/core/app/assets/stylesheets/admin/sections/_products.scss deleted file mode 100644 index 3faccd84de3..00000000000 --- a/core/app/assets/stylesheets/admin/sections/_products.scss +++ /dev/null @@ -1,26 +0,0 @@ -[data-hook="admin_products_sidebar"] { - // .field.checkbox:first-child { - // margin-top: 36px; - // } - .actions { - padding: 0 !important; - } -} - -[data-hook="admin_product_form_fields"] { - label { - display: inline-block; - } - input, select, textarea, .select2-container { - width: 100%; - } -} - -.outstanding-balance { - margin-bottom: 15px; - text-transform: uppercase; - - strong { - color: $color-2; - } -} \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/shared/_forms.scss b/core/app/assets/stylesheets/admin/shared/_forms.scss deleted file mode 100644 index ec6a0850e6c..00000000000 --- a/core/app/assets/stylesheets/admin/shared/_forms.scss +++ /dev/null @@ -1,250 +0,0 @@ -input[type="text"], -input[type="password"], -input[type="email"], -input[type="date"], -input[type="datetime"], -input[type="time"], -input[type="url"], -input[type="number"], -input[type="tel"], -textarea, fieldset { - @include border-radius($border-radius); - padding: 7px 10px; - border: 1px solid $color-txt-brd; - color: $color-body-text; - font-size: 90%; - - &:focus { - outline: none; - border-color: $color-txt-hover-brd; - } - - &[disabled] { - opacity: 0.7; - } -} - -textarea { - line-height: 19px; -} - -.fullwidth { - width: 100%; -} - -label { - font-weight: 600; - text-transform: uppercase; - font-size: 85%; - display: inline; - margin-bottom: 5px; - color: $color-4; - - &.inline { - display: inline-block !important; - } -} - -.label-block label { display: block } - -input[type="submit"], -input[type="button"], -button, .button { - @include border-radius($border-radius); - display: inline-block; - padding: 8px 15px; - border: none; - background-color: $color-btn-bg; - color: $color-btn-text; - text-transform: uppercase; - font-size: 85%; - font-weight: 600; - - &:visited, &:active, &:focus { color: $color-btn-text } - - &:hover { - background-color: $color-btn-hover-bg; - color: $color-btn-hover-text; - } - - &:active:focus { - box-shadow: 0 0 8px 0 darken($color-btn-hover-bg, 5) inset; - } - - &.fullwidth { - width: 100%; - text-align: center; - } -} - -span.info { - font-style: italic; - font-size: 85%; - color: lighten($color-body-text, 15); - display: block; - line-height: 20px; - margin: 5px 0; -} - -.field { - padding: 10px 0; - - &.checkbox { - min-height: 73px; - - input[type="checkbox"] { - display: inline-block; - width: auto; - } - - label { - cursor: pointer; - display: block; - } - } - - ul { - border-top: 1px solid $color-border; - list-style: none; - padding-top: 5px; - - li { - display: inline-block; - padding-right: 10px; - - - label { - font-weight: normal; - text-transform: none; - } - } - } - - &.withError { - .field_with_errors { - label { - color: very-light($color-error, 30); - } - - input { - border-color: very-light($color-error, 15); - } - } - .formError { - color: very-light($color-error, 30); - font-style: italic; - font-size: 85%; - } - } -} - -fieldset { - box-shadow: none; - box-sizing: border-box; - border-color: $color-border; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - margin-left: 0; - margin-right: 0; - position: relative; - margin-bottom: 35px; - padding: 10px 0 15px 0; - background-color: transparent; - border-left: none; - border-right: none; - border-radius: 0; - - &.no-border-bottom { - border-bottom: none; - margin-bottom: 0; - } - - &.no-border-top { - border-top: none; - padding-top: 0; - } - - legend { - background-color: $color-1; - color: $color-2; - font-size: 14px; - font-weight: 600; - text-transform: uppercase; - text-align: center; - padding: 8px 15px; - } - - label { - color: lighten($color-body-text, 15); - } - - .filter-actions { - margin-bottom: -32px; - margin-top: 15px; - text-align: center; - - form { - display: inline-block; - } - - button, .button, input[type="submit"], input[type="button"], span.or { - @include border-radius($border-radius); - - -webkit-box-shadow: 0 0 0 15px $color-1; - -moz-box-shadow: 0 0 0 15px $color-1; - -ms-box-shadow: 0 0 0 15px $color-1; - -o-box-shadow: 0 0 0 15px $color-1; - box-shadow: 0 0 0 15px $color-1; - - &:hover { - border-color: $color-1; - } - } - - span.or { - background-color: $color-1; - border-width: 5px; - margin-left: 5px; - margin-right: 5px; - position: relative; - - -webkit-box-shadow: 0 0 0 5px $color-1; - -moz-box-shadow: 0 0 0 5px $color-1; - -ms-box-shadow: 0 0 0 5px $color-1; - -o-box-shadow: 0 0 0 5px $color-1; - box-shadow: 0 0 0 5px $color-1; - } - } - - &.labels-inline { - .field { - margin-bottom: 0; - display: table; - width: 100%; - - label, input { - display: table-cell !important; - } - input { - width: 100%; - } - - &.checkbox { - input { - width: auto !important - } - } - } - .actions { - padding: 0; - text-align: right; - } - } -} - -.form-actions { - margin-top: 18px; -} -.form-buttons { - text-align: center; -} diff --git a/core/app/assets/stylesheets/admin/shared/_icons.scss b/core/app/assets/stylesheets/admin/shared/_icons.scss deleted file mode 100644 index 87a55c69b52..00000000000 --- a/core/app/assets/stylesheets/admin/shared/_icons.scss +++ /dev/null @@ -1,21 +0,0 @@ -// Some fixes for fontwesome stylesheets -[class^="icon-"], [class*=" icon-"] { - &.button, &.icon_link { - width: auto; - - &:before { - padding-right: 5px; - } - } -} - -[class^="icon-"]:before, [class*=" icon-"]:before { - -webkit-font-smoothing: antialiased; -} -.icon-email:before { @extend .icon-envelope:before } -.icon-resume:before { @extend .icon-refresh:before } - -.icon-cancel:before, -.icon-void:before { @extend .icon-remove:before } - -.icon-capture:before { @extend .icon-ok:before } \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/shared/_layout.scss b/core/app/assets/stylesheets/admin/shared/_layout.scss deleted file mode 100644 index b111eb9675e..00000000000 --- a/core/app/assets/stylesheets/admin/shared/_layout.scss +++ /dev/null @@ -1,84 +0,0 @@ -// Basics -//--------------------------------------------------- -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -// Helpers -.block-table { - display: table; - width: 100%; - - .table-cell { - display: table-cell; - vertical-align: middle; - padding: 0 10px; - - &:first-child { - padding-left: 0; - } - &:last-child { - padding-right: 0; - } - } -} - -// For block grids -.frameless { - margin-left: -10px; - margin-right: -10px; -} - -// Header -//--------------------------------------------------- -#header { - background-color: $color-1; - padding: 5px 0; -} - -#logo { height: 40px } - -[data-hook="admin-title"] { font-size: 14px } - -.page-title { - i { - color: $color-2; - } -} - -// Content -//--------------------------------------------------- -#content { - background-color: $color-1; - position: relative; - z-index: 0; - padding: 0; - margin-top: 15px; -} - -#content-header { - padding: 15px 0; - background-color: very-light($color-3); - border-bottom: 1px solid $color-border; - - .page-title { - font-size: 20px; - } - .page-actions { - text-align: right; - - .button { - font-size: 85%; - } - } -} - -// Footer -//--------------------------------------------------- -#footer { - margin-top: 15px; - border-top: 1px solid $color-border; - padding: 10px 0; -} diff --git a/core/app/assets/stylesheets/admin/shared/_tables.scss b/core/app/assets/stylesheets/admin/shared/_tables.scss deleted file mode 100644 index 434c28d7a2e..00000000000 --- a/core/app/assets/stylesheets/admin/shared/_tables.scss +++ /dev/null @@ -1,180 +0,0 @@ -table { - width: 100%; - margin-bottom: 15px; - border-collapse: separate; - - th, td { - padding: 15px 10px; - border-right: 1px solid $color-border; - border-bottom: 1px solid $color-border; - vertical-align: middle; - text-overflow: ellipsis; - - img { - border: 1px solid transparent; - } - - &:first-child { - border-left: 1px solid $color-border; - } - - &.actions { - background-color: transparent; - border: none !important; - text-align: center; - - span.text { - font-size: $body-font-size; - } - - [class*='icon-'].no-text { - font-size: 120%; - background-color: very-light($color-3); - border: 1px solid $color-border; - border-radius: 15px; - width: 29px; - height: 29px; - display: inline-block; - padding-top: 2px; - - &:before { - text-align: center !important; - width: 27px; - display: inline-block; - } - - &:hover { - border-color: transparent; - } - } - .icon-envelope-alt { - color: $color-link; - padding-left: 0px; - - &:hover { - background-color: $color-3; - color: $color-1; - } - } - .icon-trash:hover, .icon-void:hover { - background-color: $color-error; - color: $color-1; - } - .icon-edit:hover, .icon-capture:hover { - background-color: $color-success; - color: $color-1; - } - .icon-copy:hover { - background-color: $color-notice; - color: $color-1; - } - } - - input[type="number"], - input[type="text"] { - width: 100%; - } - - &.no-border { - border-right: none; - } - - .handle { - @extend [class^="icon-"]:before; - @extend .icon-reorder; - cursor: move; - } - - } - - &.no-borders { - td, th { - border: none !important; - } - - } - - thead { - th { - padding: 10px; - border-top: 1px solid $color-border; - border-bottom: none; - background-color: $color-tbl-thead; - text-transform: uppercase; - font-size: 85%; - font-weight: $font-weight-bold; - } - } - - tbody { - tr { - &:first-child td { - border-top: 1px solid $color-border; - } - &.even td { - background-color: $color-tbl-even; - - img { - border: 1px solid very-light($color-3, 6); - } - } - - &:hover td { - background-color: very-light($color-3, 5); - - img { - border: 1px solid $color-border; - } - } - - &.deleted td { - background-color: very-light($color-error, 6); - border-color: very-light($color-error, 15); - } - - &.ui-sortable-placeholder td { - border: 1px solid $color-2 !important; - visibility: visible !important; - - &.actions { - background-color: transparent; - border-right: none !important; - border-top: none !important; - border-bottom: none !important; - border-left: 1px solid $color-2 !important; - } - } - - &.ui-sortable-helper { - width: 100%; - - td { - background-color: lighten($color-3, 33); - border-bottom: 1px solid $color-border; - - &.actions { - display: none; - } - } - } - } - - &.no-border-top tr:first-child td { - border-top: none; - } - - &.grand-total { - td { - border-color: $color-2 !important; - text-transform: uppercase; - font-size: 110%; - font-weight: 600; - background-color: lighten($color-2, 50); - } - .total { - background-color: $color-2; - color: $color-1; - } - } - } -} diff --git a/core/app/assets/stylesheets/admin/shared/_typography.scss b/core/app/assets/stylesheets/admin/shared/_typography.scss deleted file mode 100644 index 3689d076e54..00000000000 --- a/core/app/assets/stylesheets/admin/shared/_typography.scss +++ /dev/null @@ -1,132 +0,0 @@ -// Base -//-------------------------------------------------------------- -body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; font-size: 13px; } - -body { - font-family: $base-font-family; - font-size: $body-font-size; - font-weight: 400; - color: $color-body-text; - text-rendering: optimizeLegibility; -} - -hr { - border-top: 1px solid $color-border; - border-bottom: 1px solid white; - border-left: none; -} - -strong, b { - font-weight: 600; -} - -// links -//-------------------------------------------------------------- -a { - color: $color-link; - text-decoration: none; - line-height: inherit; - - &, &:hover, &:active, &:visited, &:focus { - outline: none; - } - - &:hover { - color: $color-link-hover; - } - &:active { - color: $color-link-active; - } - &:visited { - color: $color-link-visited; - } - &:focus { - color: $color-link-focus; - } -} - -// Headings -//-------------------------------------------------------------- - -h1,h2,h3,h4,h5,h6 { - font-weight: 600; - color: $color-headers; - line-height: 1.1; -} - -h1 { font-size: $h1-size; line-height: $h1-size + 6 } -h2 { font-size: $h2-size; line-height: $h1-size + 4 } -h3 { font-size: $h3-size; line-height: $h1-size + 2 } -h4 { font-size: $h4-size; line-height: $h1-size } -h5 { font-size: $h5-size; line-height: $h1-size } -h6 { font-size: $h6-size; line-height: $h1-size } - - -// Lists -//-------------------------------------------------------------- -ul { - &.inline-menu { - li { - display: inline-block; - } - } - &.fields { - list-style: none; - padding: 0; - margin: 0; - } -} - -dl { - width: 100%; - overflow: hidden; - margin: 5px 0; - color: lighten($color-body-text, 15); - - dt, dd { - float: left; - line-height: 16px; - padding: 5px; - text-align: justify; - } - - dt { - width: 40%; - font-weight: 600; - padding-left: 0; - text-transform: uppercase; - font-size: 85%; - } - - dd { - width: 60%; - padding-right: 0; - } - - dd:after { - content: ''; - clear: both; - } - -} - -// Helpers -.align-center { text-align: center } -.align-right { text-align: right } -.align-left { text-align: left } -.align-justify { text-align: justify } - -.uppercase { text-transform: uppercase } - -.green { color: $color-2 } -.blue { color: $color-3 } -.red { color: $color-5 } -.yellow { color: $color-6 } - -.no-objects-found { - text-align: center; - font-size: 120%; - text-transform: uppercase; - padding: 40px 0px; - color: lighten($color-body-text, 15); -} \ No newline at end of file diff --git a/core/app/assets/stylesheets/admin/spree_admin.scss b/core/app/assets/stylesheets/admin/spree_admin.scss deleted file mode 100644 index 13463597402..00000000000 --- a/core/app/assets/stylesheets/admin/spree_admin.scss +++ /dev/null @@ -1,38 +0,0 @@ -@import 'globals/functions'; -@import 'globals/variables_override'; -@import 'globals/variables'; -@import 'globals/mixins'; - -@import 'shared/typography'; -@import 'shared/tables'; -@import 'shared/icons'; -@import 'shared/forms'; -@import 'shared/layout'; - -@import 'components/states'; -@import 'components/actions'; -@import 'components/date-picker'; -@import 'components/messages'; -@import 'components/pagination'; -@import 'components/progress'; -@import 'components/table-filter'; -@import 'components/navigation'; -@import 'components/sidebar'; -@import 'components/product_autocomplete'; - -@import 'plugins/font-awesome'; -@import 'plugins/powertip'; -@import 'plugins/select2'; -@import 'plugins/token-input'; -@import 'plugins/jstree'; - -@import 'sections/image_settings'; -@import 'sections/orders'; -@import 'sections/overview'; -@import 'sections/products'; -@import 'sections/promotions'; -@import 'sections/edit_checkouts'; - -@import 'hacks/mozilla'; -@import 'hacks/opera'; -@import 'hacks/ie'; diff --git a/core/app/assets/stylesheets/admin/spree_core.css b/core/app/assets/stylesheets/admin/spree_core.css deleted file mode 100644 index 527dcdca43c..00000000000 --- a/core/app/assets/stylesheets/admin/spree_core.css +++ /dev/null @@ -1,16 +0,0 @@ -/* - * This is a manifest file that'll automatically include all the stylesheets available in this directory - * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at - * the top of the compiled file, but it's generally better to create a new file per style scope. - - *= require jquery.alerts/jquery.alerts - *= require jquery.alerts/jquery.alerts.spree - *= require responsive-tables - *= require normalize - *= require skeleton - *= require jquery-ui.datepicker - *= require jquery.powertip - *= require select2 - - *= require ./spree_admin -*/ diff --git a/core/app/assets/stylesheets/store/screen.css.scss b/core/app/assets/stylesheets/store/screen.css.scss deleted file mode 100644 index f7235421568..00000000000 --- a/core/app/assets/stylesheets/store/screen.css.scss +++ /dev/null @@ -1,1169 +0,0 @@ -@import "./variables.css.scss"; // Must call explicit path or else extension integration tests cannot import file. - -/*--------------------------------------*/ -/* Basic styles -/*--------------------------------------*/ -body { - font-family: $ff_base; - font-size: $base_font_size; - font-weight: 400; - color: $body_text_color; - line-height: 18px; - background-color: $layout_background_color; - -webkit-font-smoothing: antialiased; -} - -/* Line style */ -hr { - height: 0; - background-color: transparent; - color: transparent; - border: none; - border-bottom: 1px solid $border_color; -} - -/* Custom text-selection colors (remove any text shadows: twitter.com/miketaylr/status/12228805301) */ -::-moz-selection{background: $link_text_color; color: $layout_background_color; text-shadow: none;} -::selection {background: $link_text_color; color: $layout_background_color; text-shadow: none;} - -/* j.mp/webkit-tap-highlight-color */ -a:link {-webkit-tap-highlight-color: $link_text_color;} - -ins {background-color: $link_text_color; color: $layout_background_color; text-decoration: none;} -mark {background-color: $link_text_color; color: $layout_background_color; font-style: italic; font-weight: bold;} - - /*--------------------------------------*/ - /* Links - /*--------------------------------------*/ - a { - text-decoration: none; - color: $link_text_color; - - &:hover { - color: darken($link_text_color, 10) !important; - } - - &:active, &:focus { - outline: none; - } - } - - /*--------------------------------------*/ - /* Lists - /*--------------------------------------*/ - ul, ol { - margin-left: 0; - margin-top: 0; - -webkit-padding-start: 0px; - padding-left: 0; - list-style-position: inside; - - &.inline { - li { - display: inline-block; - } - } - } - - dl { - dt, dd { - display: inline-block; - width: 50%; - padding: 5px; - - &.odd { - background-color: lighten($body_text_color, 60); - } - } - dt { - font-weight: bold; - text-transform: uppercase; - } - dd { - margin-left: -23px; - } - } - - /*--------------------------------------*/ - /* Headers - /*--------------------------------------*/ - h1 { font-size: $heading_font_size; line-height: $heading_font_size + 10; } - h2 { font-size: $heading_font_size - 2; line-height: $heading_font_size - 2 + 10; } - h3 { font-size: $heading_font_size - 4; line-height: $heading_font_size - 4 + 10; } - h4 { font-size: $heading_font_size - 6; line-height: $heading_font_size - 6 + 10; } - h5 { font-size: $sub_heading_font_size; line-height: $sub_heading_font_size + 10; } - h6 { font-size: $sub_heading_font_size - 2; line-height: $sub_heading_font_size - 2 + 10; } - - h1, h2, h3, h4, h5, h6 { - font-weight: 700; - color: $title_text_color; - -webkit-margin-before: 0; - -webkit-margin-after: 0; - margin-top: 0; - margin-bottom: 0; - } - - /*--------------------------------------*/ - /* Forms - /*--------------------------------------*/ - textarea, input[type="date"], - input[type="datetime"], input[type="datetime-local"], - input[type="email"], input[type="month"], input[type="number"], - input[type="password"], input[type="search"], input[type="tel"], - input[type="text"], input[type="time"], input[type="url"], - input[type="week"] { - border: $default_border; - padding: 5px 10px; - font-family: $ff_base; - font-size: $input_box_font_size; - - &:active, &:focus { - border-color: $link_text_color;; - outline: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - -o-box-shadow: none; - box-shadow: none; - } - - &.error { - border-color: $c_red; - } - } - - label.error { - display: block; - font-size: $base_font_size - 1; - color: $c_red; - margin-top: 3px; - } - - span.required { - color: $c_red; - font-weight: bold; - font-size: 1.2em; - } - - fieldset { - margin: 0; - } - - input[type="submit"], input[type="button"], - input[type= "reset"], button, a.button { - background-color: $link_text_color; - background-image: none; - text-shadow: none; - color: $layout_background_color; - font-weight: bold; - font-size: $button_font_size; - font-family: $ff_base; - border: 1px solid $button_border_color; - padding: 6px 10px 5px; - vertical-align: top; - - -webkit-font-smoothing: antialiased; - - -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); - -khtml-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); - -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); - -o-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); - -webkit-border-radius: 0px; - -khtml-border-radius: 0px; - -moz-border-radius: 0px; - -ms-border-radius: 0px; - -o-border-radius: 0px; - border-radius: 0px; - - &.large { - padding: 7px 10px; - font-size: $button_font_size + 2; - } - - &.gray { - background-color: lighten($body_text_color, 20); - border-color: lighten($body_text_color, 10); - } - - &:hover { - background-image: none; - background-color: $body_text_color; - border-color: $body_text_color; - color: $layout_background_color !important; - } - } - - .ie8 { - a.button { - line-height: 16px; - } - } - - input[type="checkbox"], label { - vertical-align: middle; - } - - a.button { - display: inline-block; - line-height: 15px; - margin-top: -2px; - vertical-align: bottom; - } - - /*--------------------------------------*/ - /* Footer - /*--------------------------------------*/ - footer#footer { - padding: 10px 0; - border-top: 1px solid lighten($body_text_color, 60); - } - - /*--------------------------------------*/ - /* Paragraphs - /*--------------------------------------*/ - p { - padding: 10px 0; - } - - /*--------------------------------------*/ - /* Tables - /*--------------------------------------*/ - table { - thead { - background-color: lighten($body_text_color, 60); - text-transform: uppercase; - - tr { - th { - padding: 5px 10px; - } - } - } - - tbody, tfoot { - tr { - border-bottom: 1px solid lighten($body_text_color, 60); - - td { - vertical-align: middle; - padding: 5px 10px; - } - - &.alt, &.odd { - background-color: lighten($link_text_color, 50); - } - } - } - } - - /*--------------------------------------*/ - /* Navigation - /*--------------------------------------*/ - nav#top-nav-bar { - text-align: right; - margin-top: 20px; - - ul { - li { - margin-bottom: 5px; - padding-left: 10px; - - a{ - font-weight: bold; - font-size: $header_navigation_font_size; - text-transform: uppercase; - } - } - } - } - - nav #main-nav-bar { - text-transform: uppercase; - font-weight: bold; - margin-top: 20px; - border-bottom: 1px solid lighten($body_text_color, 60); - padding-bottom: 6px; - - li { - - a { - font-size: $horizontal_navigation_font_size; - padding: 5px; - } - - &:first-child { - a { - padding-left: 0 - } - } - - &#link-to-cart { - float: right; - padding-left: 24px; - background: url("cart.png") no-repeat left center; - - &:hover { - border-color: $link_text_color; - - .amount { - border-color: $link_text_color; - } - } - - a { - font-weight: normal; - font-size: $horizontal_navigation_font_size; - color: $link_text_color; - - .amount { - font-size: $horizontal_navigation_font_size + 2; - font-weight: bold; - border-left: 1px solid lighten($body_text_color, 60); - padding-left: 5px; - padding-bottom: 5px; - } - } - } - } - } - - nav#taxonomies { - .taxonomy-root { - text-transform: uppercase; - border-bottom: 1px solid lighten($body_text_color, 60); - margin-bottom: 5px; - font-size: $main_navigation_header_font_size; - } - - .taxons-list { - li { - a { - font-size: $main_navigation_font_size - } - } - } - } - - #breadcrumbs { - border-bottom: 1px solid lighten($body_text_color, 60); - padding: 3px 0; - margin-bottom: 15px; - - li { - a { - color: $link_text_color; - } - span { - text-transform: uppercase; - font-weight: bold; - } - } - } - - /*--------------------------------------*/ - /* Flash notices & errors - /*--------------------------------------*/ - .flash { - padding: 10px; - color: $layout_background_color; - font-weight: bold; - margin-bottom: 10px; - - &.notice { - background-color: $link_text_color; - } - &.success { - background-color: $c_green; - } - &.error { - background-color: $c_red; - } - } - - .errorExplanation { - @extend .flash.error; - @extend .flash; - - p { - font-weight: normal; - } - - ul { - list-style: disc outside; - margin-left: 30px; - - li { - font-weight: normal; - } - } - } - - /*--------------------------------------*/ - /* Main search bar - /*--------------------------------------*/ - #search-bar { - display: block; - } - - /*--------------------------------------*/ - /* Products - /*--------------------------------------*/ - [data-hook="product_show"] { - h6 { - font-size: $product_detail_title_font_size; - } - } - - .product-section-title { - text-transform: uppercase; - margin-top: 15px; - } - - .add-to-cart { - margin-top: 15px; - - input[type="number"] { - margin-right: 3px; - width: 60px; - vertical-align: middle; - padding: 8px 10px; - } - } - - span.price { - font-weight: bold; - color: $link_text_color; - - &.selling { - font-size: $product_detail_price_font_size; - } - &.diff { - font-weight: bold; - } - } - - .taxon-title { - font-size: $product_list_header_font_size; - } - - .search-results-title { - font-size: $product_list_search_font_size; - } - - ul#products { - &:after { - content: " "; - display: block; - clear: both; - visibility: hidden; - line-height: 0; - height: 0; - } - - li { - text-align: center; - font-weight: bold; - margin-bottom: 20px; - - a { - display: block; - - &.info { - height: 35px; - margin-top: 5px; - font-size: $product_list_name_font_size; - color: $product_link_text_color; - border-bottom: 1px solid lighten($body_text_color, 60); - overflow: hidden; - } - } - - .product-image { - border: $default_border; - padding: 5px; - min-height: 110px; - background-color: $product_background_color; - - &:hover { - border-color: $link_text_color; - } - - } - - .price { - color: $link_text_color; - font-size: $product_list_price_font_size; - padding-top: 5px; - display: block; - } - } - } - - .subtaxon-title { - text-transform: uppercase; - - a { - color: $link_text_color; - } - } - - .search-results-title { - text-transform: uppercase; - border-bottom: 1px solid lighten($body_text_color, 60); - margin-bottom: 10px; - } - - #sidebar_products_search { - .navigation { - margin-bottom: 15px; - } - - .filter-title { - display: block; - font-weight: bold; - text-transform: uppercase; - border-bottom: 1px solid #ededed; - margin-bottom: 5px; - color: $link_text_color; - font-size: $base_font_size + 2; - line-height: 24px; - } - } - - .taxon { - overflow: hidden; - } - - #product-images { - #main-image { - text-align: center; - border: $default_border; - background-color: $product_background_color; - - img { - min-height: 240px; - } - } - #product-thumbnails { - li { - background-color: $product_background_color; - } - } - } - - #product-description { - .product-title { - border-bottom: 1px solid lighten($body_text_color, 60); - margin-bottom: 15px; - color: $product_title_text_color; - font-size: $product_detail_name_font_size; - } - - [data-hook="product-description"] { - font-size: $product_detail_description_font_size; - color: $product_body_text_color; - } - } - - #product-thumbnails { - margin-top: 10px; - - li { - margin-right: 6px; - border: $default_border; - - img { - padding: 5px; - } - - &:hover, &.selected { - border-color: $link_text_color; - } - } - } - - #product-properties { - border: $default_border; - padding: 10px; - width: 100%; - } - - #product-variants { - ul { - li { - padding: 5px; - } - } - } - - #cart-form { - #inside-product-cart-form:after { - content: " "; - display: block; - clear: both; - visibility: hidden; - line-height: 0; - height: 0; - } - } - - /*--------------------------------------*/ - /* Checkout - /*--------------------------------------*/ - .progress-steps { - list-style: decimal inside; - overflow: auto; - - li { - float: left; - margin-right: 20px; - font-weight: bold; - text-transform: uppercase; - padding: 5px 20px; - color: lighten($body_text_color, 20); - - &.current-first, &.current { - background-color: $link_text_color; - color: $layout_background_color; - } - - &.completed-first, &.completed { - background-color: lighten($body_text_color, 60); - color: $layout_background_color; - - a { - color: $layout_background_color; - } - - &:hover { - background-color: $link_text_color; - color: $layout_background_color; - - a { - color: $layout_background_color; - - &:hover { - color: $layout_background_color !important; - } - } - } - } - } - } - - #payment-methods { - list-style: none; - - li { - fieldset { - border: none; - padding: 0; - } - } - } - - #checkout-summary { - text-align: center; - border: $default_border; - margin-top: 23px; - margin-left: 0; - - h3 { - text-transform: uppercase; - font-size: $base_font_size + 2; - border-bottom: 1px solid lighten($body_text_color, 60); - } - - table { - width: 100%; - - tr[data-hook="item_total"] { - td:last-child { - strong { - @extend span.price; - } - } - } - - tr[data-hook="order_total"] { - border-bottom: none; - }; - - #summary-order-total { - @extend span.price; - font-size: $base_font_size + 2; - } - } - } - - #billing, #shipping, #shipping_method, - #payment, #order_details, #order_summary { - margin-top: 10px; - border: $default_border; - padding: 10px; - - legend { - text-transform: uppercase; - font-weight: bold; - font-size: $base_font_size + 2; - color: $link_text_color; - padding: 5px; - margin-left: 15px; - } - } - - #order_details, #order_summary { - padding: 0; - - div:last-child { - margin-left: -1px; - } - - .payment-info { - .cc-type { - img { - vertical-align: middle; - } - } - } - - td.price, td.total { - span { - @extend span.price; - } - } - - table tfoot { - text-align: right; - color: lighten($body_text_color, 20); - - tr { - border: none; - } - - &#order-total { - text-transform: uppercase; - font-size: $base_font_size + 4; - color: $body_text_color; - - tr { - border-top: 1px solid lighten($body_text_color, 60); - - td { - padding: 10px; - } - } - } - } - - .steps-data { - div.columns { - padding: 5px; - margin: 0; - - &:first-child { - margin-left: 10px; - } - } - - h6 { - border-bottom: 1px solid lighten($body_text_color, 60); - margin-bottom: 5px; - } - } - } - - #shipping_method { - p { - label { - float: left; - font-weight: bold; - font-size: $base_font_size + 2; - margin-right: 40px; - padding: 5px; - } - } - } - - p[data-hook="use_billing"] { - float: right; - margin-top: -18px; - background-color: $layout_background_color; - padding: 5px; - } - - /*--------------------------------------*/ - /* Cart - /*--------------------------------------*/ - table#cart-detail { - width: 100%; - tbody#line_items { - tr { - - td[data-hook="cart_item_price"], td[data-hook="cart_item_total"] { - @extend span.price; - @extend span.price.selling; - } - td[data-hook="cart_item_quantity"] { - .line_item_quantity { - width: 40px; - } - } - td[data-hook="cart_item_delete"] { - .delete { - display: block; - width: 20px; - } - } - } - } - } - - div[data-hook="inside_cart_form"] { - .links { - margin-top: 15px; - text-align: right; - } - - #subtotal { - text-align: right; - text-transform: uppercase; - margin-top: 15px; - - span.order-total { - @extend span.price; - } - } - } - - #empty-cart { - margin-top: -75px !important; - float: left !important; - } - - /*--------------------------------------*/ - /* Account - /*--------------------------------------*/ - #existing-customer, #new-customer, #forgot-password { - h6 { - text-transform: uppercase; - } - } - - #registration { - h6 { - text-transform: uppercase; - } - - #existing-customer { - width: auto; - text-align: left; - } - } - - #user-info { - margin-bottom: 15px; - border: $default_border; - padding: 10px; - } - - /*--------------------------------------*/ - /* Order - /*--------------------------------------*/ - #order_summary { - margin-top: 0; - } - #order { - p[data-hook="links"] { - margin-left: 10px; - overflow: auto; - } - } - - table.order-summary { - tbody { - tr { - td { - width: 10%; - text-align: center; - - &:first-child { - a { - text-transform: uppercase; - font-weight: bold; - color: $link_text_color; - } - } - } - } - } - } - -/* #Media Queries -================================================== */ - - /* Smaller than standard 960 (devices and browsers) */ - @media only screen and (max-width: 959px) { - - } - - /* Tablet Portrait size to standard 960 (devices and browsers) */ - @media only screen and (min-width: 768px) and (max-width: 959px) { - .container { - padding-left: 10px; - width: 758px; - } - footer#footer { - width: 748px; - } - p[data-hook="use_billing"] { - margin-top: -15px; - } - } - - /* All Mobile Sizes (devices and browser) */ - @media only screen and (max-width: 767px) { - - html { - -webkit-text-size-adjust: none; - } - - #order_details .steps-data div.columns, - #order_summary .steps-data div.columns { - padding: 0; - margin: 0; - - &:first-child { - margin: 0 - } - } - - nav#taxonomies { - text-align: center; - - ul { - padding-left: 0 !important; - list-style: none !important; - } - } - - ul#nav-bar { - text-align: center; - } - - .steps-data div.columns { - margin-bottom: 15px; - text-align: center; - } - - #order_details, #order { - table[data-hook="order_details"] { - width: 100%; - } - } - - #update-cart { - #subtotal, .links { - width: 50%; - float: left; - text-align: left; - } - #subtotal { - text-align: right; - } - } - } - - /* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ - @media only screen and (min-width: 480px) and (max-width: 767px) { - - footer#footer { - width: auto !important; - } - - input, select { - vertical-align: baseline !important; - } - - figure#logo { - text-align: center; - } - - #link-to-login { - display: block; - text-align: center; - } - - #search-bar { - display: block; - text-align: center; - - select { - margin-bottom: 10px; - } - } - - ul#products { - margin-left: 0; - margin-right: -20px; - - li { - width: 133px; - margin-right: 10px; - } - } - - table#cart-detail, table[data-hook="order_details"] { - tbody { - tr { - td[data-hook="cart_item_description"], td[data-hook="order_item_description"] { - font-size: $base_font_size - 1; - line-height: 15px; - width: 100px; - - h4 { - font-size: $base_font_size + 2; - line-height: 17px; - margin-bottom: 10px; - } - } - td[data-hook="cart_item_price"], td[data-hook="cart_item_total"], - td[data-hook="order_item_price"], td[data-hook="order_item_total"] { - font-size: $base_font_size !important; - } - td[data-hook="cart_item_image"], td[data-hook="order_item_image"] { - img { - width: 70px; - } - } - } - } - } - - } - - @media only screen and (max-width: 767px) { - #empty-cart { - clear: both; - margin-top: 0 !important; - float: none !important; - } - } - - @media only screen and (min-width: 768px) and (max-width: 959px) { - .container .offset-by-nine.coupon-code-field { - padding-left: 380px; - } - } - - - /* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ - @media only screen and (max-width: 479px) { - - .progress-steps li { - padding: 0; - margin: 0; - width: 50%; - - span { - display: block; - padding: 10px 20px; - } - } - - #shipping_method p label { - float: none; - display: block; - text-align: center; - margin-right: 0; - } - - p[data-hook="use_billing"] { - float: none; - margin-top: 0; - } - - table#cart-detail, table[data-hook="order_details"] { - tbody { - tr { - td[data-hook="cart_item_description"], td[data-hook="order_item_description"] { - padding: 0 !important; - text-indent: -9999px; - - h4 { - display: none; - } - } - td[data-hook="cart_item_image"], td[data-hook="order_item_image"] { - img { - width: 70px; - } - } - td[data-hook="cart_item_price"], td[data-hook="cart_item_total"] { - font-size: $base_font_size + 2 !important; - } - } - } - } - - table.order-summary { - display: block; position: relative; width: 100%; - - thead { display: block; float: left; } - tbody { display: block; width: auto; position: relative; overflow-x: auto; white-space: nowrap; } - thead tr { display: block; } - th { display: block; } - tbody tr { display: inline-block; vertical-align: top; } - td { display: block; min-height: 1.25em; } - } - - - figure#logo { - text-align: center; - } - - #link-to-login { - display: block; - text-align: center; - } - - #search-bar { - display: block; - text-align: center; - - select { - margin-bottom: 10px; - } - } - - aside#sidebar { - text-align: center; - - ul { - padding-left: 0 !important; - - li { - list-style-type: none; - } - } - } - - ul#products { - - li { - width: 142px; - margin-right: 15px; - - &.secondary, &.omega { - margin-right: 0; - } - } - - - } - - #content { - text-align: center; - } - - } diff --git a/core/app/assets/stylesheets/store/spree_core.css b/core/app/assets/stylesheets/store/spree_core.css deleted file mode 100644 index dd2700da15c..00000000000 --- a/core/app/assets/stylesheets/store/spree_core.css +++ /dev/null @@ -1,6 +0,0 @@ -/* -* This is a manifest file that includes stylesheets for spree_core - *= require normalize - *= require skeleton - *= require store/screen -*/ \ No newline at end of file diff --git a/core/app/assets/stylesheets/store/variables.css.scss b/core/app/assets/stylesheets/store/variables.css.scss deleted file mode 100644 index 2d0000d74fd..00000000000 --- a/core/app/assets/stylesheets/store/variables.css.scss +++ /dev/null @@ -1,60 +0,0 @@ -/*--------------------------------------*/ -/* Colors -/*--------------------------------------*/ -$c_green: #8dba53 !default; /* Spree green */ -$c_red: #e45353 !default; /* Error red */ - -$layout_background_color: #FFFFFF !default; -$title_text_color: #404042 !default; -$body_text_color: #404042 !default; -$link_text_color: #00ADEE !default; - -$product_background_color: #FFFFFF !default; -$product_title_text_color: #404042 !default; -$product_body_text_color: #404042 !default; -$product_link_text_color: #BBBBBB !default; - -/*--------------------------------------*/ -/* Fonts import from remote -/*--------------------------------------*/ -@import url(//fonts.googleapis.com/css?family=Ubuntu:400,700,400italic,700italic|&subset=latin,cyrillic,greek,greek-ext,latin-ext,cyrillic-ext); - -/*--------------------------------------*/ -/* Font families -/*--------------------------------------*/ -$ff_base: 'Ubuntu', sans-serif !default; - -/*-------------------------------------- - | Font sizes - |-------------------------------------- - |- Navigation - | */ - $header_navigation_font_size: 14px !default; - $horizontal_navigation_font_size: 16px !default; - $main_navigation_header_font_size: 14px !default; - $main_navigation_font_size: 12px !default; -/*|------------------------------------ - |- Product Listing - | */ - $product_list_name_font_size: 12px !default; - $product_list_price_font_size: 16px !default; - $product_list_header_font_size: 20px !default; - $product_list_search_font_size: 14px !default; -/*|------------------------------------ - |- Product Details - | */ - $product_detail_name_font_size: 24px !default; - $product_detail_description_font_size: 12px !default; - $product_detail_price_font_size: 20px !default; - $product_detail_title_font_size: 14px !default; -/*|------------------------------------ - |- Basic - | */ - $heading_font_size: 24px !default; - $sub_heading_font_size: 14px !default; - $button_font_size: 12px !default; - $input_box_font_size: 13px !default; - $base_font_size: 12px !default; - $border_color: lighten($body_text_color, 60); - $default_border: 1px solid $border_color; - $button_border_color: rgba(0, 138, 189, .75) !default; diff --git a/core/app/controllers/spree/admin/adjustments_controller.rb b/core/app/controllers/spree/admin/adjustments_controller.rb deleted file mode 100644 index 616dcd54de5..00000000000 --- a/core/app/controllers/spree/admin/adjustments_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Spree - module Admin - class AdjustmentsController < ResourceController - belongs_to 'spree/order', :find_by => :number - destroy.after :reload_order - - private - def reload_order - @order.reload - end - end - end -end diff --git a/core/app/controllers/spree/admin/banners_controller.rb b/core/app/controllers/spree/admin/banners_controller.rb deleted file mode 100644 index 2c33b71cf7d..00000000000 --- a/core/app/controllers/spree/admin/banners_controller.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Spree - module Admin - class BannersController < Spree::Admin::BaseController - def dismiss - if params[:id] - if user = try_spree_current_user - user.dismiss_banner(params[:id]) - end - end - render :nothing => true - end - end - end -end diff --git a/core/app/controllers/spree/admin/base_controller.rb b/core/app/controllers/spree/admin/base_controller.rb deleted file mode 100644 index e452da07cc9..00000000000 --- a/core/app/controllers/spree/admin/base_controller.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Spree - module Admin - class BaseController < Spree::BaseController - ssl_required - - helper 'spree/admin/navigation' - helper 'spree/admin/tables' - layout '/spree/layouts/admin' - - before_filter :check_alerts - before_filter :authorize_admin - - protected - def authorize_admin - begin - record = model_class.new - rescue - record = Object.new - end - authorize! :admin, record - authorize! params[:action].to_sym, record - end - - def check_alerts - return unless should_check_alerts? - - unless session.has_key? :alerts - begin - session[:alerts] = Spree::Alert.current(request.host) - filter_dismissed_alerts - Spree::Config.set :last_check_for_spree_alerts => DateTime.now.to_s - rescue - session[:alerts] = nil - end - end - end - - def should_check_alerts? - return false if !Rails.env.production? || !Spree::Config[:check_for_spree_alerts] - - last_check = Spree::Config[:last_check_for_spree_alerts] - return true if last_check.blank? - - DateTime.parse(last_check) < 12.hours.ago - end - - def flash_message_for(object, event_sym) - resource_desc = object.class.model_name.human - resource_desc += " \"#{object.name}\"" if object.respond_to?(:name) && object.name.present? - I18n.t(event_sym, :resource => resource_desc) - end - - def render_js_for_destroy - render :partial => '/spree/admin/shared/destroy' - end - - # Index request for JSON needs to pass a CSRF token in order to prevent JSON Hijacking - def check_json_authenticity - return unless request.format.js? or request.format.json? - return unless protect_against_forgery? - auth_token = params[request_forgery_protection_token] - unless (auth_token and form_authenticity_token == URI.unescape(auth_token)) - raise(ActionController::InvalidAuthenticityToken) - end - end - - def filter_dismissed_alerts - return unless session[:alerts] - dismissed = (Spree::Config[:dismissed_spree_alerts] || '').split(',') - session[:alerts].reject! { |a| dismissed.include? a.id.to_s } - end - end - end -end diff --git a/core/app/controllers/spree/admin/general_settings_controller.rb b/core/app/controllers/spree/admin/general_settings_controller.rb deleted file mode 100644 index ef063b7899b..00000000000 --- a/core/app/controllers/spree/admin/general_settings_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Spree - module Admin - class GeneralSettingsController < Spree::Admin::BaseController - - def edit - @preferences_general = [:site_name, :default_seo_title, :default_meta_keywords, - :default_meta_description, :site_url] - @preferences_security = [:allow_ssl_in_production, - :allow_ssl_in_staging, :allow_ssl_in_development_and_test, - :check_for_spree_alerts] - @preferences_currency = [:display_currency, :hide_cents] - end - - def update - params.each do |name, value| - next unless Spree::Config.has_preference? name - Spree::Config[name] = value - end - flash[:success] = t(:successfully_updated, :resource => t(:general_settings)) - - redirect_to edit_admin_general_settings_path - end - - def dismiss_alert - if request.xhr? and params[:alert_id] - dismissed = Spree::Config[:dismissed_spree_alerts] || '' - Spree::Config.set :dismissed_spree_alerts => dismissed.split(',').push(params[:alert_id]).join(',') - filter_dismissed_alerts - render :nothing => true - end - end - end - end -end diff --git a/core/app/controllers/spree/admin/image_settings_controller.rb b/core/app/controllers/spree/admin/image_settings_controller.rb deleted file mode 100644 index 388b9688ec3..00000000000 --- a/core/app/controllers/spree/admin/image_settings_controller.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Spree - module Admin - class ImageSettingsController < Spree::Admin::BaseController - def edit - @styles = ActiveSupport::JSON.decode(Spree::Config[:attachment_styles]) - @headers = ActiveSupport::JSON.decode(Spree::Config[:s3_headers]) - end - - def update - update_styles(params) - update_headers(params) if Spree::Config[:use_s3] - - Spree::Config.set(params[:preferences]) - update_paperclip_settings - - respond_to do |format| - format.html { - flash[:success] = t(:image_settings_updated) - redirect_to edit_admin_image_settings_path - } - end - end - - - private - - def update_styles(params) - params[:new_attachment_styles].each do |index, style| - params[:attachment_styles][style[:name]] = style[:value] unless style[:value].empty? - end if params[:new_attachment_styles].present? - - styles = params[:attachment_styles] - - Spree::Config[:attachment_styles] = ActiveSupport::JSON.encode(styles) unless styles.nil? - end - - def update_headers(params) - params[:new_s3_headers].each do |index, header| - params[:s3_headers][header[:name]] = header[:value] unless header[:value].empty? - end if params[:new_s3_headers].present? - - headers = params[:s3_headers] - - Spree::Config[:s3_headers] = ActiveSupport::JSON.encode(headers) unless headers.nil? - end - - def update_paperclip_settings - if Spree::Config[:use_s3] - s3_creds = { :access_key_id => Spree::Config[:s3_access_key], :secret_access_key => Spree::Config[:s3_secret], :bucket => Spree::Config[:s3_bucket] } - Spree::Image.attachment_definitions[:attachment][:storage] = :s3 - Spree::Image.attachment_definitions[:attachment][:s3_credentials] = s3_creds - Spree::Image.attachment_definitions[:attachment][:s3_headers] = ActiveSupport::JSON.decode(Spree::Config[:s3_headers]) - Spree::Image.attachment_definitions[:attachment][:bucket] = Spree::Config[:s3_bucket] - else - Spree::Image.attachment_definitions[:attachment].delete :storage - end - - Spree::Image.attachment_definitions[:attachment][:styles] = ActiveSupport::JSON.decode(Spree::Config[:attachment_styles]) - Spree::Image.attachment_definitions[:attachment][:path] = Spree::Config[:attachment_path] - Spree::Image.attachment_definitions[:attachment][:default_url] = Spree::Config[:attachment_default_url] - Spree::Image.attachment_definitions[:attachment][:default_style] = Spree::Config[:attachment_default_style] - end - end - end -end - diff --git a/core/app/controllers/spree/admin/images_controller.rb b/core/app/controllers/spree/admin/images_controller.rb deleted file mode 100644 index a48a44c5c4f..00000000000 --- a/core/app/controllers/spree/admin/images_controller.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Spree - module Admin - class ImagesController < ResourceController - before_filter :load_data - - create.before :set_viewable - update.before :set_viewable - destroy.before :destroy_before - - private - - def location_after_save - admin_product_images_url(@product) - end - - def load_data - @product = Product.where(:permalink => params[:product_id]).first - @variants = @product.variants.collect do |variant| - [variant.options_text, variant.id] - end - @variants.insert(0, [I18n.t(:all), @product.master.id]) - end - - def set_viewable - @image.viewable_type = 'Spree::Variant' - @image.viewable_id = params[:image][:viewable_id] - end - - def destroy_before - @viewable = @image.viewable - end - - end - end -end diff --git a/core/app/controllers/spree/admin/inventory_settings_controller.rb b/core/app/controllers/spree/admin/inventory_settings_controller.rb deleted file mode 100644 index 80f95e64245..00000000000 --- a/core/app/controllers/spree/admin/inventory_settings_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Spree - module Admin - class InventorySettingsController < Spree::Admin::BaseController - - def update - Spree::Config.set(params[:preferences]) - flash[:success] = t(:successfully_updated, :resource => t(:inventory_settings)) - redirect_to edit_admin_inventory_settings_path - end - end - end - -end diff --git a/core/app/controllers/spree/admin/inventory_units_controller.rb b/core/app/controllers/spree/admin/inventory_units_controller.rb deleted file mode 100644 index 61f231efbcc..00000000000 --- a/core/app/controllers/spree/admin/inventory_units_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Spree - module Admin - class InventoryUnitsController < Spree::Admin::BaseController - end - end -end \ No newline at end of file diff --git a/core/app/controllers/spree/admin/line_items_controller.rb b/core/app/controllers/spree/admin/line_items_controller.rb deleted file mode 100644 index 7303ead9bc9..00000000000 --- a/core/app/controllers/spree/admin/line_items_controller.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Spree - module Admin - class LineItemsController < Spree::Admin::BaseController - layout nil, :only => [:create, :destroy, :update] - - before_filter :load_order - before_filter :load_line_item, :only => [:destroy, :update] - - respond_to :html, :js - - def create - variant = Variant.find(params[:line_item][:variant_id]) - @line_item = @order.add_variant(variant, params[:line_item][:quantity].to_i) - - if @order.save - respond_with(@line_item) do |format| - format.html { render :partial => 'spree/admin/orders/form', :locals => { :order => @order.reload } } - end - else - respond_with(@line_item) do |format| - format.js { render :action => 'create', :locals => { :order => @order.reload } } - end - end - end - - def destroy - @line_item.destroy - respond_with(@line_item) do |format| - format.html { render :partial => 'spree/admin/orders/form', :locals => { :order => @order.reload } } - end - end - - def update - if @line_item.update_attributes(params[:line_item]) - respond_with(@line_item) do |format| - format.html { render :partial => 'spree/admin/orders/form', :locals => { :order => @order.reload } } - end - else - respond_with(@line_item) do |format| - format.html { render :partial => 'spree/admin/orders/form', :locals => { :order => @order.reload } } - end - end - end - - private - - def load_order - @order = Order.find_by_number!(params[:order_id]) - end - - def load_line_item - @line_item = @order.line_items.find(params[:id]) - end - end - end -end diff --git a/core/app/controllers/spree/admin/mail_methods_controller.rb b/core/app/controllers/spree/admin/mail_methods_controller.rb deleted file mode 100644 index 9d1c4e11782..00000000000 --- a/core/app/controllers/spree/admin/mail_methods_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Spree - module Admin - class MailMethodsController < ResourceController - after_filter :initialize_mail_settings - - def update - if params[:mail_method][:preferred_smtp_password].blank? - params[:mail_method].delete(:preferred_smtp_password) - end - super - end - - def testmail - @mail_method = Spree::MailMethod.find(params[:id]) - if TestMailer.test_email(@mail_method, try_spree_current_user).deliver - flash[:success] = t('admin.mail_methods.testmail.delivery_success') - else - flash[:error] = t('admin.mail_methods.testmail.delivery_error') - end - rescue Exception => e - flash[:error] = t('admin.mail_methods.testmail.error') % {:e => e} - ensure - respond_with(@mail_method) { |format| format.html { redirect_to :back } } - end - - private - def initialize_mail_settings - Spree::Core::MailSettings.init - end - end - end -end diff --git a/core/app/controllers/spree/admin/orders/customer_details_controller.rb b/core/app/controllers/spree/admin/orders/customer_details_controller.rb deleted file mode 100644 index 63f52e05252..00000000000 --- a/core/app/controllers/spree/admin/orders/customer_details_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Spree - module Admin - module Orders - class CustomerDetailsController < Spree::Admin::BaseController - before_filter :load_order - - def show - edit - render :action => :edit - end - - def edit - country_id = Address.default.country.id - @order.build_bill_address(:country_id => country_id) if @order.bill_address.nil? - @order.build_ship_address(:country_id => country_id) if @order.ship_address.nil? - end - - def update - if @order.update_attributes(params[:order]) - shipping_method = @order.available_shipping_methods(:front_end).first - if shipping_method - @order.shipping_method = shipping_method - - if params[:user_id].present? - @order.user_id = params[:user_id] - @order.user true - end - @order.save - @order.create_shipment! - flash[:success] = t('customer_details_updated') - redirect_to edit_admin_order_shipment_path(@order, @order.shipment) - else - flash[:error] = t('errors.messages.no_shipping_methods_available') - redirect_to admin_order_customer_path(@order) - end - else - render :action => :edit - end - - end - - private - - def load_order - @order = Order.find_by_number!(params[:order_id], :include => :adjustments) - end - - end - end - end -end diff --git a/core/app/controllers/spree/admin/orders_controller.rb b/core/app/controllers/spree/admin/orders_controller.rb deleted file mode 100644 index d7749ccd68c..00000000000 --- a/core/app/controllers/spree/admin/orders_controller.rb +++ /dev/null @@ -1,123 +0,0 @@ -module Spree - module Admin - class OrdersController < Spree::Admin::BaseController - require 'spree/core/gateway_error' - before_filter :initialize_order_events - before_filter :load_order, :only => [:show, :edit, :update, :fire, :resend] - - respond_to :html - - def index - params[:q] ||= {} - params[:q][:completed_at_not_null] ||= '1' if Spree::Config[:show_only_complete_orders_by_default] - @show_only_completed = params[:q][:completed_at_not_null].present? - params[:q][:s] ||= @show_only_completed ? 'completed_at desc' : 'created_at desc' - - # As date params are deleted if @show_only_completed, store - # the original date so we can restore them into the params - # after the search - created_at_gt = params[:q][:created_at_gt] - created_at_lt = params[:q][:created_at_lt] - - params[:q].delete(:inventory_units_shipment_id_null) if params[:q][:inventory_units_shipment_id_null] == "0" - - if !params[:q][:created_at_gt].blank? - params[:q][:created_at_gt] = Time.zone.parse(params[:q][:created_at_gt]).beginning_of_day rescue "" - end - - if !params[:q][:created_at_lt].blank? - params[:q][:created_at_lt] = Time.zone.parse(params[:q][:created_at_lt]).end_of_day rescue "" - end - - if @show_only_completed - params[:q][:completed_at_gt] = params[:q].delete(:created_at_gt) - params[:q][:completed_at_lt] = params[:q].delete(:created_at_lt) - end - - @search = Order.ransack(params[:q]) - @orders = @search.result.includes([:user, :shipments, :payments]). - page(params[:page]). - per(params[:per_page] || Spree::Config[:orders_per_page]) - - # Restore dates - params[:q][:created_at_gt] = created_at_gt - params[:q][:created_at_lt] = created_at_lt - - respond_with(@orders) - end - - def show - respond_with(@order) - end - - def new - @order = Order.create - respond_with(@order) - end - - def edit - respond_with(@order) - end - - def update - return_path = nil - if @order.update_attributes(params[:order]) && @order.line_items.present? - @order.update! - unless @order.complete? - # Jump to next step if order is not complete. - return_path = admin_order_customer_path(@order) - else - # Otherwise, go back to first page since all necessary information has been filled out. - return_path = admin_order_path(@order) - end - else - @order.errors.add(:line_items, t('errors.messages.blank')) if @order.line_items.empty? - end - - respond_with(@order) do |format| - format.html do - if return_path - redirect_to return_path - else - render :action => :edit - end - end - end - end - - def fire - # TODO - possible security check here but right now any admin can before any transition (and the state machine - # itself will make sure transitions are not applied in the wrong state) - event = params[:e] - if @order.send("#{event}") - flash[:success] = t(:order_updated) - else - flash[:error] = t(:cannot_perform_operation) - end - rescue Spree::Core::GatewayError => ge - flash[:error] = "#{ge.message}" - ensure - respond_with(@order) { |format| format.html { redirect_to :back } } - end - - def resend - OrderMailer.confirm_email(@order, true).deliver - flash[:success] = t(:order_email_resent) - - respond_with(@order) { |format| format.html { redirect_to :back } } - end - - private - - def load_order - @order = Order.find_by_number!(params[:id], :include => :adjustments) if params[:id] - end - - # Used for extensions which need to provide their own custom event links on the order details view. - def initialize_order_events - @order_events = %w{cancel resume} - end - - end - end -end diff --git a/core/app/controllers/spree/admin/overview_controller.rb b/core/app/controllers/spree/admin/overview_controller.rb deleted file mode 100644 index 9d8ba571082..00000000000 --- a/core/app/controllers/spree/admin/overview_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# this clas was inspired (heavily) from the mephisto admin architecture -module Spree - module Admin - class OverviewController < Spree::Admin::BaseController - #todo, add rss feed of information that is happening - - def index - @users = User.all - #@users = User.find_with_deleted(:all, :order => 'updated_at desc') - # going to list today's orders, yesterday's orders, older orders - # have a filter / search at the top - # @orders, @ - end - - end - end -end \ No newline at end of file diff --git a/core/app/controllers/spree/admin/payment_methods_controller.rb b/core/app/controllers/spree/admin/payment_methods_controller.rb deleted file mode 100644 index 573d44cae72..00000000000 --- a/core/app/controllers/spree/admin/payment_methods_controller.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Spree - module Admin - class PaymentMethodsController < ResourceController - skip_before_filter :load_resource, :only => [:create] - before_filter :load_data - - respond_to :html - - def create - @payment_method = params[:payment_method].delete(:type).constantize.new(params[:payment_method]) - @object = @payment_method - invoke_callbacks(:create, :before) - if @payment_method.save - invoke_callbacks(:create, :after) - flash[:success] = I18n.t(:successfully_created, :resource => I18n.t(:payment_method)) - respond_with(@payment_method, :location => edit_admin_payment_method_path(@payment_method)) - else - invoke_callbacks(:create, :fails) - respond_with(@payment_method) - end - end - - def update - invoke_callbacks(:update, :before) - payment_method_type = params[:payment_method].delete(:type) - if @payment_method['type'].to_s != payment_method_type - @payment_method.update_column(:type, payment_method_type) - @payment_method = PaymentMethod.find(params[:id]) - end - - payment_method_params = params[ActiveModel::Naming.param_key(@payment_method)] || {} - attributes = params[:payment_method].merge(payment_method_params) - attributes.each do |k,v| - if k.include?("password") && attributes[k].blank? - attributes.delete(k) - end - end - - if @payment_method.update_attributes(attributes) - invoke_callbacks(:update, :after) - flash[:success] = I18n.t(:successfully_updated, :resource => I18n.t(:payment_method)) - respond_with(@payment_method, :location => edit_admin_payment_method_path(@payment_method)) - else - invoke_callbacks(:update, :fails) - respond_with(@payment_method) - end - end - - private - - def load_data - @providers = Gateway.providers.sort{|p1, p2| p1.name <=> p2.name } - end - end - end -end diff --git a/core/app/controllers/spree/admin/payments_controller.rb b/core/app/controllers/spree/admin/payments_controller.rb deleted file mode 100644 index 1e14e26166f..00000000000 --- a/core/app/controllers/spree/admin/payments_controller.rb +++ /dev/null @@ -1,99 +0,0 @@ -module Spree - module Admin - class PaymentsController < Spree::Admin::BaseController - before_filter :load_order, :only => [:create, :new, :index, :fire] - before_filter :load_payment, :except => [:create, :new, :index] - before_filter :load_data - - respond_to :html - - def index - @payments = @order.payments - - respond_with(@payments) - end - - def new - @payment = @order.payments.build - respond_with(@payment) - end - - def create - @payment = @order.payments.build(object_params) - if @payment.payment_method.is_a?(Spree::Gateway) && @payment.payment_method.payment_profiles_supported? && params[:card].present? and params[:card] != 'new' - @payment.source = CreditCard.find_by_id(params[:card]) - end - - begin - unless @payment.save - respond_with(@payment) { |format| format.html { redirect_to admin_order_payments_path(@order) } } - return - end - - if @order.completed? - @payment.process! - flash[:success] = flash_message_for(@payment, :successfully_created) - - respond_with(@payment) { |format| format.html { redirect_to admin_order_payments_path(@order) } } - else - #This is the first payment (admin created order) - until @order.completed? - @order.next! - end - flash[:success] = t(:new_order_completed) - respond_with(@payment) { |format| format.html { redirect_to admin_order_url(@order) } } - end - - rescue Spree::Core::GatewayError => e - flash[:error] = "#{e.message}" - respond_with(@payment) { |format| format.html { redirect_to new_admin_order_payment_path(@order) } } - end - end - - def fire - return unless event = params[:e] and @payment.payment_source - - # Because we have a transition method also called void, we do this to avoid conflicts. - event = "void_transaction" if event == "void" - if @payment.send("#{event}!") - flash[:success] = t(:payment_updated) - else - flash[:error] = t(:cannot_perform_operation) - end - rescue Spree::Core::GatewayError => ge - flash[:error] = "#{ge.message}" - ensure - respond_with(@payment) { |format| format.html { redirect_to admin_order_payments_path(@order) } } - end - - private - - def object_params - if params[:payment] and params[:payment_source] and source_params = params.delete(:payment_source)[params[:payment][:payment_method_id]] - params[:payment][:source_attributes] = source_params - end - params[:payment] - end - - def load_data - @amount = params[:amount] || load_order.total - @payment_methods = PaymentMethod.available - if @payment and @payment.payment_method - @payment_method = @payment.payment_method - else - @payment_method = @payment_methods.first - end - @previous_cards = @order.credit_cards.with_payment_profile - end - - def load_order - @order = Order.find_by_number!(params[:order_id]) - end - - def load_payment - @payment = Payment.find(params[:id]) - end - - end - end -end diff --git a/core/app/controllers/spree/admin/product_properties_controller.rb b/core/app/controllers/spree/admin/product_properties_controller.rb deleted file mode 100644 index d90f8f005d6..00000000000 --- a/core/app/controllers/spree/admin/product_properties_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Spree - module Admin - class ProductPropertiesController < ResourceController - belongs_to 'spree/product', :find_by => :permalink - before_filter :find_properties - before_filter :setup_property, :only => [:index] - - private - def find_properties - @properties = Spree::Property.pluck(:name) - end - - def setup_property - @product.product_properties.build - end - end - end -end diff --git a/core/app/controllers/spree/admin/products_controller.rb b/core/app/controllers/spree/admin/products_controller.rb deleted file mode 100644 index 8e279ced22c..00000000000 --- a/core/app/controllers/spree/admin/products_controller.rb +++ /dev/null @@ -1,105 +0,0 @@ -module Spree - module Admin - class ProductsController < ResourceController - helper 'spree/products' - -# before_filter :check_json_authenticity, :only => :index - before_filter :load_data, :except => :index - create.before :create_before - update.before :update_before - - def show - redirect_to( :action => :edit ) - end - - def index - respond_with(@collection) - end - - def update - if params[:product][:taxon_ids].present? - params[:product][:taxon_ids] = params[:product][:taxon_ids].split(',') - end - super - end - - def destroy - @product = Product.where(:permalink => params[:id]).first! - @product.delete - - flash.notice = I18n.t('notice_messages.product_deleted') - - respond_with(@product) do |format| - format.html { redirect_to collection_url } - format.js { render_js_for_destroy } - end - end - - def clone - @new = @product.duplicate - - if @new.save - flash.notice = I18n.t('notice_messages.product_cloned') - else - flash.notice = I18n.t('notice_messages.product_not_cloned') - end - - respond_with(@new) { |format| format.html { redirect_to edit_admin_product_url(@new) } } - end - - protected - - def find_resource - Product.find_by_permalink!(params[:id]) - end - - def location_after_save - edit_admin_product_url(@product) - end - - def load_data - @taxons = Taxon.order(:name) - @option_types = OptionType.order(:name) - @tax_categories = TaxCategory.order(:name) - @shipping_categories = ShippingCategory.order(:name) - end - - def collection - return @collection if @collection.present? - params[:q] ||= {} - params[:q][:deleted_at_null] ||= "1" - - params[:q][:s] ||= "name asc" - - @search = super.ransack(params[:q]) - @collection = @search.result. - group_by_products_id. - includes(product_includes). - page(params[:page]). - per(Spree::Config[:admin_products_per_page]) - - if params[:q][:s].include?("master_default_price_amount") - # PostgreSQL compatibility - @collection = @collection.group("spree_prices.amount") - end - @collection - end - - def create_before - return if params[:product][:prototype_id].blank? - @prototype = Spree::Prototype.find(params[:product][:prototype_id]) - end - - def update_before - # note: we only reset the product properties if we're receiving a post from the form on that tab - return unless params[:clear_product_properties] - params[:product] ||= {} - end - - def product_includes - [{:variants => [:images, {:option_values => :option_type}]}, {:master => [:images, :default_price]}] - end - - end - end -end diff --git a/core/app/controllers/spree/admin/properties_controller.rb b/core/app/controllers/spree/admin/properties_controller.rb deleted file mode 100644 index a0c86674192..00000000000 --- a/core/app/controllers/spree/admin/properties_controller.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Spree - module Admin - class PropertiesController < ResourceController - - # Looks like this action is unused - def filtered - @properties = Property.where('lower(name) LIKE ?', "%#{params[:q].mb_chars.downcase}%").order(:name) - respond_with(@properties) do |format| - format.html { render :template => "spree/admin/properties/filtered", :formats => [:html], :handlers => [:erb], :layout => false } - end - end - end - end -end diff --git a/core/app/controllers/spree/admin/reports_controller.rb b/core/app/controllers/spree/admin/reports_controller.rb deleted file mode 100644 index 02cd59c3f29..00000000000 --- a/core/app/controllers/spree/admin/reports_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Spree - module Admin - class ReportsController < Spree::Admin::BaseController - respond_to :html - - AVAILABLE_REPORTS = { - :sales_total => { :name => I18n.t(:sales_total), :description => I18n.t(:sales_total_description) } - } - - def index - @reports = AVAILABLE_REPORTS - respond_with(@reports) - end - - def sales_total - params[:q] = {} unless params[:q] - - if params[:q][:created_at_gt].blank? - params[:q][:created_at_gt] = Time.zone.now.beginning_of_month - else - params[:q][:created_at_gt] = Time.zone.parse(params[:q][:created_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:created_at_lt].blank? - params[:q][:created_at_lt] = Time.zone.parse(params[:q][:created_at_lt]).end_of_day rescue "" - end - - if params[:q].delete(:completed_at_not_null) == "1" - params[:q][:completed_at_not_null] = true - else - params[:q][:completed_at_not_null] = false - end - - params[:q][:s] ||= "created_at desc" - - @search = Order.complete.ransack(params[:q]) - @orders = @search.result - - @totals = {} - @orders.each do |order| - @totals[order.currency] = { :item_total => ::Money.new(0, order.currency), :adjustment_total => ::Money.new(0, order.currency), :sales_total => ::Money.new(0, order.currency) } unless @totals[order.currency] - @totals[order.currency][:item_total] += order.display_item_total.money - @totals[order.currency][:adjustment_total] += order.display_adjustment_total.money - @totals[order.currency][:sales_total] += order.display_total.money - end - end - - end - end -end diff --git a/core/app/controllers/spree/admin/resource_controller.rb b/core/app/controllers/spree/admin/resource_controller.rb deleted file mode 100644 index 2be7aee74dc..00000000000 --- a/core/app/controllers/spree/admin/resource_controller.rb +++ /dev/null @@ -1,250 +0,0 @@ -require 'spree/core/action_callbacks' - -class Spree::Admin::ResourceController < Spree::Admin::BaseController - helper_method :new_object_url, :edit_object_url, :object_url, :collection_url - before_filter :load_resource - rescue_from ActiveRecord::RecordNotFound, :with => :resource_not_found - - respond_to :html - respond_to :js, :except => [:show, :index] - - def new - invoke_callbacks(:new_action, :before) - respond_with(@object) do |format| - format.html { render :layout => !request.xhr? } - format.js { render :layout => false } - end - end - - def edit - respond_with(@object) do |format| - format.html { render :layout => !request.xhr? } - format.js { render :layout => false } - end - end - - def update - invoke_callbacks(:update, :before) - if @object.update_attributes(params[object_name]) - invoke_callbacks(:update, :after) - flash[:success] = flash_message_for(@object, :successfully_updated) - respond_with(@object) do |format| - format.html { redirect_to location_after_save } - format.js { render :layout => false } - end - else - invoke_callbacks(:update, :fails) - respond_with(@object) - end - end - - def create - invoke_callbacks(:create, :before) - if @object.save - invoke_callbacks(:create, :after) - flash[:success] = flash_message_for(@object, :successfully_created) - respond_with(@object) do |format| - format.html { redirect_to location_after_save } - format.js { render :layout => false } - end - else - invoke_callbacks(:create, :fails) - respond_with(@object) - end - end - - def update_positions - params[:positions].each do |id, index| - model_class.where(:id => id).update_all(:position => index) - end - - respond_to do |format| - format.js { render :text => 'Ok' } - end - end - - def destroy - invoke_callbacks(:destroy, :before) - if @object.destroy - invoke_callbacks(:destroy, :after) - flash[:success] = flash_message_for(@object, :successfully_removed) - respond_with(@object) do |format| - format.html { redirect_to collection_url } - format.js { render :partial => "spree/admin/shared/destroy" } - end - else - invoke_callbacks(:destroy, :fails) - respond_with(@object) do |format| - format.html { redirect_to collection_url } - end - end - end - - protected - - def resource_not_found - flash[:error] = flash_message_for(model_class.new, :not_found) - redirect_to collection_url - end - - class << self - attr_accessor :parent_data - attr_accessor :callbacks - - def belongs_to(model_name, options = {}) - @parent_data ||= {} - @parent_data[:model_name] = model_name - @parent_data[:model_class] = model_name.to_s.classify.constantize - @parent_data[:find_by] = options[:find_by] || :id - end - - def new_action - @callbacks ||= {} - @callbacks[:new_action] ||= Spree::ActionCallbacks.new - end - - def create - @callbacks ||= {} - @callbacks[:create] ||= Spree::ActionCallbacks.new - end - - def update - @callbacks ||= {} - @callbacks[:update] ||= Spree::ActionCallbacks.new - end - - def destroy - @callbacks ||= {} - @callbacks[:destroy] ||= Spree::ActionCallbacks.new - end - end - - def model_class - "Spree::#{controller_name.classify}".constantize - end - - def model_name - parent_data[:model_name].gsub('spree/', '') - end - - def object_name - controller_name.singularize - end - - def load_resource - if member_action? - @object ||= load_resource_instance - instance_variable_set("@#{object_name}", @object) - else - @collection ||= collection - instance_variable_set("@#{controller_name}", @collection) - end - end - - def load_resource_instance - if new_actions.include?(params[:action].to_sym) - build_resource - elsif params[:id] - find_resource - end - end - - def parent_data - self.class.parent_data - end - - def parent - if parent_data.present? - @parent ||= parent_data[:model_class].where(parent_data[:find_by] => params["#{model_name}_id"]).first - instance_variable_set("@#{model_name}", @parent) - else - nil - end - end - - def find_resource - if parent_data.present? - parent.send(controller_name).find(params[:id]) - else - model_class.find(params[:id]) - end - end - - def build_resource - if parent_data.present? - parent.send(controller_name).build(params[object_name]) - else - model_class.new(params[object_name]) - end - end - - def collection - return parent.send(controller_name) if parent_data.present? - if model_class.respond_to?(:accessible_by) && !current_ability.has_block?(params[:action], model_class) - model_class.accessible_by(current_ability) - else - model_class.scoped - end - end - - def location_after_save - collection_url - end - - def invoke_callbacks(action, callback_type) - callbacks = self.class.callbacks || {} - return if callbacks[action].nil? - case callback_type.to_sym - when :before then callbacks[action].before_methods.each {|method| send method } - when :after then callbacks[action].after_methods.each {|method| send method } - when :fails then callbacks[action].fails_methods.each {|method| send method } - end - end - - # URL helpers - - def new_object_url(options = {}) - if parent_data.present? - spree.new_polymorphic_url([:admin, parent, model_class], options) - else - spree.new_polymorphic_url([:admin, model_class], options) - end - end - - def edit_object_url(object, options = {}) - if parent_data.present? - spree.send "edit_admin_#{model_name}_#{object_name}_url", parent, object, options - else - spree.send "edit_admin_#{object_name}_url", object, options - end - end - - def object_url(object = nil, options = {}) - target = object ? object : @object - if parent_data.present? - spree.send "admin_#{model_name}_#{object_name}_url", parent, target, options - else - spree.send "admin_#{object_name}_url", target, options - end - end - - def collection_url(options = {}) - if parent_data.present? - spree.polymorphic_url([:admin, parent, model_class], options) - else - spree.polymorphic_url([:admin, model_class], options) - end - end - - def collection_actions - [:index] - end - - def member_action? - !collection_actions.include? params[:action].to_sym - end - - def new_actions - [:new, :create] - end -end diff --git a/core/app/controllers/spree/admin/return_authorizations_controller.rb b/core/app/controllers/spree/admin/return_authorizations_controller.rb deleted file mode 100644 index 39d7b891952..00000000000 --- a/core/app/controllers/spree/admin/return_authorizations_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Spree - module Admin - class ReturnAuthorizationsController < ResourceController - belongs_to 'spree/order', :find_by => :number - - update.after :associate_inventory_units - create.after :associate_inventory_units - - def fire - @return_authorization.send("#{params[:e]}!") - flash[:success] = t(:return_authorization_updated) - respond_with(@return_authorization) { |format| format.html { redirect_to :back } } - end - - protected - def associate_inventory_units - (params[:return_quantity] || []).each { |variant_id, qty| @return_authorization.add_variant(variant_id.to_i, qty.to_i) } - end - end - end -end diff --git a/core/app/controllers/spree/admin/shipments_controller.rb b/core/app/controllers/spree/admin/shipments_controller.rb deleted file mode 100644 index 1cf33487481..00000000000 --- a/core/app/controllers/spree/admin/shipments_controller.rb +++ /dev/null @@ -1,97 +0,0 @@ -module Spree - module Admin - class ShipmentsController < Spree::Admin::BaseController - before_filter :load_shipping_methods, :except => [:country_changed, :index] - - respond_to :html - - def index - @shipments = order.shipments - respond_with(@shipments) - end - - def new - build_shipment - respond_with(shipment) - end - - def create - build_shipment - assign_inventory_units - if shipment.save - flash[:success] = flash_message_for(shipment, :successfully_created) - respond_with(shipment) do |format| - format.html { redirect_to edit_admin_order_shipment_path(order, shipment) } - end - else - respond_with(shipment) { |format| format.html { render :action => 'new' } } - end - end - - def edit - shipment.special_instructions = order.special_instructions - respond_with(shipment) - end - - def update - assign_inventory_units - if shipment.update_attributes params[:shipment] - # copy back to order if instructions are enabled - order.special_instructions = params[:shipment][:special_instructions] if Spree::Config[:shipping_instructions] - order.shipping_method = order.shipment.shipping_method - order.save - - flash[:success] = flash_message_for(shipment, :successfully_updated) - return_path = order.completed? ? edit_admin_order_shipment_path(order, shipment) : admin_order_adjustments_path(order) - respond_with(@object) do |format| - format.html { redirect_to return_path } - end - else - respond_with(shipment) { |format| format.html { render :action => 'edit' } } - end - end - - def destroy - shipment.destroy - respond_with(shipment) { |format| format.js { render_js_for_destroy } } - end - - def fire - if shipment.send("#{params[:e]}") - flash[:success] = t(:shipment_updated) - else - flash[:error] = t(:cannot_perform_operation) - end - - respond_with(shipment) { |format| format.html { redirect_to :back } } - end - - private - - def load_shipping_methods - @shipping_methods = ShippingMethod.all_available(order, :back_end) - end - - def assign_inventory_units - return unless params.has_key? :inventory_units - shipment.inventory_unit_ids = params[:inventory_units].keys - end - - def order - @order ||= Order.find_by_number(params[:order_id]) - end - - def shipment - @shipment ||= Shipment.find_by_number(params[:id]) - end - - def build_shipment - @shipment = order.shipments.build - @shipment.address ||= order.ship_address - @shipment.address ||= Address.new(:country_id => Spree::Config[:default_country_id]) - @shipment.shipping_method ||= order.shipping_method - @shipment.attributes = params[:shipment] - end - end - end -end diff --git a/core/app/controllers/spree/admin/shipping_methods_controller.rb b/core/app/controllers/spree/admin/shipping_methods_controller.rb deleted file mode 100644 index e1edb5e4ecd..00000000000 --- a/core/app/controllers/spree/admin/shipping_methods_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Spree - module Admin - class ShippingMethodsController < ResourceController - before_filter :load_data, :except => [:index] - before_filter :set_shipping_category, :only => [:create, :update] - - def destroy - @object.touch :deleted_at - - flash[:success] = flash_message_for(@object, :successfully_removed) - - respond_with(@object) do |format| - format.html { redirect_to collection_url } - format.js { render_js_for_destroy } - end - end - - private - - def set_shipping_category - return true if params[:shipping_method][:shipping_category_id] == "" - @shipping_method.shipping_category = Spree::ShippingCategory.find(params[:shipping_method][:shipping_category_id]) - @shipping_method.save - params[:shipping_method].delete(:shipping_category_id) - end - - def location_after_save - edit_admin_shipping_method_path(@shipping_method) - end - - def load_data - @available_zones = Zone.order(:name) - @calculators = ShippingMethod.calculators.sort_by(&:name) - end - end - end -end diff --git a/core/app/controllers/spree/admin/states_controller.rb b/core/app/controllers/spree/admin/states_controller.rb deleted file mode 100644 index 20736203fe4..00000000000 --- a/core/app/controllers/spree/admin/states_controller.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Spree - module Admin - class StatesController < ResourceController - belongs_to 'spree/country' - before_filter :load_data - - def index - respond_with(@collection) do |format| - format.html - format.js { render :partial => 'state_list' } - end - end - - protected - - def location_after_save - admin_country_states_url(@country) - end - - def collection - super.order(:name) - end - - def load_data - @countries = Country.order(:name) - end - end - end -end diff --git a/core/app/controllers/spree/admin/tax_rates_controller.rb b/core/app/controllers/spree/admin/tax_rates_controller.rb deleted file mode 100644 index 89d90988498..00000000000 --- a/core/app/controllers/spree/admin/tax_rates_controller.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Spree - module Admin - class TaxRatesController < ResourceController - before_filter :load_data - - update.after :update_after - create.after :create_after - - private - - def load_data - @available_zones = Zone.order(:name) - @available_categories = TaxCategory.order(:name) - @calculators = TaxRate.calculators.sort_by(&:name) - end - - def update_after - Rails.cache.delete('vat_rates') - end - - def create_after - Rails.cache.delete('vat_rates') - end - end - end -end diff --git a/core/app/controllers/spree/admin/tax_settings_controller.rb b/core/app/controllers/spree/admin/tax_settings_controller.rb deleted file mode 100644 index 516fa6283ae..00000000000 --- a/core/app/controllers/spree/admin/tax_settings_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Spree - module Admin - class TaxSettingsController < Spree::Admin::BaseController - - def update - Spree::Config.set(params[:preferences]) - - respond_to do |format| - format.html { - redirect_to admin_tax_settings_path - } - end - end - - end - end -end diff --git a/core/app/controllers/spree/admin/taxons_controller.rb b/core/app/controllers/spree/admin/taxons_controller.rb deleted file mode 100644 index 0c6d769e766..00000000000 --- a/core/app/controllers/spree/admin/taxons_controller.rb +++ /dev/null @@ -1,120 +0,0 @@ -module Spree - module Admin - class TaxonsController < Spree::Admin::BaseController - - respond_to :html, :json, :js - - def search - if params[:ids] - @taxons = Spree::Taxon.where(:id => params[:ids].split(',')) - else - @taxons = Spree::Taxon.limit(20).search(:name_cont => params[:q]).result - end - end - - def create - @taxonomy = Taxonomy.find(params[:taxonomy_id]) - @taxon = @taxonomy.taxons.build(params[:taxon]) - if @taxon.save - respond_with(@taxon) do |format| - format.json {render :json => @taxon.to_json } - end - else - flash[:error] = t('errors.messages.could_not_create_taxon') - respond_with(@taxon) do |format| - format.html { redirect_to @taxonomy ? edit_admin_taxonomy_url(@taxonomy) : admin_taxonomies_url } - end - end - end - - def edit - @taxonomy = Taxonomy.find(params[:taxonomy_id]) - @taxon = @taxonomy.taxons.find(params[:id]) - @permalink_part = @taxon.permalink.split("/").last - - respond_with(:admin, @taxon) - end - - def update - @taxonomy = Taxonomy.find(params[:taxonomy_id]) - @taxon = @taxonomy.taxons.find(params[:id]) - parent_id = params[:taxon][:parent_id] - new_position = params[:taxon][:position] - - if parent_id || new_position #taxon is being moved - new_parent = parent_id.nil? ? @taxon.parent : Taxon.find(parent_id.to_i) - new_position = new_position.nil? ? -1 : new_position.to_i - - # Bellow is a very complicated way of finding where in nested set we - # should actually move the taxon to achieve sane results, - # JS is giving us the desired position, which was awesome for previous setup, - # but now it's quite complicated to find where we should put it as we have - # to differenciate between moving to the same branch, up down and into - # first position. - new_siblings = new_parent.children - if new_position <= 0 && new_siblings.empty? - @taxon.move_to_child_of(new_parent) - elsif new_parent.id != @taxon.parent_id - if new_position == 0 - @taxon.move_to_left_of(new_siblings.first) - else - @taxon.move_to_right_of(new_siblings[new_position-1]) - end - elsif new_position < new_siblings.index(@taxon) - @taxon.move_to_left_of(new_siblings[new_position]) # we move up - else - @taxon.move_to_right_of(new_siblings[new_position-1]) # we move down - end - # Reset legacy position, if any extensions still rely on it - new_parent.children.reload.each{|t| t.update_column(:position, t.position)} - - if parent_id - @taxon.reload - @taxon.set_permalink - @taxon.save! - @update_children = true - end - end - - if params.key? "permalink_part" - parent_permalink = @taxon.permalink.split("/")[0...-1].join("/") - parent_permalink += "/" unless parent_permalink.blank? - params[:taxon][:permalink] = parent_permalink + params[:permalink_part] - end - #check if we need to rename child taxons if parent name or permalink changes - @update_children = true if params[:taxon][:name] != @taxon.name || params[:taxon][:permalink] != @taxon.permalink - - if @taxon.update_attributes(params[:taxon]) - flash[:success] = flash_message_for(@taxon, :successfully_updated) - end - - #rename child taxons - if @update_children - @taxon.descendants.each do |taxon| - taxon.reload - taxon.set_permalink - taxon.save! - end - end - - respond_with(@taxon) do |format| - format.html {redirect_to edit_admin_taxonomy_url(@taxonomy) } - format.json {render :json => @taxon.to_json } - end - end - - def destroy - @taxon = Taxon.find(params[:id]) - @taxon.destroy - respond_with(@taxon) { |format| format.json { render :json => '' } } - end - - private - - def load_product - Product.find_by_permalink! params[:product_id] - end - - end - end -end diff --git a/core/app/controllers/spree/admin/variants_controller.rb b/core/app/controllers/spree/admin/variants_controller.rb deleted file mode 100644 index 04d635238c0..00000000000 --- a/core/app/controllers/spree/admin/variants_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -module Spree - module Admin - class VariantsController < ResourceController - belongs_to 'spree/product', :find_by => :permalink - create.before :create_before - new_action.before :new_before - - def index - respond_with(collection) - end - - def search - search_params = { :product_name_cont => params[:q], :sku_cont => params[:q] } - @variants = Spree::Variant.ransack(search_params.merge(:m => 'or')).result - end - - # override the destory method to set deleted_at value - # instead of actually deleting the product. - def destroy - @variant = Variant.find(params[:id]) - @variant.deleted_at = Time.now() - if @variant.save - flash[:success] = I18n.t('notice_messages.variant_deleted') - else - flash[:success] = I18n.t('notice_messages.variant_not_deleted') - end - - respond_with(@variant) do |format| - format.html { redirect_to admin_product_variants_url(params[:product_id]) } - format.js { render_js_for_destroy } - end - end - - protected - - def create_before - option_values = params[:new_variant] - option_values.each_value {|id| @object.option_values << OptionValue.find(id)} - @object.save - end - - - def new_before - @object.attributes = @object.product.master.attributes.except('id', 'created_at', 'deleted_at', - 'sku', 'is_master', 'count_on_hand') - # Shallow Clone of the default price to populate the price field. - @object.default_price = @object.product.master.default_price.clone - end - - def collection - @deleted = (params.key?(:deleted) && params[:deleted] == "on") ? "checked" : "" - - if @deleted.blank? - @collection ||= super - else - @collection ||= Variant.where(:product_id => parent.id).deleted - end - @collection - end - end - end -end - diff --git a/core/app/controllers/spree/admin/zones_controller.rb b/core/app/controllers/spree/admin/zones_controller.rb deleted file mode 100644 index 610dab9f88f..00000000000 --- a/core/app/controllers/spree/admin/zones_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Spree - module Admin - class ZonesController < ResourceController - before_filter :load_data, :except => [:index] - - def new - @zone.zone_members.build - respond_with(@zone) - end - - protected - - def collection - params[:q] ||= {} - params[:q][:s] ||= "ascend_by_name" - @search = super.ransack(params[:q]) - @zones = @search.result.page(params[:page]).per(Spree::Config[:orders_per_page]) - end - - def load_data - @countries = Country.order(:name) - @states = State.order(:name) - @zones = Zone.order(:name) - end - end - end -end diff --git a/core/app/controllers/spree/base_controller.rb b/core/app/controllers/spree/base_controller.rb index 820904f34c1..5feaacbe199 100644 --- a/core/app/controllers/spree/base_controller.rb +++ b/core/app/controllers/spree/base_controller.rb @@ -1,8 +1,16 @@ require 'cancan' +require_dependency 'spree/core/controller_helpers/strong_parameters' class Spree::BaseController < ApplicationController include Spree::Core::ControllerHelpers::Auth include Spree::Core::ControllerHelpers::RespondWith + include Spree::Core::ControllerHelpers::SSL include Spree::Core::ControllerHelpers::Common + include Spree::Core::ControllerHelpers::Search + include Spree::Core::ControllerHelpers::Store + include Spree::Core::ControllerHelpers::StrongParameters + respond_to :html end + +require 'spree/i18n/initializer' \ No newline at end of file diff --git a/core/app/controllers/spree/checkout_controller.rb b/core/app/controllers/spree/checkout_controller.rb deleted file mode 100644 index fb5ecfa0782..00000000000 --- a/core/app/controllers/spree/checkout_controller.rb +++ /dev/null @@ -1,123 +0,0 @@ -module Spree - # Handles checkout logic. This is somewhat contrary to standard REST convention since there is not actually a - # Checkout object. There's enough distinct logic specific to checkout which has nothing to do with updating an - # order that this approach is waranted. - - # Much of this file, especially the update action is overriden in the promo gem. - # This is to allow for the promo behavior but also allow the promo gem to be - # removed if the functionality is not needed. - - class CheckoutController < Spree::StoreController - ssl_required - - before_filter :load_order - before_filter :ensure_valid_state - before_filter :associate_user - rescue_from Spree::Core::GatewayError, :with => :rescue_from_spree_gateway_error - - respond_to :html - - # Updates the order and advances to the next state (when possible.) - # Overriden by the promo gem if it exists. - def update - if @order.update_attributes(object_params) - fire_event('spree.checkout.update') - - if @order.next - state_callback(:after) - else - flash[:error] = t(:payment_processing_failed) - respond_with(@order, :location => checkout_state_path(@order.state)) - return - end - - if @order.state == "complete" || @order.completed? - flash.notice = t(:order_processed_successfully) - flash[:commerce_tracking] = "nothing special" - respond_with(@order, :location => completion_route) - else - respond_with(@order, :location => checkout_state_path(@order.state)) - end - else - respond_with(@order) { |format| format.html { render :edit } } - end - end - - private - def ensure_valid_state - unless skip_state_validation? - if (params[:state] && !@order.checkout_steps.include?(params[:state])) || - (!params[:state] && !@order.checkout_steps.include?(@order.state)) - @order.state = 'cart' - redirect_to checkout_state_path(@order.checkout_steps.first) - end - end - end - - # Should be overriden if you have areas of your checkout that don't match - # up to a step within checkout_steps, such as a registration step - def skip_state_validation? - false - end - - def load_order - @order = current_order - redirect_to cart_path and return unless @order and @order.checkout_allowed? - raise_insufficient_quantity and return if @order.insufficient_stock_lines.present? - redirect_to cart_path and return if @order.completed? - @order.state = params[:state] if params[:state] - state_callback(:before) - end - - # Provides a route to redirect after order completion - def completion_route - order_path(@order) - end - - def object_params - # For payment step, filter order parameters to produce the expected nested attributes for a single payment and its source, discarding attributes for payment methods other than the one selected - if @order.payment? - if params[:payment_source].present? && source_params = params.delete(:payment_source)[params[:order][:payments_attributes].first[:payment_method_id].underscore] - params[:order][:payments_attributes].first[:source_attributes] = source_params - end - if (params[:order][:payments_attributes]) - params[:order][:payments_attributes].first[:amount] = @order.total - end - end - params[:order] - end - - def raise_insufficient_quantity - flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) - redirect_to cart_path - end - - def state_callback(before_or_after = :before) - method_name = :"#{before_or_after}_#{@order.state}" - send(method_name) if respond_to?(method_name, true) - end - - def before_address - @order.bill_address ||= Address.default - @order.ship_address ||= Address.default - end - - def before_delivery - return if params[:order].present? - @order.shipping_method ||= (@order.rate_hash.first && @order.rate_hash.first[:shipping_method]) - end - - def before_payment - current_order.payments.destroy_all if request.put? - end - - def after_complete - session[:order_id] = nil - end - - def rescue_from_spree_gateway_error - flash[:error] = t(:spree_gateway_error_flash_for_checkout) - render :edit - end - end -end diff --git a/core/app/controllers/spree/content_controller.rb b/core/app/controllers/spree/content_controller.rb deleted file mode 100644 index a652f8b4a40..00000000000 --- a/core/app/controllers/spree/content_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Spree - class ContentController < Spree::StoreController - # Don't serve local files or static assets - before_filter { render_404 if params[:path] =~ /(\.|\\)/ } - - rescue_from ActionView::MissingTemplate, :with => :render_404 - caches_page :show, :index, :if => Proc.new { Spree::Config[:cache_static_content] } - - respond_to :html - - def show - respond_with do |format| - format.html { render :action => params[:path] } - end - end - - def cvv - respond_with do |format| - format.html { render :layout => false } - end - end - end -end diff --git a/core/app/controllers/spree/countries_controller.rb b/core/app/controllers/spree/countries_controller.rb deleted file mode 100644 index fec92763b93..00000000000 --- a/core/app/controllers/spree/countries_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Spree - class CountriesController < BaseController - ssl_allowed :index - - respond_to :js - - def index - respond_with @states_required = Spree::Country.states_required_by_country_id.to_json, :layout => nil - end - end -end - diff --git a/core/app/controllers/spree/home_controller.rb b/core/app/controllers/spree/home_controller.rb deleted file mode 100644 index a62cf480fb5..00000000000 --- a/core/app/controllers/spree/home_controller.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Spree - class HomeController < Spree::StoreController - helper 'spree/products' - respond_to :html - - def index - @searcher = Spree::Config.searcher_class.new(params) - @searcher.current_user = try_spree_current_user - @searcher.current_currency = current_currency - @products = @searcher.retrieve_products - respond_with(@products) - end - end -end diff --git a/core/app/controllers/spree/locale_controller.rb b/core/app/controllers/spree/locale_controller.rb deleted file mode 100644 index 851f65b46e3..00000000000 --- a/core/app/controllers/spree/locale_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Spree - class LocaleController < Spree::StoreController - def set - if request.referer && request.referer.starts_with?('http://' + request.host) - session['user_return_to'] = request.referer - end - if params[:locale] && I18n.available_locales.include?(params[:locale].to_sym) - session[:locale] = I18n.locale = params[:locale].to_sym - flash.notice = t(:locale_changed) - else - flash[:error] = t(:locale_not_changed) - end - redirect_back_or_default(root_path) - end - end -end diff --git a/core/app/controllers/spree/orders_controller.rb b/core/app/controllers/spree/orders_controller.rb deleted file mode 100644 index 4b16bd40e7a..00000000000 --- a/core/app/controllers/spree/orders_controller.rb +++ /dev/null @@ -1,82 +0,0 @@ -module Spree - class OrdersController < Spree::StoreController - ssl_required :show - - rescue_from ActiveRecord::RecordNotFound, :with => :render_404 - helper 'spree/products' - - respond_to :html - - def show - @order = Order.find_by_number!(params[:id]) - respond_with(@order) - end - - def update - @order = current_order - if @order.update_attributes(params[:order]) - @order.line_items = @order.line_items.select {|li| li.quantity > 0 } - fire_event('spree.order.contents_changed') - respond_with(@order) do |format| - format.html do - if params.has_key?(:checkout) - @order.next_transition.run_callbacks - redirect_to checkout_state_path(@order.checkout_steps.first) - else - redirect_to cart_path - end - end - end - else - respond_with(@order) - end - end - - # Shows the current incomplete order from the session - def edit - @order = current_order(true) - associate_user - end - - # Adds a new item to the order (creating a new order if none already exists) - # - # Parameters can be passed using the following possible parameter configurations: - # - # * Single variant/quantity pairing - # +:variants => { variant_id => quantity }+ - # - # * Multiple products at once - # +:products => { product_id => variant_id, product_id => variant_id }, :quantity => quantity+ - # +:products => { product_id => variant_id, product_id => variant_id }, :quantity => { variant_id => quantity, variant_id => quantity }+ - def populate - @order = current_order(true) - - params[:products].each do |product_id,variant_id| - quantity = params[:quantity].to_i if !params[:quantity].is_a?(Hash) - quantity = params[:quantity][variant_id].to_i if params[:quantity].is_a?(Hash) - @order.add_variant(Variant.find(variant_id), quantity, current_currency) if quantity > 0 - end if params[:products] - - params[:variants].each do |variant_id, quantity| - quantity = quantity.to_i - @order.add_variant(Variant.find(variant_id), quantity, current_currency) if quantity > 0 - end if params[:variants] - - fire_event('spree.cart.add') - fire_event('spree.order.contents_changed') - respond_with(@order) { |format| format.html { redirect_to cart_path } } - end - - def empty - if @order = current_order - @order.empty! - end - - redirect_to spree.cart_path - end - - def accurate_title - @order && @order.completed? ? "#{Order.model_name.human} #{@order.number}" : t(:shopping_cart) - end - end -end diff --git a/core/app/controllers/spree/products_controller.rb b/core/app/controllers/spree/products_controller.rb deleted file mode 100644 index 8517599d8e2..00000000000 --- a/core/app/controllers/spree/products_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -module Spree - class ProductsController < Spree::StoreController - before_filter :load_product, :only => :show - rescue_from ActiveRecord::RecordNotFound, :with => :render_404 - helper 'spree/taxons' - - respond_to :html - - def index - @searcher = Config.searcher_class.new(params) - @searcher.current_user = try_spree_current_user - @searcher.current_currency = current_currency - @products = @searcher.retrieve_products - respond_with(@products) - end - - def show - return unless @product - - @variants = @product.variants_including_master.active(current_currency).includes([:option_values, :images]) - @product_properties = @product.product_properties.includes(:property) - - referer = request.env['HTTP_REFERER'] - if referer - begin - referer_path = URI.parse(request.env['HTTP_REFERER']).path - # Fix for #2249 - rescue URI::InvalidURIError - # Do nothing - else - if referer_path && referer_path.match(/\/t\/(.*)/) - @taxon = Taxon.find_by_permalink($1) - end - end - end - - respond_with(@product) - end - - private - def accurate_title - @product ? @product.name : super - end - - def load_product - if try_spree_current_user.try(:has_spree_role?, "admin") - @product = Product.find_by_permalink!(params[:id]) - else - @product = Product.active(current_currency).find_by_permalink!(params[:id]) - end - end - end -end diff --git a/core/app/controllers/spree/states_controller.rb b/core/app/controllers/spree/states_controller.rb deleted file mode 100644 index b118c126306..00000000000 --- a/core/app/controllers/spree/states_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Spree - class StatesController < Spree::StoreController - ssl_allowed :index - - respond_to :js - - def index - # we return ALL known information, since billing country isn't restricted - # by shipping country - respond_with @state_info = Spree::State.states_group_by_country_id.to_json, :layout => nil - end - end -end diff --git a/core/app/controllers/spree/store_controller.rb b/core/app/controllers/spree/store_controller.rb deleted file mode 100644 index ad8c4a06256..00000000000 --- a/core/app/controllers/spree/store_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Spree - class StoreController < Spree::BaseController - include Spree::Core::ControllerHelpers::Order - - def unauthorized - render 'spree/shared/unauthorized', :layout => Spree::Config[:layout], :status => 401 - end - - end -end - diff --git a/core/app/controllers/spree/taxons_controller.rb b/core/app/controllers/spree/taxons_controller.rb deleted file mode 100644 index 944fdfa9a43..00000000000 --- a/core/app/controllers/spree/taxons_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Spree - class TaxonsController < Spree::StoreController - rescue_from ActiveRecord::RecordNotFound, :with => :render_404 - helper 'spree/products' - - respond_to :html - - def show - @taxon = Taxon.find_by_permalink!(params[:id]) - return unless @taxon - - @searcher = Spree::Config.searcher_class.new(params.merge(:taxon => @taxon.id)) - @searcher.current_user = try_spree_current_user - @searcher.current_currency = current_currency - @products = @searcher.retrieve_products - - respond_with(@taxon) - end - - private - def accurate_title - @taxon ? @taxon.name : super - end - end -end diff --git a/core/app/helpers/spree/admin/inventory_settings_helper.rb b/core/app/helpers/spree/admin/inventory_settings_helper.rb deleted file mode 100644 index 713b09201d0..00000000000 --- a/core/app/helpers/spree/admin/inventory_settings_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Spree - module Admin - module InventorySettingsHelper - def show_not(true_or_false) - true_or_false ? '' : t(:not) - end - end - end -end diff --git a/core/app/helpers/spree/admin/navigation_helper.rb b/core/app/helpers/spree/admin/navigation_helper.rb deleted file mode 100644 index 6c00232895c..00000000000 --- a/core/app/helpers/spree/admin/navigation_helper.rb +++ /dev/null @@ -1,134 +0,0 @@ -module Spree - module Admin - module NavigationHelper - # Make an admin tab that coveres one or more resources supplied by symbols - # Option hash may follow. Valid options are - # * :label to override link text, otherwise based on the first resource name (translated) - # * :route to override automatically determining the default route - # * :match_path as an alternative way to control when the tab is active, /products would match /admin/products, /admin/products/5/variants etc. - def tab(*args) - options = {:label => args.first.to_s} - if args.last.is_a?(Hash) - options = options.merge(args.pop) - end - options[:route] ||= "admin_#{args.first}" - - destination_url = options[:url] || spree.send("#{options[:route]}_path") - - titleized_label = t(options[:label], :default => options[:label]).titleize - - css_classes = [] - - if options[:icon] - link = link_to_with_icon(options[:icon], titleized_label, destination_url) - css_classes << 'tab-with-icon' - else - link = link_to(titleized_label, destination_url) - end - - selected = if options[:match_path] - request.fullpath.starts_with?("#{root_path}admin#{options[:match_path]}") - else - args.include?(controller.controller_name.to_sym) - end - css_classes << 'selected' if selected - - if options[:css_class] - css_classes << options[:css_class] - end - content_tag('li', link, :class => css_classes.join(' ')) - end - - def link_to_clone(resource, options={}) - options[:data] = {:action => 'clone'} - link_to_with_icon('icon-copy', t(:clone), clone_admin_product_url(resource), options) - end - - def link_to_new(resource) - options[:data] = {:action => 'new'} - link_to_with_icon('icon-plus', t(:new), edit_object_url(resource)) - end - - def link_to_edit(resource, options={}) - options[:data] = {:action => 'edit'} - link_to_with_icon('icon-edit', t(:edit), edit_object_url(resource), options) - end - - def link_to_edit_url(url, options={}) - options[:data] = {:action => 'edit'} - link_to_with_icon('icon-edit', t(:edit), url, options) - end - - def link_to_delete(resource, options={}) - url = options[:url] || object_url(resource) - name = options[:name] || t(:delete) - options[:class] = "delete-resource" - options[:data] = { :confirm => t(:are_you_sure), :action => 'remove' } - link_to_with_icon 'icon-trash', name, url, options - end - - def link_to_with_icon(icon_name, text, url, options = {}) - options[:class] = (options[:class].to_s + " icon_link with-tip #{icon_name}").strip - options[:class] += ' no-text' if options[:no_text] - options[:title] = text if options[:no_text] - text = options[:no_text] ? '' : raw("#{text}") - options.delete(:no_text) - link_to(text, url, options) - end - - def icon(icon_name) - icon_name ? content_tag(:i, '', :class => icon_name) : '' - end - - def button(text, icon_name = nil, button_type = 'submit', options={}) - button_tag(text, options.merge(:type => button_type, :class => "#{icon_name} button")) - end - - def button_link_to(text, url, html_options = {}) - if (html_options[:method] && - html_options[:method].to_s.downcase != 'get' && - !html_options[:remote]) - form_tag(url, :method => html_options.delete(:method)) do - button(text, html_options.delete(:icon), nil, html_options) - end - else - if html_options['data-update'].nil? && html_options[:remote] - object_name, action = url.split('/')[-2..-1] - html_options['data-update'] = [action, object_name.singularize].join('_') - end - - html_options.delete('data-update') unless html_options['data-update'] - - html_options[:class] = 'button' - - if html_options[:icon] - html_options[:class] += " #{html_options[:icon]}" - end - link_to(text_for_button_link(text, html_options), url, html_options) - end - end - - def text_for_button_link(text, html_options) - s = '' - s << text - raw(s) - end - - def configurations_menu_item(link_text, url, description = '') - %( - #{link_to(link_text, url)} - #{description} - - ).html_safe - end - - def configurations_sidebar_menu_item(link_text, url, options = {}) - is_active = url.ends_with?(controller.controller_name) || url.ends_with?( "#{controller.controller_name}/edit") - options.merge!(:class => is_active ? 'active' : nil) - content_tag(:li, options) do - link_to(link_text, url) - end - end - end - end -end diff --git a/core/app/helpers/spree/admin/orders_helper.rb b/core/app/helpers/spree/admin/orders_helper.rb deleted file mode 100644 index bb48d8770a7..00000000000 --- a/core/app/helpers/spree/admin/orders_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Spree - module Admin - module OrdersHelper - # Renders all the extension partials that may have been specified in the extensions - def event_links - links = [] - @order_events.sort.each do |event| - if @order.send("can_#{event}?") - links << button_link_to(t(event), fire_admin_order_url(@order, :e => event), - :method => :put, - :icon => "icon-#{event}", - :data => { :confirm => t(:order_sure_want_to, :event => t(event)) }) - end - end - links.join(' ').html_safe - end - end - end -end diff --git a/core/app/helpers/spree/base_helper.rb b/core/app/helpers/spree/base_helper.rb index dbe02de83dd..163b5c40293 100644 --- a/core/app/helpers/spree/base_helper.rb +++ b/core/app/helpers/spree/base_helper.rb @@ -13,40 +13,26 @@ def current_spree_page?(url) end def link_to_cart(text = nil) - return "" if current_spree_page?(cart_path) - - text = text ? h(text) : t('cart') + text = text ? h(text) : Spree.t('cart') css_class = nil - if current_order.nil? or current_order.line_items.empty? - text = "#{text}: (#{t('empty')})" + if simple_current_order.nil? or simple_current_order.item_count.zero? + text = "#{text}: (#{Spree.t('empty')})" css_class = 'empty' else - text = "#{text}: (#{current_order.item_count}) #{current_order.display_total}".html_safe + text = "#{text}: (#{simple_current_order.item_count}) #{simple_current_order.display_total.to_html}" css_class = 'full' end - link_to text, cart_path, :class => css_class + link_to text.html_safe, spree.cart_path, :class => "cart-info #{css_class}" end # human readable list of variant options def variant_options(v, options={}) - list = v.options_text - - # We shouldn't show out of stock if the product is infact in stock - # or when we're not allowing backorders. - unless v.in_stock? - list = if options[:include_style] - content_tag(:span, "(#{t(:out_of_stock)}) #{list}", :class => 'out-of-stock') - else - "#{t(:out_of_stock)} #{list}" - end - end - - list + v.options_text end - def meta_data_tags + def meta_data object = instance_variable_get('@'+controller_name.singularize) meta = {} @@ -56,16 +42,19 @@ def meta_data_tags end if meta[:description].blank? && object.kind_of?(Spree::Product) - meta[:description] = strip_tags(object.description) + meta[:description] = strip_tags(truncate(object.description, length: 160, separator: ' ')) end meta.reverse_merge!({ - :keywords => Spree::Config[:default_meta_keywords], - :description => Spree::Config[:default_meta_description] - }) + keywords: current_store.meta_keywords, + description: current_store.meta_description, + }) if meta[:keywords].blank? or meta[:description].blank? + meta + end - meta.map do |name, content| - tag('meta', :name => name, :content => content) + def meta_data_tags + meta_data.map do |name, content| + tag('meta', name: name, content: content) end.join("\n") end @@ -75,41 +64,52 @@ def body_class end def logo(image_path=Spree::Config[:logo]) - link_to image_tag(image_path), root_path + link_to image_tag(image_path), spree.root_path end def flash_messages(opts = {}) - opts[:ignore_types] = [:commerce_tracking].concat(Array(opts[:ignore_types]) || []) + ignore_types = ["order_completed"].concat(Array(opts[:ignore_types]).map(&:to_s) || []) flash.each do |msg_type, text| - unless opts[:ignore_types].include?(msg_type) - concat(content_tag :div, text, :class => "flash #{msg_type}") + unless ignore_types.include?(msg_type) + concat(content_tag :div, text, class: "flash #{msg_type}") end end nil end - def breadcrumbs(taxon, separator=" » ") + def breadcrumbs(taxon, separator=" » ", breadcrumb_class="inline") return "" if current_page?("/") || taxon.nil? - separator = raw(separator) - crumbs = [content_tag(:li, link_to(t(:home) , root_path) + separator)] + + crumbs = [[Spree.t(:home), spree.root_path]] + if taxon - crumbs << content_tag(:li, link_to(t(:products) , products_path) + separator) - crumbs << taxon.ancestors.collect { |ancestor| content_tag(:li, link_to(ancestor.name , seo_url(ancestor)) + separator) } unless taxon.ancestors.empty? - crumbs << content_tag(:li, content_tag(:span, link_to(taxon.name , seo_url(taxon)))) + crumbs << [Spree.t(:products), products_path] + crumbs += taxon.ancestors.collect { |a| [a.name, spree.nested_taxons_path(a.permalink)] } unless taxon.ancestors.empty? + crumbs << [taxon.name, spree.nested_taxons_path(taxon.permalink)] else - crumbs << content_tag(:li, content_tag(:span, t(:products))) + crumbs << [Spree.t(:products), products_path] end - crumb_list = content_tag(:ul, raw(crumbs.flatten.map{|li| li.mb_chars}.join), :class => 'inline') - content_tag(:nav, crumb_list, :id => 'breadcrumbs', :class => 'sixteen columns') + + separator = raw(separator) + + crumbs.map! do |crumb| + content_tag(:li, itemscope:"itemscope", itemtype:"http://data-vocabulary.org/Breadcrumb") do + link_to(crumb.last, itemprop: "url") do + content_tag(:span, crumb.first, itemprop: "title") + end + (crumb == crumbs.last ? '' : separator) + end + end + + content_tag(:nav, content_tag(:ul, raw(crumbs.map(&:mb_chars).join), class: breadcrumb_class), id: 'breadcrumbs', class: 'sixteen columns') end def taxons_tree(root_taxon, current_taxon, max_level = 1) - return '' if max_level < 1 || root_taxon.children.empty? - content_tag :ul, :class => 'taxons-list' do + return '' if max_level < 1 || root_taxon.leaf? + content_tag :ul, class: 'taxons-list' do root_taxon.children.map do |taxon| css_class = (current_taxon && current_taxon.self_and_ancestors.include?(taxon)) ? 'current' : nil - content_tag :li, :class => css_class do + content_tag :li, class: css_class do link_to(taxon.name, seo_url(taxon)) + taxons_tree(taxon, current_taxon, max_level - 1) end @@ -118,7 +118,7 @@ def taxons_tree(root_taxon, current_taxon, max_level = 1) end def available_countries - checkout_zone = Zone.find_by_name(Spree::Config[:checkout_zone]) + checkout_zone = Zone.find_by(name: Spree::Config[:checkout_zone]) if checkout_zone && checkout_zone.kind == 'country' countries = checkout_zone.country_list @@ -127,9 +127,9 @@ def available_countries end countries.collect do |country| - country.name = I18n.t(country.iso, :scope => 'countries', :default => country.name) + country.name = Spree.t(country.iso, scope: 'country_names', default: country.name) country - end.sort { |a, b| a.name <=> b.name } + end.sort_by { |c| c.name.parameterize } end def seo_url(taxon) @@ -144,14 +144,13 @@ def gem_available?(name) Gem.available?(name) end - def money(amount) - ActiveSupport::Deprecation.warn("[SPREE] Spree::BaseHelper#money will be deprecated. It relies upon a single master currency. You can instead create a Spree::Money.new(amount, { :currency => your_currency}) or see if the object you're working with returns a Spree::Money object to use.") - Spree::Money.new(amount) + def display_price(product_or_variant) + product_or_variant.price_in(current_currency).display_price.to_html end def pretty_time(time) - [I18n.l(time.to_date, :format => :long), - time.strftime("%H:%m %p")].join(" ") + [I18n.l(time.to_date, format: :long), + time.strftime("%l:%M %p")].join(" ") end def method_missing(method_name, *args, &block) @@ -163,28 +162,48 @@ def method_missing(method_name, *args, &block) end end + def link_to_tracking(shipment, options = {}) + return unless shipment.tracking && shipment.shipping_method + + if shipment.tracking_url + link_to(shipment.tracking, shipment.tracking_url, options) + else + content_tag(:span, shipment.tracking) + end + end + private # Returns style of image or nil def image_style_from_method_name(method_name) - if style = method_name.to_s.sub(/_image$/, '') + if method_name.to_s.match(/_image$/) && style = method_name.to_s.sub(/_image$/, '') possible_styles = Spree::Image.attachment_definitions[:attachment][:styles] style if style.in? possible_styles.with_indifferent_access end end + def create_product_image_tag(image, product, options, style) + options.reverse_merge! alt: image.alt.blank? ? product.name : image.alt + image_tag image.attachment.url(style), options + end + def define_image_method(style) self.class.send :define_method, "#{style}_image" do |product, *options| options = options.first || {} if product.images.empty? - image_tag "noimage/#{style}.png", options + if !product.is_a?(Spree::Variant) && !product.variant_images.empty? + create_product_image_tag(product.variant_images.first, product, options, style) + else + if product.is_a?(Variant) && !product.product.variant_images.empty? + create_product_image_tag(product.product.variant_images.first, product, options, style) + else + image_tag "noimage/#{style}.png", options + end + end else - image = product.images.first - options.reverse_merge! :alt => image.alt.blank? ? product.name : image.alt - image_tag image.attachment.url(style), options + create_product_image_tag(product.images.first, product, options, style) end end end - end end diff --git a/core/app/helpers/spree/checkout_helper.rb b/core/app/helpers/spree/checkout_helper.rb index dbd4a600835..55eca96a7a0 100644 --- a/core/app/helpers/spree/checkout_helper.rb +++ b/core/app/helpers/spree/checkout_helper.rb @@ -7,7 +7,7 @@ def checkout_states def checkout_progress states = checkout_states items = states.map do |state| - text = t("order_state.#{state}").titleize + text = Spree.t("order_state.#{state}").titleize css_classes = [] current_index = states.index(@order.state) @@ -23,9 +23,9 @@ def checkout_progress css_classes << 'first' if state_index == 0 css_classes << 'last' if state_index == states.length - 1 # It'd be nice to have separate classes but combining them with a dash helps out for IE6 which only sees the last class - content_tag('li', content_tag('span', text), :class => css_classes.join('-')) + content_tag('li', content_tag('span', text), class: css_classes.join('-')) end - content_tag('ol', raw(items.join("\n")), :class => 'progress-steps', :id => "checkout-step-#{@order.state}") + content_tag('ol', raw(items.join("\n")), class: 'progress-steps', id: "checkout-step-#{@order.state}") end end end diff --git a/core/app/helpers/spree/orders_helper.rb b/core/app/helpers/spree/orders_helper.rb new file mode 100644 index 00000000000..456b01bcc16 --- /dev/null +++ b/core/app/helpers/spree/orders_helper.rb @@ -0,0 +1,17 @@ +require 'truncate_html' +require 'app/helpers/truncate_html_helper' + +module Spree + module OrdersHelper + include TruncateHtmlHelper + + def truncated_product_description(product) + truncate_html(raw(product.description)) + end + + def order_just_completed?(order) + flash[:order_completed] && order.present? + end + end +end + diff --git a/core/app/helpers/spree/products_helper.rb b/core/app/helpers/spree/products_helper.rb index 047c2e5a828..599b3466807 100644 --- a/core/app/helpers/spree/products_helper.rb +++ b/core/app/helpers/spree/products_helper.rb @@ -9,42 +9,52 @@ def variant_price(variant) end end - # returns the formatted price for the specified variant as a difference from product price def variant_price_diff(variant) - diff = variant.amount_in(current_currency) - variant.product.amount_in(current_currency) - return nil if diff == 0 - if diff > 0 - "(#{t(:add)}: #{Spree::Money.new(diff.abs, { :currency => current_currency })})" - else - "(#{t(:subtract)}: #{Spree::Money.new(diff.abs, { :currency => current_currency })})" - end + variant_amount = variant.amount_in(current_currency) + product_amount = variant.product.amount_in(current_currency) + return if variant_amount == product_amount || product_amount.nil? + diff = variant.amount_in(current_currency) - product_amount + amount = Spree::Money.new(diff.abs, currency: current_currency).to_html + label = diff > 0 ? :add : :subtract + "(#{Spree.t(label)}: #{amount})".html_safe end # returns the formatted full price for the variant, if at least one variant price differs from product price def variant_full_price(variant) product = variant.product unless product.variants.active(current_currency).all? { |v| v.price == product.price } - Spree::Money.new(variant.price, { :currency => current_currency }).to_s + Spree::Money.new(variant.price, { currency: current_currency }).to_html end end # converts line breaks in product description into

    tags (for html display purposes) def product_description(product) - raw(product.description.gsub(/(.*?)\r?\n\r?\n/m, '

    \1

    ')) + if Spree::Config[:show_raw_product_description] + raw(product.description) + else + raw(product.description.gsub(/(.*?)\r?\n\r?\n/m, '

    \1

    ')) + end end def line_item_description(variant) - description = variant.product.description - if description.present? - truncate(strip_tags(description.gsub(' ', ' ')), :length => 100) + ActiveSupport::Deprecation.warn "line_item_description(variant) is deprecated and may be removed from future releases, use line_item_description_text(line_item.description) instead.", caller + + line_item_description_text(variant.product.description) + end + + def line_item_description_text description_text + if description_text.present? + truncate(strip_tags(description_text.gsub(' ', ' ').squish), length: 100) else - t(:product_has_no_description) + Spree.t(:product_has_no_description) end end - def get_taxonomies - @taxonomies ||= Spree::Taxonomy.includes(:root => :children) + def cache_key_for_products + count = @products.count + max_updated_at = (@products.maximum(:updated_at) || Date.today).to_s(:number) + "#{I18n.locale}/#{current_currency}/spree/products/all-#{params[:page]}-#{max_updated_at}-#{count}" end end end diff --git a/core/app/helpers/spree/store_helper.rb b/core/app/helpers/spree/store_helper.rb new file mode 100644 index 00000000000..631c9a0fce3 --- /dev/null +++ b/core/app/helpers/spree/store_helper.rb @@ -0,0 +1,16 @@ +# Methods added to this helper will be available to all templates in the frontend. +module Spree + module StoreHelper + + # helper to determine if its appropriate to show the store menu + def store_menu? + %w{thank_you}.exclude? params[:action] + end + + def cache_key_for_taxons + max_updated_at = @taxons.maximum(:updated_at).to_i + parts = [@taxon.try(:id), max_updated_at].compact.join("-") + "#{I18n.locale}/taxons/#{parts}" + end + end +end diff --git a/core/app/helpers/spree/taxons_helper.rb b/core/app/helpers/spree/taxons_helper.rb index 6571eb3bcd4..aed476e733e 100644 --- a/core/app/helpers/spree/taxons_helper.rb +++ b/core/app/helpers/spree/taxons_helper.rb @@ -4,12 +4,12 @@ module TaxonsHelper # that we can use configurations as well as make it easier for end users to override this determination. One idea is # to show the most popular products for a particular taxon (that is an exercise left to the developer.) def taxon_preview(taxon, max=4) - products = taxon.active_products.limit(max) - if (products.size < max) && Spree::Config[:show_descendents] + products = taxon.active_products.select("DISTINCT (spree_products.id), spree_products.*, spree_products_taxons.position").limit(max) + if (products.size < max) + products_arel = Spree::Product.arel_table taxon.descendants.each do |taxon| to_get = max - products.length - products += taxon.active_products.limit(to_get) - products = products.uniq + products += taxon.active_products.select("DISTINCT (spree_products.id), spree_products.*, spree_products_taxons.position").where(products_arel[:id].not_in(products.map(&:id))).limit(to_get) break if products.size >= max end end diff --git a/core/app/mailers/spree/base_mailer.rb b/core/app/mailers/spree/base_mailer.rb new file mode 100644 index 00000000000..075513594ab --- /dev/null +++ b/core/app/mailers/spree/base_mailer.rb @@ -0,0 +1,18 @@ +module Spree + class BaseMailer < ActionMailer::Base + + def from_address + Spree::Config[:mails_from] + end + + def money(amount, currency = Spree::Config[:currency]) + Spree::Money.new(amount, currency: currency).to_s + end + helper_method :money + + def mail(headers={}, &block) + super if Spree::Config[:send_core_emails] + end + + end +end diff --git a/core/app/mailers/spree/order_mailer.rb b/core/app/mailers/spree/order_mailer.rb index e8c84260796..ef462e8656e 100644 --- a/core/app/mailers/spree/order_mailer.rb +++ b/core/app/mailers/spree/order_mailer.rb @@ -1,21 +1,17 @@ module Spree - class OrderMailer < ActionMailer::Base - helper 'spree/base' - + class OrderMailer < BaseMailer def confirm_email(order, resend = false) - @order = order - subject = (resend ? "[#{t(:resend).upcase}] " : '') - subject += "#{Spree::Config[:site_name]} #{t('order_mailer.confirm_email.subject')} ##{order.number}" - mail(:to => order.email, - :subject => subject) + @order = order.respond_to?(:id) ? order : Spree::Order.find(order) + subject = (resend ? "[#{Spree.t(:resend).upcase}] " : '') + subject += "#{Spree::Store.current.name} #{Spree.t('order_mailer.confirm_email.subject')} ##{@order.number}" + mail(to: @order.email, from: from_address, subject: subject) end def cancel_email(order, resend = false) - @order = order - subject = (resend ? "[#{t(:resend).upcase}] " : '') - subject += "#{Spree::Config[:site_name]} #{t('order_mailer.cancel_email.subject')} ##{order.number}" - mail(:to => order.email, - :subject => subject) + @order = order.respond_to?(:id) ? order : Spree::Order.find(order) + subject = (resend ? "[#{Spree.t(:resend).upcase}] " : '') + subject += "#{Spree::Store.current.name} #{Spree.t('order_mailer.cancel_email.subject')} ##{@order.number}" + mail(to: @order.email, from: from_address, subject: subject) end end end diff --git a/core/app/mailers/spree/reimbursement_mailer.rb b/core/app/mailers/spree/reimbursement_mailer.rb new file mode 100644 index 00000000000..c4fce321758 --- /dev/null +++ b/core/app/mailers/spree/reimbursement_mailer.rb @@ -0,0 +1,10 @@ +module Spree + class ReimbursementMailer < BaseMailer + def reimbursement_email(reimbursement, resend = false) + @reimbursement = reimbursement.respond_to?(:id) ? reimbursement : Spree::Reimbursement.find(reimbursement) + subject = (resend ? "[#{Spree.t(:resend).upcase}] " : '') + subject += "#{Spree::Store.current.name} #{Spree.t('reimbursement_mailer.reimbursement_email.subject')} ##{@reimbursement.order.number}" + mail(to: @reimbursement.order.email, from: from_address, subject: subject) + end + end +end diff --git a/core/app/mailers/spree/shipment_mailer.rb b/core/app/mailers/spree/shipment_mailer.rb index 145ea6cd57a..b01e4ddb140 100644 --- a/core/app/mailers/spree/shipment_mailer.rb +++ b/core/app/mailers/spree/shipment_mailer.rb @@ -1,13 +1,10 @@ module Spree - class ShipmentMailer < ActionMailer::Base - helper 'spree/base' - + class ShipmentMailer < BaseMailer def shipped_email(shipment, resend = false) - @shipment = shipment - subject = (resend ? "[#{t(:resend).upcase}] " : '') - subject += "#{Spree::Config[:site_name]} #{t('shipment_mailer.shipped_email.subject')} ##{shipment.order.number}" - mail(:to => shipment.order.email, - :subject => subject) + @shipment = shipment.respond_to?(:id) ? shipment : Spree::Shipment.find(shipment) + subject = (resend ? "[#{Spree.t(:resend).upcase}] " : '') + subject += "#{Spree::Store.current.name} #{Spree.t('shipment_mailer.shipped_email.subject')} ##{@shipment.order.number}" + mail(to: @shipment.order.email, from: from_address, subject: subject) end end end diff --git a/core/app/mailers/spree/test_mailer.rb b/core/app/mailers/spree/test_mailer.rb index 6a365834e48..804cb8f1691 100644 --- a/core/app/mailers/spree/test_mailer.rb +++ b/core/app/mailers/spree/test_mailer.rb @@ -1,10 +1,8 @@ module Spree - class TestMailer < ActionMailer::Base - def test_email(mail_method, user) - @mail_method = mail_method - subject = "#{Spree::Config[:site_name]} #{t('test_mailer.test_email.subject')}" - mail(:to => user.email, - :subject => subject) + class TestMailer < BaseMailer + def test_email(email) + subject = "#{Spree::Store.current.name} #{Spree.t('test_mailer.test_email.subject')}" + mail(to: email, from: from_address, subject: subject) end end end diff --git a/core/app/models/concerns/spree/adjustment_source.rb b/core/app/models/concerns/spree/adjustment_source.rb new file mode 100644 index 00000000000..dd8f486eb8e --- /dev/null +++ b/core/app/models/concerns/spree/adjustment_source.rb @@ -0,0 +1,24 @@ +module Spree + module AdjustmentSource + extend ActiveSupport::Concern + + included do + def deals_with_adjustments_for_deleted_source + adjustment_scope = self.adjustments.includes(:order).references(:spree_orders) + + # For incomplete orders, remove the adjustment completely. + adjustment_scope.where("spree_orders.completed_at IS NULL").destroy_all + + # For complete orders, the source will be invalid. + # Therefore we nullify the source_id, leaving the adjustment in place. + # This would mean that the order's total is not altered at all. + adjustment_scope.where("spree_orders.completed_at IS NOT NULL").each do |adjustment| + adjustment.update_columns( + source_id: nil, + updated_at: Time.now, + ) + end + end + end + end +end diff --git a/core/app/models/concerns/spree/calculated_adjustments.rb b/core/app/models/concerns/spree/calculated_adjustments.rb new file mode 100644 index 00000000000..89655f28a54 --- /dev/null +++ b/core/app/models/concerns/spree/calculated_adjustments.rb @@ -0,0 +1,33 @@ +module Spree + module CalculatedAdjustments + extend ActiveSupport::Concern + + included do + has_one :calculator, class_name: "Spree::Calculator", as: :calculable, inverse_of: :calculable, dependent: :destroy, autosave: true + accepts_nested_attributes_for :calculator + validates :calculator, presence: true + + def self.calculators + spree_calculators.send model_name_without_spree_namespace + end + + def calculator_type + calculator.class.to_s if calculator + end + + def calculator_type=(calculator_type) + klass = calculator_type.constantize if calculator_type + self.calculator = klass.new if klass && !self.calculator.is_a?(klass) + end + + private + def self.model_name_without_spree_namespace + self.to_s.tableize.gsub('/', '_').sub('spree_', '') + end + + def self.spree_calculators + Rails.application.config.spree.calculators + end + end + end +end diff --git a/core/app/models/concerns/spree/default_price.rb b/core/app/models/concerns/spree/default_price.rb new file mode 100644 index 00000000000..4d15a69e5f8 --- /dev/null +++ b/core/app/models/concerns/spree/default_price.rb @@ -0,0 +1,34 @@ +module Spree + module DefaultPrice + extend ActiveSupport::Concern + + included do + has_one :default_price, + -> { where currency: Spree::Config[:currency] }, + class_name: 'Spree::Price', + dependent: :destroy + + delegate_belongs_to :default_price, :display_price, :display_amount, :price, :price=, :currency + + after_save :save_default_price + + def default_price + Spree::Price.unscoped { super } + end + + def has_default_price? + !self.default_price.nil? + end + + private + + def default_price_changed? + default_price && (default_price.changed? || default_price.new_record?) + end + + def save_default_price + default_price.save if default_price_changed? + end + end + end +end diff --git a/core/app/models/concerns/spree/named_type.rb b/core/app/models/concerns/spree/named_type.rb new file mode 100644 index 00000000000..477c9175f33 --- /dev/null +++ b/core/app/models/concerns/spree/named_type.rb @@ -0,0 +1,12 @@ +module Spree + module NamedType + extend ActiveSupport::Concern + + included do + scope :active, -> { where(active: true) } + default_scope -> { order("LOWER(#{self.table_name}.name)") } + + validates :name, presence: true, uniqueness: { case_sensitive: false } + end + end +end diff --git a/core/app/models/concerns/spree/ransackable_attributes.rb b/core/app/models/concerns/spree/ransackable_attributes.rb new file mode 100644 index 00000000000..ef1388f6a34 --- /dev/null +++ b/core/app/models/concerns/spree/ransackable_attributes.rb @@ -0,0 +1,19 @@ +module Spree::RansackableAttributes + extend ActiveSupport::Concern + included do + class_attribute :whitelisted_ransackable_associations + class_attribute :whitelisted_ransackable_attributes + + class_attribute :default_ransackable_attributes + self.default_ransackable_attributes = %w[id name] + + def self.ransackable_associations(*args) + self.whitelisted_ransackable_associations || [] + end + + def self.ransackable_attributes(*args) + self.default_ransackable_attributes | (self.whitelisted_ransackable_attributes || []) + end + end + +end diff --git a/core/app/models/concerns/spree/user_address.rb b/core/app/models/concerns/spree/user_address.rb new file mode 100644 index 00000000000..433365f0b2d --- /dev/null +++ b/core/app/models/concerns/spree/user_address.rb @@ -0,0 +1,30 @@ +module Spree + module UserAddress + extend ActiveSupport::Concern + + included do + belongs_to :bill_address, foreign_key: :bill_address_id, class_name: 'Spree::Address' + alias_attribute :billing_address, :bill_address + + belongs_to :ship_address, foreign_key: :ship_address_id, class_name: 'Spree::Address' + alias_attribute :shipping_address, :ship_address + + accepts_nested_attributes_for :ship_address, :bill_address + + def persist_order_address(order) + b_address = self.bill_address || self.build_bill_address + b_address.attributes = order.bill_address.attributes.except('id', 'updated_at', 'created_at') + b_address.save + self.update_attributes(bill_address_id: b_address.id) + + # May not be present if delivery step has been removed + if order.ship_address + s_address = self.ship_address || self.build_ship_address + s_address.attributes = order.ship_address.attributes.except('id', 'updated_at', 'created_at') + s_address.save + self.update_attributes(ship_address_id: s_address.id) + end + end + end + end +end diff --git a/core/app/models/concerns/spree/user_api_authentication.rb b/core/app/models/concerns/spree/user_api_authentication.rb new file mode 100644 index 00000000000..4785a437228 --- /dev/null +++ b/core/app/models/concerns/spree/user_api_authentication.rb @@ -0,0 +1,13 @@ +module Spree + module UserApiAuthentication + def generate_spree_api_key! + self.spree_api_key = SecureRandom.hex(24) + save! + end + + def clear_spree_api_key! + self.spree_api_key = nil + save! + end + end +end diff --git a/core/app/models/concerns/spree/user_payment_source.rb b/core/app/models/concerns/spree/user_payment_source.rb new file mode 100644 index 00000000000..a6185a18441 --- /dev/null +++ b/core/app/models/concerns/spree/user_payment_source.rb @@ -0,0 +1,19 @@ +module Spree + module UserPaymentSource + extend ActiveSupport::Concern + + included do + has_many :credit_cards, class_name: "Spree::CreditCard", foreign_key: :user_id + def default_credit_card; credit_cards.default.first; end + + def payment_sources + credit_cards.with_payment_profile + end + + def drop_payment_source(source) + gateway = source.payment_method + gateway.disable_customer_profile(source) + end + end + end +end diff --git a/core/app/models/concerns/spree/user_reporting.rb b/core/app/models/concerns/spree/user_reporting.rb new file mode 100644 index 00000000000..70f4fa6ce38 --- /dev/null +++ b/core/app/models/concerns/spree/user_reporting.rb @@ -0,0 +1,27 @@ +module Spree + module UserReporting + def lifetime_value + spree_orders.complete.pluck(:total).sum + end + + def display_lifetime_value + Spree::Money.new(lifetime_value) + end + + def order_count + BigDecimal(spree_orders.complete.count) + end + + def average_order_value + if order_count.to_i > 0 + lifetime_value / order_count + else + BigDecimal("0.00") + end + end + + def display_average_order_value + Spree::Money.new(average_order_value) + end + end +end diff --git a/core/app/models/friendly_id/slug_decorator.rb b/core/app/models/friendly_id/slug_decorator.rb new file mode 100644 index 00000000000..6accaf18634 --- /dev/null +++ b/core/app/models/friendly_id/slug_decorator.rb @@ -0,0 +1,3 @@ +FriendlyId::Slug.class_eval do + acts_as_paranoid +end diff --git a/core/app/models/spree.rb b/core/app/models/spree.rb deleted file mode 100644 index 11e0699bd96..00000000000 --- a/core/app/models/spree.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Spree - def self.table_name_prefix - 'spree_' - end -end diff --git a/core/app/models/spree/ability.rb b/core/app/models/spree/ability.rb index a6864a7fb55..cd873e33a18 100644 --- a/core/app/models/spree/ability.rb +++ b/core/app/models/spree/ability.rb @@ -26,46 +26,40 @@ def initialize(user) self.clear_aliased_actions # override cancan default aliasing (we don't want to differentiate between read and index) - alias_action :edit, :to => :update - alias_action :new, :to => :create - alias_action :new_action, :to => :create - alias_action :show, :to => :read + alias_action :delete, to: :destroy + alias_action :edit, to: :update + alias_action :new, to: :create + alias_action :new_action, to: :create + alias_action :show, to: :read + alias_action :index, :read, to: :display + user ||= Spree.user_class.new + if user.respond_to?(:has_spree_role?) && user.has_spree_role?('admin') can :manage, :all else - ############################# - can :read, Spree.user_class do |resource| - resource == user - end - can :update, Spree.user_class do |resource| - resource == user - end - can :create, Spree.user_class - ############################# - can :read, Order do |order, token| - order.user == user || order.token && token == order.token - end - can :update, Order do |order, token| - order.user == user || order.token && token == order.token - end + can :display, Country + can :display, OptionType + can :display, OptionValue can :create, Order - - can :read, Address do |address| - address.user == user + can [:read, :update], Order do |order, token| + order.user == user || order.guest_token && token == order.guest_token end - - ############################# - can :read, Product - can :index, Product - ############################# - can :read, Taxon - can :index, Taxon - ############################# + can :display, CreditCard, user_id: user.id + can :display, Product + can :display, ProductProperty + can :display, Property + can :create, Spree.user_class + can [:read, :update, :destroy], Spree.user_class, id: user.id + can :display, State + can :display, Taxon + can :display, Taxonomy + can :display, Variant + can :display, Zone end - #include any abilities registered by extensions, etc. + # Include any abilities registered by extensions, etc. Ability.abilities.each do |clazz| ability = clazz.send(:new, user) @rules = rules + ability.send(:rules) diff --git a/core/app/models/spree/activator.rb b/core/app/models/spree/activator.rb deleted file mode 100644 index 97659ac9221..00000000000 --- a/core/app/models/spree/activator.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Spree - class Activator < ActiveRecord::Base - cattr_accessor :event_names - - self.event_names = [ - 'spree.cart.add', - 'spree.order.contents_changed', - 'spree.user.signup' - ] - - def self.register_event_name(name) - self.event_names << name - end - - scope :event_name_starts_with, lambda{ |name| where('event_name LIKE ?', "#{name}%") } - - def self.active - where('(starts_at IS NULL OR starts_at < ?) AND (expires_at IS NULL OR expires_at > ?)', Time.now, Time.now) - end - - def activate(payload) - end - - def expired? - starts_at && Time.now < starts_at || - expires_at && Time.now > expires_at - end - end -end diff --git a/core/app/models/spree/address.rb b/core/app/models/spree/address.rb index 3b03cac29f5..3b6320152a8 100644 --- a/core/app/models/spree/address.rb +++ b/core/app/models/spree/address.rb @@ -1,29 +1,34 @@ module Spree - class Address < ActiveRecord::Base - belongs_to :country - belongs_to :state - - has_many :shipments - - validates :firstname, :lastname, :address1, :city, :zipcode, :country, :phone, :presence => true - validate :state_validate - - attr_accessible :firstname, :lastname, :address1, :address2, - :city, :zipcode, :country_id, :state_id, - :country, :state, :phone, :state_name, - :company, :alternative_phone - - # Disconnected since there's no code to display error messages yet OR matching client-side validation - def phone_validate - return if phone.blank? - n_digits = phone.scan(/[0-9]/).size - valid_chars = (phone =~ /^[-+()\/\s\d]+$/) - errors.add :phone, :invalid unless (n_digits > 5 && valid_chars) - end + class Address < Spree::Base + require 'twitter_cldr' + + belongs_to :country, class_name: "Spree::Country" + belongs_to :state, class_name: "Spree::State" + + has_many :shipments, inverse_of: :address + + validates :firstname, :lastname, :address1, :city, :country, presence: true + validates :zipcode, presence: true, if: :require_zipcode? + validates :phone, presence: true, if: :require_phone? + + validate :state_validate, :postal_code_validate - def self.default + alias_attribute :first_name, :firstname + alias_attribute :last_name, :lastname + + self.whitelisted_ransackable_attributes = %w[firstname lastname] + + def self.build_default country = Spree::Country.find(Spree::Config[:default_country_id]) rescue Spree::Country.first - new({:country => country}, :without_protection => true) + new(country: country) + end + + def self.default(user = nil, kind = "bill") + if user && user_address = user.send(:"#{kind}_address") + user_address.clone + else + build_default + end end # Can modify an address if it's not been used in an order (but checkouts controller has finer control) @@ -70,19 +75,26 @@ def empty? # Generates an ActiveMerchant compatible address hash def active_merchant_hash { - :name => full_name, - :address1 => address1, - :address2 => address2, - :city => city, - :state => state_text, - :zip => zipcode, - :country => country.try(:iso), - :phone => phone + name: full_name, + address1: address1, + address2: address2, + city: city, + state: state_text, + zip: zipcode, + country: country.try(:iso), + phone: phone } end - private + def require_phone? + true + end + + def require_zipcode? + true + end + private def state_validate # Skip state validation without country (also required) # or when disabled by preference @@ -120,5 +132,12 @@ def state_validate errors.add :state, :blank if state.blank? && state_name.blank? end + def postal_code_validate + return if country.blank? || country.iso.blank? || !require_zipcode? + return if !TwitterCldr::Shared::PostalCodes.territories.include?(country.iso.downcase.to_sym) + + postal_code = TwitterCldr::Shared::PostalCodes.for_territory(country.iso) + errors.add(:zipcode, :invalid) if !postal_code.valid?(zipcode.to_s) + end end end diff --git a/core/app/models/spree/adjustment.rb b/core/app/models/spree/adjustment.rb index 9067e8809e2..7fef61de9a0 100644 --- a/core/app/models/spree/adjustment.rb +++ b/core/app/models/spree/adjustment.rb @@ -1,113 +1,116 @@ -# Adjustments represent a change to the +item_total+ of an Order. Each adjustment has an +amount+ that be either -# positive or negative. Adjustments have two useful boolean flags +# Adjustments represent a change to the +item_total+ of an Order. Each adjustment +# has an +amount+ that can be either positive or negative. # -# +mandatory+ +# Adjustments can be "opened" or "closed". +# Once an adjustment is closed, it will not be automatically updated. # -# If this flag is set to true then it means the the charge is required and will not be removed from the -# order, even if the amount is zero. In other words a record will be created even if the amount is zero. -# This is useful for representing things such as shipping and tax charges where you may want to make it explicitly -# clear that no charge was made for such things. +# Boolean attributes: # -# +locked+ +# +mandatory+ # -# The charge is never to be udpated. Typically you would want to freeze certain adjustments after checkout. -# One use case for this is if you want to lock a shipping adjustment so that its value does not change -# in the future when making other trivial edits to the order (like an email change). +# If this flag is set to true then it means the the charge is required and will not +# be removed from the order, even if the amount is zero. In other words a record +# will be created even if the amount is zero. This is useful for representing things +# such as shipping and tax charges where you may want to make it explicitly clear +# that no charge was made for such things. # # +eligible?+ # -# This boolean attributes stores whether this adjustment is currently eligible for its order. Only eligible -# adjustments count towards the order's adjustment total. This allows an adjustment to be preserved if it -# becomes ineligible so it might be reinstated. -# +# This boolean attributes stores whether this adjustment is currently eligible +# for its order. Only eligible adjustments count towards the order's adjustment +# total. This allows an adjustment to be preserved if it becomes ineligible so +# it might be reinstated. module Spree - class Adjustment < ActiveRecord::Base - attr_accessible :amount, :label - - belongs_to :adjustable, :polymorphic => true - belongs_to :source, :polymorphic => true - belongs_to :originator, :polymorphic => true - - validates :label, :presence => true - validates :amount, :numericality => true - - after_save :update_adjustable - after_destroy :update_adjustable + class Adjustment < Spree::Base + belongs_to :adjustable, polymorphic: true, touch: true + belongs_to :source, polymorphic: true + belongs_to :order, class_name: 'Spree::Order', inverse_of: :all_adjustments + + validates :adjustable, presence: true + validates :order, presence: true + validates :label, presence: true + validates :amount, numericality: true + + state_machine :state, initial: :open do + event :close do + transition from: :open, to: :closed + end - # Update the boolean _eligible_ attribute which deterimes which adjustments count towards the order's - # adjustment_total. - def set_eligibility - update_attribute_without_callbacks(:eligible, - mandatory || - (amount != 0 && eligible_for_originator?)) + event :open do + transition from: :closed, to: :open + end end - # Allow originator of the adjustment to perform an additional eligibility of the adjustment - # Should return _true_ if originator is absent or doesn't implement _eligible?_ - def eligible_for_originator? - return true if originator.nil? - !originator.respond_to?(:eligible?) || originator.eligible?(source) - end + after_create :update_adjustable_adjustment_total + after_destroy :update_adjustable_adjustment_total - # Update both the eligibility and amount of the adjustment. Adjustments delegate updating of amount to their Originator - # when present, but only if +locked+ is false. Adjustments that are +locked+ will never change their amount. - # The new adjustment amount will be set by by the +originator+ and is not automatically saved. This makes it save - # to use this method in an after_save hook for other models without causing an infinite recursion problem. - # - # order#update_adjustments passes self as the src, this is so calculations can be performed on the - # current values. If we used source it would load the old record from db for the association - def update!(src = nil) - src ||= source - return if locked? - if originator.present? - originator.update_adjustment(self, src) - end - set_eligibility + scope :open, -> { where(state: 'open') } + scope :closed, -> { where(state: 'closed') } + scope :tax, -> { where(source_type: 'Spree::TaxRate') } + scope :non_tax, -> do + source_type = arel_table[:source_type] + where(source_type.not_eq('Spree::TaxRate').or source_type.eq(nil)) + end + scope :price, -> { where(adjustable_type: 'Spree::LineItem') } + scope :shipping, -> { where(adjustable_type: 'Spree::Shipment') } + scope :optional, -> { where(mandatory: false) } + scope :eligible, -> { where(eligible: true) } + scope :charge, -> { where("#{quoted_table_name}.amount >= 0") } + scope :credit, -> { where("#{quoted_table_name}.amount < 0") } + scope :nonzero, -> { where("#{quoted_table_name}.amount != 0") } + scope :promotion, -> { where(source_type: 'Spree::PromotionAction') } + scope :return_authorization, -> { where(source_type: "Spree::ReturnAuthorization") } + scope :is_included, -> { where(included: true) } + scope :additional, -> { where(included: false) } + + def closed? + state == "closed" end def currency - adjustable.nil? ? Spree::Config[:currency] : adjustable.currency + adjustable ? adjustable.currency : Spree::Config[:currency] end def display_amount - Spree::Money.new(amount, { :currency => currency }).to_s + Spree::Money.new(amount, { currency: currency }) end - private - - def update_adjustable - adjustable.update! if adjustable.is_a? Order - end - - class << self - def tax - where(:originator_type => 'Spree::TaxRate', :adjustable_type => 'Spree::Order') - end - - def price - where(:adjustable_type => 'Spree::LineItem') - end - - def shipping - where(:originator_type => 'Spree::ShippingMethod') - end - - def optional - where(:mandatory => false) - end + def promotion? + source.class < Spree::PromotionAction + end - def eligible - where(:eligible => true) + # Recalculate amount given a target e.g. Order, Shipment, LineItem + # + # Passing a target here would always be recommended as it would avoid + # hitting the database again and would ensure you're compute values over + # the specific object amount passed here. + # + # Noop if the adjustment is locked. + # + # If the adjustment has no source, do not attempt to re-calculate the amount. + # Chances are likely that this was a manually created adjustment in the admin backend. + def update!(target = nil) + amount = self.amount + return amount if closed? + if source.present? + amount = source.compute_amount(target || adjustable) + self.update_columns( + amount: amount, + updated_at: Time.now, + ) + if promotion? + self.update_column(:eligible, source.promotion.eligible?(adjustable)) end + end + amount + end - def charge - where('amount >= 0') - end + private - def credit - where('amount < 0') - end - end + def update_adjustable_adjustment_total + # Cause adjustable's total to be recalculated + ItemAdjustments.new(adjustable).update + end end end diff --git a/core/app/models/spree/alert.rb b/core/app/models/spree/alert.rb deleted file mode 100644 index 21c1a1c39d8..00000000000 --- a/core/app/models/spree/alert.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Spree - class Alert < ActiveResource::Base - self.site = 'http://alerts.spreecommerce.com/' - self.format = :json - - def self.current(host) - find(:all, :params => { :version => Spree.version, - :name => Spree::Config[:site_name], - :host => host, - :rails_env => Rails.env, - :rails_version => Rails.version }) - end - end -end diff --git a/core/app/models/spree/app_configuration.rb b/core/app/models/spree/app_configuration.rb index 3384f94cb15..c5aafe7d826 100644 --- a/core/app/models/spree/app_configuration.rb +++ b/core/app/models/spree/app_configuration.rb @@ -15,70 +15,62 @@ # a.get :color # a.preferred_color # +require "spree/core/search/base" + module Spree class AppConfiguration < Preferences::Configuration - # Alphabetized to more easily lookup particular preferences - preference :address_requires_state, :boolean, :default => true # should state/state_name be required - preference :admin_interface_logo, :string, :default => 'admin/bg/spree_50.png' - preference :admin_products_per_page, :integer, :default => 10 - preference :allow_backorder_shipping, :boolean, :default => false # should only be true if you don't need to track inventory - preference :allow_backorders, :boolean, :default => true - preference :allow_checkout_on_gateway_error, :boolean, :default => false - preference :allow_guest_checkout, :boolean, :default => true - preference :allow_ssl_in_development_and_test, :boolean, :default => false - preference :allow_ssl_in_production, :boolean, :default => true - preference :allow_ssl_in_staging, :boolean, :default => true - preference :alternative_billing_phone, :boolean, :default => false # Request extra phone for bill addr - preference :alternative_shipping_phone, :boolean, :default => false # Request extra phone for ship addr - preference :always_put_site_name_in_title, :boolean, :default => true - preference :auto_capture, :boolean, :default => false # automatically capture the credit card (as opposed to just authorize and capture later) - preference :cache_static_content, :boolean, :default => true - preference :check_for_spree_alerts, :boolean, :default => true - preference :checkout_zone, :string, :default => nil # replace with the name of a zone if you would like to limit the countries - preference :company, :boolean, :default => false # Request company field for billing and shipping addr - preference :create_inventory_units, :boolean, :default => true # should only be false when track_inventory_levels is false, also disables RMA's - preference :currency, :string, :default => "USD" - preference :currency_symbol_position, :string, :default => "before" - preference :display_currency, :boolean, :default => false - preference :default_country_id, :integer, :default => 214 - preference :default_meta_description, :string, :default => 'Spree demo site' - preference :default_meta_keywords, :string, :default => 'spree, demo' - preference :default_seo_title, :string, :default => '' - preference :dismissed_spree_alerts, :string, :default => '' - preference :hide_cents, :boolean, :default => false - preference :last_check_for_spree_alerts, :string, :default => nil - preference :layout, :string, :default => 'spree/layouts/spree_application' - preference :logo, :string, :default => 'admin/bg/spree_50.png' - preference :max_level_in_taxons_menu, :integer, :default => 1 # maximum nesting level in taxons menu - preference :orders_per_page, :integer, :default => 15 - preference :prices_inc_tax, :boolean, :default => false - preference :products_per_page, :integer, :default => 12 - preference :require_master_price, :boolean, :default => true - preference :shipment_inc_vat, :boolean, :default => false - preference :shipping_instructions, :boolean, :default => false # Request instructions/info for shipping - preference :show_descendents, :boolean, :default => true - preference :show_only_complete_orders_by_default, :boolean, :default => true - preference :show_zero_stock_products, :boolean, :default => true - preference :show_variant_full_price, :boolean, :default => false #Displays variant full price or difference with product price. Default false to be compatible with older behavior - preference :site_name, :string, :default => 'Spree Demo Site' - preference :site_url, :string, :default => 'demo.spreecommerce.com' - preference :tax_using_ship_address, :boolean, :default => true - preference :track_inventory_levels, :boolean, :default => true # will not track on_hand values for variants /products + preference :address_requires_state, :boolean, default: true # should state/state_name be required + preference :admin_interface_logo, :string, default: 'logo/spree_50.png' + preference :admin_products_per_page, :integer, default: 10 + preference :allow_checkout_on_gateway_error, :boolean, default: false + preference :allow_guest_checkout, :boolean, default: true + preference :allow_return_item_amount_editing, :boolean, default: false # Determines whether an admin is allowed to change a return item's pre-calculated amount + preference :allow_ssl_in_development_and_test, :boolean, default: false + preference :allow_ssl_in_production, :boolean, default: true + preference :allow_ssl_in_staging, :boolean, default: true + preference :alternative_billing_phone, :boolean, default: false # Request extra phone for bill addr + preference :alternative_shipping_phone, :boolean, default: false # Request extra phone for ship addr + preference :always_include_confirm_step, :boolean, default: false # Ensures confirmation step is always in checkout_progress bar, but does not force a confirm step if your payment methods do not support it. + preference :always_put_site_name_in_title, :boolean, default: true + preference :auto_capture, :boolean, default: false # automatically capture the credit card (as opposed to just authorize and capture later) + preference :auto_capture_on_dispatch, :boolean, default: false # Captures payment for each shipment in Shipment#after_ship callback, and makes Shipment.ready when payment authorized. + preference :binary_inventory_cache, :boolean, default: false # only invalidate product cache when a stock item changes whether it is in_stock + preference :checkout_zone, :string, default: nil # replace with the name of a zone if you would like to limit the countries + preference :company, :boolean, default: false # Request company field for billing and shipping addr + preference :currency, :string, default: "USD" + preference :currency_decimal_mark, :string, default: "." + preference :currency_symbol_position, :string, default: "before" + preference :currency_sign_before_symbol, :boolean, default: true + preference :currency_thousands_separator, :string, default: "," + preference :display_currency, :boolean, default: false + preference :default_country_id, :integer + preference :expedited_exchanges, :boolean, default: false # NOTE this requires payment profiles to be supported on your gateway of choice as well as a delayed job handler to be configured with activejob. kicks off an exchange shipment upon return authorization save. charge customer if they do not return items within timely manner. + preference :expedited_exchanges_days_window, :integer, default: 14 # the amount of days the customer has to return their item after the expedited exchange is shipped in order to avoid being charged + preference :hide_cents, :boolean, default: false + preference :layout, :string, default: 'spree/layouts/spree_application' + preference :logo, :string, default: 'logo/spree_50.png' + preference :max_level_in_taxons_menu, :integer, default: 1 # maximum nesting level in taxons menu + preference :orders_per_page, :integer, default: 15 + preference :properties_per_page, :integer, default: 15 + preference :products_per_page, :integer, default: 12 + preference :promotions_per_page, :integer, default: 15 + preference :customer_returns_per_page, :integer, default: 15 + preference :redirect_https_to_http, :boolean, :default => false + preference :require_master_price, :boolean, default: true + preference :restock_inventory, :boolean, default: true # Determines if a return item is restocked automatically once it has been received + preference :return_eligibility_number_of_days, :integer, default: 365 + preference :shipping_instructions, :boolean, default: false # Request instructions/info for shipping + preference :show_only_complete_orders_by_default, :boolean, default: true + preference :show_variant_full_price, :boolean, default: false #Displays variant full price or difference with product price. Default false to be compatible with older behavior + preference :show_products_without_price, :boolean, default: false + preference :show_raw_product_description, :boolean, :default => false + preference :tax_using_ship_address, :boolean, default: true + preference :track_inventory_levels, :boolean, default: true # Determines whether to track on_hand values for variants / products. - # Preferences related to image settings - preference :attachment_default_url, :string, :default => '/spree/products/:id/:style/:basename.:extension' - preference :attachment_path, :string, :default => ':rails_root/public/spree/products/:id/:style/:basename.:extension' - preference :attachment_url, :string, :default => '/spree/products/:id/:style/:basename.:extension' - preference :attachment_styles, :string, :default => "{\"mini\":\"48x48>\",\"small\":\"100x100>\",\"product\":\"240x240>\",\"large\":\"600x600>\"}" - preference :attachment_default_style, :string, :default => 'product' - preference :s3_access_key, :string - preference :s3_bucket, :string - preference :s3_secret, :string - preference :s3_headers, :string, :default => "{\"Cache-Control\":\"max-age=31557600\"}" - preference :use_s3, :boolean, :default => false # Use S3 for images rather than the file system - preference :s3_protocol, :string - preference :s3_host_alias, :string + # Default mail headers settings + preference :send_core_emails, :boolean, :default => true + preference :mails_from, :string, :default => 'spree@example.com' # searcher_class allows spree extension writers to provide their own Search class def searcher_class @@ -89,6 +81,21 @@ def searcher_class=(sclass) @searcher_class = sclass end - end + # all the following can be deprecated when store prefs are no longer supported + DEPRECATED_STORE_PREFERENCES = { + site_name: :name, + site_url: :url, + default_meta_description: :meta_description, + default_meta_keywords: :meta_keywords, + default_seo_title: :seo_title, + } + DEPRECATED_STORE_PREFERENCES.each do |old_preference_name, store_method| + # support all the old preference methods with a warning + define_method "preferred_#{old_preference_name}" do + ActiveSupport::Deprecation.warn("#{old_preference_name} is no longer supported on Spree::Config, please access it through #{store_method} on Spree::Store") + Store.default.send(store_method) + end + end + end end diff --git a/core/app/models/spree/asset.rb b/core/app/models/spree/asset.rb index 6155e8f241b..288562fafb2 100644 --- a/core/app/models/spree/asset.rb +++ b/core/app/models/spree/asset.rb @@ -1,6 +1,6 @@ module Spree - class Asset < ActiveRecord::Base - belongs_to :viewable, :polymorphic => true - acts_as_list :scope => :viewable + class Asset < Spree::Base + belongs_to :viewable, polymorphic: true, touch: true + acts_as_list scope: [:viewable_id, :viewable_type] end end diff --git a/core/app/models/spree/base.rb b/core/app/models/spree/base.rb new file mode 100644 index 00000000000..4a5fa11fadd --- /dev/null +++ b/core/app/models/spree/base.rb @@ -0,0 +1,18 @@ +class Spree::Base < ActiveRecord::Base + include Spree::Preferences::Preferable + serialize :preferences, Hash + + include Spree::RansackableAttributes + + after_initialize do + self.preferences = default_preferences.merge(preferences) if has_attribute?(:preferences) + end + + if Kaminari.config.page_method_name != :page + def self.page num + send Kaminari.config.page_method_name, num + end + end + + self.abstract_class = true +end diff --git a/core/app/models/spree/billing_integration.rb b/core/app/models/spree/billing_integration.rb index 6241035abb0..53287742af2 100644 --- a/core/app/models/spree/billing_integration.rb +++ b/core/app/models/spree/billing_integration.rb @@ -1,23 +1,20 @@ module Spree class BillingIntegration < PaymentMethod - validates :name, :presence => true + validates :name, presence: true - preference :server, :string, :default => 'test' - preference :test_mode, :boolean, :default => true + preference :server, :string, default: 'test' + preference :test_mode, :boolean, default: true def provider integration_options = options ActiveMerchant::Billing::Base.integration_mode = integration_options[:server].to_sym - integration_options = options integration_options[:test] = true if integration_options[:test_mode] @provider ||= provider_class.new(integration_options) end def options options_hash = {} - self.preferences.each do |key,value| - options_hash[key.to_sym] = value - end + preferences.each { |key, value| options_hash[key.to_sym] = value } options_hash end end diff --git a/core/app/models/spree/calculator.rb b/core/app/models/spree/calculator.rb index 6e7421bf09f..132d63e69a5 100644 --- a/core/app/models/spree/calculator.rb +++ b/core/app/models/spree/calculator.rb @@ -1,12 +1,25 @@ module Spree - class Calculator < ActiveRecord::Base - belongs_to :calculable, :polymorphic => true + class Calculator < Spree::Base + # Conditional check for backwards compatibilty since acts as paranoid was added late https://github.com/spree/spree/issues/5858 + if connection.table_exists?(:spree_calculators) && connection.column_exists?(:spree_calculators, :deleted_at) + acts_as_paranoid + end + + belongs_to :calculable, polymorphic: true - # This method must be overriden in concrete calculator. + # This method calls a compute_ method. must be overriden in concrete calculator. # - # It should return amount computed based on #calculable and/or optional parameter - def compute(something=nil) - raise(NotImplementedError, 'please use concrete calculator') + # It should return amount computed based on #calculable and the computable parameter + def compute(computable) + # Spree::LineItem -> :compute_line_item + computable_name = computable.class.name.demodulize.underscore + method = "compute_#{computable_name}".to_sym + calculator_class = self.class + if respond_to?(method) + self.send(method, computable) + else + raise NotImplementedError, "Please implement '#{method}(#{computable_name})' in your calculator: #{calculator_class.name}" + end end # overwrite to provide description for your calculators @@ -21,7 +34,7 @@ def self.register(*klasses) # Returns all calculators applicable for kind of work def self.calculators - Rails.application.config.spree.calculators.all + Rails.application.config.spree.calculators end def to_s diff --git a/core/app/models/spree/calculator/default_tax.rb b/core/app/models/spree/calculator/default_tax.rb index 69e0b490756..e6df8c8776f 100644 --- a/core/app/models/spree/calculator/default_tax.rb +++ b/core/app/models/spree/calculator/default_tax.rb @@ -3,49 +3,65 @@ module Spree class Calculator::DefaultTax < Calculator def self.description - I18n.t(:default_tax) - end - - def compute(computable) - case computable - when Spree::Order - compute_order(computable) - when Spree::LineItem - compute_line_item(computable) - end + Spree.t(:default_tax) end + # Default tax calculator still needs to support orders for legacy reasons + # Orders created before Spree 2.1 had tax adjustments applied to the order, as a whole. + # Orders created with Spree 2.2 and after, have them applied to the line items individually. + def compute_order(order) - private - - def rate - self.calculable + matched_line_items = order.line_items.select do |line_item| + line_item.tax_category == rate.tax_category end - def compute_order(order) - matched_line_items = order.line_items.select do |line_item| - line_item.product.tax_category == rate.tax_category - end - - line_items_total = matched_line_items.sum(&:total) + line_items_total = matched_line_items.sum(&:total) + if rate.included_in_price + round_to_two_places(line_items_total - ( line_items_total / (1 + rate.amount) ) ) + else round_to_two_places(line_items_total * rate.amount) end + end + + # When it comes to computing shipments or line items: same same. + def compute_shipment_or_line_item(item) + if rate.included_in_price + deduced_total_by_rate(item.pre_tax_amount, rate) + else + round_to_two_places(item.discounted_amount * rate.amount) + end + end - def compute_line_item(line_item) - if line_item.product.tax_category == rate.tax_category - deduced_total_by_rate(line_item.total, rate) + alias_method :compute_shipment, :compute_shipment_or_line_item + alias_method :compute_line_item, :compute_shipment_or_line_item + + def compute_shipping_rate(shipping_rate) + if rate.included_in_price + pre_tax_amount = shipping_rate.cost / (1 + rate.amount) + if rate.zone == shipping_rate.shipment.order.tax_zone + deduced_total_by_rate(pre_tax_amount, rate) else - 0 + deduced_total_by_rate(pre_tax_amount, rate) * - 1 end + else + with_tax_amount = shipping_rate.cost * rate.amount + round_to_two_places(with_tax_amount) end + end - def round_to_two_places(amount) - BigDecimal.new(amount.to_s).round(2, BigDecimal::ROUND_HALF_UP) - end + private - def deduced_total_by_rate(total, rate) - round_to_two_places(total - ( total / (1 + rate.amount) ) ) - end + def rate + self.calculable + end + + def round_to_two_places(amount) + BigDecimal.new(amount.to_s).round(2, BigDecimal::ROUND_HALF_UP) + end + + def deduced_total_by_rate(pre_tax_amount, rate) + round_to_two_places(pre_tax_amount * rate.amount) + end end end diff --git a/core/app/models/spree/calculator/flat_percent_item_total.rb b/core/app/models/spree/calculator/flat_percent_item_total.rb index e5491b449d9..43c667c70cd 100644 --- a/core/app/models/spree/calculator/flat_percent_item_total.rb +++ b/core/app/models/spree/calculator/flat_percent_item_total.rb @@ -2,19 +2,21 @@ module Spree class Calculator::FlatPercentItemTotal < Calculator - preference :flat_percent, :decimal, :default => 0 - - attr_accessible :preferred_flat_percent + preference :flat_percent, :decimal, default: 0 def self.description - I18n.t(:flat_percent) + Spree.t(:flat_percent) end def compute(object) - return unless object.present? and object.line_items.present? - item_total = object.line_items.map(&:amount).sum - value = item_total * BigDecimal(self.preferred_flat_percent.to_s) / 100.0 - (value * 100).round.to_f / 100 + computed_amount = (object.amount * preferred_flat_percent / 100).round(2) + + # We don't want to cause the promotion adjustments to push the order into a negative total. + if computed_amount > object.amount + object.amount + else + computed_amount + end end end end diff --git a/core/app/models/spree/calculator/flat_rate.rb b/core/app/models/spree/calculator/flat_rate.rb index c9637934718..fb46e720b7a 100644 --- a/core/app/models/spree/calculator/flat_rate.rb +++ b/core/app/models/spree/calculator/flat_rate.rb @@ -2,17 +2,19 @@ module Spree class Calculator::FlatRate < Calculator - preference :amount, :decimal, :default => 0 - preference :currency, :string, :default => Spree::Config[:currency] - - attr_accessible :preferred_amount, :preferred_currency + preference :amount, :decimal, default: 0 + preference :currency, :string, default: ->{ Spree::Config[:currency] } def self.description - I18n.t(:flat_rate_per_order) + Spree.t(:flat_rate_per_order) end def compute(object=nil) - self.preferred_amount + if object && preferred_currency.upcase == object.currency.upcase + preferred_amount + else + 0 + end end end end diff --git a/core/app/models/spree/calculator/flexi_rate.rb b/core/app/models/spree/calculator/flexi_rate.rb index e8dbe1d01a5..93fe5c991d0 100644 --- a/core/app/models/spree/calculator/flexi_rate.rb +++ b/core/app/models/spree/calculator/flexi_rate.rb @@ -2,15 +2,13 @@ module Spree class Calculator::FlexiRate < Calculator - preference :first_item, :decimal, :default => 0.0 - preference :additional_item, :decimal, :default => 0.0 - preference :max_items, :integer, :default => 0 - preference :currency, :string, :default => Spree::Config[:currency] - - attr_accessible :preferred_first_item, :preferred_additional_item, :preferred_max_items, :preferred_currency + preference :first_item, :decimal, default: 0.0 + preference :additional_item, :decimal, default: 0.0 + preference :max_items, :integer, default: 0 + preference :currency, :string, default: ->{ Spree::Config[:currency] } def self.description - I18n.t(:flexible_rate) + Spree.t(:flexible_rate) end def self.available?(object) @@ -20,12 +18,11 @@ def self.available?(object) def compute(object) sum = 0 max = self.preferred_max_items.to_i - items_count = object.line_items.map(&:quantity).sum + items_count = object.quantity items_count.times do |i| - # check max value to avoid divide by 0 errors - if (max == 0 && i == 0) || (max > 0) && (i % max == 0) + if i == 0 sum += self.preferred_first_item.to_f - else + elsif ((max > 0) && (i <= (max - 1))) || (max == 0) sum += self.preferred_additional_item.to_f end end diff --git a/core/app/models/spree/calculator/free_shipping.rb b/core/app/models/spree/calculator/free_shipping.rb new file mode 100644 index 00000000000..488af63ec3e --- /dev/null +++ b/core/app/models/spree/calculator/free_shipping.rb @@ -0,0 +1,22 @@ +# TODO: Deprecate this class. +# This calculator will be removed in future versions of Spree. +# The only case where it was used was for Free Shipping Promotions. +# There is now a Promotion Action which deals with these types of promotions instead. +module Spree + class Calculator::FreeShipping < Calculator + def self.description + Spree.t(:free_shipping) + end + + def compute(object) + if object.is_a?(Array) + return if object.empty? + order = object.first.order + else + order = object + end + + order.ship_total + end + end +end \ No newline at end of file diff --git a/core/app/models/spree/calculator/per_item.rb b/core/app/models/spree/calculator/per_item.rb deleted file mode 100644 index 682d37e2c15..00000000000 --- a/core/app/models/spree/calculator/per_item.rb +++ /dev/null @@ -1,41 +0,0 @@ -require_dependency 'spree/calculator' - -module Spree - class Calculator::PerItem < Calculator - preference :amount, :decimal, :default => 0 - preference :currency, :string, :default => Spree::Config[:currency] - - attr_accessible :preferred_amount, :preferred_currency - - def self.description - I18n.t(:flat_rate_per_item) - end - - def compute(object=nil) - return 0 if object.nil? - self.preferred_amount * object.line_items.reduce(0) do |sum, value| - if !matching_products || matching_products.include?(value.product) - value_to_add = value.quantity - else - value_to_add = 0 - end - sum + value_to_add - end - end - - # Returns all products that match this calculator, but only if the calculator - # is attached to a promotion. If attached to a ShippingMethod, nil is returned. - def matching_products - # Regression check for #1596 - # Calculator::PerItem can be used in two cases. - # The first is in a typical promotion, providing a discount per item of a particular item - # The second is a ShippingMethod, where it applies to an entire order - # - # Shipping methods do not have promotions attached, but promotions do - # Therefore we must check for promotions - if self.calculable.respond_to?(:promotion) - self.calculable.promotion.rules.map(&:products).flatten - end - end - end -end diff --git a/core/app/models/spree/calculator/percent_on_line_item.rb b/core/app/models/spree/calculator/percent_on_line_item.rb new file mode 100644 index 00000000000..e4f0076b744 --- /dev/null +++ b/core/app/models/spree/calculator/percent_on_line_item.rb @@ -0,0 +1,15 @@ +module Spree + class Calculator + class PercentOnLineItem < Calculator + preference :percent, :decimal, default: 0 + + def self.description + Spree.t(:percent_per_item) + end + + def compute(object) + (object.amount * preferred_percent) / 100 + end + end + end +end diff --git a/core/app/models/spree/calculator/percent_per_item.rb b/core/app/models/spree/calculator/percent_per_item.rb new file mode 100644 index 00000000000..0e0ff9a6a1b --- /dev/null +++ b/core/app/models/spree/calculator/percent_per_item.rb @@ -0,0 +1,50 @@ +module Spree + + # A calculator for promotions that calculates a percent-off discount + # for all matching products in an order. This should not be used as a + # shipping calculator since it would be the same thing as a flat percent + # off the entire order. + # + # + # TODO Should be deprecated now that we have adjustments at the line item level in spree core + + class Calculator::PercentPerItem < Calculator + preference :percent, :decimal, default: 0 + + def self.description + Spree.t(:percent_per_item) + end + + def compute(object=nil) + return 0 if object.nil? + object.line_items.reduce(0) do |sum, line_item| + sum += value_for_line_item(line_item) + end + end + + private + + # Returns all products that match this calculator, but only if the calculator + # is attached to a promotion. If attached to a ShippingMethod, nil is returned. + # Copied from per_item.rb + def matching_products + if compute_on_promotion? + self.calculable.promotion.rules.map do |rule| + rule.respond_to?(:products) ? rule.products : [] + end.flatten + end + end + + def value_for_line_item(line_item) + if compute_on_promotion? + return 0 unless matching_products.blank? or matching_products.include?(line_item.product) + end + ((line_item.price * line_item.quantity) * preferred_percent) / 100 + end + + # Determines wether or not the calculable object is a promotion + def compute_on_promotion? + @compute_on_promotion ||= self.calculable.respond_to?(:promotion) + end + end +end diff --git a/core/app/models/spree/calculator/price_sack.rb b/core/app/models/spree/calculator/price_sack.rb index ecfae4ce450..46e25796db0 100644 --- a/core/app/models/spree/calculator/price_sack.rb +++ b/core/app/models/spree/calculator/price_sack.rb @@ -1,21 +1,14 @@ require_dependency 'spree/calculator' -# For #to_d method on Ruby 1.8 -require 'bigdecimal/util' module Spree class Calculator::PriceSack < Calculator - preference :minimal_amount, :decimal, :default => 0 - preference :normal_amount, :decimal, :default => 0 - preference :discount_amount, :decimal, :default => 0 - preference :currency, :string, :default => Spree::Config[:currency] - - attr_accessible :preferred_minimal_amount, - :preferred_normal_amount, - :preferred_discount_amount, - :preferred_currency + preference :minimal_amount, :decimal, default: 0 + preference :normal_amount, :decimal, default: 0 + preference :discount_amount, :decimal, default: 0 + preference :currency, :string, default: ->{ Spree::Config[:currency] } def self.description - I18n.t(:price_sack) + Spree.t(:price_sack) end # as object we always get line items, as calculable we have Coupon, ShippingMethod diff --git a/core/app/models/spree/calculator/returns/default_refund_amount.rb b/core/app/models/spree/calculator/returns/default_refund_amount.rb new file mode 100644 index 00000000000..9d1d7bab729 --- /dev/null +++ b/core/app/models/spree/calculator/returns/default_refund_amount.rb @@ -0,0 +1,36 @@ +require_dependency 'spree/returns_calculator' + +module Spree + module Calculator::Returns + class DefaultRefundAmount < ReturnsCalculator + + def self.description + Spree.t(:default_refund_amount) + end + + def compute(return_item) + return 0.0.to_d if return_item.exchange_requested? + weighted_order_adjustment_amount(return_item.inventory_unit) + weighted_line_item_pre_tax_amount(return_item.inventory_unit) + end + + private + + def weighted_order_adjustment_amount(inventory_unit) + inventory_unit.order.adjustments.eligible.non_tax.sum(:amount) * percentage_of_order_total(inventory_unit) + end + + def weighted_line_item_pre_tax_amount(inventory_unit) + inventory_unit.line_item.pre_tax_amount * percentage_of_line_item(inventory_unit) + end + + def percentage_of_order_total(inventory_unit) + return 0.0 if inventory_unit.order.pre_tax_item_amount.zero? + weighted_line_item_pre_tax_amount(inventory_unit) / inventory_unit.order.pre_tax_item_amount + end + + def percentage_of_line_item(inventory_unit) + 1 / BigDecimal.new(inventory_unit.line_item.quantity) + end + end + end +end diff --git a/core/app/models/spree/calculator/shipping/flat_percent_item_total.rb b/core/app/models/spree/calculator/shipping/flat_percent_item_total.rb new file mode 100644 index 00000000000..c9a19c7e49c --- /dev/null +++ b/core/app/models/spree/calculator/shipping/flat_percent_item_total.rb @@ -0,0 +1,22 @@ +require_dependency 'spree/shipping_calculator' + +module Spree + module Calculator::Shipping + class FlatPercentItemTotal < ShippingCalculator + preference :flat_percent, :decimal, default: 0 + + def self.description + Spree.t(:flat_percent) + end + + def compute_package(package) + compute_from_price(total(package.contents)) + end + + def compute_from_price(price) + value = price * BigDecimal(self.preferred_flat_percent.to_s) / 100.0 + (value * 100).round.to_f / 100 + end + end + end +end diff --git a/core/app/models/spree/calculator/shipping/flat_rate.rb b/core/app/models/spree/calculator/shipping/flat_rate.rb new file mode 100644 index 00000000000..ad28c124934 --- /dev/null +++ b/core/app/models/spree/calculator/shipping/flat_rate.rb @@ -0,0 +1,18 @@ +require_dependency 'spree/shipping_calculator' + +module Spree + module Calculator::Shipping + class FlatRate < ShippingCalculator + preference :amount, :decimal, default: 0 + preference :currency, :string, default: ->{ Spree::Config[:currency] } + + def self.description + Spree.t(:shipping_flat_rate_per_order) + end + + def compute_package(package) + self.preferred_amount + end + end + end +end diff --git a/core/app/models/spree/calculator/shipping/flexi_rate.rb b/core/app/models/spree/calculator/shipping/flexi_rate.rb new file mode 100644 index 00000000000..0944b705a5d --- /dev/null +++ b/core/app/models/spree/calculator/shipping/flexi_rate.rb @@ -0,0 +1,35 @@ +require_dependency 'spree/shipping_calculator' + +module Spree + module Calculator::Shipping + class FlexiRate < ShippingCalculator + preference :first_item, :decimal, default: 0.0 + preference :additional_item, :decimal, default: 0.0 + preference :max_items, :integer, default: 0 + preference :currency, :string, default: ->{ Spree::Config[:currency] } + + def self.description + Spree.t(:shipping_flexible_rate) + end + + def compute_package(package) + compute_from_quantity(package.contents.sum(&:quantity)) + end + + def compute_from_quantity(quantity) + sum = 0 + max = self.preferred_max_items.to_i + quantity.times do |i| + # check max value to avoid divide by 0 errors + if (max == 0 && i == 0) || (max > 0) && (i % max == 0) + sum += self.preferred_first_item.to_f + else + sum += self.preferred_additional_item.to_f + end + end + + sum + end + end + end +end diff --git a/core/app/models/spree/calculator/shipping/per_item.rb b/core/app/models/spree/calculator/shipping/per_item.rb new file mode 100644 index 00000000000..bb3cf4b9ec1 --- /dev/null +++ b/core/app/models/spree/calculator/shipping/per_item.rb @@ -0,0 +1,22 @@ +require_dependency 'spree/shipping_calculator' + +module Spree + module Calculator::Shipping + class PerItem < ShippingCalculator + preference :amount, :decimal, default: 0 + preference :currency, :string, default: ->{ Spree::Config[:currency] } + + def self.description + Spree.t(:shipping_flat_rate_per_item) + end + + def compute_package(package) + compute_from_quantity(package.contents.sum(&:quantity)) + end + + def compute_from_quantity(quantity) + self.preferred_amount * quantity + end + end + end +end diff --git a/core/app/models/spree/calculator/shipping/price_sack.rb b/core/app/models/spree/calculator/shipping/price_sack.rb new file mode 100644 index 00000000000..f65c2af04e8 --- /dev/null +++ b/core/app/models/spree/calculator/shipping/price_sack.rb @@ -0,0 +1,28 @@ +require_dependency 'spree/shipping_calculator' + +module Spree + module Calculator::Shipping + class PriceSack < ShippingCalculator + preference :minimal_amount, :decimal, default: 0 + preference :normal_amount, :decimal, default: 0 + preference :discount_amount, :decimal, default: 0 + preference :currency, :string, default: ->{ Spree::Config[:currency] } + + def self.description + Spree.t(:shipping_price_sack) + end + + def compute_package(package) + compute_from_price(total(package.contents)) + end + + def compute_from_price(price) + if price < self.preferred_minimal_amount + self.preferred_normal_amount + else + self.preferred_discount_amount + end + end + end + end +end diff --git a/core/app/models/spree/calculator/tiered_flat_rate.rb b/core/app/models/spree/calculator/tiered_flat_rate.rb new file mode 100644 index 00000000000..9dc33ca9140 --- /dev/null +++ b/core/app/models/spree/calculator/tiered_flat_rate.rb @@ -0,0 +1,37 @@ +require_dependency 'spree/calculator' + +module Spree + class Calculator::TieredFlatRate < Calculator + preference :base_amount, :decimal, default: 0 + preference :tiers, :hash, default: {} + + before_validation do + # Convert tier values to decimals. Strings don't do us much good. + if preferred_tiers.is_a?(Hash) + self.preferred_tiers = Hash[*preferred_tiers.flatten.map(&:to_f)] + end + end + + validate :preferred_tiers_content + + def self.description + Spree.t(:tiered_flat_rate) + end + + def compute(object) + base, amount = preferred_tiers.sort.reverse.detect{ |b,_| object.amount >= b } + amount || preferred_base_amount + end + + private + def preferred_tiers_content + if preferred_tiers.is_a? Hash + unless preferred_tiers.keys.all?{ |k| k.is_a?(Numeric) && k > 0 } + errors.add(:base, :keys_should_be_positive_number) + end + else + errors.add(:preferred_tiers, :should_be_hash) + end + end + end +end diff --git a/core/app/models/spree/calculator/tiered_percent.rb b/core/app/models/spree/calculator/tiered_percent.rb new file mode 100644 index 00000000000..27c2baf3658 --- /dev/null +++ b/core/app/models/spree/calculator/tiered_percent.rb @@ -0,0 +1,44 @@ +require_dependency 'spree/calculator' + +module Spree + class Calculator::TieredPercent < Calculator + preference :base_percent, :decimal, default: 0 + preference :tiers, :hash, default: {} + + before_validation do + # Convert tier values to decimals. Strings don't do us much good. + if preferred_tiers.is_a?(Hash) + self.preferred_tiers = Hash[*preferred_tiers.flatten.map(&:to_f)] + end + end + + validates :preferred_base_percent, numericality: { + greater_than_or_equal_to: 0, + less_than_or_equal_to: 100 + } + validate :preferred_tiers_content + + def self.description + Spree.t(:tiered_percent) + end + + def compute(object) + base, percent = preferred_tiers.sort.reverse.detect{ |b,_| object.amount >= b } + (object.amount * (percent || preferred_base_percent) / 100).round(2) + end + + private + def preferred_tiers_content + if preferred_tiers.is_a? Hash + unless preferred_tiers.keys.all?{ |k| k.is_a?(Numeric) && k > 0 } + errors.add(:base, :keys_should_be_positive_number) + end + unless preferred_tiers.values.all?{ |k| k.is_a?(Numeric) && k >= 0 && k <= 100 } + errors.add(:base, :values_should_be_percent) + end + else + errors.add(:preferred_tiers, :should_be_hash) + end + end + end +end diff --git a/core/app/models/spree/classification.rb b/core/app/models/spree/classification.rb new file mode 100644 index 00000000000..cd11cd74594 --- /dev/null +++ b/core/app/models/spree/classification.rb @@ -0,0 +1,11 @@ +module Spree + class Classification < Spree::Base + self.table_name = 'spree_products_taxons' + acts_as_list scope: :taxon + belongs_to :product, class_name: "Spree::Product", inverse_of: :classifications, touch: true + belongs_to :taxon, class_name: "Spree::Taxon", inverse_of: :classifications, touch: true + + # For #3494 + validates_uniqueness_of :taxon_id, scope: :product_id, message: :already_linked + end +end diff --git a/core/app/models/spree/configuration.rb b/core/app/models/spree/configuration.rb index 02b6075deba..0d760bdd640 100644 --- a/core/app/models/spree/configuration.rb +++ b/core/app/models/spree/configuration.rb @@ -1,5 +1,5 @@ module Spree - class Configuration < ActiveRecord::Base + class Configuration < Spree::Base end end diff --git a/core/app/models/spree/country.rb b/core/app/models/spree/country.rb index 873562bb0e6..1b0ef59a6cc 100644 --- a/core/app/models/spree/country.rb +++ b/core/app/models/spree/country.rb @@ -1,17 +1,15 @@ module Spree - class Country < ActiveRecord::Base - has_many :states, :order => "name ASC" + class Country < Spree::Base + has_many :states, -> { order('name ASC') }, dependent: :destroy + has_many :addresses, dependent: :nullify - validates :name, :iso_name, :presence => true - - attr_accessible :name, :iso_name, :states_required + has_many :zone_members, as: :zoneable, dependent: :destroy + validates :name, :iso_name, presence: true def self.states_required_by_country_id states_required = Hash.new(true) - self.all.each { |country| - states_required[country.id.to_s]= country.states_required - } + all.each { |country| states_required[country.id.to_s]= country.states_required } states_required end diff --git a/core/app/models/spree/credit_card.rb b/core/app/models/spree/credit_card.rb index c34d0abed03..b395301f885 100644 --- a/core/app/models/spree/credit_card.rb +++ b/core/app/models/spree/credit_card.rb @@ -1,56 +1,81 @@ module Spree - class CreditCard < ActiveRecord::Base - has_many :payments, :as => :source + class CreditCard < Spree::Base + belongs_to :payment_method + belongs_to :user, class_name: Spree.user_class, foreign_key: 'user_id' + has_many :payments, as: :source before_save :set_last_digits - after_validation :set_card_type - attr_accessor :number, :verification_value + after_save :ensure_one_default - validates :month, :year, :numericality => { :only_integer => true } - validates :number, :presence => true, :unless => :has_payment_profile?, :on => :create - validates :verification_value, :presence => true, :unless => :has_payment_profile?, :on => :create + attr_accessor :encrypted_data, + :number, + :imported, + :verification_value - attr_accessible :first_name, :last_name, :number, :verification_value, :year, - :month, :gateway_customer_profile_id, :gateway_payment_profile_id + validates :month, :year, numericality: { only_integer: true }, if: :require_card_numbers?, on: :create + validates :number, presence: true, if: :require_card_numbers?, on: :create, unless: :imported + validates :name, presence: true, if: :require_card_numbers?, on: :create + validates :verification_value, presence: true, if: :require_card_numbers?, on: :create, unless: :imported - def number=(num) - @number = num.gsub(/[^0-9]/, '') rescue nil - end + validate :expiry_not_in_the_past - def set_last_digits - number.to_s.gsub!(/\s/,'') - verification_value.to_s.gsub!(/\s/,'') - self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1) - end + scope :with_payment_profile, -> { where('gateway_customer_profile_id IS NOT NULL') } + scope :default, -> { where(default: true) } - # cheap hack to get to the type? method from deep within ActiveMerchant without stomping on - # potentially existing methods in CreditCard - class CardDetector - class << self - include ActiveMerchant::Billing::CreditCardMethods::ClassMethods + # needed for some of the ActiveMerchant gateways (eg. SagePay) + alias_attribute :brand, :cc_type + + CARD_TYPES = { + visa: /^4[0-9]{12}(?:[0-9]{3})?$/, + master: /(^5[1-5][0-9]{14}$)|(^6759[0-9]{2}([0-9]{10})$)|(^6759[0-9]{2}([0-9]{12})$)|(^6759[0-9]{2}([0-9]{13})$)/, + diners_club: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/, + american_express: /^3[47][0-9]{13}$/, + discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/, + jcb: /^(?:2131|1800|35\d{3})\d{11}$/ + } + + def expiry=(expiry) + return unless expiry.present? + + self[:month], self[:year] = + if expiry.match(/\d{2}\s?\/\s?\d{2,4}/) # will match mm/yy and mm / yyyy + expiry.delete(' ').split('/') + elsif match = expiry.match(/(\d{2})(\d{2,4})/) # will match mmyy and mmyyyy + [match[1], match[2]] end + if self[:year] + self[:year] = "20" + self[:year] if self[:year].length == 2 + self[:year] = self[:year].to_i + end + self[:month] = self[:month].to_i if self[:month] end - # sets self.cc_type while we still have the card number - def set_card_type - self.cc_type ||= CardDetector.brand?(number) - end - - def name? - first_name? && last_name? + def number=(num) + @number = num.gsub(/[^0-9]/, '') rescue nil end - def first_name? - first_name.present? + # cc_type is set by jquery.payment, which helpfully provides different + # types from Active Merchant. Converting them is necessary. + def cc_type=(type) + self[:cc_type] = case type + when 'mastercard', 'maestro' then 'master' + when 'amex' then 'american_express' + when 'dinersclub' then 'diners_club' + when '' then try_type_from_number + else type + end end - def last_name? - last_name.present? + def set_last_digits + number.to_s.gsub!(/\s/,'') + verification_value.to_s.gsub!(/\s/,'') + self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1) end - def name - "#{first_name} #{last_name}" + def try_type_from_number + numbers = number.delete(' ') if number + CARD_TYPES.find{|type, pattern| return type.to_s if numbers =~ pattern}.to_s end def verification_value? @@ -62,42 +87,77 @@ def display_number "XXXX-XXXX-XXXX-#{last_digits}" end - # needed for some of the ActiveMerchant gateways (eg. SagePay) - def brand - cc_type - end - - scope :with_payment_profile, lambda { where('gateway_customer_profile_id IS NOT NULL') } - def actions %w{capture void credit} end # Indicates whether its possible to capture the payment def can_capture?(payment) - payment.state == 'pending' || payment.state == 'checkout' + payment.pending? || payment.checkout? end # Indicates whether its possible to void the payment. def can_void?(payment) - payment.state != 'void' + !payment.failed? && !payment.void? end # Indicates whether its possible to credit the payment. Note that most gateways require that the # payment be settled first which generally happens within 12-24 hours of the transaction. def can_credit?(payment) - return false unless payment.state == 'completed' - return false unless payment.order.payment_state == 'credit_owed' - payment.credit_allowed > 0 + payment.completed? && payment.credit_allowed > 0 end def has_payment_profile? - gateway_customer_profile_id.present? + gateway_customer_profile_id.present? || gateway_payment_profile_id.present? + end + + # ActiveMerchant needs first_name/last_name because we pass it a Spree::CreditCard and it calls those methods on it. + # Looking at the ActiveMerchant source code we should probably be calling #to_active_merchant before passing + # the object to ActiveMerchant but this should do for now. + def first_name + name.to_s.split(/[[:space:]]/, 2)[0] + end + + def last_name + name.to_s.split(/[[:space:]]/, 2)[1] end - def spree_cc_type - return 'visa' if Rails.env.development? - cc_type + def to_active_merchant + ActiveMerchant::Billing::CreditCard.new( + :number => number, + :month => month, + :year => year, + :verification_value => verification_value, + :first_name => first_name, + :last_name => last_name, + ) + end + + private + + def expiry_not_in_the_past + if year.present? && month.present? + if month.to_i < 1 || month.to_i > 12 + errors.add(:base, :expiry_invalid) + else + current = Time.current + if year.to_i < current.year or (year.to_i == current.year and month.to_i < current.month) + errors.add(:base, :card_expired) + end + end + end + end + + def require_card_numbers? + !self.encrypted_data.present? && !self.has_payment_profile? + end + + def ensure_one_default + if self.user_id && self.default + CreditCard.where(default: true).where.not(id: self.id).where(user_id: self.user_id).each do |ucc| + ucc.update_columns(default: false) + end + end end end end diff --git a/core/app/models/spree/customer_return.rb b/core/app/models/spree/customer_return.rb new file mode 100644 index 00000000000..19c9d862812 --- /dev/null +++ b/core/app/models/spree/customer_return.rb @@ -0,0 +1,76 @@ +module Spree + class CustomerReturn < Spree::Base + belongs_to :stock_location + + has_many :reimbursements, inverse_of: :customer_return + has_many :return_authorizations, through: :return_items + has_many :return_items, inverse_of: :customer_return + + after_create :process_return! + before_create :generate_number + + validates :return_items, presence: true + validates :stock_location, presence: true + + validate :must_have_return_authorization, on: :create + validate :return_items_belong_to_same_order + + accepts_nested_attributes_for :return_items + + def completely_decided? + !return_items.undecided.exists? + end + + def fully_reimbursed? + completely_decided? && return_items.accepted.includes(:reimbursement).all? { |return_item| return_item.reimbursement.try(:reimbursed?) } + end + + def display_pre_tax_total + Spree::Money.new(pre_tax_total, { currency: Spree::Config[:currency] }) + end + + # Temporarily tie a customer_return to one order + def order + return nil if return_items.blank? + return_items.first.inventory_unit.order + end + + def order_id + order.try(:id) + end + + def pre_tax_total + return_items.sum(:pre_tax_amount) + end + + private + + def inventory_units + return_items.flat_map(&:inventory_unit) + end + + def must_have_return_authorization + if item = return_items.find { |ri| ri.return_authorization.blank? } + errors.add(:base, Spree.t(:missing_return_authorization, item_name: item.inventory_unit.variant.name)) + end + end + + def generate_number + self.number ||= loop do + random = "CR#{Array.new(9){rand(9)}.join}" + break random unless self.class.exists?(number: random) + end + end + + def process_return! + return_items.each(&:receive!) + order.return! if order.all_inventory_units_returned? + end + + def return_items_belong_to_same_order + if return_items.select { |return_item| return_item.inventory_unit.order_id != order_id }.any? + errors.add(:base, Spree.t(:return_items_cannot_be_associated_with_multiple_orders)) + end + end + end +end diff --git a/core/app/models/spree/exchange.rb b/core/app/models/spree/exchange.rb new file mode 100644 index 00000000000..f93876eebf4 --- /dev/null +++ b/core/app/models/spree/exchange.rb @@ -0,0 +1,46 @@ +module Spree + class Exchange + class UnableToCreateShipments < StandardError; end + + def initialize(order, reimbursement_objects) + @order = order + @reimbursement_objects = reimbursement_objects + end + + def description + @reimbursement_objects.map do |reimbursement_object| + "#{reimbursement_object.variant.options_text} => #{reimbursement_object.exchange_variant.options_text}" + end.join(" | ") + end + + def display_amount + Spree::Money.new @reimbursement_objects.map(&:total).sum + end + + def perform! + shipments = Spree::Stock::Coordinator.new(@order, @reimbursement_objects.map(&:build_exchange_inventory_unit)).shipments + if shipments.flat_map(&:inventory_units).size != @reimbursement_objects.size + raise UnableToCreateShipments.new("Could not generate shipments for all items. Out of stock?") + end + @order.shipments += shipments + @order.save! + shipments.each do |shipment| + shipment.update!(@order) + shipment.finalize! + end + end + + def to_key + nil + end + + def self.param_key + "spree_exchange" + end + + def self.model_name + Spree::Exchange + end + + end +end diff --git a/core/app/models/spree/gateway.rb b/core/app/models/spree/gateway.rb index 208e10121af..882f0b5e437 100644 --- a/core/app/models/spree/gateway.rb +++ b/core/app/models/spree/gateway.rb @@ -1,13 +1,11 @@ module Spree class Gateway < PaymentMethod - delegate_belongs_to :provider, :authorize, :purchase, :capture, :void, :credit + delegate :authorize, :purchase, :capture, :void, :credit, to: :provider - validates :name, :type, :presence => true + validates :name, :type, presence: true - preference :server, :string, :default => 'test' - preference :test_mode, :boolean, :default => true - - attr_accessible :preferred_server, :preferred_test_mode + preference :server, :string, default: 'test' + preference :test_mode, :boolean, default: true def payment_source_class CreditCard @@ -21,7 +19,9 @@ def self.current def provider gateway_options = options gateway_options.delete :login if gateway_options.has_key?(:login) and gateway_options[:login].nil? - ActiveMerchant::Billing::Base.gateway_mode = gateway_options[:server].to_sym + if gateway_options[:server] + ActiveMerchant::Billing::Base.gateway_mode = gateway_options[:server].to_sym + end @provider ||= provider_class.new(gateway_options) end @@ -33,7 +33,7 @@ def method_missing(method, *args) if @provider.nil? || !@provider.respond_to?(method) super else - provider.send(method) + provider.send(method, *args) end end @@ -44,5 +44,36 @@ def payment_profiles_supported? def method_type 'gateway' end + + def supports?(source) + return true unless provider_class.respond_to? :supports? + return false unless source.brand + provider_class.supports?(source.brand) + end + + def disable_customer_profile(source) + if source.is_a? CreditCard + source.update_column :gateway_customer_profile_id, nil + else + raise 'You must implement disable_customer_profile method for this gateway.' + end + end + + def sources_by_order(order) + source_ids = order.payments.where(source_type: payment_source_class.to_s, payment_method_id: self.id).pluck(:source_id).uniq + payment_source_class.where(id: source_ids).with_payment_profile + end + + def reusable_sources(order) + if order.completed? + sources_by_order order + else + if order.user_id + self.credit_cards.where(user_id: order.user_id).with_payment_profile + else + [] + end + end + end end end diff --git a/core/app/models/spree/gateway/bogus.rb b/core/app/models/spree/gateway/bogus.rb index 003b1eea376..0109f57c64b 100644 --- a/core/app/models/spree/gateway/bogus.rb +++ b/core/app/models/spree/gateway/bogus.rb @@ -1,11 +1,11 @@ module Spree class Gateway::Bogus < Gateway - TEST_VISA = '4111111111111111' - TEST_MC = '5500000000000004' - TEST_AMEX = '340000000000009' - TEST_DISC = '6011000000000004' + TEST_VISA = ['4111111111111111','4012888888881881','4222222222222'] + TEST_MC = ['5500000000000004','5555555555554444','5105105105105100'] + TEST_AMEX = ['378282246310005','371449635398431','378734493671000','340000000000009'] + TEST_DISC = ['6011000000000004','6011111111111117','6011000990139424'] - VALID_CCS = ['1', TEST_VISA, TEST_MC, TEST_AMEX, TEST_DISC] + VALID_CCS = ['1', TEST_VISA, TEST_MC, TEST_AMEX, TEST_DISC].flatten attr_accessor :test @@ -18,15 +18,17 @@ def preferences end def create_profile(payment) + return if payment.source.has_payment_profile? # simulate the storage of credit card profile using remote service - success = VALID_CCS.include? payment.source.number - payment.source.update_attributes(:gateway_customer_profile_id => generate_profile_id(success)) + if success = VALID_CCS.include?(payment.source.number) + payment.source.update_attributes(:gateway_customer_profile_id => generate_profile_id(success)) + end end def authorize(money, credit_card, options = {}) profile_id = credit_card.gateway_customer_profile_id if VALID_CCS.include? credit_card.number or (profile_id and profile_id.starts_with? 'BGS-') - ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true, :authorization => '12345', :avs_result => { :code => 'A' }) + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true, :authorization => '12345', :avs_result => { :code => 'D' }) else ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', { :message => 'Bogus Gateway: Forced failure' }, :test => true) end @@ -35,7 +37,7 @@ def authorize(money, credit_card, options = {}) def purchase(money, credit_card, options = {}) profile_id = credit_card.gateway_customer_profile_id if VALID_CCS.include? credit_card.number or (profile_id and profile_id.starts_with? 'BGS-') - ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true, :authorization => '12345', :avs_result => { :code => 'A' }) + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true, :authorization => '12345', :avs_result => { :code => 'M' }) else ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', :message => 'Bogus Gateway: Forced failure', :test => true) end @@ -45,9 +47,9 @@ def credit(money, credit_card, response_code, options = {}) ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true, :authorization => '12345') end - def capture(authorization, credit_card, gateway_options) - if authorization.response_code == '12345' - ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true, :authorization => '67890') + def capture(money, authorization, gateway_options) + if authorization == '12345' + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true) else ActiveMerchant::Billing::Response.new(false, 'Bogus Gateway: Forced failure', :error => 'Bogus Gateway: Forced failure', :test => true) end @@ -58,6 +60,10 @@ def void(response_code, credit_card, options = {}) ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, :test => true, :authorization => '12345') end + def cancel(_response_code) + ActiveMerchant::Billing::Response.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: '12345') + end + def test? # Test mode is not really relevant with bogus gateway (no such thing as live server) true diff --git a/core/app/models/spree/image.rb b/core/app/models/spree/image.rb index d52138fbffc..3fc26403db1 100644 --- a/core/app/models/spree/image.rb +++ b/core/app/models/spree/image.rb @@ -1,29 +1,21 @@ module Spree class Image < Asset - validates_attachment_presence :attachment validate :no_attachment_errors - attr_accessible :alt, :attachment, :position, :viewable_type, :viewable_id - has_attached_file :attachment, - :styles => { :mini => '48x48>', :small => '100x100>', :product => '240x240>', :large => '600x600>' }, - :default_style => :product, - :url => '/spree/products/:id/:style/:basename.:extension', - :path => ':rails_root/public/spree/products/:id/:style/:basename.:extension', - :convert_options => { :all => '-strip' } + styles: { mini: '48x48>', small: '100x100>', product: '240x240>', large: '600x600>' }, + default_style: :product, + url: '/spree/products/:id/:style/:basename.:extension', + path: ':rails_root/public/spree/products/:id/:style/:basename.:extension', + convert_options: { all: '-strip -auto-orient -colorspace sRGB' } + validates_attachment :attachment, + :presence => true, + :content_type => { :content_type => %w(image/jpeg image/jpg image/png image/gif) } + # save the w,h of the original image (from which others can be calculated) # we need to look at the write-queue for images which have not been saved yet after_post_process :find_dimensions - include Spree::Core::S3Support - supports_s3 :attachment - - Spree::Image.attachment_definitions[:attachment][:styles] = ActiveSupport::JSON.decode(Spree::Config[:attachment_styles]) - Spree::Image.attachment_definitions[:attachment][:path] = Spree::Config[:attachment_path] - Spree::Image.attachment_definitions[:attachment][:url] = Spree::Config[:attachment_url] - Spree::Image.attachment_definitions[:attachment][:default_url] = Spree::Config[:attachment_default_url] - Spree::Image.attachment_definitions[:attachment][:default_style] = Spree::Config[:attachment_default_style] - #used by admin products autocomplete def mini_url attachment.url(:mini, false) @@ -41,7 +33,7 @@ def find_dimensions # if there are errors from the plugin, then add a more meaningful message def no_attachment_errors unless attachment.errors.empty? - # uncomment this to get rid of the less-than-useful interrim messages + # uncomment this to get rid of the less-than-useful interim messages # errors.clear errors.add :attachment, "Paperclip returned errors for file '#{attachment_file_name}' - check ImageMagick installation or image source file." false diff --git a/core/app/models/spree/inventory_unit.rb b/core/app/models/spree/inventory_unit.rb index 0c712e1d01d..ac5f25c0800 100644 --- a/core/app/models/spree/inventory_unit.rb +++ b/core/app/models/spree/inventory_unit.rb @@ -1,126 +1,102 @@ module Spree - class InventoryUnit < ActiveRecord::Base - belongs_to :variant - belongs_to :order - belongs_to :shipment - belongs_to :return_authorization - - scope :backordered, lambda { where(:state => 'backordered') } - scope :shipped, lambda { where(:state => 'shipped') } - - def self.backorder - warn "[SPREE] Spree::InventoryUnit.backorder will be deprecated in Spree 1.3. Please use Spree::Product.backordered instead." - backordered + class InventoryUnit < Spree::Base + belongs_to :variant, class_name: "Spree::Variant", inverse_of: :inventory_units + belongs_to :order, class_name: "Spree::Order", inverse_of: :inventory_units + belongs_to :shipment, class_name: "Spree::Shipment", touch: true, inverse_of: :inventory_units + belongs_to :return_authorization, class_name: "Spree::ReturnAuthorization", inverse_of: :inventory_units + belongs_to :line_item, class_name: "Spree::LineItem", inverse_of: :inventory_units + + has_many :return_items, inverse_of: :inventory_unit + has_one :original_return_item, class_name: "Spree::ReturnItem", foreign_key: :exchange_inventory_unit_id + + scope :backordered, -> { where state: 'backordered' } + scope :on_hand, -> { where state: 'on_hand' } + scope :shipped, -> { where state: 'shipped' } + scope :returned, -> { where state: 'returned' } + scope :backordered_per_variant, ->(stock_item) do + includes(:shipment, :order) + .where("spree_shipments.state != 'canceled'").references(:shipment) + .where(variant_id: stock_item.variant_id) + .where('spree_orders.completed_at is not null') + .backordered.order("spree_orders.completed_at ASC") end - attr_accessible :shipment - # state machine (see http://github.com/pluginaweek/state_machine/tree/master for details) - state_machine :initial => 'on_hand' do + state_machine initial: :on_hand do event :fill_backorder do - transition :to => 'sold', :from => 'backordered' + transition to: :on_hand, from: :backordered end + after_transition on: :fill_backorder, do: :fulfill_order + event :ship do - transition :to => 'shipped', :if => :allow_ship? + transition to: :shipped, if: :allow_ship? end + event :return do - transition :to => 'returned', :from => 'shipped' + transition to: :returned, from: :shipped end - - after_transition :on => :fill_backorder, :do => :update_order - after_transition :to => 'returned', :do => :restock_variant end - # Assigns inventory to a newly completed order. - # Should only be called once during the life-cycle of an order, on transition to completed. + # This was refactored from a simpler query because the previous implementation + # led to issues once users tried to modify the objects returned. That's due + # to ActiveRecord `joins(shipment: :stock_location)` only returning readonly + # objects # - def self.assign_opening_inventory(order) - return [] unless order.completed? - - #increase inventory to meet initial requirements - order.line_items.each do |line_item| - increase(order, line_item.variant, line_item.quantity) + # Returns an array of backordered inventory units as per a given stock item + def self.backordered_for_stock_item(stock_item) + backordered_per_variant(stock_item).select do |unit| + unit.shipment.stock_location == stock_item.stock_location end end - # manages both variant.count_on_hand and inventory unit creation - # - def self.increase(order, variant, quantity) - back_order = determine_backorder(order, variant, quantity) - sold = quantity - back_order - - #set on_hand if configured - if self.track_levels?(variant) - variant.decrement!(:count_on_hand, quantity) - end - - #create units if configured - if Spree::Config[:create_inventory_units] - create_units(order, variant, sold, back_order) + def self.finalize_units!(inventory_units) + inventory_units.map do |iu| + iu.update_columns( + pending: false, + updated_at: Time.now, + ) end end - def self.decrease(order, variant, quantity) - if self.track_levels?(variant) - variant.increment!(:count_on_hand, quantity) - end - - if Spree::Config[:create_inventory_units] - destroy_units(order, variant, quantity) - end + def find_stock_item + Spree::StockItem.where(stock_location_id: shipment.stock_location_id, + variant_id: variant_id).first end - def self.track_levels?(variant) - Spree::Config[:track_inventory_levels] && !variant.on_demand + # Remove variant default_scope `deleted_at: nil` + def variant + Spree::Variant.unscoped { super } end - private - def allow_ship? - Spree::Config[:allow_backorder_shipping] || self.sold? - end + def current_or_new_return_item + Spree::ReturnItem.from_inventory_unit(self) + end - def self.determine_backorder(order, variant, quantity) - if variant.on_hand == 0 - quantity - elsif variant.on_hand.present? and variant.on_hand < quantity - quantity - (variant.on_hand < 0 ? 0 : variant.on_hand) - else - 0 - end - end + def additional_tax_total + line_item.additional_tax_total * percentage_of_line_item + end - def self.destroy_units(order, variant, quantity) - variant_units = order.inventory_units.group_by(&:variant_id) - return unless variant_units.include? variant.id + def included_tax_total + line_item.included_tax_total * percentage_of_line_item + end - variant_units = variant_units[variant.id].reject do |variant_unit| - variant_unit.state == 'shipped' - end.sort_by(&:state) + private - quantity.times do - inventory_unit = variant_units.shift - inventory_unit.destroy - end + def allow_ship? + self.on_hand? end - def self.create_units(order, variant, sold, back_order) - return if back_order > 0 && !Spree::Config[:allow_backorders] - - shipment = order.shipments.detect { |shipment| !shipment.shipped? } - - sold.times { order.inventory_units.create({:variant => variant, :state => 'sold', :shipment => shipment}, :without_protection => true) } - back_order.times { order.inventory_units.create({:variant => variant, :state => 'backordered', :shipment => shipment}, :without_protection => true) } + def fulfill_order + self.reload + order.fulfill! end - def update_order - order.update! + def percentage_of_line_item + 1 / BigDecimal.new(line_item.quantity) end - def restock_variant - if self.class.track_levels?(variant) - variant.on_hand += 1 - variant.save - end + def current_return_item + return_items.not_cancelled.first end end end diff --git a/core/app/models/spree/item_adjustments.rb b/core/app/models/spree/item_adjustments.rb new file mode 100644 index 00000000000..6d0bc78cbe0 --- /dev/null +++ b/core/app/models/spree/item_adjustments.rb @@ -0,0 +1,82 @@ +module Spree + # Manage (recalculate) item (LineItem or Shipment) adjustments + class ItemAdjustments + include ActiveSupport::Callbacks + define_callbacks :promo_adjustments, :tax_adjustments + attr_reader :item + + delegate :adjustments, :order, to: :item + + def initialize(item) + @item = item + + # Don't attempt to reload the item from the DB if it's not there + @item.reload if @item.instance_of?(Shipment) && @item.persisted? + end + + def update + update_adjustments if item.persisted? + item + end + + # TODO this should be probably the place to calculate proper item taxes + # values after promotions are applied + def update_adjustments + # Promotion adjustments must be applied first, then tax adjustments. + # This fits the criteria for VAT tax as outlined here: + # http://www.hmrc.gov.uk/vat/managing/charging/discounts-etc.htm#1 + # + # It also fits the criteria for sales tax as outlined here: + # http://www.boe.ca.gov/formspubs/pub113/ + # + # Tax adjustments come in not one but *two* exciting flavours: + # Included & additional + + # Included tax adjustments are those which are included in the price. + # These ones should not affect the eventual total price. + # + # Additional tax adjustments are the opposite, affecting the final total. + promo_total = 0 + run_callbacks :promo_adjustments do + promotion_total = adjustments.promotion.reload.map do |adjustment| + adjustment.update!(@item) + end.compact.sum + + unless promotion_total == 0 + choose_best_promotion_adjustment + end + promo_total = best_promotion_adjustment.try(:amount).to_f + end + + included_tax_total = 0 + additional_tax_total = 0 + run_callbacks :tax_adjustments do + tax = (item.respond_to?(:all_adjustments) ? item.all_adjustments : item.adjustments).tax + included_tax_total = tax.is_included.reload.map(&:update!).compact.sum + additional_tax_total = tax.additional.reload.map(&:update!).compact.sum + end + + item.update_columns( + :promo_total => promo_total, + :included_tax_total => included_tax_total, + :additional_tax_total => additional_tax_total, + :adjustment_total => promo_total + additional_tax_total, + :updated_at => Time.now, + ) + end + + # Picks one (and only one) promotion to be eligible for this order + # This promotion provides the most discount, and if two promotions + # have the same amount, then it will pick the latest one. + def choose_best_promotion_adjustment + if best_promotion_adjustment + other_promotions = self.adjustments.promotion.where.not(id: best_promotion_adjustment.id) + other_promotions.update_all(:eligible => false) + end + end + + def best_promotion_adjustment + @best_promotion_adjustment ||= adjustments.promotion.eligible.reorder("amount ASC, created_at DESC, id DESC").first + end + end +end diff --git a/core/app/models/spree/legacy_user.rb b/core/app/models/spree/legacy_user.rb index 587931c70fe..9b973887e7b 100644 --- a/core/app/models/spree/legacy_user.rb +++ b/core/app/models/spree/legacy_user.rb @@ -1,22 +1,14 @@ # Default implementation of User. This class is intended to be modified by extensions (ex. spree_auth_devise) module Spree - class LegacyUser < ActiveRecord::Base - self.table_name = 'spree_users' - attr_accessible :email, :password, :password_confirmation + class LegacyUser < Spree::Base + include UserAddress + include UserPaymentSource - belongs_to :ship_address, :class_name => 'Spree::Address' - belongs_to :bill_address, :class_name => 'Spree::Address' + self.table_name = 'spree_users' - scope :registered + has_many :orders, foreign_key: :user_id - def anonymous? - false - end - - # Creates an anonymous user - def self.anonymous! - create - end + before_destroy :check_completed_orders def has_spree_role?(role) true @@ -24,5 +16,11 @@ def has_spree_role?(role) attr_accessor :password attr_accessor :password_confirmation + + private + + def check_completed_orders + raise Spree::Core::DestroyWithOrdersError if orders.complete.present? + end end end diff --git a/core/app/models/spree/line_item.rb b/core/app/models/spree/line_item.rb index 32849d32e40..dfdb7c8df31 100644 --- a/core/app/models/spree/line_item.rb +++ b/core/app/models/spree/line_item.rb @@ -1,120 +1,156 @@ module Spree - class LineItem < ActiveRecord::Base - before_validation :adjust_quantity - belongs_to :order - belongs_to :variant + class LineItem < Spree::Base + before_validation :invalid_quantity_check + belongs_to :order, class_name: "Spree::Order", inverse_of: :line_items, touch: true + belongs_to :variant, class_name: "Spree::Variant", inverse_of: :line_items + belongs_to :tax_category, class_name: "Spree::TaxCategory" - has_one :product, :through => :variant - has_many :adjustments, :as => :adjustable, :dependent => :destroy + has_one :product, through: :variant + + has_many :adjustments, as: :adjustable, dependent: :destroy + has_many :inventory_units, inverse_of: :line_item before_validation :copy_price + before_validation :copy_tax_category + + validates :variant, presence: true + validates :quantity, numericality: { + only_integer: true, + greater_than: -1, + message: Spree.t('validation.must_be_int') + } + validates :price, numericality: true + validates_with Stock::AvailabilityValidator + + validate :ensure_proper_currency + before_destroy :update_inventory + before_destroy :destroy_inventory_units + + after_save :update_inventory + after_save :update_adjustments - validates :variant, :presence => true - validates :quantity, :numericality => { :only_integer => true, :message => I18n.t('validation.must_be_int'), :greater_than => -1 } - validates :price, :numericality => true - validate :stock_availability - validate :quantity_no_less_than_shipped + after_create :update_tax_charge - attr_accessible :quantity, :variant_id + delegate :name, :description, :sku, :should_track_inventory?, to: :variant - before_save :update_inventory - before_destroy :ensure_not_shipped, :remove_inventory + attr_accessor :target_shipment - after_save :update_order - after_destroy :update_order + self.whitelisted_ransackable_associations = ['variant'] + self.whitelisted_ransackable_attributes = ['variant_id'] def copy_price if variant self.price = variant.price if price.nil? + self.cost_price = variant.cost_price if cost_price.nil? self.currency = variant.currency if currency.nil? end end - def increment_quantity - self.quantity += 1 - end - - def decrement_quantity - self.quantity -= 1 + def copy_tax_category + if variant + self.tax_category = variant.tax_category + end end def amount price * quantity end - alias total amount + alias subtotal amount + + def discounted_amount + amount + promo_total + end + + def discounted_money + Spree::Money.new(discounted_amount, { currency: currency }) + end + + def final_amount + amount + adjustment_total + end + alias total final_amount def single_money - Spree::Money.new(price, { :currency => currency }) + Spree::Money.new(price, { currency: currency }) end alias single_display_amount single_money def money - Spree::Money.new(amount, { :currency => currency }) + Spree::Money.new(amount, { currency: currency }) end alias display_total money alias display_amount money - def adjust_quantity + def invalid_quantity_check self.quantity = 0 if quantity.nil? || quantity < 0 end def sufficient_stock? - return true if Spree::Config[:allow_backorders] - if new_record? || !order.completed? - variant.on_hand >= quantity - else - variant.on_hand >= (quantity - self.changed_attributes['quantity'].to_i) - end + Stock::Quantifier.new(variant).can_supply? quantity end def insufficient_stock? !sufficient_stock? end + # Remove product default_scope `deleted_at: nil` + def product + variant.product + end + + # Remove variant default_scope `deleted_at: nil` + def variant + Spree::Variant.unscoped { super } + end + + def options=(options={}) + return unless options.present? + + opts = options.dup # we will be deleting from the hash, so leave the caller's copy intact + + currency = opts.delete(:currency) || order.try(:currency) + + if currency + self.currency = currency + self.price = variant.price_in(currency).amount + + variant.price_modifier_amount_in(currency, opts) + else + self.price = variant.price + + variant.price_modifier_amount(opts) + end + + self.assign_attributes opts + end + private def update_inventory - return true unless order.completed? - - if new_record? - InventoryUnit.increase(order, variant, quantity) - elsif old_quantity = self.changed_attributes['quantity'] - if old_quantity < quantity - InventoryUnit.increase(order, variant, (quantity - old_quantity)) - elsif old_quantity > quantity - InventoryUnit.decrease(order, variant, (old_quantity - quantity)) - end + if (changed? || target_shipment.present?) && self.order.has_checkout_step?("delivery") + Spree::OrderInventory.new(self.order, self).verify(target_shipment) end end - def remove_inventory - return true unless order.completed? - - InventoryUnit.decrease(order, variant, quantity) + def destroy_inventory_units + inventory_units.destroy_all end - def update_order - # update the order totals, etc. - order.create_tax_charge! - order.update! + def update_adjustments + if quantity_changed? + update_tax_charge # Called to ensure pre_tax_amount is updated. + recalculate_adjustments + end end - def ensure_not_shipped - if order.try(:inventory_units).to_a.any?{ |unit| unit.variant_id == variant_id && unit.shipped? } - errors.add :base, I18n.t('validation.cannot_destory_line_item_as_inventory_units_have_shipped') - return false - end + def recalculate_adjustments + Spree::ItemAdjustments.new(self).update end - # Validation - def stock_availability - return if sufficient_stock? - errors.add(:quantity, I18n.t('validation.cannot_be_greater_than_available_stock')) + def update_tax_charge + Spree::TaxRate.adjust(order.tax_zone, [self]) end - def quantity_no_less_than_shipped - already_shipped = order.shipments.reduce(0) { |acc, s| acc + s.inventory_units.shipped.where(:variant_id => variant_id).count } - unless quantity >= already_shipped - errors.add(:quantity, I18n.t('validation.cannot_be_less_than_shipped_units')) + def ensure_proper_currency + unless currency == order.currency + errors.add(:currency, :must_match_order_currency) end end end diff --git a/core/app/models/spree/log_entry.rb b/core/app/models/spree/log_entry.rb index d0ba032a6ea..a17fceb4dab 100644 --- a/core/app/models/spree/log_entry.rb +++ b/core/app/models/spree/log_entry.rb @@ -1,5 +1,20 @@ module Spree - class LogEntry < ActiveRecord::Base - belongs_to :source, :polymorphic => true + class LogEntry < Spree::Base + belongs_to :source, polymorphic: true + + # Fix for #1767 + # If a payment fails, we want to make sure we keep the record of it failing + after_rollback :save_anyway + + def save_anyway + log = Spree::LogEntry.new + log.source = source + log.details = details + log.save! + end + + def parsed_details + @details ||= YAML.load(details) + end end end diff --git a/core/app/models/spree/mail_method.rb b/core/app/models/spree/mail_method.rb deleted file mode 100644 index 0c5acb5effd..00000000000 --- a/core/app/models/spree/mail_method.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Spree - class MailMethod < ActiveRecord::Base - - MAIL_AUTH = ['none', 'plain', 'login', 'cram_md5'] - SECURE_CONNECTION_TYPES = ['None','SSL','TLS'] - - preference :enable_mail_delivery, :boolean, :default => false - preference :mail_host, :string, :default => 'localhost' - preference :mail_domain, :string, :default => 'localhost' - preference :mail_port, :integer, :default => 25 - preference :mail_auth_type, :string, :default => MAIL_AUTH[0] - preference :smtp_username, :string - preference :smtp_password, :string - preference :secure_connection_type, :string, :default => SECURE_CONNECTION_TYPES[0] - preference :mails_from, :string, :default => 'no-reply@example.com' - preference :mail_bcc, :string, :default => 'spree@example.com' - preference :intercept_email, :string, :default => nil - - attr_accessible :environment, :preferred_enable_mail_delivery, - :preferred_mails_from, :preferred_mail_bcc, - :preferred_intercept_email, :preferred_mail_domain, - :preferred_mail_host, :preferred_mail_port, - :preferred_secure_connection_type, :preferred_mail_auth_type, - :preferred_smtp_username, :preferred_smtp_password - - validates :environment, :presence => true - - def self.current - where(:environment => Rails.env).first - end - end -end diff --git a/core/app/models/spree/option_type.rb b/core/app/models/spree/option_type.rb index d22efbd921b..66371c81586 100644 --- a/core/app/models/spree/option_type.rb +++ b/core/app/models/spree/option_type.rb @@ -1,14 +1,23 @@ module Spree - class OptionType < ActiveRecord::Base - has_many :option_values, :order => :position, :dependent => :destroy - has_many :product_option_types, :dependent => :destroy - has_and_belongs_to_many :prototypes, :join_table => 'spree_option_types_prototypes' + class OptionType < Spree::Base + acts_as_list - attr_accessible :name, :presentation, :option_values_attributes + has_many :option_values, -> { order(:position) }, dependent: :destroy, inverse_of: :option_type + has_many :product_option_types, dependent: :destroy, inverse_of: :option_type + has_many :products, through: :product_option_types + has_and_belongs_to_many :prototypes, join_table: 'spree_option_types_prototypes' - validates :name, :presentation, :presence => true - default_scope :order => "#{self.table_name}.position" + validates :name, presence: true, uniqueness: true + validates :presentation, presence: true - accepts_nested_attributes_for :option_values, :reject_if => lambda { |ov| ov[:name].blank? || ov[:presentation].blank? }, :allow_destroy => true + default_scope -> { order("#{self.table_name}.position") } + + accepts_nested_attributes_for :option_values, reject_if: lambda { |ov| ov[:name].blank? || ov[:presentation].blank? }, allow_destroy: true + + after_touch :touch_all_products + + def touch_all_products + products.update_all(updated_at: Time.current) + end end end diff --git a/core/app/models/spree/option_value.rb b/core/app/models/spree/option_value.rb index c9e3af2e1fe..a4c6051edbe 100644 --- a/core/app/models/spree/option_value.rb +++ b/core/app/models/spree/option_value.rb @@ -1,9 +1,18 @@ module Spree - class OptionValue < ActiveRecord::Base - belongs_to :option_type - acts_as_list :scope => :option_type - has_and_belongs_to_many :variants, :join_table => 'spree_option_values_variants', :class_name => "Spree::Variant" + class OptionValue < Spree::Base + belongs_to :option_type, class_name: 'Spree::OptionType', touch: true, inverse_of: :option_values + acts_as_list scope: :option_type + has_and_belongs_to_many :variants, join_table: 'spree_option_values_variants', class_name: "Spree::Variant" - attr_accessible :name, :presentation + validates :name, presence: true, uniqueness: { scope: :option_type_id } + validates :presentation, presence: true + + after_touch :touch_all_variants + + self.whitelisted_ransackable_attributes = ['presentation'] + + def touch_all_variants + variants.update_all(updated_at: Time.current) + end end end diff --git a/core/app/models/spree/order.rb b/core/app/models/spree/order.rb index 829ffcf209f..22d870b5e12 100644 --- a/core/app/models/spree/order.rb +++ b/core/app/models/spree/order.rb @@ -2,59 +2,76 @@ require 'spree/order/checkout' module Spree - class Order < ActiveRecord::Base - # TODO: - # Need to use fully qualified name here because during sandbox migration - # there is a class called Checkout which conflicts if you use this: - # - # include Checkout - # - # rather than the qualified name. This will most likely be fixed with the - # 1.3 release. + class Order < Spree::Base + + ORDER_NUMBER_LENGTH = 9 + ORDER_NUMBER_LETTERS = false + ORDER_NUMBER_PREFIX = 'R' + include Spree::Order::Checkout + include Spree::Order::CurrencyUpdater + include Spree::Order::Payments + checkout_flow do go_to_state :address go_to_state :delivery - go_to_state :payment, :if => lambda { |order| - # Fix for #2191 - if order.shipping_method - order.create_shipment! - order.update_totals - end - order.payment_required? - } - go_to_state :confirm, :if => lambda { |order| order.confirmation_required? } - go_to_state :complete, :if => lambda { |order| (order.payment_required? && order.paid?) || !order.payment_required? } - remove_transition :from => :delivery, :to => :confirm + go_to_state :payment, if: ->(order) { order.payment_required? } + go_to_state :confirm, if: ->(order) { order.confirmation_required? } + go_to_state :complete + remove_transition from: :delivery, to: :confirm end - token_resource + self.whitelisted_ransackable_associations = %w[shipments user promotions bill_address ship_address line_items] + self.whitelisted_ransackable_attributes = %w[completed_at created_at email number state payment_state shipment_state total considered_risky] - attr_accessible :line_items, :bill_address_attributes, :ship_address_attributes, :payments_attributes, - :ship_address, :bill_address, :line_items_attributes, :number, - :shipping_method_id, :email, :use_billing, :special_instructions, :currency + attr_reader :coupon_code + attr_accessor :temporary_address, :temporary_credit_card if Spree.user_class - belongs_to :user, :class_name => Spree.user_class.to_s + belongs_to :user, class_name: Spree.user_class.to_s + belongs_to :created_by, class_name: Spree.user_class.to_s + belongs_to :approver, class_name: Spree.user_class.to_s + belongs_to :canceler, class_name: Spree.user_class.to_s else belongs_to :user + belongs_to :created_by + belongs_to :approver + belongs_to :canceler end - belongs_to :bill_address, :foreign_key => :bill_address_id, :class_name => "Spree::Address" + belongs_to :bill_address, foreign_key: :bill_address_id, class_name: 'Spree::Address' alias_attribute :billing_address, :bill_address - belongs_to :ship_address, :foreign_key => :ship_address_id, :class_name => "Spree::Address" + belongs_to :ship_address, foreign_key: :ship_address_id, class_name: 'Spree::Address' alias_attribute :shipping_address, :ship_address - - belongs_to :shipping_method - - has_many :state_changes, :as => :stateful - has_many :line_items, :dependent => :destroy, :order => "created_at ASC" - has_many :inventory_units - has_many :payments, :dependent => :destroy - has_many :shipments, :dependent => :destroy - has_many :return_authorizations, :dependent => :destroy - has_many :adjustments, :as => :adjustable, :dependent => :destroy, :order => "created_at ASC" + alias_attribute :ship_total, :shipment_total + + belongs_to :store, class_name: 'Spree::Store' + has_many :state_changes, as: :stateful, dependent: :destroy + has_many :line_items, -> { order("#{LineItem.table_name}.created_at ASC") }, dependent: :destroy, inverse_of: :order + has_many :payments, dependent: :destroy + has_many :return_authorizations, dependent: :destroy, inverse_of: :order + has_many :reimbursements, inverse_of: :order + has_many :adjustments, -> { order("#{Adjustment.table_name}.created_at ASC") }, as: :adjustable, dependent: :destroy + has_many :line_item_adjustments, through: :line_items, source: :adjustments + has_many :shipment_adjustments, through: :shipments, source: :adjustments + has_many :inventory_units, inverse_of: :order + has_many :products, through: :variants + has_many :variants, through: :line_items + has_many :refunds, through: :payments + has_many :all_adjustments, + class_name: 'Spree::Adjustment', + foreign_key: :order_id, + dependent: :destroy, + inverse_of: :order + + has_and_belongs_to_many :promotions, join_table: 'spree_orders_promotions' + + has_many :shipments, dependent: :destroy, inverse_of: :order do + def states + pluck(:state).uniq + end + end accepts_nested_attributes_for :line_items accepts_nested_attributes_for :bill_address @@ -64,55 +81,77 @@ class Order < ActiveRecord::Base # Needs to happen before save_permalink is called before_validation :set_currency - before_validation :generate_order_number, :on => :create - before_validation :clone_billing_address, :if => :use_billing? + before_validation :generate_order_number, on: :create + before_validation :clone_billing_address, if: :use_billing? attr_accessor :use_billing + + before_create :create_token before_create :link_by_email - after_create :create_tax_charge! + before_update :homogenize_line_item_currencies, if: :currency_changed? - validates :email, :presence => true, :if => :require_email - validates :email, :email => true, :if => :require_email, :allow_blank => true + validates :email, presence: true, if: :require_email + validates :email, email: true, if: :require_email, allow_blank: true + validates :number, presence: true, uniqueness: { allow_blank: true } validate :has_available_shipment - validate :has_available_payment - make_permalink :field => :number + make_permalink field: :number + + delegate :update_totals, :persist_totals, :to => :updater + delegate :merge!, to: :merger class_attribute :update_hooks self.update_hooks = Set.new + class_attribute :line_item_comparison_hooks + self.line_item_comparison_hooks = Set.new + def self.by_number(number) - where(:number => number) + where(number: number) end - def self.between(start_date, end_date) - where(:created_at => start_date..end_date) - end + scope :created_between, ->(start_date, end_date) { where(created_at: start_date..end_date) } + scope :completed_between, ->(start_date, end_date) { where(completed_at: start_date..end_date) } + + # shows completed orders first, by their completed_at date, then uncompleted orders by their created_at + scope :reverse_chronological, -> { order('spree_orders.completed_at IS NULL', completed_at: :desc, created_at: :desc) } def self.by_customer(customer) joins(:user).where("#{Spree.user_class.table_name}.email" => customer) end def self.by_state(state) - where(:state => state) + where(state: state) end def self.complete - where('completed_at IS NOT NULL') + where.not(completed_at: nil) end def self.incomplete - where(:completed_at => nil) + where(completed_at: nil) end - # Use this method in other gems that wish to register their own custom logic that should be called after Order#updat + # Use this method in other gems that wish to register their own custom logic + # that should be called after Order#update def self.register_update_hook(hook) self.update_hooks.add(hook) end + # Use this method in other gems that wish to register their own custom logic + # that should be called when determining if two line items are equal. + def self.register_line_item_comparison_hook(hook) + self.line_item_comparison_hooks.add(hook) + end + # For compatiblity with Calculator::PriceSack def amount - line_items.sum(&:amount) + line_items.inject(0.0) { |sum, li| sum + li.amount } + end + + # Sum of all line item amounts pre-tax + def pre_tax_item_amount + line_items.to_a.sum(&:pre_tax_amount) end def currency @@ -120,19 +159,40 @@ def currency end def display_outstanding_balance - Spree::Money.new(outstanding_balance, { :currency => currency }) + Spree::Money.new(outstanding_balance, { currency: currency }) end def display_item_total - Spree::Money.new(item_total, { :currency => currency }) + Spree::Money.new(item_total, { currency: currency }) end def display_adjustment_total - Spree::Money.new(adjustment_total, { :currency => currency }) + Spree::Money.new(adjustment_total, { currency: currency }) end + def display_included_tax_total + Spree::Money.new(included_tax_total, { currency: currency }) + end + + def display_additional_tax_total + Spree::Money.new(additional_tax_total, { currency: currency }) + end + + def display_tax_total + Spree::Money.new(tax_total, { currency: currency }) + end + + def display_shipment_total + Spree::Money.new(shipment_total, { currency: currency }) + end + alias :display_ship_total :display_shipment_total + def display_total - Spree::Money.new(total, { :currency => currency }) + Spree::Money.new(total, { currency: currency }) + end + + def shipping_discount + shipment_adjustments.eligible.sum(:amount) * - 1 end def to_param @@ -140,88 +200,57 @@ def to_param end def completed? - !! completed_at + completed_at.present? end - # Indicates whether or not the user is allowed to proceed to checkout. Currently this is implemented as a - # check for whether or not there is at least one LineItem in the Order. Feel free to override this logic - # in your own application if you require additional steps before allowing a checkout. + # Indicates whether or not the user is allowed to proceed to checkout. + # Currently this is implemented as a check for whether or not there is at + # least one LineItem in the Order. Feel free to override this logic in your + # own application if you require additional steps before allowing a checkout. def checkout_allowed? line_items.count > 0 end # Is this a free order in which case the payment step should be skipped def payment_required? - update_totals total.to_f > 0.0 end # If true, causes the confirmation step to happen during the checkout process def confirmation_required? - payment_method && payment_method.payment_profiles_supported? - end - - # Indicates the number of items in the order - def item_count - line_items.sum(:quantity) + Spree::Config[:always_include_confirm_step] || + payments.valid.map(&:payment_method).compact.any?(&:payment_profiles_supported?) || + # Little hacky fix for #4117 + # If this wasn't here, order would transition to address state on confirm failure + # because there would be no valid payments any more. + state == 'confirm' end - # Indicates whether there are any backordered InventoryUnits associated with the Order. def backordered? - return false unless Spree::Config[:track_inventory_levels] - inventory_units.backordered.present? + shipments.any?(&:backordered?) end - # Returns the relevant zone (if any) to be used for taxation purposes. Uses default tax zone - # unless there is a specific match + # Returns the relevant zone (if any) to be used for taxation purposes. + # Uses default tax zone unless there is a specific match def tax_zone - zone_address = Spree::Config[:tax_using_ship_address] ? ship_address : bill_address - Zone.match(zone_address) || Zone.default_tax + @tax_zone ||= Zone.match(tax_address) || Zone.default_tax end - # Indicates whether tax should be backed out of the price calcualtions in cases where prices - # include tax but the customer is not required to pay taxes in that case. - def exclude_tax? - return false unless Spree::Config[:prices_inc_tax] - return tax_zone != Zone.default_tax - end - - # Array of adjustments that are inclusive in the variant price. Useful for when prices - # include tax (ex. VAT) and you need to record the tax amount separately. - def price_adjustments - adjustments = [] - - line_items.each do |line_item| - adjustments.concat line_item.adjustments - end - - adjustments - end - - # Array of totals grouped by Adjustment#label. Useful for displaying price adjustments on an - # invoice. For example, you can display tax breakout for cases where tax is included in price. - def price_adjustment_totals - totals = {} - - price_adjustments.each do |adjustment| - label = adjustment.label - totals[label] ||= 0 - totals[label] = totals[label] + adjustment.amount - end - - totals + # Returns the address for taxation based on configuration + def tax_address + Spree::Config[:tax_using_ship_address] ? ship_address : bill_address end def updater - OrderUpdater.new(self) + @updater ||= OrderUpdater.new(self) end def update! updater.update end - def update_totals - updater.update_totals + def merger + @merger ||= Spree::OrderMerger.new(self) end def clone_billing_address @@ -238,115 +267,112 @@ def allow_cancel? shipment_state.nil? || %w{ready backorder pending}.include?(shipment_state) end - def allow_resume? - # we shouldn't allow resume for legacy orders b/c we lack the information necessary to restore to a previous state - return false if state_changes.empty? || state_changes.last.previous_state.nil? - true - end - - def awaiting_returns? - return_authorizations.any? { |return_authorization| return_authorization.authorized? } + def all_inventory_units_returned? + inventory_units.all? { |inventory_unit| inventory_unit.returned? } end - - def add_variant(variant, quantity = 1, currency = nil) - current_item = find_line_item_by_variant(variant) - if current_item - current_item.quantity += quantity - current_item.currency = currency unless currency.nil? - current_item.save - else - current_item = LineItem.new(:quantity => quantity) - current_item.variant = variant - if currency - current_item.currency = currency unless currency.nil? - current_item.price = variant.price_in(currency).amount - else - current_item.price = variant.price - end - self.line_items << current_item - end - self.reload - current_item + def contents + @contents ||= Spree::OrderContents.new(self) end # Associates the specified user with the order. - def associate_user!(user) + def associate_user!(user, override_email = true) self.user = user - self.email = user.email - # disable validations since they can cause issues when associating - # an incomplete address during the address step - save(:validate => false) - end - - # FIXME refactor this method and implement validation using validates_* utilities - def generate_order_number - record = true - while record - random = "R#{Array.new(9){rand(9)}.join}" - record = self.class.where(:number => random).first + attrs_to_set = { user_id: user.id } + attrs_to_set[:email] = user.email if override_email + attrs_to_set[:created_by_id] = user.id if self.created_by.blank? + assign_attributes(attrs_to_set) + + if persisted? + # immediately persist the changes we just made, but don't use save since we might have an invalid address associated + self.class.unscoped.where(id: id).update_all(attrs_to_set) end - self.number = random if self.number.blank? - self.number end - # convenience method since many stores will not allow user to create multiple shipments - def shipment - @shipment ||= shipments.last - end + def generate_order_number(options = {}) + options[:length] ||= ORDER_NUMBER_LENGTH + options[:letters] ||= ORDER_NUMBER_LETTERS + options[:prefix] ||= ORDER_NUMBER_PREFIX - def contains?(variant) - find_line_item_by_variant(variant).present? + possible = (0..9).to_a + possible += ('A'..'Z').to_a if options[:letters] + + self.number ||= loop do + # Make a random number. + random = "#{options[:prefix]}#{(0...options[:length]).map { possible.shuffle.first }.join}" + # Use the random number if no other order exists with it. + if self.class.exists?(number: random) + # If over half of all possible options are taken add another digit. + options[:length] += 1 if self.class.count > (10 ** options[:length] / 2) + else + break random + end + end end - def quantity_of(variant) - line_item = find_line_item_by_variant(variant) - line_item ? line_item.quantity : 0 + def shipped_shipments + shipments.shipped end - def find_line_item_by_variant(variant) - line_items.detect { |line_item| line_item.variant_id == variant.id } + def contains?(variant, options = {}) + find_line_item_by_variant(variant, options).present? end - def ship_total - adjustments.shipping.map(&:amount).sum + def quantity_of(variant, options = {}) + line_item = find_line_item_by_variant(variant, options) + line_item ? line_item.quantity : 0 end - def tax_total - adjustments.tax.map(&:amount).sum + def find_line_item_by_variant(variant, options = {}) + line_items.detect { |line_item| + line_item.variant_id == variant.id && + line_item_options_match(line_item, options) + } end - # Clear shipment when transitioning to delivery step of checkout if the - # current shipping address is not eligible for the existing shipping method - def remove_invalid_shipments! - shipments.each { |s| s.destroy unless s.shipping_method.available_to_order?(self) } + # This method enables extensions to participate in the + # "Are these line items equal" decision. + # + # When adding to cart, an extension would send something like: + # params[:product_customizations]={...} + # + # and would provide: + # + # def product_customizations_match + def line_item_options_match(line_item, options) + return true unless options + + self.line_item_comparison_hooks.all? { |hook| + self.send(hook, line_item, options) + } end # Creates new tax charges if there are any applicable rates. If prices already # include taxes then price adjustments are created instead. def create_tax_charge! - Spree::TaxRate.adjust(self) + # We want to only look up the applicable tax zone once and pass it to TaxRate calculation to avoid duplicated lookups. + order_tax_zone = self.tax_zone + Spree::TaxRate.adjust(order_tax_zone, line_items) + Spree::TaxRate.adjust(order_tax_zone, shipments) if shipments.any? end - # Creates a new shipment (adjustment is created by shipment model) - def create_shipment! - shipping_method(true) - if shipment.present? - shipment.update_attributes!(:shipping_method => shipping_method) + def outstanding_balance + if state == 'canceled' + -1 * payment_total + elsif reimbursements.includes(:refunds).size > 0 + reimbursed = reimbursements.includes(:refunds).inject(0) do |sum, reimbursement| + sum + reimbursement.refunds.sum(:amount) + end + # If reimbursement has happened add it back to total to prevent balance_due payment state + # See: https://github.com/spree/spree/issues/6229 + total - (payment_total + reimbursed) else - self.shipments << Shipment.create!({ :order => self, - :shipping_method => shipping_method, - :address => self.ship_address, - :inventory_units => self.inventory_units}, :without_protection => true) + total - payment_total end end - def outstanding_balance - total - payment_total - end - def outstanding_balance? - self.outstanding_balance != 0 + self.outstanding_balance != 0 end def name @@ -355,239 +381,330 @@ def name end end + def can_ship? + self.complete? || self.resumed? || self.awaiting_return? || self.returned? + end + def credit_cards - credit_card_ids = payments.from_credit_card.map(&:source_id).uniq - CreditCard.scoped(:conditions => { :id => credit_card_ids }) + credit_card_ids = payments.from_credit_card.pluck(:source_id).uniq + CreditCard.where(id: credit_card_ids) + end + + def valid_credit_cards + credit_card_ids = payments.from_credit_card.valid.pluck(:source_id).uniq + CreditCard.where(id: credit_card_ids) end # Finalizes an in progress order after checkout is complete. # Called after transition to complete state when payments will have been processed def finalize! - touch :completed_at - InventoryUnit.assign_opening_inventory(self) - # lock all adjustments (coupon promotions, etc.) - adjustments.each { |adjustment| adjustment.update_column('locked', true) } + all_adjustments.each{|a| a.close} # update payment and shipment(s) states, and save - updater = OrderUpdater.new(self) updater.update_payment_state - shipments.each { |shipment| shipment.update!(self) } + shipments.each do |shipment| + shipment.update!(self) + shipment.finalize! + end + updater.update_shipment_state - save + save! + updater.run_hooks - deliver_order_confirmation_email + touch :completed_at + + deliver_order_confirmation_email unless confirmation_delivered? + + consider_risk + end - self.state_changes.create({ - :previous_state => 'cart', - :next_state => 'complete', - :name => 'order' , - :user_id => self.user_id - }, :without_protection => true) + def fulfill! + shipments.each { |shipment| shipment.update!(self) if shipment.persisted? } + updater.update_shipment_state + save! end def deliver_order_confirmation_email - begin - OrderMailer.confirm_email(self).deliver - rescue Exception => e - logger.error("#{e.class.name}: #{e.message}") - logger.error(e.backtrace * "\n") - end + OrderMailer.confirm_email(self.id).deliver + update_column(:confirmation_delivered, true) end # Helper methods for checkout steps + def paid? + payment_state == 'paid' || payment_state == 'credit_owed' + end - def available_shipping_methods(display_on = nil) - return [] unless ship_address - ShippingMethod.all_available(self, display_on) + def available_payment_methods + @available_payment_methods ||= (PaymentMethod.available(:front_end) + PaymentMethod.available(:both)).uniq end - def rate_hash - return @rate_hash if @rate_hash.present? + def billing_firstname + bill_address.try(:firstname) + end - # reserve one slot for each shipping method computation - computed_costs = Array.new(available_shipping_methods(:front_end).size) + def billing_lastname + bill_address.try(:lastname) + end - # create all the threads and kick off their execution - threads = available_shipping_methods(:front_end).each_with_index.map do |ship_method, index| - Thread.new { computed_costs[index] = [ship_method, ship_method.calculator.compute(self)] } - end + def insufficient_stock_lines + line_items.select(&:insufficient_stock?) + end - # wait for all threads to finish - threads.map(&:join) + ## + # Check to see if any line item variants are soft deleted. + # If so add error and restart checkout. + def ensure_line_item_variants_are_not_deleted + if line_items.any?{ |li| !li.variant || li.variant.paranoia_destroyed? } + errors.add(:base, Spree.t(:deleted_variants_present)) + restart_checkout_flow + false + else + true + end + end - # now consolidate and memoize the threaded results - @rate_hash ||= computed_costs.map do |pair| - ship_method,cost = *pair - next unless cost - ShippingRate.new( :id => ship_method.id, - :shipping_method => ship_method, - :name => ship_method.name, - :cost => cost, - :currency => currency) - end.compact.sort_by { |r| r.cost } + def ensure_line_items_are_in_stock + if insufficient_stock_lines.present? + errors.add(:base, Spree.t(:insufficient_stock_lines_present)) + restart_checkout_flow + false + else + true + end end - def paid? - payment_state == 'paid' + def empty! + line_items.destroy_all + updater.update_item_count + adjustments.destroy_all + shipments.destroy_all + state_changes.destroy_all + + update_totals + persist_totals end - def payment - payments.first + def has_step?(step) + checkout_steps.include?(step) end - def available_payment_methods - @available_payment_methods ||= PaymentMethod.available + def state_changed(name) + state = "#{name}_state" + if persisted? + old_state = self.send("#{state}_was") + new_state = self.send(state) + unless old_state == new_state + self.state_changes.create( + previous_state: old_state, + next_state: new_state, + name: name, + user_id: self.user_id + ) + end + end end - def payment_method - if payment and payment.payment_method - payment.payment_method - else - available_payment_methods.first + def coupon_code=(code) + @coupon_code = code.strip.downcase rescue nil + end + + def can_add_coupon? + Spree::Promotion.order_activatable?(self) + end + + + def shipped? + %w(partial shipped).include?(shipment_state) + end + + def create_proposed_shipments + all_adjustments.shipping.delete_all + shipments.destroy_all + self.shipments = Spree::Stock::Coordinator.new(self).shipments + end + + def apply_free_shipping_promotions + Spree::PromotionHandler::FreeShipping.new(self).activate + shipments.each { |shipment| ItemAdjustments.new(shipment).update } + updater.update_shipment_total + persist_totals + end + + # Clean shipments and make order back to address state + # + # At some point the might need to force the order to transition from address + # to delivery again so that proper updated shipments are created. + # e.g. customer goes back from payment step and changes order items + def ensure_updated_shipments + if shipments.any? && !self.completed? + self.shipments.destroy_all + self.update_column(:shipment_total, 0) + restart_checkout_flow end end - def pending_payments - payments.select {|p| p.state == "checkout"} + def restart_checkout_flow + self.update_columns( + state: 'cart', + updated_at: Time.now, + ) + self.next! if self.line_items.size > 0 end - def process_payments! - begin - pending_payments.each do |payment| - break if payment_total >= total + def refresh_shipment_rates + shipments.map &:refresh_rates + end - payment.process! + def shipping_eq_billing_address? + (bill_address.empty? && ship_address.empty?) || bill_address.same_as?(ship_address) + end - if payment.completed? - self.payment_total += payment.amount - end - end - rescue Core::GatewayError - !!Spree::Config[:allow_checkout_on_gateway_error] + def set_shipments_cost + shipments.each(&:update_amounts) + updater.update_shipment_total + persist_totals + end + + def is_risky? + self.payments.risky.count > 0 + end + + def canceled_by(user) + self.transaction do + cancel! + self.update_columns( + canceler_id: user.id, + canceled_at: Time.now, + ) end end - def billing_firstname - bill_address.try(:firstname) + def approved_by(user) + self.transaction do + approve! + self.update_columns( + approver_id: user.id, + approved_at: Time.now, + ) + end end - def billing_lastname - bill_address.try(:lastname) + def approved? + !!self.approved_at end - def products - line_items.map { |li| li.variant.product } + def can_approve? + !approved? end - def variants - line_items.map(&:variant) + def consider_risk + if is_risky? && !approved? + considered_risky! + end end - def insufficient_stock_lines - line_items.select &:insufficient_stock? + def considered_risky! + update_column(:considered_risky, true) end - def merge!(order) - order.line_items.each do |line_item| - next unless line_item.currency == currency - current_line_item = self.line_items.find_by_variant_id(line_item.variant_id) - if current_line_item - current_line_item.quantity += line_item.quantity - current_line_item.save + def approve! + update_column(:considered_risky, false) + end + + # moved from api order_decorator. This is a better place for it. + def update_line_items(line_item_params) + return if line_item_params.blank? + line_item_params.each_value do |attributes| + if attributes[:id].present? + self.line_items.find(attributes[:id]).update_attributes!(attributes) else - line_item.order_id = self.id - line_item.save + self.line_items.create!(attributes) end end - # So that the destroy doesn't take out line items which may have been re-assigned - order.line_items.reload - order.destroy + self.ensure_updated_shipments end - def empty! - line_items.destroy_all - adjustments.destroy_all + def reload(options=nil) + remove_instance_variable(:@tax_zone) if defined?(@tax_zone) + super end - # destroy any previous adjustments. - # Adjustments will be recalculated during order update. - def clear_adjustments! - adjustments.tax.each(&:destroy) - price_adjustments.each(&:destroy) + def tax_total + included_tax_total + additional_tax_total end - def has_step?(step) - checkout_steps.include?(step) + def quantity + line_items.sum(:quantity) end - def state_changed(name) - state = "#{name}_state" - if persisted? - old_state = self.send("#{state}_was") - self.state_changes.create({ - :previous_state => old_state, - :next_state => self.send(state), - :name => name, - :user_id => self.user_id - }, :without_protection => true) - end + def has_non_reimbursement_related_refunds? + refunds.non_reimbursement.exists? || + payments.offset_payment.exists? # how old versions of spree stored refunds end private - def link_by_email - self.email = user.email if self.user - end - # Determine if email is required (we don't want validation errors before we hit the checkout) - def require_email - return true unless new_record? or state == 'cart' - end + def link_by_email + self.email = user.email if self.user + end - def has_available_shipment - return unless has_step?("delivery") - return unless address? - return unless ship_address && ship_address.valid? - errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty? - end + # Determine if email is required (we don't want validation errors before we hit the checkout) + def require_email + true unless new_record? or ['cart', 'address'].include?(state) + end - def has_available_payment - return unless delivery? - errors.add(:base, :no_payment_methods_available) if available_payment_methods.empty? + def ensure_line_items_present + unless line_items.present? + errors.add(:base, Spree.t(:there_are_no_items_for_this_order)) and return false end + end - def after_cancel - restock_items! + def has_available_shipment + return unless has_step?("delivery") + return unless has_step?(:address) && address? + return unless ship_address && ship_address.valid? + # errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty? + end - #TODO: make_shipments_pending - OrderMailer.cancel_email(self).deliver - unless %w(partial shipped).include?(shipment_state) - self.payment_state = 'credit_owed' - end + def ensure_available_shipping_rates + if shipments.empty? || shipments.any? { |shipment| shipment.shipping_rates.blank? } + # After this point, order redirects back to 'address' state and asks user to pick a proper address + # Therefore, shipments are not necessary at this point. + shipments.destroy_all + errors.add(:base, Spree.t(:items_cannot_be_shipped)) and return false end + end - def restock_items! - line_items.each do |line_item| - InventoryUnit.decrease(self, line_item.variant, line_item.quantity) - end - end + def after_cancel + shipments.each { |shipment| shipment.cancel! } + payments.completed.each { |payment| payment.cancel! } + send_cancel_email + self.update! + end - def after_resume - unstock_items! - end + def send_cancel_email + OrderMailer.cancel_email(self.id).deliver + end - def unstock_items! - line_items.each do |line_item| - InventoryUnit.increase(self, line_item.variant, line_item.quantity) - end - end + def after_resume + shipments.each { |shipment| shipment.resume! } + consider_risk + end - def use_billing? - @use_billing == true || @use_billing == "true" || @use_billing == "1" - end + def use_billing? + @use_billing == true || @use_billing == 'true' || @use_billing == '1' + end - def set_currency - self.currency = Spree::Config[:currency] if self[:currency].nil? + def set_currency + self.currency = Spree::Config[:currency] if self[:currency].nil? + end + + def create_token + self.guest_token ||= loop do + random_token = SecureRandom.urlsafe_base64(nil, false) + break random_token unless self.class.exists?(guest_token: random_token) end + end end end diff --git a/core/app/models/spree/order/checkout.rb b/core/app/models/spree/order/checkout.rb index 908abd3f03b..4e5cdcdbedb 100644 --- a/core/app/models/spree/order/checkout.rb +++ b/core/app/models/spree/order/checkout.rb @@ -1,5 +1,5 @@ module Spree - class Order < ActiveRecord::Base + class Order < Spree::Base module Checkout def self.included(klass) klass.class_eval do @@ -7,6 +7,7 @@ def self.included(klass) class_attribute :previous_states class_attribute :checkout_flow class_attribute :checkout_steps + class_attribute :removed_transitions def self.checkout_flow(&block) if block_given? @@ -18,10 +19,10 @@ def self.checkout_flow(&block) end def self.define_state_machine! - # Needs to be an ordered hash to preserve flow order - self.checkout_steps = ActiveSupport::OrderedHash.new + self.checkout_steps = {} self.next_event_transitions = [] self.previous_states = [:cart] + self.removed_transitions = [] # Build the checkout flow using the checkout_flow defined either # within the Order class, or a decorator for that class. @@ -37,54 +38,90 @@ def self.define_state_machine! # To avoid multiple occurrences of the same transition being defined # On first definition, state_machines will not be defined state_machines.clear if respond_to?(:state_machines) - state_machine :state, :initial => :cart do - klass.next_event_transitions.each { |t| transition(t.merge(:on => :next)) } + state_machine :state, initial: :cart, use_transactions: false, action: :save_state do + klass.next_event_transitions.each { |t| transition(t.merge(on: :next)) } # Persist the state on the order - after_transition do |order| + after_transition do |order, transition| order.state = order.state + order.state_changes.create( + previous_state: transition.from, + next_state: transition.to, + name: 'order', + user_id: order.user_id + ) order.save end event :cancel do - transition :to => :canceled, :if => :allow_cancel? + transition to: :canceled, if: :allow_cancel? end event :return do - transition :to => :returned, :from => :awaiting_return, :unless => :awaiting_returns? + transition to: :returned, from: [:complete, :awaiting_return, :canceled], if: :all_inventory_units_returned? end event :resume do - transition :to => :resumed, :from => :canceled, :if => :allow_resume? + transition to: :resumed, from: :canceled, if: :canceled? end event :authorize_return do - transition :to => :awaiting_return + transition to: :awaiting_return end - before_transition :to => :complete do |order| - begin - order.process_payments! if order.payment_required? - rescue Spree::Core::GatewayError - !!Spree::Config[:allow_checkout_on_gateway_error] + if states[:payment] + before_transition to: :complete do |order| + if order.payment_required? && order.payments.valid.empty? + order.errors.add(:base, Spree.t(:no_payment_found)) + false + elsif order.payment_required? + order.process_payments! + end end + after_transition to: :complete, do: :persist_user_credit_card + before_transition to: :payment, do: :set_shipments_cost + before_transition to: :payment, do: :create_tax_charge! + before_transition to: :payment, do: :assign_default_credit_card end - before_transition :to => :delivery, :do => :remove_invalid_shipments! + before_transition from: :cart, do: :ensure_line_items_present - after_transition :to => :complete, :do => :finalize! - after_transition :to => :delivery, :do => :create_tax_charge! - after_transition :to => :resumed, :do => :after_resume - after_transition :to => :canceled, :do => :after_cancel + if states[:address] + before_transition from: :address, do: :create_tax_charge! + before_transition to: :address, do: :assign_default_addresses! + before_transition from: :address, do: :persist_user_address! + end + + if states[:delivery] + before_transition to: :delivery, do: :create_proposed_shipments + before_transition to: :delivery, do: :ensure_available_shipping_rates + before_transition to: :delivery, do: :set_shipments_cost + before_transition from: :delivery, do: :apply_free_shipping_promotions + end + + before_transition to: :resumed, do: :ensure_line_item_variants_are_not_deleted + before_transition to: :resumed, do: :ensure_line_items_are_in_stock + + before_transition to: :complete, do: :ensure_line_item_variants_are_not_deleted + before_transition to: :complete, do: :ensure_line_items_are_in_stock - after_transition :from => :delivery, :do => :create_shipment! + after_transition to: :complete, do: :finalize! + after_transition to: :resumed, do: :after_resume + after_transition to: :canceled, do: :after_cancel + + after_transition from: any - :cart, to: any - [:confirm, :complete] do |order| + order.update_totals + order.persist_totals + end end + + alias_method :save_state, :save end def self.go_to_state(name, options={}) self.checkout_steps[name] = options previous_states.each do |state| - add_transition({:from => state, :to => name}.merge(options)) + add_transition({from: state, to: name}.merge(options)) end if options[:if] self.previous_states << name @@ -93,13 +130,45 @@ def self.go_to_state(name, options={}) end end - def self.remove_transition(options={}) - if transition = find_transition(options) - self.next_event_transitions.delete(transition) + def self.insert_checkout_step(name, options = {}) + before = options.delete(:before) + after = options.delete(:after) unless before + after = self.checkout_steps.keys.last unless before || after + + cloned_steps = self.checkout_steps.clone + cloned_removed_transitions = self.removed_transitions.clone + self.checkout_flow do + cloned_steps.each_pair do |key, value| + self.go_to_state(name, options) if key == before + self.go_to_state(key, value) + self.go_to_state(name, options) if key == after + end + cloned_removed_transitions.each do |transition| + self.remove_transition(transition) + end + end + end + + def self.remove_checkout_step(name) + cloned_steps = self.checkout_steps.clone + cloned_removed_transitions = self.removed_transitions.clone + self.checkout_flow do + cloned_steps.each_pair do |key, value| + self.go_to_state(key, value) unless key == name + end + cloned_removed_transitions.each do |transition| + self.remove_transition(transition) + end end end + def self.remove_transition(options={}) + self.removed_transitions << options + self.next_event_transitions.delete(find_transition(options)) + end + def self.find_transition(options={}) + return nil if options.nil? || !options.include?(:from) || !options.include?(:to) self.next_event_transitions.detect do |transition| transition[options[:from].to_sym] == options[:to].to_sym end @@ -110,7 +179,11 @@ def self.next_event_transitions end def self.checkout_steps - @checkout_steps ||= ActiveSupport::OrderedHash.new + @checkout_steps ||= {} + end + + def self.checkout_step_names + self.checkout_steps.keys end def self.add_transition(options) @@ -118,15 +191,133 @@ def self.add_transition(options) end def checkout_steps - checkout_steps = [] - # TODO: replace this with each_with_object once Ruby 1.9 is standard - self.class.checkout_steps.each do |step, options| - if options[:if] - next unless options[:if].call(self) - end + steps = self.class.checkout_steps.each_with_object([]) { |(step, options), checkout_steps| + next if options.include?(:if) && !options[:if].call(self) checkout_steps << step + }.map(&:to_s) + # Ensure there is always a complete step + steps << "complete" unless steps.include?("complete") + steps + end + + def has_checkout_step?(step) + step.present? && self.checkout_steps.include?(step) + end + + def passed_checkout_step?(step) + has_checkout_step?(step) && checkout_step_index(step) < checkout_step_index(self.state) + end + + def checkout_step_index(step) + self.checkout_steps.index(step).to_i + end + + def self.removed_transitions + @removed_transitions ||= [] + end + + def can_go_to_state?(state) + return false unless has_checkout_step?(self.state) && has_checkout_step?(state) + checkout_step_index(state) > checkout_step_index(self.state) + end + + define_callbacks :updating_from_params, terminator: ->(target, result) { result == false } + + set_callback :updating_from_params, :before, :update_params_payment_source + + def update_from_params(params, permitted_params, request_env = {}) + success = false + @updating_params = params + run_callbacks :updating_from_params do + attributes = @updating_params[:order] ? @updating_params[:order].permit(permitted_params).delete_if { |k,v| v.nil? } : {} + + # Set existing card after setting permitted parameters because + # rails would slice parameters containg ruby objects, apparently + existing_card_id = @updating_params[:order] ? @updating_params[:order][:existing_card] : nil + + if existing_card_id.present? + credit_card = CreditCard.find existing_card_id + if credit_card.user_id != self.user_id || credit_card.user_id.blank? + raise Core::GatewayError.new Spree.t(:invalid_credit_card) + end + + credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present? + + attributes[:payments_attributes].first[:source] = credit_card + attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id + attributes[:payments_attributes].first.delete :source_attributes + end + + if attributes[:payments_attributes] + attributes[:payments_attributes].first[:request_env] = request_env + end + + success = self.update_attributes(attributes) + set_shipments_cost if self.shipments.any? + end + + @updating_params = nil + success + end + + def assign_default_addresses! + if self.user + self.bill_address = user.bill_address.try(:clone) if !self.bill_address_id && user.bill_address.try(:valid?) + # Skip setting ship address if order doesn't have a delivery checkout step + # to avoid triggering validations on shipping address + self.ship_address = user.ship_address.try(:clone) if !self.ship_address_id && user.ship_address.try(:valid?) && self.checkout_steps.include?("delivery") + end + end + + def persist_user_address! + if !self.temporary_address && self.user && self.user.respond_to?(:persist_order_address) && self.bill_address_id + self.user.persist_order_address(self) + end + end + + def persist_user_credit_card + if !self.temporary_credit_card && self.user_id && self.valid_credit_cards.present? + default_cc = self.valid_credit_cards.first + default_cc.user_id = self.user_id + default_cc.default = true + default_cc.save + end + end + + def assign_default_credit_card + if self.payments.from_credit_card.count == 0 && self.user && self.user.default_credit_card.try(:valid?) + cc = self.user.default_credit_card + self.payments.create!(payment_method_id: cc.payment_method_id, source: cc) + end + end + + private + # For payment step, filter order parameters to produce the expected nested + # attributes for a single payment and its source, discarding attributes + # for payment methods other than the one selected + # + # In case a existing credit card is provided it needs to build the payment + # attributes from scratch so we can set the amount. example payload: + # + # { + # "order": { + # "existing_card": "2" + # } + # } + # + def update_params_payment_source + if @updating_params[:payment_source].present? + source_params = @updating_params.delete(:payment_source)[@updating_params[:order][:payments_attributes].first[:payment_method_id].to_s] + + if source_params + @updating_params[:order][:payments_attributes].first[:source_attributes] = source_params + end + end + + if @updating_params[:order] && (@updating_params[:order][:payments_attributes] || @updating_params[:order][:existing_card]) + @updating_params[:order][:payments_attributes] ||= [{}] + @updating_params[:order][:payments_attributes].first[:amount] = self.total end - checkout_steps.map(&:to_s) end end end diff --git a/core/app/models/spree/order/currency_updater.rb b/core/app/models/spree/order/currency_updater.rb new file mode 100644 index 00000000000..761bf2aa914 --- /dev/null +++ b/core/app/models/spree/order/currency_updater.rb @@ -0,0 +1,40 @@ +module Spree + class Order < Spree::Base + module CurrencyUpdater + extend ActiveSupport::Concern + + included do + + def homogenize_line_item_currencies + update_line_item_currencies! + update! + end + + end + + # Updates prices of order's line items + def update_line_item_currencies! + line_items.where('currency != ?', currency).each do |line_item| + update_line_item_price!(line_item) + end + end + + # Returns the price object from given item + def price_from_line_item(line_item) + line_item.variant.prices.where(currency: currency).first + end + + # Updates price from given line item + def update_line_item_price!(line_item) + price = price_from_line_item(line_item) + + if price + line_item.update_attributes!(currency: price.currency, price: price.amount) + else + raise RuntimeError, "no #{currency} price found for #{line_item.product.name} (#{line_item.variant.sku})" + end + end + + end + end +end diff --git a/core/app/models/spree/order/payments.rb b/core/app/models/spree/order/payments.rb new file mode 100644 index 00000000000..76c3bd830bd --- /dev/null +++ b/core/app/models/spree/order/payments.rb @@ -0,0 +1,66 @@ +module Spree + class Order < Spree::Base + module Payments + extend ActiveSupport::Concern + included do + # processes any pending payments and must return a boolean as it's + # return value is used by the checkout state_machine to determine + # success or failure of the 'complete' event for the order + # + # Returns: + # + # - true if all pending_payments processed successfully + # + # - true if a payment failed, ie. raised a GatewayError + # which gets rescued and converted to TRUE when + # :allow_checkout_gateway_error is set to true + # + # - false if a payment failed, ie. raised a GatewayError + # which gets rescued and converted to FALSE when + # :allow_checkout_on_gateway_error is set to false + # + def process_payments! + process_payments_with(:process!) + end + + def authorize_payments! + process_payments_with(:authorize!) + end + + def capture_payments! + process_payments_with(:purchase!) + end + + def pending_payments + payments.select { |payment| payment.pending? } + end + + def unprocessed_payments + payments.select { |payment| payment.checkout? } + end + + private + + def process_payments_with(method) + # Don't run if there is nothing to pay. + return if payment_total >= total + # Prevent orders from transitioning to complete without a successfully processed payment. + raise Core::GatewayError.new(Spree.t(:no_payment_found)) if unprocessed_payments.empty? + + unprocessed_payments.each do |payment| + break if payment_total >= total + + payment.public_send(method) + + if payment.completed? + self.payment_total += payment.amount + end + end + rescue Core::GatewayError => e + result = !!Spree::Config[:allow_checkout_on_gateway_error] + errors.add(:base, e.message) and return result + end + end + end + end +end diff --git a/core/app/models/spree/order_contents.rb b/core/app/models/spree/order_contents.rb new file mode 100644 index 00000000000..53b3dd7b7ea --- /dev/null +++ b/core/app/models/spree/order_contents.rb @@ -0,0 +1,111 @@ +module Spree + class OrderContents + attr_accessor :order, :currency + + def initialize(order) + @order = order + end + + def add(variant, quantity = 1, options = {}) + line_item = add_to_line_item(variant, quantity, options) + after_add_or_remove(line_item, options) + end + + def remove(variant, quantity = 1, options = {}) + line_item = remove_from_line_item(variant, quantity, options) + after_add_or_remove(line_item, options) + end + + def update_cart(params) + if order.update_attributes(filter_order_items(params)) + order.line_items = order.line_items.select { |li| li.quantity > 0 } + # Update totals, then check if the order is eligible for any cart promotions. + # If we do not update first, then the item total will be wrong and ItemTotal + # promotion rules would not be triggered. + persist_totals + PromotionHandler::Cart.new(order).activate + order.ensure_updated_shipments + persist_totals + true + else + false + end + end + + private + def after_add_or_remove(line_item, options = {}) + persist_totals + shipment = options[:shipment] + shipment.present? ? shipment.update_amounts : order.ensure_updated_shipments + PromotionHandler::Cart.new(order, line_item).activate + ItemAdjustments.new(line_item).update + persist_totals + line_item + end + + def filter_order_items(params) + filtered_params = params.symbolize_keys + return filtered_params if filtered_params[:line_items_attributes].nil? || filtered_params[:line_items_attributes][:id] + + line_item_ids = order.line_items.pluck(:id) + + params[:line_items_attributes].each_pair do |id, value| + unless line_item_ids.include?(value[:id].to_i) || value[:variant_id].present? + filtered_params[:line_items_attributes].delete(id) + end + end + filtered_params + end + + def order_updater + @updater ||= OrderUpdater.new(order) + end + + def persist_totals + order_updater.update_item_count + order_updater.update + end + + def add_to_line_item(variant, quantity, options = {}) + line_item = grab_line_item_by_variant(variant, false, options) + + if line_item + line_item.quantity += quantity.to_i + line_item.currency = currency unless currency.nil? + else + opts = { currency: order.currency }.merge ActionController::Parameters.new(options). + permit(PermittedAttributes.line_item_attributes) + line_item = order.line_items.new(quantity: quantity, + variant: variant, + options: opts) + end + line_item.target_shipment = options[:shipment] if options.has_key? :shipment + line_item.save! + line_item + end + + def remove_from_line_item(variant, quantity, options = {}) + line_item = grab_line_item_by_variant(variant, true, options) + line_item.quantity -= quantity + line_item.target_shipment= options[:shipment] + + if line_item.quantity.zero? + order.line_items.destroy(line_item) + else + line_item.save! + end + + line_item + end + + def grab_line_item_by_variant(variant, raise_error = false, options = {}) + line_item = order.find_line_item_by_variant(variant, options) + + if !line_item.present? && raise_error + raise ActiveRecord::RecordNotFound, "Line item not found for variant #{variant.sku}" + end + + line_item + end + end +end diff --git a/core/app/models/spree/order_inventory.rb b/core/app/models/spree/order_inventory.rb new file mode 100644 index 00000000000..5534b3b6470 --- /dev/null +++ b/core/app/models/spree/order_inventory.rb @@ -0,0 +1,107 @@ +module Spree + class OrderInventory + attr_accessor :order, :line_item, :variant + + def initialize(order, line_item) + @order = order + @line_item = line_item + @variant = line_item.variant + end + + # Only verify inventory for completed orders (as orders in frontend checkout + # have inventory assigned via +order.create_proposed_shipment+) or when + # shipment is explicitly passed + # + # In case shipment is passed the stock location should only unstock or + # restock items if the order is completed. That is so because stock items + # are always unstocked when the order is completed through +shipment.finalize+ + def verify(shipment = nil) + if order.completed? || shipment.present? + + if inventory_units.size < line_item.quantity + quantity = line_item.quantity - inventory_units.size + + shipment = determine_target_shipment unless shipment + add_to_shipment(shipment, quantity) + elsif inventory_units.size > line_item.quantity + remove(inventory_units, shipment) + end + end + end + + def inventory_units + line_item.inventory_units + end + + private + def remove(item_units, shipment = nil) + quantity = item_units.size - line_item.quantity + + if shipment.present? + remove_from_shipment(shipment, quantity) + else + order.shipments.each do |shipment| + break if quantity == 0 + quantity -= remove_from_shipment(shipment, quantity) + end + end + end + + # Returns either one of the shipment: + # + # first unshipped that already includes this variant + # first unshipped that's leaving from a stock_location that stocks this variant + def determine_target_shipment + shipment = order.shipments.detect do |shipment| + shipment.ready_or_pending? && shipment.include?(variant) + end + + shipment ||= order.shipments.detect do |shipment| + shipment.ready_or_pending? && variant.stock_location_ids.include?(shipment.stock_location_id) + end + end + + def add_to_shipment(shipment, quantity) + if variant.should_track_inventory? + on_hand, back_order = shipment.stock_location.fill_status(variant, quantity) + + on_hand.times { shipment.set_up_inventory('on_hand', variant, order, line_item) } + back_order.times { shipment.set_up_inventory('backordered', variant, order, line_item) } + else + quantity.times { shipment.set_up_inventory('on_hand', variant, order, line_item) } + end + + # adding to this shipment, and removing from stock_location + if order.completed? + shipment.stock_location.unstock(variant, quantity, shipment) + end + + quantity + end + + def remove_from_shipment(shipment, quantity) + return 0 if quantity == 0 || shipment.shipped? + + shipment_units = shipment.inventory_units_for_item(line_item, variant).reject do |variant_unit| + variant_unit.state == 'shipped' + end.sort_by(&:state) + + removed_quantity = 0 + + shipment_units.each do |inventory_unit| + break if removed_quantity == quantity + inventory_unit.destroy + removed_quantity += 1 + end + + shipment.destroy if shipment.inventory_units.count == 0 + + # removing this from shipment, and adding to stock_location + if order.completed? + shipment.stock_location.restock variant, removed_quantity, shipment + end + + removed_quantity + end + end +end diff --git a/core/app/models/spree/order_merger.rb b/core/app/models/spree/order_merger.rb new file mode 100644 index 00000000000..d971c01e700 --- /dev/null +++ b/core/app/models/spree/order_merger.rb @@ -0,0 +1,65 @@ +module Spree + class OrderMerger + attr_accessor :order + delegate :updater, to: :order + + def initialize(order) + @order = order + end + + def merge!(other_order, user = nil) + other_order.line_items.each do |other_order_line_item| + next unless other_order_line_item.currency == order.currency + + current_line_item = find_matching_line_item(other_order_line_item) + handle_merge(current_line_item, other_order_line_item) + end + + set_user(user) + persist_merge + + # So that the destroy doesn't take out line items which may have been re-assigned + other_order.line_items.reload + other_order.destroy + end + + # Compare the line item of the other order with mine. + # Make sure you allow any extensions to chime in on whether or + # not the extension-specific parts of the line item match + def find_matching_line_item(other_order_line_item) + order.line_items.detect do |my_li| + my_li.variant == other_order_line_item.variant && + order.line_item_comparison_hooks.all? do |hook| + order.send(hook, my_li, other_order_line_item.serializable_hash) + end + end + end + + def set_user(user = nil) + order.associate_user!(user) if !order.user && !user.blank? + end + + # The idea is the end developer can choose to override the merge + # to their own choosing. Default is merge with errors. + def handle_merge(current_line_item, other_order_line_item) + if current_line_item + current_line_item.quantity += other_order_line_item.quantity + handle_error(current_line_item) unless current_line_item.save + else + other_order_line_item.order_id = order.id + handle_error(other_order_line_item) unless other_order_line_item.save + end + end + + # Change the error messages as you choose. + def handle_error(line_item) + order.errors[:base] << line_item.errors.full_messages + end + + def persist_merge + updater.update_item_count + updater.update_item_total + updater.persist_totals + end + end +end diff --git a/core/app/models/spree/order_populator.rb b/core/app/models/spree/order_populator.rb new file mode 100644 index 00000000000..a3604b26245 --- /dev/null +++ b/core/app/models/spree/order_populator.rb @@ -0,0 +1,43 @@ +module Spree + class OrderPopulator + attr_accessor :order, :currency + attr_reader :errors + + def initialize(order, currency) + @order = order + @currency = currency + @errors = ActiveModel::Errors.new(self) + end + + def populate(variant_id, quantity, options = {}) + ActiveSupport::Deprecation.warn "OrderPopulator is deprecated and will be removed from Spree 3, use OrderContents with order.contents.add instead.", caller + # protect against passing a nil hash being passed in + # due to an empty params[:options] + attempt_cart_add(variant_id, quantity, options || {}) + valid? + end + + def valid? + errors.empty? + end + + private + + def attempt_cart_add(variant_id, quantity, options = {}) + quantity = quantity.to_i + # 2,147,483,647 is crazy. + # See issue #2695. + if quantity > 2_147_483_647 + errors.add(:base, Spree.t(:please_enter_reasonable_quantity, scope: :order_populator)) + return false + end + + variant = Spree::Variant.find(variant_id) + begin + @order.contents.add(variant, quantity, options.merge(currency: currency)) + rescue ActiveRecord::RecordInvalid => e + errors.add(:base, e.record.errors.messages.values.join(" ")) + end + end + end +end diff --git a/core/app/models/spree/order_updater.rb b/core/app/models/spree/order_updater.rb index 11e55042741..09526c23077 100644 --- a/core/app/models/spree/order_updater.rb +++ b/core/app/models/spree/order_updater.rb @@ -1,7 +1,7 @@ module Spree class OrderUpdater attr_reader :order - delegate :payments, :line_items, :adjustments, :shipments, :update_hooks, :to => :order + delegate :payments, :line_items, :adjustments, :all_adjustments, :shipments, :update_hooks, :quantity, to: :order def initialize(order) @order = order @@ -16,43 +16,100 @@ def initialize(order) # associations try to save and then in turn try to call +update!+ again.) def update update_totals - update_payment_state - - # give each of the shipments a chance to update themselves - shipments.each { |shipment| shipment.update!(order) }#(&:update!) - update_shipment_state - update_adjustments - # update totals a second time in case updated adjustments have an effect on the total - update_totals - - order.update_attributes_without_callbacks({ - :payment_state => order.payment_state, - :shipment_state => order.shipment_state, - :item_total => order.item_total, - :adjustment_total => order.adjustment_total, - :payment_total => order.payment_total, - :total => order.total - }) - - #ensure checkout payment always matches order total - if order.payment and order.payment.checkout? and order.payment.amount != order.total - order.payment.update_attributes_without_callbacks(:amount => order.total) + if order.completed? + update_payment_state + update_shipments + update_shipment_state end + run_hooks + persist_totals + end + def run_hooks update_hooks.each { |hook| order.send hook } end + def recalculate_adjustments + all_adjustments.includes(:adjustable).map(&:adjustable).uniq.each { |adjustable| Spree::ItemAdjustments.new(adjustable).update } + end + # Updates the following Order total values: # # +payment_total+ The total value of all finalized Payments (NOTE: non-finalized Payments are excluded) # +item_total+ The total value of all LineItems # +adjustment_total+ The total value of all adjustments (promotions, credits, etc.) + # +promo_total+ The total value of all promotion adjustments # +total+ The so-called "order total." This is equivalent to +item_total+ plus +adjustment_total+. def update_totals - order.payment_total = payments.completed.map(&:amount).sum - order.item_total = line_items.map(&:amount).sum - order.adjustment_total = adjustments.eligible.map(&:amount).sum - order.total = order.item_total + order.adjustment_total + update_payment_total + update_item_total + update_shipment_total + update_adjustment_total + end + + + # give each of the shipments a chance to update themselves + def update_shipments + shipments.each do |shipment| + next unless shipment.persisted? + shipment.update!(order) + shipment.refresh_rates + shipment.update_amounts + end + end + + def update_payment_total + order.payment_total = payments.completed.includes(:refunds).inject(0) { |sum, payment| sum + payment.amount - payment.refunds.sum(:amount) } + end + + def update_shipment_total + order.shipment_total = shipments.sum(:cost) + update_order_total + end + + def update_order_total + order.total = order.item_total + order.shipment_total + order.adjustment_total + end + + def update_adjustment_total + recalculate_adjustments + order.adjustment_total = line_items.sum(:adjustment_total) + + shipments.sum(:adjustment_total) + + adjustments.eligible.sum(:amount) + order.included_tax_total = line_items.sum(:included_tax_total) + shipments.sum(:included_tax_total) + order.additional_tax_total = line_items.sum(:additional_tax_total) + shipments.sum(:additional_tax_total) + + order.promo_total = line_items.sum(:promo_total) + + shipments.sum(:promo_total) + + adjustments.promotion.eligible.sum(:amount) + + update_order_total + end + + def update_item_count + order.item_count = quantity + end + + def update_item_total + order.item_total = line_items.sum('price * quantity') + update_order_total + end + + def persist_totals + order.update_columns( + payment_state: order.payment_state, + shipment_state: order.shipment_state, + item_total: order.item_total, + item_count: order.item_count, + adjustment_total: order.adjustment_total, + included_tax_total: order.included_tax_total, + additional_tax_total: order.additional_tax_total, + payment_total: order.payment_total, + shipment_total: order.shipment_total, + promo_total: order.promo_total, + total: order.total, + updated_at: Time.now, + ) end # Updates the +shipment_state+ attribute according to the following logic: @@ -69,22 +126,24 @@ def update_shipment_state if order.backordered? order.shipment_state = 'backorder' else - order.shipment_state = - case shipments.count - when 0 - nil - when shipments.shipped.count - 'shipped' - when shipments.ready.count - 'ready' - when shipments.pending.count - 'pending' + # get all the shipment states for this order + shipment_states = shipments.states + if shipment_states.size > 1 + # multiple shiment states means it's most likely partially shipped + order.shipment_state = 'partial' else - 'partial' + # will return nil if no shipments are found + order.shipment_state = shipment_states.first + # TODO inventory unit states? + # if order.shipment_state && order.inventory_units.where(:shipment_id => nil).exists? + # shipments exist but there are unassigned inventory units + # order.shipment_state = 'partial' + # end end end order.state_changed('shipment') + order.shipment_state end # Updates the +payment_state+ attribute according to the following logic: @@ -96,39 +155,18 @@ def update_shipment_state # # The +payment_state+ value helps with reporting, etc. since it provides a quick and easy way to locate Orders needing attention. def update_payment_state - - #line_item are empty when user empties cart - if line_items.empty? || round_money(order.payment_total) < round_money(order.total) - if payments.present? && payments.last.state == 'failed' - order.payment_state = 'failed' - else - order.payment_state = 'balance_due' - end - elsif round_money(order.payment_total) > round_money(order.total) - order.payment_state = 'credit_owed' + last_state = order.payment_state + if payments.present? && payments.valid.size == 0 + order.payment_state = 'failed' + elsif order.state == 'canceled' && order.payment_total == 0 + order.payment_state = 'void' else - order.payment_state = 'paid' + order.payment_state = 'balance_due' if order.outstanding_balance > 0 + order.payment_state = 'credit_owed' if order.outstanding_balance < 0 + order.payment_state = 'paid' if !order.outstanding_balance? end - - order.state_changed('payment') + order.state_changed('payment') if last_state != order.payment_state + order.payment_state end - - # Updates each of the Order adjustments. - # - # This is intended to be called from an Observer so that the Order can - # respond to external changes to LineItem, Shipment, other Adjustments, etc. - # - # Adjustments will check if they are still eligible. Ineligible adjustments - # are preserved but not counted towards adjustment_total. - def update_adjustments - order.adjustments.reload.each { |adjustment| adjustment.update!(order) } - end - - - private - - def round_money(n) - (n * 100).round / 100.0 - end end end diff --git a/core/app/models/spree/payment.rb b/core/app/models/spree/payment.rb index d09ada6b09c..0256debdc76 100644 --- a/core/app/models/spree/payment.rb +++ b/core/app/models/spree/payment.rb @@ -1,49 +1,96 @@ module Spree - class Payment < ActiveRecord::Base + class Payment < Spree::Base include Spree::Payment::Processing - belongs_to :order - belongs_to :source, :polymorphic => true, :validate => true - belongs_to :payment_method - has_many :offsets, :class_name => "Spree::Payment", :foreign_key => :source_id, :conditions => "source_type = 'Spree::Payment' AND amount < 0 AND state = 'completed'" - has_many :log_entries, :as => :source + IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze + NON_RISKY_AVS_CODES = ['B', 'D', 'H', 'J', 'M', 'Q', 'T', 'V', 'X', 'Y'].freeze + RISKY_AVS_CODES = ['A', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'N', 'O', 'P', 'R', 'S', 'U', 'W', 'Z'].freeze - after_save :create_payment_profile, :if => :profiles_supported? + belongs_to :order, class_name: 'Spree::Order', touch: true, inverse_of: :payments + belongs_to :source, polymorphic: true + belongs_to :payment_method, class_name: 'Spree::PaymentMethod', inverse_of: :payments + + has_many :offsets, -> { offset_payment }, class_name: "Spree::Payment", foreign_key: :source_id + has_many :log_entries, as: :source + has_many :state_changes, as: :stateful + has_many :capture_events, :class_name => 'Spree::PaymentCaptureEvent' + has_many :refunds, inverse_of: :payment + + validates_presence_of :payment_method + before_validation :validate_source + before_create :set_unique_identifier + + after_save :create_payment_profile, if: :profiles_supported? # update the order totals, etc. after_save :update_order - attr_accessor :source_attributes + # invalidate previously entered payments + after_create :invalidate_old_payments + + attr_accessor :source_attributes, :request_env + after_initialize :build_source - attr_accessible :amount, :payment_method_id, :source_attributes + validates :amount, numericality: true + + default_scope -> { order("#{self.table_name}.created_at") } - scope :from_credit_card, lambda { where(:source_type => 'Spree::CreditCard') } - scope :with_state, lambda { |s| where(:state => s) } - scope :completed, with_state('completed') - scope :pending, with_state('pending') - scope :failed, with_state('failed') + scope :from_credit_card, -> { where(source_type: 'Spree::CreditCard') } + scope :with_state, ->(s) { where(state: s.to_s) } + # "offset" is reserved by activerecord + scope :offset_payment, -> { where("source_type = 'Spree::Payment' AND amount < 0 AND state = 'completed'") } + + scope :checkout, -> { with_state('checkout') } + scope :completed, -> { with_state('completed') } + scope :pending, -> { with_state('pending') } + scope :processing, -> { with_state('processing') } + scope :failed, -> { with_state('failed') } + + scope :risky, -> { where("avs_response IN (?) OR (cvv_response_code IS NOT NULL and cvv_response_code != 'M') OR state = 'failed'", RISKY_AVS_CODES) } + scope :valid, -> { where.not(state: %w(failed invalid)) } + + # transaction_id is much easier to understand + def transaction_id + response_code + end # order state machine (see http://github.com/pluginaweek/state_machine/tree/master for details) - state_machine :initial => 'checkout' do + state_machine initial: :checkout do # With card payments, happens before purchase or authorization happens + # + # Setting it after creating a profile and authorizing a full amount will + # prevent the payment from being authorized again once Order transitions + # to complete event :started_processing do - transition :from => ['checkout', 'pending', 'completed', 'processing'], :to => 'processing' + transition from: [:checkout, :pending, :completed, :processing], to: :processing end # When processing during checkout fails event :failure do - transition :from => ['pending', 'processing'], :to => 'failed' + transition from: [:pending, :processing], to: :failed end # With card payments this represents authorizing the payment event :pend do - transition :from => ['checkout', 'processing'], :to => 'pending' + transition from: [:checkout, :processing], to: :pending end # With card payments this represents completing a purchase or capture transaction event :complete do - transition :from => ['processing', 'pending', 'checkout'], :to => 'completed' + transition from: [:processing, :pending, :checkout], to: :completed end event :void do - transition :from => ['pending', 'completed', 'checkout'], :to => 'void' + transition from: [:pending, :processing, :completed, :checkout], to: :void + end + # when the card brand isnt supported + event :invalidate do + transition from: [:checkout], to: :invalid + end + + after_transition do |payment, transition| + payment.state_changes.create!( + previous_state: transition.from, + next_state: transition.to, + name: 'payment', + ) end end @@ -51,16 +98,27 @@ def currency order.currency end - def display_amount - Spree::Money.new(amount, { :currency => currency }) + def money + Spree::Money.new(amount, { currency: currency }) + end + alias display_amount money + + def amount=(amount) + self[:amount] = + case amount + when String + separator = I18n.t('number.currency.format.separator') + number = amount.delete("^0-9-#{separator}\.").tr(separator, '.') + number.to_d if number.present? + end || amount end def offsets_total - offsets.map(&:amount).sum + offsets.pluck(:amount).sum end def credit_allowed - amount - offsets_total + amount - (offsets_total.abs + refunds.sum(:amount)) end def can_credit? @@ -69,9 +127,11 @@ def can_credit? # see https://github.com/spree/spree/issues/981 def build_source - return if source_attributes.nil? - if payment_method and payment_method.payment_source_class + return unless new_record? + if source_attributes.present? && source.blank? && payment_method.try(:payment_source_class) self.source = payment_method.payment_source_class.new(source_attributes) + self.source.payment_method_id = payment_method.id + self.source.user_id = self.order.user_id if self.order end end @@ -85,12 +145,40 @@ def payment_source res || payment_method end + def is_avs_risky? + return false if avs_response.blank? || NON_RISKY_AVS_CODES.include?(avs_response) + return true + end + + def is_cvv_risky? + return false if cvv_response_code == "M" + return false if cvv_response_code.nil? + return false if cvv_response_message.present? + return true + end + + def captured_amount + capture_events.sum(:amount) + end + + def uncaptured_amount + amount - captured_amount + end + + def editable? + checkout? || pending? + end + private - def amount_is_valid_for_outstanding_balance_or_credit - return unless order - if amount != order.outstanding_balance - errors.add(:amount, "does not match outstanding balance (#{order.outstanding_balance})") + + def validate_source + if source && !source.valid? + source.errors.each do |field, error| + field_name = I18n.t("activerecord.attributes.#{source.class.to_s.underscore}.#{field}") + self.errors.add(Spree.t(source.class.to_s.demodulize.underscore), "#{field_name} #{error}") + end end + return !errors.present? end def profiles_supported? @@ -98,15 +186,69 @@ def profiles_supported? end def create_payment_profile - return unless source.is_a?(CreditCard) && source.number && !source.has_payment_profile? + # Don't attempt to create on bad payments. + return if %w(invalid failed).include?(state) + # Payment profile cannot be created without source + return unless source + # Imported payments shouldn't create a payment profile. + return if source.imported + payment_method.create_profile(self) rescue ActiveMerchant::ConnectionError => e gateway_error e end + def invalidate_old_payments + if state != 'invalid' and state != 'failed' + order.payments.with_state('checkout').where("id != ?", self.id).each do |payment| + payment.invalidate! + end + end + end + + def split_uncaptured_amount + if uncaptured_amount > 0 + order.payments.create! amount: uncaptured_amount, + avs_response: avs_response, + cvv_response_code: cvv_response_code, + cvv_response_message: cvv_response_message, + payment_method: payment_method, + response_code: response_code, + source: source, + state: 'pending' + update_attributes(amount: captured_amount) + end + end + def update_order - order.payments.reload - order.update! + if completed? || void? + order.updater.update_payment_total + end + + if order.completed? + order.updater.update_payment_state + order.updater.update_shipments + order.updater.update_shipment_state + end + + if self.completed? || order.completed? + order.persist_totals + end + end + + # Necessary because some payment gateways will refuse payments with + # duplicate IDs. We *were* using the Order number, but that's set once and + # is unchanging. What we need is a unique identifier on a per-payment basis, + # and this is it. Related to #1998. + # See https://github.com/spree/spree/issues/1998#issuecomment-12869105 + def set_unique_identifier + begin + self.identifier = generate_identifier + end while self.class.exists?(identifier: self.identifier) + end + + def generate_identifier + Array.new(8){ IDENTIFIER_CHARS.sample }.join end end end diff --git a/core/app/models/spree/payment/processing.rb b/core/app/models/spree/payment/processing.rb index 9c2a44bf455..71b0d5560cf 100644 --- a/core/app/models/spree/payment/processing.rb +++ b/core/app/models/spree/payment/processing.rb @@ -1,49 +1,41 @@ module Spree - class Payment < ActiveRecord::Base + class Payment < Spree::Base module Processing def process! - if payment_method && payment_method.source_required? - if source - if !processing? - if Spree::Config[:auto_capture] - purchase! - else - authorize! - end - end - else - raise Core::GatewayError.new(I18n.t(:payment_processing_failed)) - end + if payment_method && payment_method.auto_capture? + purchase! + else + authorize! end end def authorize! - started_processing! - gateway_action(source, :authorize, :pend) + handle_payment_preconditions { process_authorization } end + # Captures the entire amount of a payment. def purchase! - started_processing! - gateway_action(source, :purchase, :complete) + handle_payment_preconditions { process_purchase } end - def capture! + # Takes the amount in cents to capture. + # Can be used to capture partial amounts of a payment, and will create + # a new pending payment record for the remaining amount to capture later. + def capture!(amount = nil) return true if completed? + amount ||= money.money.cents started_processing! protect_from_connection_error do check_environment - - if payment_method.payment_profiles_supported? - # Gateways supporting payment profiles will need access to credit card object because this stores the payment profile information - # so supply the authorization itself as well as the credit card, rather than just the authorization code - response = payment_method.capture(self, source, gateway_options) - else - # Standard ActiveMerchant capture usage - response = payment_method.capture((amount * 100).round, - response_code, - gateway_options) - end - + # Standard ActiveMerchant capture usage + response = payment_method.capture( + amount, + response_code, + gateway_options + ) + money = ::Money.new(amount, currency) + capture_events.create!(amount: money.to_f) + split_uncaptured_amount handle_response(response, :complete, :failure) end end @@ -71,121 +63,143 @@ def void_transaction! end end end - end - - def credit!(credit_amount=nil) - protect_from_connection_error do - check_environment - credit_amount ||= credit_allowed >= order.outstanding_balance.abs ? order.outstanding_balance.abs : credit_allowed.abs - credit_amount = credit_amount.to_f + def cancel! + response = payment_method.cancel(response_code) + handle_response(response, :void, :failure) + end - if payment_method.payment_profiles_supported? - response = payment_method.credit((credit_amount * 100).round, source, response_code, gateway_options) - else - response = payment_method.credit((credit_amount * 100).round, response_code, gateway_options) - end + def gateway_options + order.reload + options = { :email => order.email, + :customer => order.email, + :customer_id => order.user_id, + :ip => order.last_ip_address, + # Need to pass in a unique identifier here to make some + # payment gateways happy. + # + # For more information, please see Spree::Payment#set_unique_identifier + :order_id => gateway_order_id } + + options.merge!({ :shipping => order.ship_total * 100, + :tax => order.additional_tax_total * 100, + :subtotal => order.item_total * 100, + :discount => order.promo_total * 100, + :currency => currency }) + + options.merge!({ :billing_address => order.bill_address.try(:active_merchant_hash), + :shipping_address => order.ship_address.try(:active_merchant_hash) }) + + options + end - record_response(response) + private - if response.success? - self.class.create({ :order => order, - :source => self, - :payment_method => payment_method, - :amount => credit_amount.abs * -1, - :response_code => response.authorization, - :state => 'completed' }, :without_protection => true) - else - gateway_error(response) - end + def process_authorization + started_processing! + gateway_action(source, :authorize, :pend) end - end - def partial_credit(amount) - return if amount > credit_allowed - started_processing! - credit!(amount) - end - - def gateway_options - options = { :email => order.email, - :customer => order.email, - :ip => '192.168.1.100', # TODO: Use an actual IP - :order_id => order.number } + def process_purchase + started_processing! + result = gateway_action(source, :purchase, :complete) + # This won't be called if gateway_action raises a GatewayError + capture_events.create!(amount: amount) + end - options.merge!({ :shipping => order.ship_total * 100, - :tax => order.tax_total * 100, - :subtotal => order.item_total * 100 }) + def handle_payment_preconditions(&block) + unless block_given? + raise ArgumentError.new("handle_payment_preconditions must be called with a block") + end - options.merge!({ :currency => currency }) + if payment_method && payment_method.source_required? + if source + if !processing? + if payment_method.supports?(source) || token_based? + yield + else + invalidate! + raise Core::GatewayError.new(Spree.t(:payment_method_not_supported)) + end + end + else + raise Core::GatewayError.new(Spree.t(:payment_processing_failed)) + end + end + end - options.merge!({ :billing_address => order.bill_address.try(:active_merchant_hash), - :shipping_address => order.ship_address.try(:active_merchant_hash) }) + def gateway_action(source, action, success_state) + protect_from_connection_error do + check_environment - options.merge!(:discount => promo_total) if respond_to?(:promo_total) - options - end + response = payment_method.send(action, money.money.cents, + source, + gateway_options) + handle_response(response, success_state, :failure) + end + end - private + def handle_response(response, success_state, failure_state) + record_response(response) - def gateway_action(source, action, success_state) - protect_from_connection_error do - check_environment + if response.success? + unless response.authorization.nil? + self.response_code = response.authorization + self.avs_response = response.avs_result['code'] - response = payment_method.send(action, (amount * 100).round, - source, - gateway_options) - handle_response(response, success_state, :failure) + if response.cvv_result + self.cvv_response_code = response.cvv_result['code'] + self.cvv_response_message = response.cvv_result['message'] + end + end + self.send("#{success_state}!") + else + self.send(failure_state) + gateway_error(response) + end end - end - def handle_response(response, success_state, failure_state) - record_response(response) + def record_response(response) + log_entries.create!(:details => response.to_yaml) + end - if response.success? - unless response.authorization.nil? - self.response_code = response.authorization - self.avs_response = response.avs_result['code'] + def protect_from_connection_error + begin + yield + rescue ActiveMerchant::ConnectionError => e + gateway_error(e) end - self.send("#{success_state}!") - else - self.send(failure_state) - gateway_error(response) end - end - def record_response(response) - log_entries.create({:details => response.to_yaml}, :without_protection => true) - end + def gateway_error(error) + if error.is_a? ActiveMerchant::Billing::Response + text = error.params['message'] || error.params['response_reason_text'] || error.message + elsif error.is_a? ActiveMerchant::ConnectionError + text = Spree.t(:unable_to_connect_to_gateway) + else + text = error.to_s + end + logger.error(Spree.t(:gateway_error)) + logger.error(" #{error.to_yaml}") + raise Core::GatewayError.new(text) + end - def protect_from_connection_error - begin - yield - rescue ActiveMerchant::ConnectionError => e - gateway_error(e) + # Saftey check to make sure we're not accidentally performing operations on a live gateway. + # Ex. When testing in staging environment with a copy of production data. + def check_environment + return if payment_method.environment == Rails.env + message = Spree.t(:gateway_config_unavailable) + " - #{Rails.env}" + raise Core::GatewayError.new(message) end - end - def gateway_error(error) - if error.is_a? ActiveMerchant::Billing::Response - text = error.params['message'] || error.params['response_reason_text'] || error.message - elsif error.is_a? ActiveMerchant::ConnectionError - text = I18n.t(:unable_to_connect_to_gateway) - else - text = error.to_s - end - logger.error(I18n.t(:gateway_error)) - logger.error(" #{error.to_yaml}") - raise Core::GatewayError.new(text) - end + # The unique identifier to be passed in to the payment gateway + def gateway_order_id + "#{order.number}-#{self.identifier}" + end - # Saftey check to make sure we're not accidentally performing operations on a live gateway. - # Ex. When testing in staging environment with a copy of production data. - def check_environment - return if payment_method.environment == Rails.env - message = I18n.t(:gateway_config_unavailable) + " - #{Rails.env}" - raise Core::GatewayError.new(message) + def token_based? + source.gateway_customer_profile_id.present? || source.gateway_payment_profile_id.present? + end end - end end diff --git a/core/app/models/spree/payment_capture_event.rb b/core/app/models/spree/payment_capture_event.rb new file mode 100644 index 00000000000..03c2e68f167 --- /dev/null +++ b/core/app/models/spree/payment_capture_event.rb @@ -0,0 +1,9 @@ +module Spree + class PaymentCaptureEvent < Spree::Base + belongs_to :payment, class_name: 'Spree::Payment' + + def display_amount + Spree::Money.new(amount, { currency: payment.currency }) + end + end +end diff --git a/core/app/models/spree/payment_method.rb b/core/app/models/spree/payment_method.rb index 080e98ab76c..ec0216b2bca 100644 --- a/core/app/models/spree/payment_method.rb +++ b/core/app/models/spree/payment_method.rb @@ -1,47 +1,49 @@ module Spree - class PaymentMethod < ActiveRecord::Base - default_scope where(:deleted_at => nil) + class PaymentMethod < Spree::Base + acts_as_paranoid + DISPLAY = [:both, :front_end, :back_end] + default_scope -> { where(deleted_at: nil) } - scope :production, lambda { where(:environment => 'production') } + scope :production, -> { where(environment: 'production') } - attr_accessible :name, :description, :environment, :active - validates :name, :presence => true + validates :name, presence: true + + has_many :payments, class_name: "Spree::Payment", inverse_of: :payment_method + has_many :credit_cards, class_name: "Spree::CreditCard" def self.providers Rails.application.config.spree.payment_methods end def provider_class - raise 'You must implement provider_class method for this gateway.' + raise ::NotImplementedError, 'You must implement provider_class method for this gateway.' end # The class that will process payments for this payment type, used for @payment.source # e.g. CreditCard in the case of a the Gateway payment type # nil means the payment method doesn't require a source e.g. check def payment_source_class - raise 'You must implement payment_source_class method for this gateway.' + raise ::NotImplementedError, 'You must implement payment_source_class method for this gateway.' end - def self.available + def self.available(display_on = 'both') all.select do |p| - p.active && (p.environment == Rails.env || p.environment.blank?) + p.active && + (p.display_on == display_on.to_s || p.display_on.blank?) && + (p.environment == Rails.env || p.environment.blank?) end end def self.active? - where(:type => self.to_s, :environment => Rails.env, :active => true).count > 0 + where(type: self.to_s, environment: Rails.env, active: true).count > 0 end def method_type type.demodulize.downcase end - def destroy - touch :deleted_at - end - def self.find_with_destroyed *args - self.with_exclusive_scope { find(*args) } + unscoped { find(*args) } end def payment_profiles_supported? @@ -51,5 +53,23 @@ def payment_profiles_supported? def source_required? true end + + # Custom gateways should redefine this method. See Gateway implementation + # as an example + def reusable_sources(order) + [] + end + + def auto_capture? + self.auto_capture.nil? ? Spree::Config[:auto_capture] : self.auto_capture + end + + def supports?(source) + true + end + + def cancel(response) + raise ::NotImplementedError, 'You must implement cancel method for this payment method.' + end end end diff --git a/core/app/models/spree/payment_method/check.rb b/core/app/models/spree/payment_method/check.rb index 108d8317d14..97680e1fd89 100644 --- a/core/app/models/spree/payment_method/check.rb +++ b/core/app/models/spree/payment_method/check.rb @@ -18,6 +18,8 @@ def capture(*args) ActiveMerchant::Billing::Response.new(true, "", {}, {}) end + def cancel(response); end + def void(*args) ActiveMerchant::Billing::Response.new(true, "", {}, {}) end diff --git a/core/app/models/spree/preference.rb b/core/app/models/spree/preference.rb index fb657f9f835..eb325fb4efa 100644 --- a/core/app/models/spree/preference.rb +++ b/core/app/models/spree/preference.rb @@ -1,65 +1,5 @@ -class Spree::Preference < ActiveRecord::Base - attr_accessible :key, :value_type, :value - - validates :key, :presence => true - validates :value_type, :presence => true - - scope :valid, lambda { where(Spree::Preference.arel_table[:key].not_eq(nil)).where(Spree::Preference.arel_table[:value_type].not_eq(nil)) } - - # The type conversions here should match - # the ones in spree::preferences::preferrable#convert_preference_value - def value - if self[:value_type].present? - case self[:value_type].to_sym - when :string, :text - self[:value].to_s - when :password - self[:value].to_s - when :decimal - BigDecimal.new(self[:value].to_s).round(2, BigDecimal::ROUND_HALF_UP) - when :integer - self[:value].to_i - when :boolean - (self[:value].to_s =~ /^[t|1]/i) != nil - else - self[:value].is_a?(String) ? YAML.load(self[:value]) : self[:value] - end - else - self[:value] - end - end - - def raw_value - self[:value] - end - - # For the rc releases of 1.0, we stored the object class names, this converts - # to preferences definition types. This code should eventually be removed. - # it is called during the load_preferences of the Preferences::Store - def self.convert_old_value_types(preference) - classes = [Symbol.to_s, Fixnum.to_s, Bignum.to_s, - Float.to_s, TrueClass.to_s, FalseClass.to_s] - return unless classes.map(&:downcase).include? preference.value_type.downcase - - case preference.value_type.downcase - when "symbol" - preference.value_type = 'string' - when "fixnum" - preference.value_type = 'integer' - when "bignum" - preference.value_type = 'integer' - preference.value = preference.value.to_f.to_i - when "float" - preference.value_type = 'decimal' - when "trueclass" - preference.value_type = 'boolean' - preference.value = "true" - when "falseclass" - preference.value_type = 'boolean' - preference.value = "false" - end - - preference.save - end +class Spree::Preference < Spree::Base + serialize :value + validates :key, presence: true, uniqueness: true end diff --git a/core/app/models/spree/preferences/configuration.rb b/core/app/models/spree/preferences/configuration.rb index 64365a4c5d3..bdf7a584498 100644 --- a/core/app/models/spree/preferences/configuration.rb +++ b/core/app/models/spree/preferences/configuration.rb @@ -28,8 +28,8 @@ def configure yield(self) if block_given? end - def preference_cache_key(name) - [self.class.name, name].join('::').underscore + def preferences + ScopedStore.new(self.class.name.underscore) end def reset diff --git a/core/app/models/spree/preferences/preferable.rb b/core/app/models/spree/preferences/preferable.rb index 700c2e52a66..9fbff01effc 100644 --- a/core/app/models/spree/preferences/preferable.rb +++ b/core/app/models/spree/preferences/preferable.rb @@ -1,40 +1,47 @@ -# The preference_cache_key is used to determine if the preference -# can be set. The default behavior is to return nil if there is no -# id value. On ActiveRecords, new objects will have their preferences -# saved to a pending hash until it is persisted. +# Preferable allows defining preference accessor methods. # -# class_attributes are inheritied unless you reassign them in -# the subclass, so when you inherit a Preferable class, the -# inherited hook will assign a new hash for the subclass definitions -# and copy all the definitions allowing the subclass to add -# additional defintions without affecting the base +# A class including Preferable must implement #preferences which should return +# an object responding to .fetch(key), []=(key, val), and .delete(key). +# +# The generated writer method performs typecasting before assignment into the +# preferences object. +# +# Examples: +# +# # Spree::Base includes Preferable and defines preferences as a serialized +# # column. +# class Settings < Spree::Base +# preference :color, :string, default: 'red' +# preference :temperature, :integer, default: 21 +# end +# +# s = Settings.new +# s.preferred_color # => 'red' +# s.preferred_temperature # => 21 +# +# s.preferred_color = 'blue' +# s.preferred_color # => 'blue' +# +# # Typecasting is performed on assignment +# s.preferred_temperature = '24' +# s.preferred_color # => 24 +# +# # Modifications have been made to the .preferences hash +# s.preferences #=> {color: 'blue', temperature: 24} +# +# # Save the changes. All handled by activerecord +# s.save! module Spree::Preferences::Preferable + extend ActiveSupport::Concern - def self.included(base) - base.class_eval do - extend Spree::Preferences::PreferableClassMethods - - if respond_to?(:after_create) - after_create do |obj| - obj.save_pending_preferences - end - end - - if respond_to?(:after_destroy) - after_destroy do |obj| - obj.clear_preferences - end - end - - end + included do + extend Spree::Preferences::PreferableClassMethods end def get_preference(name) has_preference! name send self.class.preference_getter_method(name) end - alias :preferred :get_preference - alias :prefers? :get_preference def set_preference(name, value) has_preference! name @@ -51,11 +58,6 @@ def preference_default(name) send self.class.preference_default_getter_method(name) end - def preference_description(name) - has_preference! name - send self.class.preference_description_getter_method(name) - end - def has_preference!(name) raise NoMethodError.new "#{name} preference not defined" unless has_preference? name end @@ -64,46 +66,26 @@ def has_preference?(name) respond_to? self.class.preference_getter_method(name) end - def preferences - prefs = {} - methods.grep(/^prefers_.*\?$/).each do |pref_method| - prefs[pref_method.to_s.gsub(/prefers_|\?/, '').to_sym] = send(pref_method) + def defined_preferences + methods.grep(/\Apreferred_.*=\Z/).map do |pref_method| + pref_method.to_s.gsub(/\Apreferred_|=\Z/, '').to_sym end - prefs - end - - def prefers?(name) - get_preference(name) - end - - def preference_cache_key(name) - return unless id - [self.class.name, name, id].join('::').underscore end - def save_pending_preferences - return unless @pending_preferences - @pending_preferences.each do |name, value| - set_preference(name, value) - end + def default_preferences + Hash[ + defined_preferences.map do |preference| + [preference, preference_default(preference)] + end + ] end def clear_preferences - preferences.keys.each {|pref| preference_store.delete preference_cache_key(pref)} + preferences.keys.each {|pref| preferences.delete pref} end private - def add_pending_preference(name, value) - @pending_preferences ||= {} - @pending_preferences[name] = value - end - - def get_pending_preference(name) - return unless @pending_preferences - @pending_preferences[name] - end - def convert_preference_value(value, type) case type when :string, :text @@ -111,7 +93,7 @@ def convert_preference_value(value, type) when :password value.to_s when :decimal - BigDecimal.new(value.to_s).round(2, BigDecimal::ROUND_HALF_UP) + BigDecimal.new(value.to_s) when :integer value.to_i when :boolean @@ -124,14 +106,28 @@ def convert_preference_value(value, type) else true end + when :array + value.is_a?(Array) ? value : Array.wrap(value) + when :hash + case value.class.to_s + when "Hash" + value + when "String" + # only works with hashes whose keys are strings + JSON.parse value.gsub('=>', ':') + when "Array" + begin + value.try(:to_h) + rescue TypeError + Hash[*value] + rescue ArgumentError + raise 'An even count is required when passing an array to be converted to a hash' + end + else + value.class.ancestors.include?(Hash) ? value : {} + end else value end end - - def preference_store - Spree::Preferences::Store.instance - end - end - diff --git a/core/app/models/spree/preferences/preferable_class_methods.rb b/core/app/models/spree/preferences/preferable_class_methods.rb index 926c10fc003..d0d00cc73b7 100644 --- a/core/app/models/spree/preferences/preferable_class_methods.rb +++ b/core/app/models/spree/preferences/preferable_class_methods.rb @@ -3,58 +3,33 @@ module PreferableClassMethods def preference(name, type, *args) options = args.extract_options! - options.assert_valid_keys(:default, :description) + options.assert_valid_keys(:default) default = options[:default] - description = options[:description] || name + default = ->{ options[:default] } unless default.is_a?(Proc) # cache_key will be nil for new objects, then if we check if there # is a pending preference before going to default define_method preference_getter_method(name) do - if preference_cache_key(name) && preference_store.exist?(preference_cache_key(name)) - preference_store.get preference_cache_key(name) - else - if get_pending_preference(name) - get_pending_preference(name) - elsif Spree::Preference.table_exists? && preference = Spree::Preference.find_by_key(name.to_s) - preference.value - else - send self.class.preference_default_getter_method(name) - end + preferences.fetch(name) do + default.call end end - alias_method prefers_getter_method(name), preference_getter_method(name) define_method preference_setter_method(name) do |value| value = convert_preference_value(value, type) - if preference_cache_key(name) - preference_store.set preference_cache_key(name), value, type - else - add_pending_preference(name, value) - end - end - alias_method prefers_setter_method(name), preference_setter_method(name) + preferences[name] = value - define_method preference_default_getter_method(name) do - default + # If this is an activerecord object, we need to inform + # ActiveRecord::Dirty that this value has changed, since this is an + # in-place update to the preferences hash. + preferences_will_change! if respond_to?(:preferences_will_change!) end + define_method preference_default_getter_method(name), &default + define_method preference_type_getter_method(name) do type end - - define_method preference_description_getter_method(name) do - description - end - end - - def remove_preference(name) - remove_method preference_getter_method(name) if method_defined? preference_getter_method(name) - remove_method preference_setter_method(name) if method_defined? preference_setter_method(name) - remove_method prefers_getter_method(name) if method_defined? prefers_getter_method(name) - remove_method prefers_setter_method(name) if method_defined? prefers_setter_method(name) - remove_method preference_default_getter_method(name) if method_defined? preference_default_getter_method(name) - remove_method preference_type_getter_method(name) if method_defined? preference_type_getter_method(name) - remove_method preference_description_getter_method(name) if method_defined? preference_description_getter_method(name) end def preference_getter_method(name) @@ -65,14 +40,6 @@ def preference_setter_method(name) "preferred_#{name}=".to_sym end - def prefers_getter_method(name) - "prefers_#{name}?".to_sym - end - - def prefers_setter_method(name) - "prefers_#{name}=".to_sym - end - def preference_default_getter_method(name) "preferred_#{name}_default".to_sym end @@ -80,10 +47,5 @@ def preference_default_getter_method(name) def preference_type_getter_method(name) "preferred_#{name}_type".to_sym end - - def preference_description_getter_method(name) - "preferred_#{name}_description".to_sym - end - end end diff --git a/core/app/models/spree/preferences/scoped_store.rb b/core/app/models/spree/preferences/scoped_store.rb new file mode 100644 index 00000000000..62ce98d757b --- /dev/null +++ b/core/app/models/spree/preferences/scoped_store.rb @@ -0,0 +1,33 @@ +module Spree::Preferences + class ScopedStore + def initialize prefix, suffix=nil + @prefix = prefix + @suffix = suffix + end + + def store + Spree::Preferences::Store.instance + end + + def fetch key, &block + store.fetch(key_for(key), &block) + end + + def []= key, value + store[key_for(key)] = value + end + + def delete key + store.delete(key_for(key)) + end + + private + def key_for key + [rails_cache_id, @prefix, key, @suffix].compact.join('/') + end + + def rails_cache_id + ENV['RAILS_CACHE_ID'] + end + end +end diff --git a/core/app/models/spree/preferences/store.rb b/core/app/models/spree/preferences/store.rb index c4022d02508..b60b5d0e545 100644 --- a/core/app/models/spree/preferences/store.rb +++ b/core/app/models/spree/preferences/store.rb @@ -14,13 +14,13 @@ class StoreInstance def initialize @cache = Rails.cache @persistence = true - load_preferences end - def set(key, value, type) + def set(key, value) @cache.write(key, value) - persist(key, value, type) + persist(key, value) end + alias_method :[]=, :set def exist?(key) @cache.exist?(key) || @@ -29,38 +29,52 @@ def exist?(key) def get(key) # return the retrieved value, if it's in the cache - if (val = @cache.read(key)).present? + # use unless nil? incase the value is actually boolean false + # + unless (val = @cache.read(key)).nil? return val end - return nil unless should_persist? + if should_persist? + # If it's not in the cache, maybe it's in the database, but + # has been cleared from the cache - # If it's not in the cache, maybe it's in the database, but - # has been cleared from the cache + # does it exist in the database? + if preference = Spree::Preference.find_by_key(key) + # it does exist + val = preference.value + else + # use the fallback value + val = yield + end - # does it exist in the database? - if preference = Spree::Preference.find_by_key(key) - # it does exist, so let's put it back into the cache - @cache.write(preference.key, preference.value) + # Cache either the value from the db or the fallback value. + # This avoids hitting the db with subsequent queries. + @cache.write(key, val) - # and return the value - preference.value + return val + else + yield end end + alias_method :fetch, :get def delete(key) @cache.delete(key) destroy(key) end + def clear_cache + @cache.clear + end + private - def persist(cache_key, value, type) + def persist(cache_key, value) return unless should_persist? preference = Spree::Preference.where(:key => cache_key).first_or_initialize preference.value = value - preference.value_type = type preference.save end @@ -71,15 +85,6 @@ def destroy(cache_key) preference.destroy if preference end - def load_preferences - return unless should_persist? - - Spree::Preference.valid.each do |p| - Spree::Preference.convert_old_value_types(p) # see comment - @cache.write(p.key, p.value) - end - end - def should_persist? @persistence and Spree::Preference.table_exists? end diff --git a/core/app/models/spree/price.rb b/core/app/models/spree/price.rb index 4780e22e739..627f83479ad 100644 --- a/core/app/models/spree/price.rb +++ b/core/app/models/spree/price.rb @@ -1,20 +1,21 @@ module Spree - class Price < ActiveRecord::Base - belongs_to :variant, :class_name => 'Spree::Variant' + class Price < Spree::Base + acts_as_paranoid + belongs_to :variant, class_name: 'Spree::Variant', inverse_of: :prices, touch: true validate :check_price - validates :amount, :numericality => { :greater_than_or_equal_to => 0 }, :allow_nil => true - - attr_accessible :variant_id, :currency, :amount + validates :amount, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true + validate :validate_amount_maximum def display_amount - return nil if amount.nil? - money.to_s + money end alias :display_price :display_amount + self.whitelisted_ransackable_attributes = ['amount'] + def money - Spree::Money.new(amount, { :currency => currency }) + Spree::Money.new(amount || 0, { currency: currency }) end def price @@ -22,30 +23,28 @@ def price end def price=(price) - self[:amount] = parse_price(price) + self[:amount] = Spree::LocalizedNumber.parse(price) end - private - def check_price - raise "Price must belong to a variant" if variant.nil? - - if currency.nil? - self.currency = Spree::Config[:currency] - end + # Remove variant default_scope `deleted_at: nil` + def variant + Spree::Variant.unscoped { super } end - # strips all non-price-like characters from the price, taking into account locale settings - def parse_price(price) - return price unless price.is_a?(String) + private - separator, delimiter = I18n.t([:'number.currency.format.separator', :'number.currency.format.delimiter']) - non_price_characters = /[^0-9\-#{separator}]/ - price.gsub!(non_price_characters, '') # strip everything else first - price.gsub!(separator, '.') unless separator == '.' # then replace the locale-specific decimal separator with the standard separator if necessary + def check_price + self.currency ||= Spree::Config[:currency] + end - price.to_d + def maximum_amount + BigDecimal '999999.99' end + def validate_amount_maximum + if amount && amount > maximum_amount + errors.add :amount, I18n.t('errors.messages.less_than_or_equal_to', count: maximum_amount) + end + end end end - diff --git a/core/app/models/spree/product.rb b/core/app/models/spree/product.rb old mode 100755 new mode 100644 index 0420405f435..b07473b97ca --- a/core/app/models/spree/product.rb +++ b/core/app/models/spree/product.rb @@ -17,140 +17,104 @@ # All other variants have option values and may have inventory units. # Sum of on_hand each variant's inventory level determine "on_hand" level for the product. # + module Spree - class Product < ActiveRecord::Base - has_many :product_option_types, :dependent => :destroy - has_many :option_types, :through => :product_option_types - has_many :product_properties, :dependent => :destroy - has_many :properties, :through => :product_properties + class Product < Spree::Base + extend FriendlyId + friendly_id :slug_candidates, use: :history + + acts_as_paranoid + + has_many :product_option_types, dependent: :destroy, inverse_of: :product + has_many :option_types, through: :product_option_types + has_many :product_properties, dependent: :destroy, inverse_of: :product + has_many :properties, through: :product_properties - has_and_belongs_to_many :taxons, :join_table => 'spree_products_taxons' + has_many :classifications, dependent: :delete_all, inverse_of: :product + has_many :taxons, through: :classifications, before_remove: :remove_taxon + has_and_belongs_to_many :promotion_rules, join_table: :spree_products_promotion_rules - belongs_to :tax_category - belongs_to :shipping_category + belongs_to :tax_category, class_name: 'Spree::TaxCategory' + belongs_to :shipping_category, class_name: 'Spree::ShippingCategory', inverse_of: :products has_one :master, - :class_name => 'Spree::Variant', - :conditions => { :is_master => true }, - :dependent => :destroy + -> { where is_master: true }, + inverse_of: :product, + class_name: 'Spree::Variant' has_many :variants, - :class_name => 'Spree::Variant', - :conditions => { :is_master => false, :deleted_at => nil }, - :order => "#{::Spree::Variant.quoted_table_name}.position ASC" + -> { where(is_master: false).order("#{::Spree::Variant.quoted_table_name}.position ASC") }, + inverse_of: :product, + class_name: 'Spree::Variant' has_many :variants_including_master, - :class_name => 'Spree::Variant', - :conditions => { :deleted_at => nil }, - :dependent => :destroy + -> { order("#{::Spree::Variant.quoted_table_name}.position ASC") }, + inverse_of: :product, + class_name: 'Spree::Variant', + dependent: :destroy - has_many :variants_including_master_and_deleted, :class_name => 'Spree::Variant' + has_many :prices, -> { order('spree_variants.position, spree_variants.id, currency') }, through: :variants - has_many :prices, :through => :variants, :order => 'spree_variants.position, spree_variants.id, currency' + has_many :stock_items, through: :variants_including_master + + has_many :line_items, through: :variants_including_master + has_many :orders, through: :line_items delegate_belongs_to :master, :sku, :price, :currency, :display_amount, :display_price, :weight, :height, :width, :depth, :is_master, :has_default_price?, :cost_currency, :price_in, :amount_in - delegate_belongs_to :master, :cost_price if Variant.table_exists? && Variant.column_names.include?('cost_price') - after_create :set_master_variant_defaults - after_create :add_properties_and_option_types_from_prototype - after_create :build_variants_from_option_values_hash, :if => :option_values_hash - before_save :recalculate_count_on_hand + delegate_belongs_to :master, :cost_price - after_save :save_master - after_save :set_master_on_hand_to_zero_when_product_has_variants - - delegate :images, :to => :master, :prefix => true + delegate :images, to: :master, prefix: true alias_method :images, :master_images - has_many :variant_images, :source => :images, :through => :variants_including_master, :order => :position + has_many :variant_images, -> { order(:position) }, source: :images, through: :variants_including_master - accepts_nested_attributes_for :variants, :allow_destroy => true + after_create :set_master_variant_defaults + after_create :add_associations_from_prototype + after_create :build_variants_from_option_values_hash, if: :option_values_hash - validates :name, :permalink, :presence => true - validates :price, :presence => true, :if => proc { Spree::Config[:require_master_price] } + after_destroy :punch_slug + after_restore :update_slug_history - attr_accessor :option_values_hash - - attr_accessible :name, :description, :available_on, :permalink, :meta_description, - :meta_keywords, :price, :sku, :deleted_at, :prototype_id, - :option_values_hash, :on_demand, :on_hand, :weight, :height, :width, :depth, - :shipping_category_id, :tax_category_id, :product_properties_attributes, - :variants_attributes, :taxon_ids, :option_type_ids, :cost_currency + after_initialize :ensure_master - attr_accessible :cost_price if Variant.table_exists? && Variant.column_names.include?('cost_price') + after_save :save_master + after_save :run_touch_callbacks, if: :anything_changed? + after_save :reset_nested_changes + after_touch :touch_taxons - accepts_nested_attributes_for :product_properties, :allow_destroy => true, :reject_if => lambda { |pp| pp[:property_name].blank? } + before_validation :normalize_slug, on: :update + before_validation :validate_master - make_permalink :order => :name + validates :meta_keywords, length: { maximum: 255 } + validates :meta_title, length: { maximum: 255 } + validates :name, presence: true + validates :price, presence: true, if: proc { Spree::Config[:require_master_price] } + validates :shipping_category_id, presence: true + validates :slug, length: { minimum: 3 }, uniqueness: { allow_blank: true } - alias :options :product_option_types + attr_accessor :option_values_hash - after_initialize :ensure_master + accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? } - def variants_with_only_master - ActiveSupport::Deprecation.warn("[SPREE] Spree::Product#variants_with_only_master will be deprecated in Spree 1.3. Please use Spree::Product#master instead.") - master - end + alias :options :product_option_types - def to_param - permalink.present? ? permalink : (permalink_was || name.to_s.to_url) - end + self.whitelisted_ransackable_associations = %w[stores variants_including_master master variants] + self.whitelisted_ransackable_attributes = %w[description name slug] - # returns true if the product has any variants (the master variant is not a member of the variants array) + # the master variant is not a member of the variants array def has_variants? variants.any? end - # should product be displayed on products pages and search - def on_display? - has_stock? || Spree::Config[:show_zero_stock_products] - end - - # is this product actually available for purchase - def on_sale? - has_stock? || Spree::Config[:allow_backorders] - end - - # returns the number of inventory units "on_hand" for this product - def on_hand - has_variants? ? variants.sum(&:on_hand) : master.on_hand - end - - # adjusts the "on_hand" inventory level for the product up or down to match the given new_level - def on_hand=(new_level) - unless self.on_demand - raise 'cannot set on_hand of product with variants' if has_variants? && Spree::Config[:track_inventory_levels] - master.on_hand = new_level - end - end - - def on_demand=(new_on_demand) - raise 'cannot set on_demand of product with variants' if has_variants? && Spree::Config[:track_inventory_levels] - master.on_demand = on_demand - self[:on_demand] = new_on_demand - end - - # Returns true if there are inventory units (any variant) with "on_hand" state for this product - # Variants take precedence over master - def has_stock? - has_variants? ? variants.any?(&:in_stock?) : master.in_stock? - end - def tax_category if self[:tax_category_id].nil? - TaxCategory.where(:is_default => true).first + TaxCategory.where(is_default: true).first else TaxCategory.find(self[:tax_category_id]) end end - # override the delete method to set deleted_at value - # instead of actually deleting the product. - def delete - self.update_column(:deleted_at, Time.now) - variants_including_master.update_all(:deleted_at => Time.now) - end - # Adding properties and option types on creation based on a chosen prototype attr_reader :prototype_id def prototype_id=(value) @@ -162,38 +126,15 @@ def ensure_option_types_exist_for_values_hash return if option_values_hash.nil? option_values_hash.keys.map(&:to_i).each do |id| self.option_type_ids << id unless option_type_ids.include?(id) - product_option_types.create({:option_type_id => id}, :without_protection => true) unless product_option_types.map(&:option_type_id).include?(id) + product_option_types.create(option_type_id: id) unless product_option_types.pluck(:option_type_id).include?(id) end end # for adding products which are closely related to existing ones # define "duplicate_extra" for site-specific actions, eg for additional fields def duplicate - p = self.dup - p.name = 'COPY OF ' + name - p.deleted_at = nil - p.created_at = p.updated_at = nil - p.taxons = taxons - - p.product_properties = product_properties.map { |q| r = q.dup; r.created_at = r.updated_at = nil; r } - - image_dup = lambda { |i| j = i.dup; j.attachment = i.attachment.clone; j } - - variant = master.dup - variant.sku = 'COPY OF ' + master.sku - variant.deleted_at = nil - variant.images = master.images.map { |i| image_dup.call i } - variant.price = master.price - variant.currency = master.currency - p.master = variant - - # don't dup the actual variants, just the characterising types - p.option_types = option_types if has_variants? - - # allow site to do some customization - p.send(:duplicate_extra, self) if p.respond_to?(:duplicate_extra) - p.save! - p + duplicator = ProductDuplicator.new(self) + duplicator.duplicate end # use deleted? rather than checking the attribute directly. this @@ -203,8 +144,11 @@ def deleted? !!deleted_at end + # determine if product is available. + # deleted products and products with nil or future available_on date + # are not available def available? - !(available_on.nil? || available_on.future?) + !(available_on.nil? || available_on.future?) && !deleted? end # split variants list into hash which shows mapping of opt value onto matching variants @@ -215,8 +159,22 @@ def categorise_variants_from_option(opt_type) end def self.like_any(fields, values) - where_str = fields.map { |field| Array.new(values.size, "#{self.quoted_table_name}.#{field} #{LIKE} ?").join(' OR ') }.join(' OR ') - self.where([where_str, values.map { |value| "%#{value}%" } * fields.size].flatten) + where fields.map { |field| + values.map { |value| + arel_table[field].matches("%#{value}%") + }.inject(:or) + }.inject(:or) + end + + # Suitable for displaying only variants that has at least one option value. + # There may be scenarios where an option type is removed and along with it + # all option values. At that point all variants associated with only those + # values should not be displayed to frontend users. Otherwise it breaks the + # idea of having variants + def variants_and_option_values(current_currency = nil) + variants.includes(:option_values).active(current_currency).select do |variant| + variant.option_values.any? + end end def empty_option_values? @@ -226,72 +184,157 @@ def empty_option_values? end def property(property_name) - return nil unless prop = properties.find_by_name(property_name) - product_properties.find_by_property_id(prop.id).try(:value) + return nil unless prop = properties.find_by(name: property_name) + product_properties.find_by(property: prop).try(:value) end def set_property(property_name, property_value) ActiveRecord::Base.transaction do - property = Property.where(:name => property_name).first_or_initialize - property.presentation = property_name - property.save! - - product_property = ProductProperty.where(:product_id => id, :property_id => property.id).first_or_initialize + # Works around spree_i18n #301 + property = if Property.exists?(name: property_name) + Property.where(name: property_name).first + else + Property.create(name: property_name, presentation: property_name) + end + product_property = ProductProperty.where(product: self, property: property).first_or_initialize product_property.value = property_value product_property.save! end end - private - - # Builds variants from a hash of option types & values - def build_variants_from_option_values_hash - ensure_option_types_exist_for_values_hash - values = option_values_hash.values - values = values.inject(values.shift) { |memo, value| memo.product(value).map(&:flatten) } + def possible_promotions + promotion_ids = promotion_rules.map(&:promotion_id).uniq + Spree::Promotion.advertised.where(id: promotion_ids).reject(&:expired?) + end - values.each do |ids| - variant = variants.create({ :option_value_ids => ids, :price => master.price }, :without_protection => true) - end - save + def total_on_hand + if any_variants_not_track_inventory? + Float::INFINITY + else + stock_items.sum(:count_on_hand) end + end + + # Master variant may be deleted (i.e. when the product is deleted) + # which would make AR's default finder return nil. + # This is a stopgap for that little problem. + def master + super || variants_including_master.with_deleted.where(is_master: true).first + end + + private - def add_properties_and_option_types_from_prototype - if prototype_id && prototype = Spree::Prototype.find_by_id(prototype_id) - prototype.properties.each do |property| - product_properties.create({:property => property}, :without_protection => true) - end - self.option_types = prototype.option_types + def add_associations_from_prototype + if prototype_id && prototype = Spree::Prototype.find_by(id: prototype_id) + prototype.properties.each do |property| + product_properties.create(property: property) end + self.option_types = prototype.option_types + self.taxons = prototype.taxons end + end - def recalculate_count_on_hand - product_count_on_hand = has_variants? ? - variants.sum(:count_on_hand) : (master ? master.count_on_hand : 0) - self.count_on_hand = product_count_on_hand + def any_variants_not_track_inventory? + if variants_including_master.loaded? + variants_including_master.any? { |v| !v.should_track_inventory? } + else + !Spree::Config.track_inventory_levels || variants_including_master.where(track_inventory: false).any? end + end - # the master on_hand is meaningless once a product has variants as the inventory - # units are now "contained" within the product variants - def set_master_on_hand_to_zero_when_product_has_variants - master.on_hand = 0 if has_variants? && Spree::Config[:track_inventory_levels] && !self.on_demand + # Builds variants from a hash of option types & values + def build_variants_from_option_values_hash + ensure_option_types_exist_for_values_hash + values = option_values_hash.values + values = values.inject(values.shift) { |memo, value| memo.product(value).map(&:flatten) } + + values.each do |ids| + variant = variants.create( + option_value_ids: ids, + price: master.price + ) end + save + end - # ensures the master variant is flagged as such - def set_master_variant_defaults - master.is_master = true - end + def ensure_master + return unless new_record? + self.master ||= build_master + end + + def normalize_slug + self.slug = normalize_friendly_id(slug) + end + + def punch_slug + # punch slug with date prefix to allow reuse of original + update_column :slug, "#{Time.now.to_i}_#{slug}"[0..254] unless frozen? + end - # there's a weird quirk with the delegate stuff that does not automatically save the delegate object - # when saving so we force a save using a hook. - def save_master - master.save if master && (master.changed? || master.new_record? || (master.default_price && (master.default_price.changed || master.default_price.new_record))) + def update_slug_history + self.save! + end + + def anything_changed? + changed? || @nested_changes + end + + def reset_nested_changes + @nested_changes = false + end + + # there's a weird quirk with the delegate stuff that does not automatically save the delegate object + # when saving so we force a save using a hook + # Fix for issue #5306 + def save_master + if master && (master.changed? || master.new_record? || (master.default_price && (master.default_price.changed? || master.default_price.new_record?))) + master.save! + @nested_changes = true end + end - def ensure_master - return unless new_record? - self.master ||= Variant.new + # If the master cannot be saved, the Product object will get its errors + # and will be destroyed + def validate_master + # We call master.default_price here to ensure price is initialized. + # Required to avoid Variant#check_price validation failing on create. + unless master.default_price && master.valid? + master.errors.each do |att, error| + self.errors.add(att, error) + end end + end + + # ensures the master variant is flagged as such + def set_master_variant_defaults + master.is_master = true + end + + # Try building a slug based on the following fields in increasing order of specificity. + def slug_candidates + [ + :name, + [:name, :sku] + ] + end + + def run_touch_callbacks + run_callbacks(:touch) + end + + # Iterate through this products taxons and taxonomies and touch their timestamps in a batch + def touch_taxons + taxons_to_touch = taxons.map(&:self_and_ancestors).flatten.uniq + Spree::Taxon.where(id: taxons_to_touch.map(&:id)).update_all(updated_at: Time.current) + + taxonomy_ids_to_touch = taxons_to_touch.map(&:taxonomy_id).flatten.uniq + Spree::Taxonomy.where(id: taxonomy_ids_to_touch).update_all(updated_at: Time.current) + end + + def remove_taxon(taxon) + removed_classifications = classifications.where(taxon: taxon) + removed_classifications.each &:remove_from_list + end end end diff --git a/core/app/models/spree/product/scopes.rb b/core/app/models/spree/product/scopes.rb index a9e5ba5c61f..0658a8e658c 100644 --- a/core/app/models/spree/product/scopes.rb +++ b/core/app/models/spree/product/scopes.rb @@ -1,5 +1,5 @@ module Spree - class Product < ActiveRecord::Base + class Product < Spree::Base cattr_accessor :search_scopes do [] end @@ -18,14 +18,26 @@ def self.simple_scopes ] end - simple_scopes.each do |name| - # We should not define price scopes here, as they require something slightly different - next if name.to_s.include?("master_price") - parts = name.to_s.match(/(.*)_by_(.*)/) - order_text = "#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? "ASC" : "DESC"}" - self.scope(name.to_s, relation.order(order_text)) + def self.add_simple_scopes(scopes) + scopes.each do |name| + # We should not define price scopes here, as they require something slightly different + next if name.to_s.include?("master_price") + parts = name.to_s.match(/(.*)_by_(.*)/) + self.scope(name.to_s, -> { order("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? "ASC" : "DESC"}") }) + end + end + + def self.property_conditions(property) + properties = Property.table_name + conditions = case property + when String then { "#{properties}.name" => property } + when Property then { "#{properties}.id" => property.id } + else { "#{properties}.id" => property.to_i } + end end + add_simple_scopes simple_scopes + add_search_scope :ascend_by_master_price do joins(:master => :default_price).order("#{price_table_name}.amount ASC") end @@ -49,8 +61,8 @@ def self.simple_scopes # This scope selects products in taxon AND all its descendants # If you need products only within one taxon use # - # Spree::Product.taxons_id_eq(x) - # + # Spree::Product.joins(:taxons).where(Taxon.table_name => { :id => taxon.id }) + # # If you're using count on the result of this scope, you must use the # `:distinct` option as well: # @@ -64,9 +76,9 @@ def self.simple_scopes # # SELECT COUNT(*) ... add_search_scope :in_taxon do |taxon| - select("DISTINCT(spree_products.id), spree_products.*"). - joins(:taxons). - where(Taxon.table_name => { :id => taxon.self_and_descendants.map(&:id) }) + includes(:classifications). + where("spree_products_taxons.taxon_id" => taxon.self_and_descendants.pluck(:id)). + order("spree_products_taxons.position ASC") end # This scope selects products in all taxons AND all its descendants @@ -75,33 +87,20 @@ def self.simple_scopes # Spree::Product.taxons_id_eq([x,y]) add_search_scope :in_taxons do |*taxons| taxons = get_taxons(taxons) - taxons.first ? prepare_taxon_conditions(taxons) : scoped + taxons.first ? prepare_taxon_conditions(taxons) : where(nil) end # a scope that finds all products having property specified by name, object or id add_search_scope :with_property do |property| - properties = Property.table_name - conditions = case property - when String then { "#{properties}.name" => property } - when Property then { "#{properties}.id" => property.id } - else { "#{properties}.id" => property.to_i } - end - - joins(:properties).where(conditions) + joins(:properties).where(property_conditions(property)) end # a simple test for product with a certain property-value pairing # note that it can test for properties with NULL values, but not for absent values add_search_scope :with_property_value do |property, value| - properties = Spree::Property.table_name - conditions = case property - when String then ["#{properties}.name = ?", property] - when Property then ["#{properties}.id = ?", property.id] - else ["#{properties}.id = ?", property.to_i] - end - conditions = ["#{ProductProperty.table_name}.value = ? AND #{conditions[0]}", value, conditions[1]] - - joins(:properties).where(conditions) + joins(:properties) + .where("#{ProductProperty.table_name}.value = ?", value) + .where(property_conditions(property)) end add_search_scope :with_option do |option| @@ -118,20 +117,20 @@ def self.simple_scopes add_search_scope :with_option_value do |option, value| option_values = OptionValue.table_name option_type_id = case option - when String then OptionType.find_by_name(option) || option.to_i + when String then OptionType.find_by(name: option) || option.to_i when OptionType then option.id else option.to_i end conditions = "#{option_values}.name = ? AND #{option_values}.option_type_id = ?", value, option_type_id - group("spree_products.id").joins(:variants_including_master => :option_values).where(conditions) + group('spree_products.id').joins(variants_including_master: :option_values).where(conditions) end # Finds all products which have either: # 1) have an option value with the name matching the one given # 2) have a product property with a value matching the one given add_search_scope :with do |value| - includes(:variants_including_master => :option_values). + includes(variants_including_master: :option_values). includes(:product_properties). where("#{OptionValue.table_name}.name = ? OR #{ProductProperty.table_name}.value = ?", value, value) end @@ -154,7 +153,7 @@ def self.simple_scopes # Finds all products that have the ids matching the given collection of ids. # Alternatively, you could use find(collection_of_ids), but that would raise an exception if one product couldn't be found add_search_scope :with_ids do |*ids| - where(:id => ids) + where(id: ids) end # Sorts products from most popular (popularity is extracted from how many @@ -184,12 +183,12 @@ def self.simple_scopes end add_search_scope :not_deleted do - where(:deleted_at => nil) + where("#{Product.quoted_table_name}.deleted_at IS NULL or #{Product.quoted_table_name}.deleted_at >= ?", Time.zone.now) end # Can't use add_search_scope for this as it needs a default argument def self.available(available_on = nil, currency = nil) - joins(:master => :prices).where("#{Product.quoted_table_name}.available_on <= ?", available_on || Time.now).where('spree_prices.currency' => currency || Spree::Config[:currency]).where('spree_prices.amount IS NOT NULL') + joins(:master => :prices).where("#{Product.quoted_table_name}.available_on <= ?", available_on || Time.now) end search_scopes << :available @@ -198,23 +197,34 @@ def self.active(currency = nil) end search_scopes << :active - add_search_scope :on_hand do - variants_table = Variant.table_name - where("#{table_name}.id in (select product_id from #{variants_table} where product_id = #{table_name}.id and #{variants_table}.deleted_at IS NULL group by product_id having sum(count_on_hand) > 0)") - end - add_search_scope :taxons_name_eq do |name| group("spree_products.id").joins(:taxons).where(Taxon.arel_table[:name].eq(name)) end - if (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL') - if table_exists? - scope :group_by_products_id, { :group => column_names.map { |col_name| "#{table_name}.#{col_name}"} } + def self.distinct_by_product_ids(sort_order = nil) + sort_column = sort_order.split(" ").first + + # Postgres will complain when using ordering by expressions not present in + # SELECT DISTINCT. e.g. + # + # PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY + # expressions must appear in select list. e.g. + # + # SELECT DISTINCT "spree_products".* FROM "spree_products" LEFT OUTER JOIN + # "spree_variants" ON "spree_variants"."product_id" = "spree_products"."id" AND "spree_variants"."is_master" = 't' + # AND "spree_variants"."deleted_at" IS NULL LEFT OUTER JOIN "spree_prices" ON + # "spree_prices"."variant_id" = "spree_variants"."id" AND "spree_prices"."currency" = 'USD' + # AND "spree_prices"."deleted_at" IS NULL WHERE "spree_products"."deleted_at" IS NULL AND ('t'='t') + # ORDER BY "spree_prices"."amount" ASC LIMIT 10 OFFSET 0 + # + # Don't allow sort_column, a variable coming from params, + # to be anything but a column in the database + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' && !column_names.include?(sort_column) + all + else + distinct end - else - scope :group_by_products_id, { :group => "#{self.quoted_table_name}.id" } end - search_scopes << :group_by_products_id private @@ -224,7 +234,7 @@ def self.price_table_name # specifically avoid having an order for taxon search (conflicts with main order) def self.prepare_taxon_conditions(taxons) - ids = taxons.map { |taxon| taxon.self_and_descendants.map(&:id) }.flatten.uniq + ids = taxons.map { |taxon| taxon.self_and_descendants.pluck(:id) }.flatten.uniq joins(:taxons).where("#{Taxon.table_name}.id" => ids) end @@ -240,10 +250,10 @@ def self.get_taxons(*ids_or_records_or_names) taxons = Taxon.table_name ids_or_records_or_names.flatten.map { |t| case t - when Integer then Taxon.find_by_id(t) + when Integer then Taxon.find_by(id: t) when ActiveRecord::Base then t when String - Taxon.find_by_name(t) || + Taxon.find_by(name: t) || Taxon.where("#{taxons}.permalink LIKE ? OR #{taxons}.permalink = ?", "%/#{t}/", "#{t}/").first end }.compact.flatten.uniq diff --git a/core/app/models/spree/product_option_type.rb b/core/app/models/spree/product_option_type.rb index a83509d21ad..02d8bf0a56f 100644 --- a/core/app/models/spree/product_option_type.rb +++ b/core/app/models/spree/product_option_type.rb @@ -1,7 +1,7 @@ module Spree - class ProductOptionType < ActiveRecord::Base - belongs_to :product - belongs_to :option_type - acts_as_list :scope => :product + class ProductOptionType < Spree::Base + belongs_to :product, class_name: 'Spree::Product', inverse_of: :product_option_types + belongs_to :option_type, class_name: 'Spree::OptionType', inverse_of: :product_option_types + acts_as_list scope: :product end end diff --git a/core/app/models/spree/product_property.rb b/core/app/models/spree/product_property.rb index 86b2c3a3acf..4581c0e6297 100644 --- a/core/app/models/spree/product_property.rb +++ b/core/app/models/spree/product_property.rb @@ -1,12 +1,17 @@ module Spree - class ProductProperty < ActiveRecord::Base - belongs_to :product - belongs_to :property + class ProductProperty < Spree::Base + acts_as_list scope: :product - validates :property, :presence => true - validates :value, :length => { :maximum => 255 } + belongs_to :product, touch: true, class_name: 'Spree::Product', inverse_of: :product_properties + belongs_to :property, class_name: 'Spree::Property', inverse_of: :product_properties - attr_accessible :property_name, :value + validates :property, presence: true + + validates_with Spree::Validations::DbMaximumLengthValidator, field: :value + + default_scope -> { order("#{self.table_name}.position") } + + self.whitelisted_ransackable_attributes = ['value'] # virtual attributes for use with AJAX completion stuff def property_name @@ -15,8 +20,8 @@ def property_name def property_name=(name) unless name.blank? - unless property = Property.find_by_name(name) - property = Property.create(:name => name, :presentation => name) + unless property = Property.find_by(name: name) + property = Property.create(name: name, presentation: name) end self.property = property end diff --git a/core/app/models/spree/product_scope/scopes.rb b/core/app/models/spree/product_scope/scopes.rb index 9cb70f2425d..efa849205f6 100644 --- a/core/app/models/spree/product_scope/scopes.rb +++ b/core/app/models/spree/product_scope/scopes.rb @@ -1,5 +1,5 @@ module Spree - class ProductScope < ActiveRecord::Base + class ProductScope < Spree::Base before_validation(:on => :create) do # Add default empty arguments so scope validates and errors aren't caused when previewing it if name && args = self.class.arguments_for_scope_name(name) diff --git a/core/app/models/spree/promotion.rb b/core/app/models/spree/promotion.rb new file mode 100644 index 00000000000..509bc7149a9 --- /dev/null +++ b/core/app/models/spree/promotion.rb @@ -0,0 +1,196 @@ +module Spree + class Promotion < Spree::Base + MATCH_POLICIES = %w(all any) + UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"] + + attr_reader :eligibility_errors + + belongs_to :promotion_category + + has_many :promotion_rules, autosave: true, dependent: :destroy + alias_method :rules, :promotion_rules + + has_many :promotion_actions, autosave: true, dependent: :destroy + alias_method :actions, :promotion_actions + + has_and_belongs_to_many :orders, join_table: 'spree_orders_promotions' + + accepts_nested_attributes_for :promotion_actions, :promotion_rules + + validates_associated :rules + + validates :name, presence: true + validates :path, uniqueness: { allow_blank: true } + validates :usage_limit, numericality: { greater_than: 0, allow_nil: true } + validates :description, length: { maximum: 255 } + + before_save :normalize_blank_values + + scope :coupons, ->{ where("#{table_name}.code IS NOT NULL") } + + order_join_table = reflect_on_association(:orders).join_table + + scope :applied, -> { joins("INNER JOIN #{order_join_table} ON #{order_join_table}.promotion_id = #{table_name}.id").uniq } + + self.whitelisted_ransackable_attributes = ['code', 'path', 'promotion_category_id'] + + def self.advertised + where(advertise: true) + end + + def self.with_coupon_code(coupon_code) + where("lower(#{self.table_name}.code) = ?", coupon_code.strip.downcase).first + end + + def self.active + where('spree_promotions.starts_at IS NULL OR spree_promotions.starts_at < ?', Time.now). + where('spree_promotions.expires_at IS NULL OR spree_promotions.expires_at > ?', Time.now) + end + + def self.order_activatable?(order) + order && !UNACTIVATABLE_ORDER_STATES.include?(order.state) + end + + def expired? + !!(starts_at && Time.now < starts_at || expires_at && Time.now > expires_at) + end + + def activate(payload) + order = payload[:order] + return unless self.class.order_activatable?(order) + + payload[:promotion] = self + + # Track results from actions to see if any action has been taken. + # Actions should return nil/false if no action has been taken. + # If an action returns true, then an action has been taken. + results = actions.map do |action| + action.perform(payload) + end + # If an action has been taken, report back to whatever activated this promotion. + action_taken = results.include?(true) + + if action_taken + # connect to the order + # create the join_table entry. + self.orders << order + self.save + end + + return action_taken + end + + # called anytime order.update! happens + def eligible?(promotable) + return false if expired? || usage_limit_exceeded?(promotable) || blacklisted?(promotable) + !!eligible_rules(promotable, {}) + end + + # eligible_rules returns an array of promotion rules where eligible? is true for the promotable + # if there are no such rules, an empty array is returned + # if the rules make this promotable ineligible, then nil is returned (i.e. this promotable is not eligible) + def eligible_rules(promotable, options = {}) + # Promotions without rules are eligible by default. + return [] if rules.none? + eligible = lambda { |r| r.eligible?(promotable, options) } + specific_rules = rules.select { |rule| rule.applicable?(promotable) } + return [] if specific_rules.none? + + rule_eligibility = Hash[specific_rules.map do |rule| + [rule, rule.eligible?(promotable, options)] + end] + + if match_all? + # If there are rules for this promotion, but no rules for this + # particular promotable, then the promotion is ineligible by default. + unless rule_eligibility.values.all? + @eligibility_errors = specific_rules.map(&:eligibility_errors).detect(&:present?) + return nil + end + specific_rules + else + unless rule_eligibility.values.any? + @eligibility_errors = specific_rules.map(&:eligibility_errors).detect(&:present?) + return nil + end + + [rule_eligibility.detect { |_, eligibility| eligibility }.first] + end + end + + def products + rules.where(type: "Spree::Promotion::Rules::Product").map(&:products).flatten.uniq + end + + def usage_limit_exceeded?(promotable) + usage_limit.present? && usage_limit > 0 && adjusted_credits_count(promotable) >= usage_limit + end + + def adjusted_credits_count(promotable) + adjustments = promotable.is_a?(Order) ? promotable.all_adjustments : promotable.adjustments + credits_count - adjustments.promotion.where(:source_id => actions.pluck(:id)).count + end + + def credits + Adjustment.eligible.promotion.where(source_id: actions.map(&:id)) + end + + def credits_count + credits.count + end + + def line_item_actionable?(order, line_item) + if eligible? order + rules = eligible_rules(order) + if rules.blank? + true + else + rules.send(match_all? ? :all? : :any?) do |rule| + rule.actionable? line_item + end + end + else + false + end + end + + def used_by?(user, excluded_orders = []) + [ + :adjustments, + :line_item_adjustments, + :shipment_adjustments + ].any? do |adjustment_type| + user.orders.complete.joins(adjustment_type).where( + spree_adjustments: { + source_type: 'Spree::PromotionAction', + source_id: actions.map(&:id), + eligible: true + } + ).where.not( + id: excluded_orders.map(&:id) + ).any? + end + end + + private + def blacklisted?(promotable) + case promotable + when Spree::LineItem + !promotable.product.promotionable? + when Spree::Order + promotable.line_items.any? && + !promotable.line_items.joins(:product).where(spree_products: {promotionable: true}).any? + end + end + + def normalize_blank_values + [:code, :path].each do |column| + self[column] = nil if self[column].blank? + end + end + + def match_all? + match_policy == 'all' + end + end +end diff --git a/core/app/models/spree/promotion/actions/create_adjustment.rb b/core/app/models/spree/promotion/actions/create_adjustment.rb new file mode 100644 index 00000000000..6abc6d409f8 --- /dev/null +++ b/core/app/models/spree/promotion/actions/create_adjustment.rb @@ -0,0 +1,62 @@ +module Spree + class Promotion + module Actions + class CreateAdjustment < PromotionAction + include Spree::CalculatedAdjustments + include Spree::AdjustmentSource + + has_many :adjustments, as: :source + + delegate :eligible?, to: :promotion + + before_validation :ensure_action_has_calculator + before_destroy :deals_with_adjustments_for_deleted_source + + # Creates the adjustment related to a promotion for the order passed + # through options hash + # + # Returns `true` if an adjustment is applied to an order, + # `false` if the promotion has already been applied. + def perform(options = {}) + order = options[:order] + return if promotion_credit_exists?(order) + + amount = compute_amount(order) + return if amount == 0 + Spree::Adjustment.create!( + amount: amount, + order: order, + adjustable: order, + source: self, + label: "#{Spree.t(:promotion)} (#{promotion.name})" + ) + true + end + + # Ensure a negative amount which does not exceed the sum of the order's + # item_total and ship_total + def compute_amount(calculable) + amount = self.calculator.compute(calculable).to_f.abs + [(calculable.item_total + calculable.ship_total), amount].min * -1 + end + + private + # Tells us if there if the specified promotion is already associated with the line item + # regardless of whether or not its currently eligible. Useful because generally + # you would only want a promotion action to apply to order no more than once. + # + # Receives an adjustment +source+ (here a PromotionAction object) and tells + # if the order has adjustments from that already + def promotion_credit_exists?(adjustable) + self.adjustments.where(:adjustable_id => adjustable.id).exists? + end + + def ensure_action_has_calculator + return if self.calculator + self.calculator = Calculator::FlatPercentItemTotal.new + end + + end + end + end +end diff --git a/core/app/models/spree/promotion/actions/create_item_adjustments.rb b/core/app/models/spree/promotion/actions/create_item_adjustments.rb new file mode 100644 index 00000000000..10090bf1660 --- /dev/null +++ b/core/app/models/spree/promotion/actions/create_item_adjustments.rb @@ -0,0 +1,79 @@ +module Spree + class Promotion + module Actions + class CreateItemAdjustments < PromotionAction + include Spree::CalculatedAdjustments + include Spree::AdjustmentSource + + has_many :adjustments, as: :source + + delegate :eligible?, to: :promotion + + before_validation :ensure_action_has_calculator + before_destroy :deals_with_adjustments_for_deleted_source + + def perform(payload = {}) + order = payload[:order] + promotion = payload[:promotion] + + result = false + + line_items_to_adjust(promotion, order).each do |line_item| + current_result = self.create_adjustment(line_item, order) + result ||= current_result + end + return result + end + + def create_adjustment(adjustable, order) + amount = self.compute_amount(adjustable) + return if amount == 0 + self.adjustments.create!( + amount: amount, + adjustable: adjustable, + order: order, + label: "#{Spree.t(:promotion)} (#{promotion.name})", + ) + true + end + + # Ensure a negative amount which does not exceed the sum of the order's + # item_total and ship_total + def compute_amount(adjustable) + order = adjustable.is_a?(Order) ? adjustable : adjustable.order + return 0 unless promotion.line_item_actionable?(order, adjustable) + promotion_amount = self.calculator.compute(adjustable).to_f.abs + + [adjustable.amount, promotion_amount].min * -1 + end + + private + # Tells us if there if the specified promotion is already associated with the line item + # regardless of whether or not its currently eligible. Useful because generally + # you would only want a promotion action to apply to line item no more than once. + # + # Receives an adjustment +source+ (here a PromotionAction object) and tells + # if the order has adjustments from that already + def promotion_credit_exists?(adjustable) + self.adjustments.where(:adjustable_id => adjustable.id).exists? + end + + def ensure_action_has_calculator + return if self.calculator + self.calculator = Calculator::PercentOnLineItem.new + end + + def line_items_to_adjust(promotion, order) + excluded_ids = self.adjustments. + where(adjustable_id: order.line_items.pluck(:id), adjustable_type: 'Spree::LineItem'). + pluck(:adjustable_id) + + order.line_items.where.not(id: excluded_ids).select do |line_item| + promotion.line_item_actionable? order, line_item + end + end + + end + end + end +end diff --git a/core/app/models/spree/promotion/actions/create_line_items.rb b/core/app/models/spree/promotion/actions/create_line_items.rb new file mode 100644 index 00000000000..8c14284ffad --- /dev/null +++ b/core/app/models/spree/promotion/actions/create_line_items.rb @@ -0,0 +1,62 @@ +module Spree + class Promotion + module Actions + class CreateLineItems < PromotionAction + has_many :promotion_action_line_items, foreign_key: :promotion_action_id + accepts_nested_attributes_for :promotion_action_line_items + + delegate :eligible?, :to => :promotion + + # Adds a line item to the Order if the promotion is eligible + # + # This doesn't play right with Add to Cart events because at the moment + # the item was added to cart the promo may not be eligible. However it + # might become eligible as the order gets updated. + # + # e.g. + # - A promo adds a line item to cart if order total greater then $30 + # - Customer add 1 item of $10 to cart + # - This action shouldn't perform because the order is not eligible + # - Customer increases item quantity to 5 (order total goes to $50) + # - Now the order is eligible for the promo and the action should perform + # + # Another complication is when the same line item created by the promo + # is also added to cart on a separate action. + # + # e.g. + # - Promo adds 1 item A to cart if order total greater then $30 + # - Customer add 2 items B to cart, current order total is $40 + # - This action performs adding item A to cart since order is eligible + # - Customer changes his mind and updates item B quantity to 1 + # - At this point order is no longer eligible and one might expect + # that item A should be removed + # + # It doesn't remove items from the order here because there's no way + # it can know whether that item was added via this promo action or if + # it was manually populated somewhere else. In that case the item + # needs to be manually removed from the order by the customer + def perform(options = {}) + order = options[:order] + return unless self.eligible? order + + action_taken = false + promotion_action_line_items.each do |item| + current_quantity = order.quantity_of(item.variant) + if current_quantity < item.quantity && item_available?(item) + line_item = order.contents.add(item.variant, item.quantity - current_quantity) + action_taken = true if line_item.try(:valid?) + end + end + action_taken + end + + # Checks that there's enough stock to add the line item to the order + def item_available?(item) + quantifier = Spree::Stock::Quantifier.new(item.variant) + quantifier.can_supply? item.quantity + end + + end + end + end +end diff --git a/core/app/models/spree/promotion/actions/free_shipping.rb b/core/app/models/spree/promotion/actions/free_shipping.rb new file mode 100644 index 00000000000..c4e64f90d04 --- /dev/null +++ b/core/app/models/spree/promotion/actions/free_shipping.rb @@ -0,0 +1,38 @@ +module Spree + class Promotion + module Actions + class FreeShipping < Spree::PromotionAction + def perform(payload={}) + order = payload[:order] + results = order.shipments.map do |shipment| + return false if promotion_credit_exists?(shipment) + shipment.adjustments.create!( + order: shipment.order, + amount: compute_amount(shipment), + source: self, + label: label, + ) + true + end + # Did we actually end up applying any adjustments? + # If so, then this action should be classed as 'successful' + results.any? { |r| r == true } + end + + def label + "#{Spree.t(:promotion)} (#{promotion.name})" + end + + def compute_amount(shipment) + shipment.cost * -1 + end + + private + + def promotion_credit_exists?(shipment) + shipment.adjustments.where(:source_id => self.id).exists? + end + end + end + end +end \ No newline at end of file diff --git a/core/app/models/spree/promotion/rules/first_order.rb b/core/app/models/spree/promotion/rules/first_order.rb new file mode 100644 index 00000000000..7a8fc77e221 --- /dev/null +++ b/core/app/models/spree/promotion/rules/first_order.rb @@ -0,0 +1,37 @@ +module Spree + class Promotion + module Rules + class FirstOrder < PromotionRule + attr_reader :user, :email + + def applicable?(promotable) + promotable.is_a?(Spree::Order) + end + + def eligible?(order, options = {}) + @user = order.try(:user) || options[:user] + @email = order.email + + if user || email + if !completed_orders.blank? && completed_orders.first != order + eligibility_errors.add(:base, eligibility_error_message(:not_first_order)) + end + else + eligibility_errors.add(:base, eligibility_error_message(:no_user_or_email_specified)) + end + + eligibility_errors.empty? + end + + private + def completed_orders + user ? user.orders.complete : orders_by_email + end + + def orders_by_email + Spree::Order.where(email: email).complete + end + end + end + end +end diff --git a/core/app/models/spree/promotion/rules/item_total.rb b/core/app/models/spree/promotion/rules/item_total.rb new file mode 100644 index 00000000000..9a3e5178335 --- /dev/null +++ b/core/app/models/spree/promotion/rules/item_total.rb @@ -0,0 +1,61 @@ +# A rule to apply to an order greater than (or greater than or equal to) +# a specific amount +module Spree + class Promotion + module Rules + class ItemTotal < PromotionRule + preference :amount_min, :decimal, default: 100.00 + preference :operator_min, :string, default: '>' + preference :amount_max, :decimal, default: 1000.00 + preference :operator_max, :string, default: '<' + + + OPERATORS_MIN = ['gt', 'gte'] + OPERATORS_MAX = ['lt','lte'] + + def applicable?(promotable) + promotable.is_a?(Spree::Order) + end + + def eligible?(order, options = {}) + item_total = order.item_total + + lower_limit_condition = item_total.send(preferred_operator_min == 'gte' ? :>= : :>, BigDecimal.new(preferred_amount_min.to_s)) + upper_limit_condition = item_total.send(preferred_operator_max == 'lte' ? :<= : :<, BigDecimal.new(preferred_amount_max.to_s)) + + eligibility_errors.add(:base, ineligible_message_max) unless upper_limit_condition + eligibility_errors.add(:base, ineligible_message_min) unless lower_limit_condition + + eligibility_errors.empty? + end + + private + def formatted_amount_min + Spree::Money.new(preferred_amount_min).to_s + end + + def formatted_amount_max + Spree::Money.new(preferred_amount_max).to_s + end + + + def ineligible_message_max + if preferred_operator_max == 'gte' + eligibility_error_message(:item_total_more_than_or_equal, amount: formatted_amount_max) + else + eligibility_error_message(:item_total_more_than, amount: formatted_amount_max) + end + end + + def ineligible_message_min + if preferred_operator_min == 'gte' + eligibility_error_message(:item_total_less_than, amount: formatted_amount_min) + else + eligibility_error_message(:item_total_less_than_or_equal, amount: formatted_amount_min) + end + end + + end + end + end +end diff --git a/core/app/models/spree/promotion/rules/one_use_per_user.rb b/core/app/models/spree/promotion/rules/one_use_per_user.rb new file mode 100644 index 00000000000..8db0bb4d513 --- /dev/null +++ b/core/app/models/spree/promotion/rules/one_use_per_user.rb @@ -0,0 +1,24 @@ +module Spree + class Promotion + module Rules + class OneUsePerUser < PromotionRule + def applicable?(promotable) + promotable.is_a?(Spree::Order) + end + + def eligible?(order, options = {}) + if order.user.present? + if promotion.used_by?(order.user, [order]) + eligibility_errors.add(:base, eligibility_error_message(:limit_once_per_user)) + end + else + eligibility_errors.add(:base, eligibility_error_message(:no_user_specified)) + end + + eligibility_errors.empty? + end + end + end + end +end + diff --git a/core/app/models/spree/promotion/rules/product.rb b/core/app/models/spree/promotion/rules/product.rb new file mode 100644 index 00000000000..a71d7214c94 --- /dev/null +++ b/core/app/models/spree/promotion/rules/product.rb @@ -0,0 +1,63 @@ +# A rule to limit a promotion based on products in the order. +# Can require all or any of the products to be present. +# Valid products either come from assigned product group or are assingned directly to the rule. +module Spree + class Promotion + module Rules + class Product < PromotionRule + has_and_belongs_to_many :products, class_name: '::Spree::Product', join_table: 'spree_products_promotion_rules', foreign_key: 'promotion_rule_id' + + MATCH_POLICIES = %w(any all none) + preference :match_policy, :string, default: MATCH_POLICIES.first + + # scope/association that is used to test eligibility + def eligible_products + products + end + + def applicable?(promotable) + promotable.is_a?(Spree::Order) + end + + def eligible?(order, options = {}) + return true if eligible_products.empty? + + if preferred_match_policy == 'all' + unless eligible_products.all? {|p| order.products.include?(p) } + eligibility_errors.add(:base, eligibility_error_message(:missing_product)) + end + elsif preferred_match_policy == 'any' + unless order.products.any? {|p| eligible_products.include?(p) } + eligibility_errors.add(:base, eligibility_error_message(:no_applicable_products)) + end + else + unless order.products.none? {|p| eligible_products.include?(p) } + eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product)) + end + end + + eligibility_errors.empty? + end + + def actionable?(line_item) + case preferred_match_policy + when 'any', 'all' + product_ids.include? line_item.variant.product_id + when 'none' + product_ids.exclude? line_item.variant.product_id + else + raise "unexpected match policy: #{preferred_match_policy.inspect}" + end + end + + def product_ids_string + product_ids.join(',') + end + + def product_ids_string=(s) + self.product_ids = s.to_s.split(',').map(&:strip) + end + end + end + end +end diff --git a/core/app/models/spree/promotion/rules/taxon.rb b/core/app/models/spree/promotion/rules/taxon.rb new file mode 100644 index 00000000000..109988629f3 --- /dev/null +++ b/core/app/models/spree/promotion/rules/taxon.rb @@ -0,0 +1,69 @@ +module Spree + class Promotion + module Rules + class Taxon < PromotionRule + has_and_belongs_to_many :taxons, class_name: '::Spree::Taxon', join_table: 'spree_taxons_promotion_rules', foreign_key: 'promotion_rule_id' + + MATCH_POLICIES = %w(any all) + preference :match_policy, default: MATCH_POLICIES.first + + def applicable?(promotable) + promotable.is_a?(Spree::Order) + end + + def eligible?(order, options = {}) + if preferred_match_policy == 'all' + unless (taxons.to_a - taxons_in_order_including_parents(order)).empty? + eligibility_errors.add(:base, eligibility_error_message(:missing_taxon)) + end + else + order_taxons = taxons_in_order_including_parents(order) + unless taxons.any?{ |taxon| order_taxons.include? taxon } + eligibility_errors.add(:base, eligibility_error_message(:no_matching_taxons)) + end + end + + eligibility_errors.empty? + end + + def actionable?(line_item) + taxon_product_ids.include? line_item.variant.product_id + end + + def taxon_ids_string + taxons.pluck(:id).join(',') + end + + def taxon_ids_string=(s) + ids = s.to_s.split(',').map(&:strip) + self.taxons = Spree::Taxon.find(ids) + end + + private + + # All taxons in an order + def order_taxons(order) + Spree::Taxon.joins(products: {variants_including_master: :line_items}).where(spree_line_items: {order_id: order.id}).uniq + end + + # ids of taxons rules and taxons rules children + def taxons_including_children_ids + taxons.inject([]){ |ids,taxon| ids += taxon.self_and_descendants.ids } + end + + # taxons order vs taxons rules and taxons rules children + def order_taxons_in_taxons_and_children(order) + order_taxons(order).where(id: taxons_including_children_ids) + end + + def taxons_in_order_including_parents(order) + order_taxons_in_taxons_and_children(order).inject([]){ |taxons, taxon| taxons << taxon.self_and_ancestors }.flatten.uniq + end + + def taxon_product_ids + Spree::Product.joins(:taxons).where(spree_taxons: {id: taxons.pluck(:id)}).pluck(:id).uniq + end + end + end + end +end diff --git a/core/app/models/spree/promotion/rules/user.rb b/core/app/models/spree/promotion/rules/user.rb new file mode 100644 index 00000000000..4f039e55f27 --- /dev/null +++ b/core/app/models/spree/promotion/rules/user.rb @@ -0,0 +1,30 @@ +module Spree + class Promotion + module Rules + class User < PromotionRule + belongs_to :user, class_name: "::#{Spree.user_class.to_s}" + + has_and_belongs_to_many :users, class_name: "::#{Spree.user_class.to_s}", + join_table: 'spree_promotion_rules_users', + foreign_key: 'promotion_rule_id', + association_foreign_key: :user_id + + def applicable?(promotable) + promotable.is_a?(Spree::Order) + end + + def eligible?(order, options = {}) + users.include?(order.user) + end + + def user_ids_string + user_ids.join(',') + end + + def user_ids_string=(s) + self.user_ids = s.to_s.split(',').map(&:strip) + end + end + end + end +end diff --git a/core/app/models/spree/promotion/rules/user_logged_in.rb b/core/app/models/spree/promotion/rules/user_logged_in.rb new file mode 100644 index 00000000000..6de456781e8 --- /dev/null +++ b/core/app/models/spree/promotion/rules/user_logged_in.rb @@ -0,0 +1,18 @@ +module Spree + class Promotion + module Rules + class UserLoggedIn < PromotionRule + def applicable?(promotable) + promotable.is_a?(Spree::Order) + end + + def eligible?(order, options = {}) + unless order.user.present? + eligibility_errors.add(:base, eligibility_error_message(:no_user_specified)) + end + eligibility_errors.empty? + end + end + end + end +end diff --git a/core/app/models/spree/promotion_action.rb b/core/app/models/spree/promotion_action.rb new file mode 100644 index 00000000000..e737ad70306 --- /dev/null +++ b/core/app/models/spree/promotion_action.rb @@ -0,0 +1,19 @@ +# Base class for all types of promotion action. +# PromotionActions perform the necessary tasks when a promotion is activated by an event and determined to be eligible. +module Spree + class PromotionAction < Spree::Base + acts_as_paranoid + + belongs_to :promotion, class_name: 'Spree::Promotion' + + scope :of_type, ->(t) { where(type: t) } + + # This method should be overriden in subclass + # Updates the state of the order or performs some other action depending on the subclass + # options will contain the payload from the event that activated the promotion. This will include + # the key :user which allows user based actions to be performed in addition to actions on the order + def perform(options = {}) + raise 'perform should be implemented in a sub-class of PromotionAction' + end + end +end diff --git a/core/app/models/spree/promotion_action_line_item.rb b/core/app/models/spree/promotion_action_line_item.rb new file mode 100644 index 00000000000..902c0a685fd --- /dev/null +++ b/core/app/models/spree/promotion_action_line_item.rb @@ -0,0 +1,6 @@ +module Spree + class PromotionActionLineItem < Spree::Base + belongs_to :promotion_action, class_name: 'Spree::Promotion::Actions::CreateLineItems' + belongs_to :variant, class_name: 'Spree::Variant' + end +end diff --git a/core/app/models/spree/promotion_category.rb b/core/app/models/spree/promotion_category.rb new file mode 100644 index 00000000000..c266d276c68 --- /dev/null +++ b/core/app/models/spree/promotion_category.rb @@ -0,0 +1,6 @@ +module Spree + class PromotionCategory < Spree::Base + validates_presence_of :name + has_many :promotions + end +end diff --git a/core/app/models/spree/promotion_handler/cart.rb b/core/app/models/spree/promotion_handler/cart.rb new file mode 100644 index 00000000000..9a957b85ee1 --- /dev/null +++ b/core/app/models/spree/promotion_handler/cart.rb @@ -0,0 +1,54 @@ +module Spree + module PromotionHandler + # Decides which promotion should be activated given the current order context + # + # By activated it doesn't necessarily mean that the order will have a + # discount for every activated promotion. It means that the discount will be + # created and might eventually become eligible. The intention here is to + # reduce overhead. e.g. a promotion that requires item A to be eligible + # shouldn't be eligible unless item A is added to the order. + # + # It can be used as a wrapper for custom handlers as well. Different + # applications might have completely different requirements to make + # the promotions system accurate and performant. Here they can plug custom + # handler to activate promos as they wish once an item is added to cart + class Cart + attr_reader :line_item, :order + attr_accessor :error, :success + + def initialize(order, line_item=nil) + @order, @line_item = order, line_item + end + + def activate + promotions.each do |promotion| + if (line_item && promotion.eligible?(line_item)) || promotion.eligible?(order) + promotion.activate(line_item: line_item, order: order) + end + end + end + + private + + def promotions + # AR cannot bind raw ASTs to prepared statements. There always must be a manager around. + # Also Postgresql requires an aliased table for `SELECT * FROM (subexpression) AS alias`. + # And Sqlite3 cannot work on outher parenthesis from `(left UNION right)`. + # So this construct makes both happy. + select = Arel::SelectManager.new( + Promotion, + Promotion.arel_table.create_table_alias( + order.promotions.active.union(Promotion.active.where(code: nil, path: nil)), + Promotion.table_name + ), + ) + select.project(Arel.star) + + Promotion.find_by_sql( + select, + order.promotions.bind_values + ) + end + end + end +end diff --git a/core/app/models/spree/promotion_handler/coupon.rb b/core/app/models/spree/promotion_handler/coupon.rb new file mode 100644 index 00000000000..d952bc158dd --- /dev/null +++ b/core/app/models/spree/promotion_handler/coupon.rb @@ -0,0 +1,113 @@ +module Spree + module PromotionHandler + class Coupon + attr_reader :order + attr_accessor :error, :success, :status_code + + def initialize(order) + @order = order + end + + def apply + if order.coupon_code.present? + if promotion.present? && promotion.actions.exists? + handle_present_promotion(promotion) + else + if Promotion.with_coupon_code(order.coupon_code).try(:expired?) + set_error_code :coupon_code_expired + else + set_error_code :coupon_code_not_found + end + end + end + + self + end + + def set_success_code(c) + @status_code = c + @success = Spree.t(c) + end + + def set_error_code(c) + @status_code = c + @error = Spree.t(c) + end + + def promotion + @promotion ||= Promotion.active.includes(:promotion_rules, :promotion_actions).with_coupon_code(order.coupon_code) + end + + def successful? + success.present? && error.blank? + end + + private + + def handle_present_promotion(promotion) + return promotion_usage_limit_exceeded if promotion.usage_limit_exceeded?(order) + return promotion_applied if promotion_exists_on_order?(order, promotion) + unless promotion.eligible?(order) + self.error = promotion.eligibility_errors.full_messages.first unless promotion.eligibility_errors.blank? + return (self.error || ineligible_for_this_order) + end + + # If any of the actions for the promotion return `true`, + # then result here will also be `true`. + result = promotion.activate(:order => order) + if result + determine_promotion_application_result + else + set_error_code :coupon_code_unknown_error + end + end + + def promotion_usage_limit_exceeded + set_error_code :coupon_code_max_usage + end + + def ineligible_for_this_order + set_error_code :coupon_code_not_eligible + end + + def promotion_applied + set_error_code :coupon_code_already_applied + end + + def promotion_exists_on_order?(order, promotion) + order.promotions.include? promotion + end + + def determine_promotion_application_result + detector = lambda { |p| + if p.source.promotion.code + p.source.promotion.code.downcase == order.coupon_code.downcase + end + } + + # Check for applied adjustments. + discount = order.line_item_adjustments.promotion.eligible.detect(&detector) + discount ||= order.shipment_adjustments.promotion.detect(&detector) + discount ||= order.adjustments.promotion.detect(&detector) + + # Check for applied line items. + created_line_items = promotion.actions.detect { |a| a.type == 'Spree::Promotion::Actions::CreateLineItems' } + + if (discount && discount.eligible) || created_line_items + order.update_totals + order.persist_totals + set_success_code :coupon_code_applied + else + # if the promotion exists on an order, but wasn't found above, + # we've already selected a better promotion + if order.promotions.with_coupon_code(order.coupon_code) + set_error_code :coupon_code_better_exists + else + # if the promotion was created after the order + set_error_code :coupon_code_not_found + end + end + end + end + end +end diff --git a/core/app/models/spree/promotion_handler/free_shipping.rb b/core/app/models/spree/promotion_handler/free_shipping.rb new file mode 100644 index 00000000000..ab02c109d60 --- /dev/null +++ b/core/app/models/spree/promotion_handler/free_shipping.rb @@ -0,0 +1,33 @@ +module Spree + module PromotionHandler + # Used for activating promotions with shipping rules + class FreeShipping + attr_reader :order, :order_promo_ids + attr_accessor :error, :success + + def initialize(order) + @order = order + @order_promo_ids = order.promotions.pluck(:id) + end + + def activate + promotions.each do |promotion| + next if promotion.code.present? && !order_promo_ids.include?(promotion.id) + + if promotion.eligible?(order) + promotion.activate(order: order) + end + end + end + + private + + def promotions + Spree::Promotion.active.where( + id: Spree::Promotion::Actions::FreeShipping.pluck(:promotion_id), + path: nil + ) + end + end + end +end diff --git a/core/app/models/spree/promotion_handler/page.rb b/core/app/models/spree/promotion_handler/page.rb new file mode 100644 index 00000000000..caf3c0a9455 --- /dev/null +++ b/core/app/models/spree/promotion_handler/page.rb @@ -0,0 +1,24 @@ +module Spree + module PromotionHandler + class Page + attr_reader :order, :path + + def initialize(order, path) + @order = order + @path = path.gsub(/\A\//, '') + end + + def activate + if promotion && promotion.eligible?(order) + promotion.activate(:order => order) + end + end + + private + + def promotion + @promotion ||= Promotion.active.find_by(:path => path) + end + end + end +end diff --git a/core/app/models/spree/promotion_rule.rb b/core/app/models/spree/promotion_rule.rb new file mode 100644 index 00000000000..a7d62f04b6a --- /dev/null +++ b/core/app/models/spree/promotion_rule.rb @@ -0,0 +1,44 @@ +# Base class for all promotion rules +module Spree + class PromotionRule < Spree::Base + belongs_to :promotion, class_name: 'Spree::Promotion', inverse_of: :promotion_rules + + scope :of_type, ->(t) { where(type: t) } + + validate :promotion, presence: true + validate :unique_per_promotion, on: :create + + def self.for(promotable) + all.select { |rule| rule.applicable?(promotable) } + end + + def applicable?(promotable) + raise 'applicable? should be implemented in a sub-class of Spree::PromotionRule' + end + + def eligible?(promotable, options = {}) + raise 'eligible? should be implemented in a sub-class of Spree::PromotionRule' + end + + # This states if a promotion can be applied to the specified line item + # It is true by default, but can be overridden by promotion rules to provide conditions + def actionable?(line_item) + true + end + + def eligibility_errors + @eligibility_errors ||= ActiveModel::Errors.new(self) + end + + private + def unique_per_promotion + if Spree::PromotionRule.exists?(promotion_id: promotion_id, type: self.class.name) + errors[:base] << "Promotion already contains this rule type" + end + end + + def eligibility_error_message(key, options = {}) + Spree.t(key, Hash[scope: [:eligibility_errors, :messages]].merge(options)) + end + end +end diff --git a/core/app/models/spree/property.rb b/core/app/models/spree/property.rb index 0c5e6c2cf3c..6bb5ea1e8be 100644 --- a/core/app/models/spree/property.rb +++ b/core/app/models/spree/property.rb @@ -1,22 +1,22 @@ module Spree - class Property < ActiveRecord::Base - has_and_belongs_to_many :prototypes, :join_table => :spree_properties_prototypes + class Property < Spree::Base + has_and_belongs_to_many :prototypes, join_table: 'spree_properties_prototypes' - has_many :product_properties, :dependent => :destroy - has_many :products, :through => :product_properties + has_many :product_properties, dependent: :delete_all, inverse_of: :property + has_many :products, through: :product_properties - attr_accessible :name, :presentation + validates :name, :presentation, presence: true - validates :name, :presentation, :presence => true + scope :sorted, -> { order(:name) } - scope :sorted, lambda { order(:name) } + after_touch :touch_all_products - def self.find_all_by_prototype(prototype) - id = prototype - if prototype.class == Prototype - id = prototype.id - end - joins("LEFT JOIN properties_prototypes ON property_id = #{self.table_name}.id").where(:prototype_id => id) + self.whitelisted_ransackable_attributes = ['presentation'] + + private + + def touch_all_products + products.update_all(updated_at: Time.current) end end end diff --git a/core/app/models/spree/prototype.rb b/core/app/models/spree/prototype.rb index 2a1aef3ab84..c1133b51758 100644 --- a/core/app/models/spree/prototype.rb +++ b/core/app/models/spree/prototype.rb @@ -1,10 +1,9 @@ module Spree - class Prototype < ActiveRecord::Base - has_and_belongs_to_many :properties, :join_table => :spree_properties_prototypes - has_and_belongs_to_many :option_types, :join_table => :spree_option_types_prototypes + class Prototype < Spree::Base + has_and_belongs_to_many :properties, join_table: :spree_properties_prototypes + has_and_belongs_to_many :option_types, join_table: :spree_option_types_prototypes + has_and_belongs_to_many :taxons, join_table: :spree_taxons_prototypes - attr_accessible :name, :property_ids, :option_type_ids - - validates :name, :presence => true + validates :name, presence: true end end diff --git a/core/app/models/spree/refund.rb b/core/app/models/spree/refund.rb new file mode 100644 index 00000000000..a193eded3f4 --- /dev/null +++ b/core/app/models/spree/refund.rb @@ -0,0 +1,96 @@ +module Spree + class Refund < Spree::Base + belongs_to :payment, inverse_of: :refunds + belongs_to :reason, class_name: 'Spree::RefundReason', foreign_key: :refund_reason_id + belongs_to :reimbursement, inverse_of: :refunds + + has_many :log_entries, as: :source + + validates :payment, presence: true + validates :reason, presence: true + validates :transaction_id, presence: true, on: :update # can't require this on create because the before_create needs to run first + validates :amount, presence: true, numericality: {greater_than: 0} + + validate :check_payment_environment, on: :create, if: :payment + validate :amount_is_less_than_or_equal_to_allowed_amount, on: :create + + after_create :perform! + after_create :create_log_entry + + scope :non_reimbursement, -> { where(reimbursement_id: nil) } + + def money + Spree::Money.new(amount, { currency: payment.currency }) + end + alias display_amount money + + class << self + def total_amount_reimbursed_for(reimbursement) + reimbursement.refunds.to_a.sum(&:amount) + end + end + + def description + payment.payment_method.name + end + + private + + # attempts to perform the refund. + # raises an error if the refund fails. + def perform! + return true if transaction_id.present? + + credit_cents = Spree::Money.new(amount.to_f, currency: payment.currency).money.cents + + @response = process!(credit_cents) + + self.transaction_id = @response.authorization + update_columns(transaction_id: transaction_id) + update_order + end + + # return an activemerchant response object if successful or else raise an error + def process!(credit_cents) + response = if payment.payment_method.payment_profiles_supported? + payment.payment_method.credit(credit_cents, payment.source, payment.transaction_id, {originator: self}) + else + payment.payment_method.credit(credit_cents, payment.transaction_id, {originator: self}) + end + + if !response.success? + logger.error(Spree.t(:gateway_error) + " #{response.to_yaml}") + text = response.params['message'] || response.params['response_reason_text'] || response.message + raise Core::GatewayError.new(text) + end + + response + rescue ActiveMerchant::ConnectionError => e + logger.error(Spree.t(:gateway_error) + " #{e.inspect}") + raise Core::GatewayError.new(Spree.t(:unable_to_connect_to_gateway)) + end + + # Saftey check to make sure we're not accidentally performing operations on a live gateway. + # Ex. When testing in staging environment with a copy of production data. + def check_payment_environment + if payment.payment_method.environment != Rails.env + message = Spree.t(:gateway_config_unavailable) + " - #{Rails.env}" + errors.add(:base, message) + end + end + + def create_log_entry + log_entries.create!(details: @response.to_yaml) + end + + def amount_is_less_than_or_equal_to_allowed_amount + if amount > payment.credit_allowed + errors.add(:amount, :greater_than_allowed) + end + end + + def update_order + payment.order.updater.update + end + end +end diff --git a/core/app/models/spree/refund_reason.rb b/core/app/models/spree/refund_reason.rb new file mode 100644 index 00000000000..c6522d911da --- /dev/null +++ b/core/app/models/spree/refund_reason.rb @@ -0,0 +1,13 @@ +module Spree + class RefundReason < Spree::Base + include Spree::NamedType + + RETURN_PROCESSING_REASON = 'Return processing' + + has_many :refunds + + def self.return_processing_reason + find_by!(name: RETURN_PROCESSING_REASON, mutable: false) + end + end +end diff --git a/core/app/models/spree/reimbursement.rb b/core/app/models/spree/reimbursement.rb new file mode 100644 index 00000000000..fd2a9eb2017 --- /dev/null +++ b/core/app/models/spree/reimbursement.rb @@ -0,0 +1,168 @@ +module Spree + class Reimbursement < Spree::Base + class IncompleteReimbursementError < StandardError; end + + belongs_to :order, inverse_of: :reimbursements + belongs_to :customer_return, inverse_of: :reimbursements, touch: true + + has_many :refunds, inverse_of: :reimbursement + has_many :credits, inverse_of: :reimbursement, class_name: 'Spree::Reimbursement::Credit' + + has_many :return_items, inverse_of: :reimbursement + + validates :order, presence: true + validate :validate_return_items_belong_to_same_order + + accepts_nested_attributes_for :return_items, allow_destroy: true + + before_create :generate_number + + scope :reimbursed, -> { where(reimbursement_status: 'reimbursed') } + + # The reimbursement_tax_calculator property should be set to an object that responds to "call" + # and accepts a reimbursement object. Invoking "call" should update the tax fields on the + # associated ReturnItems. + # This allows a store to easily integrate with third party tax services. + class_attribute :reimbursement_tax_calculator + self.reimbursement_tax_calculator = ReimbursementTaxCalculator + # A separate attribute here allows you to use a more performant calculator for estimates + # and a different one (e.g. one that hits a 3rd party API) for the final caluclations. + class_attribute :reimbursement_simulator_tax_calculator + self.reimbursement_simulator_tax_calculator = ReimbursementTaxCalculator + + # The reimbursement_models property should contain an array of all models that provide + # reimbursements. + # This allows a store to incorporate custom reimbursement methods that Spree doesn't know about. + # Each model must implement a "total_amount_reimbursed_for" method. + # Example: + # Refund.total_amount_reimbursed_for(reimbursement) + # See the `reimbursement_generator` property regarding the generation of custom reimbursements. + class_attribute :reimbursement_models + self.reimbursement_models = [Refund] + + # The reimbursement_performer property should be set to an object that responds to the following methods: + # - #perform + # - #simulate + # see ReimbursementPerformer for details. + # This allows a store to customize their reimbursement methods and logic. + class_attribute :reimbursement_performer + self.reimbursement_performer = ReimbursementPerformer + + # These are called if the call to "reimburse!" succeeds. + class_attribute :reimbursement_success_hooks + self.reimbursement_success_hooks = [] + + # These are called if the call to "reimburse!" fails. + class_attribute :reimbursement_failure_hooks + self.reimbursement_failure_hooks = [] + + state_machine :reimbursement_status, initial: :pending do + + event :errored do + transition to: :errored, from: :pending + end + + event :reimbursed do + transition to: :reimbursed, from: [:pending, :errored] + end + + end + + class << self + def build_from_customer_return(customer_return) + order = customer_return.order + order.reimbursements.build({ + customer_return: customer_return, + return_items: customer_return.return_items.accepted.not_reimbursed, + }) + end + end + + def display_total + Spree::Money.new(total, { currency: order.currency }) + end + + def calculated_total + # rounding every return item individually to handle edge cases for consecutive partial + # returns where rounding might cause us to try to reimburse more than was originally billed + return_items.map { |ri| ri.total.to_d.round(2) }.sum + end + + def paid_amount + reimbursement_models.sum do |model| + model.total_amount_reimbursed_for(self) + end + end + + def unpaid_amount + total - paid_amount + end + + def perform! + reimbursement_tax_calculator.call(self) + reload + update!(total: calculated_total) + + reimbursement_performer.perform(self) + + if unpaid_amount_within_tolerance? + reimbursed! + reimbursement_success_hooks.each { |h| h.call self } + send_reimbursement_email + else + errored! + reimbursement_failure_hooks.each { |h| h.call self } + raise IncompleteReimbursementError, Spree.t("validation.unpaid_amount_not_zero", amount: unpaid_amount) + end + end + + def simulate + reimbursement_simulator_tax_calculator.call(self) + reload + update!(total: calculated_total) + + reimbursement_performer.simulate(self) + end + + def return_items_requiring_exchange + return_items.select(&:exchange_required?) + end + + private + + def generate_number + self.number ||= loop do + random = "RI#{Array.new(9){rand(9)}.join}" + break random unless self.class.exists?(number: random) + end + end + + def validate_return_items_belong_to_same_order + if return_items.any? { |ri| ri.inventory_unit.order_id != order_id } + errors.add(:base, :return_items_order_id_does_not_match) + end + end + + def send_reimbursement_email + Spree::ReimbursementMailer.reimbursement_email(self.id).deliver + end + + # If there are multiple different reimbursement types for a single + # reimbursement we open ourselves to a one-cent rounding error for every + # type over the first one. This is due to how we round #unpaid_amount and + # how each reimbursement type will round as well. Since at this point the + # payments and credits have already been processed, we should allow the + # reimbursement to show as 'reimbursed' and not 'errored'. + def unpaid_amount_within_tolerance? + reimbursement_count = reimbursement_models.count do |model| + model.total_amount_reimbursed_for(self) > 0 + end + leniency = if reimbursement_count > 0 + (reimbursement_count - 1) * 0.01.to_d + else + 0 + end + unpaid_amount.abs.between?(0, leniency) + end + end +end diff --git a/core/app/models/spree/reimbursement/credit.rb b/core/app/models/spree/reimbursement/credit.rb new file mode 100644 index 00000000000..610d7f1aaa7 --- /dev/null +++ b/core/app/models/spree/reimbursement/credit.rb @@ -0,0 +1,25 @@ +module Spree + class Reimbursement::Credit < Spree::Base + class_attribute :default_creditable_class + self.default_creditable_class = nil + + belongs_to :reimbursement, inverse_of: :credits + belongs_to :creditable, polymorphic: true + + validates :creditable, presence: true + + class << self + def total_amount_reimbursed_for(reimbursement) + reimbursement.credits.to_a.sum(&:amount) + end + end + + def description + creditable.class.name.demodulize + end + + def display_amount + Spree::Money.new(amount, { currency: creditable.try(:currency) || "USD" }) + end + end +end diff --git a/core/app/models/spree/reimbursement/reimbursement_type_engine.rb b/core/app/models/spree/reimbursement/reimbursement_type_engine.rb new file mode 100644 index 00000000000..07c36c5b015 --- /dev/null +++ b/core/app/models/spree/reimbursement/reimbursement_type_engine.rb @@ -0,0 +1,56 @@ +module Spree + class Reimbursement::ReimbursementTypeEngine + include Spree::Reimbursement::ReimbursementTypeValidator + + class_attribute :refund_time_constraint + self.refund_time_constraint = 90.days + + class_attribute :default_reimbursement_type + self.default_reimbursement_type = Spree::ReimbursementType::OriginalPayment + + class_attribute :expired_reimbursement_type + self.expired_reimbursement_type = Spree::ReimbursementType::OriginalPayment + + class_attribute :exchange_reimbursement_type + self.exchange_reimbursement_type = Spree::ReimbursementType::Exchange + + def initialize(return_items) + @return_items = return_items + @reimbursement_type_hash = Hash.new {|h,k| h[k] = Array.new } + end + + def calculate_reimbursement_types + @return_items.each do |return_item| + reimbursement_type = calculate_reimbursement_type(return_item) + add_reimbursement_type(return_item, reimbursement_type) + end + + @reimbursement_type_hash + end + + private + + def calculate_reimbursement_type(return_item) + if return_item.exchange_required? + exchange_reimbursement_type + elsif return_item.override_reimbursement_type.present? + return_item.override_reimbursement_type.class + elsif return_item.preferred_reimbursement_type.present? + if valid_preferred_reimbursement_type?(return_item) + return_item.preferred_reimbursement_type.class + else + nil + end + elsif past_reimbursable_time_period?(return_item) + expired_reimbursement_type + else + default_reimbursement_type + end + end + + def add_reimbursement_type(return_item, reimbursement_type) + return unless reimbursement_type + @reimbursement_type_hash[reimbursement_type] << return_item + end + end +end diff --git a/core/app/models/spree/reimbursement/reimbursement_type_validator.rb b/core/app/models/spree/reimbursement/reimbursement_type_validator.rb new file mode 100644 index 00000000000..f6bc117892e --- /dev/null +++ b/core/app/models/spree/reimbursement/reimbursement_type_validator.rb @@ -0,0 +1,15 @@ +module Spree + module Reimbursement::ReimbursementTypeValidator + def valid_preferred_reimbursement_type?(return_item) + preferred_type = return_item.preferred_reimbursement_type.class + + !past_reimbursable_time_period?(return_item) || + preferred_type == expired_reimbursement_type + end + + def past_reimbursable_time_period?(return_item) + shipped_at = return_item.inventory_unit.shipment.shipped_at + shipped_at && shipped_at < refund_time_constraint.ago + end + end +end diff --git a/core/app/models/spree/reimbursement_performer.rb b/core/app/models/spree/reimbursement_performer.rb new file mode 100644 index 00000000000..bc784eea47e --- /dev/null +++ b/core/app/models/spree/reimbursement_performer.rb @@ -0,0 +1,43 @@ +module Spree + + class ReimbursementPerformer + + class << self + class_attribute :reimbursement_type_engine + self.reimbursement_type_engine = Spree::Reimbursement::ReimbursementTypeEngine + + # Simulate performing the reimbursement without actually saving anything or refunding money, etc. + # This must return an array of objects that respond to the following methods: + # - #description + # - #display_amount + # so they can be displayed in the Admin UI appropriately. + def simulate(reimbursement) + execute(reimbursement, true) + end + + # Actually perform the reimbursement + def perform(reimbursement) + execute(reimbursement, false) + end + + private + + def execute(reimbursement, simulate) + reimbursement_type_hash = calculate_reimbursement_types(reimbursement) + + reimbursement_type_hash.flat_map do |reimbursement_type, return_items| + reimbursement_type.reimburse(reimbursement, return_items, simulate) + end + end + + def calculate_reimbursement_types(reimbursement) + # Engine returns hash of preferred reimbursement types pointing at return items + # {Spree::ReimbursementType::OriginalPayment => [ReturnItem, ...], Spree::ReimbursementType::Exchange => [ReturnItem, ...]} + reimbursement_type_engine.new(reimbursement.return_items).calculate_reimbursement_types + end + + end + + end + +end diff --git a/core/app/models/spree/reimbursement_tax_calculator.rb b/core/app/models/spree/reimbursement_tax_calculator.rb new file mode 100644 index 00000000000..de5f708e305 --- /dev/null +++ b/core/app/models/spree/reimbursement_tax_calculator.rb @@ -0,0 +1,44 @@ +module Spree + + # Tax calculation is broken out at this level to allow easy integration with 3rd party + # taxation systems. Those systems are usually geared toward calculating all items at once + # rather than one at a time. + # + # To use an alternative tax calculator do this: + # Spree::ReturnAuthorization.reimbursement_tax_calculator = calculator_object + # where `calculator_object` is an object that responds to "call" and accepts a reimbursement object + + class ReimbursementTaxCalculator + + class << self + + def call(reimbursement) + reimbursement.return_items.includes(:inventory_unit).each do |return_item| + set_tax!(return_item) + end + end + + private + + def set_tax!(return_item) + calculated_refund = Spree::ReturnItem.refund_amount_calculator.new.compute(return_item) + + percent_of_tax = if return_item.pre_tax_amount <= 0 || calculated_refund <= 0 + 0 + else + return_item.pre_tax_amount / calculated_refund + end + + additional_tax_total = percent_of_tax * return_item.inventory_unit.additional_tax_total + included_tax_total = percent_of_tax * return_item.inventory_unit.included_tax_total + + return_item.update_attributes!({ + additional_tax_total: additional_tax_total, + included_tax_total: included_tax_total, + }) + end + end + + end + +end diff --git a/core/app/models/spree/reimbursement_type.rb b/core/app/models/spree/reimbursement_type.rb new file mode 100644 index 00000000000..0e583b8757e --- /dev/null +++ b/core/app/models/spree/reimbursement_type.rb @@ -0,0 +1,16 @@ +module Spree + class ReimbursementType < Spree::Base + include Spree::NamedType + + ORIGINAL = 'original' + + has_many :return_items + + # This method will reimburse the return items based on however it child implements it + # By default it takes a reimbursement, the return items it needs to reimburse, and if + # it is a simulation or a real reimbursement. This should return an array + def self.reimburse(reimbursement, return_items, simulate) + raise "Implement me" + end + end +end diff --git a/core/app/models/spree/reimbursement_type/credit.rb b/core/app/models/spree/reimbursement_type/credit.rb new file mode 100644 index 00000000000..9f0f4fda2b0 --- /dev/null +++ b/core/app/models/spree/reimbursement_type/credit.rb @@ -0,0 +1,13 @@ +module Spree + class ReimbursementType::Credit < Spree::ReimbursementType + extend Spree::ReimbursementType::ReimbursementHelpers + + class << self + def reimburse(reimbursement, return_items, simulate) + unpaid_amount = return_items.map { |ri| ri.total.to_d.round(2) }.sum + reimbursement_list, unpaid_amount = create_credits(reimbursement, unpaid_amount, simulate) + reimbursement_list + end + end + end +end diff --git a/core/app/models/spree/reimbursement_type/exchange.rb b/core/app/models/spree/reimbursement_type/exchange.rb new file mode 100644 index 00000000000..2a79bc8732e --- /dev/null +++ b/core/app/models/spree/reimbursement_type/exchange.rb @@ -0,0 +1,9 @@ +class Spree::ReimbursementType::Exchange < Spree::ReimbursementType + def self.reimburse(reimbursement, return_items, simulate) + return [] unless return_items.present? + + exchange = Spree::Exchange.new(reimbursement.order, return_items) + exchange.perform! unless simulate + [exchange] + end +end diff --git a/core/app/models/spree/reimbursement_type/original_payment.rb b/core/app/models/spree/reimbursement_type/original_payment.rb new file mode 100644 index 00000000000..b108686bd55 --- /dev/null +++ b/core/app/models/spree/reimbursement_type/original_payment.rb @@ -0,0 +1,13 @@ +class Spree::ReimbursementType::OriginalPayment < Spree::ReimbursementType + extend Spree::ReimbursementType::ReimbursementHelpers + + class << self + def reimburse(reimbursement, return_items, simulate) + unpaid_amount = return_items.map { |ri| ri.total.to_d.round(2) }.sum + payments = reimbursement.order.payments.completed + + refund_list, unpaid_amount = create_refunds(reimbursement, payments, unpaid_amount, simulate) + refund_list + end + end +end diff --git a/core/app/models/spree/reimbursement_type/reimbursement_helpers.rb b/core/app/models/spree/reimbursement_type/reimbursement_helpers.rb new file mode 100644 index 00000000000..9e83ef1b8fe --- /dev/null +++ b/core/app/models/spree/reimbursement_type/reimbursement_helpers.rb @@ -0,0 +1,50 @@ +module Spree + module ReimbursementType::ReimbursementHelpers + def create_refunds(reimbursement, payments, unpaid_amount, simulate, reimbursement_list = []) + payments.map do |payment| + break if unpaid_amount <= 0 + next unless payment.can_credit? + + amount = [unpaid_amount, payment.credit_allowed].min + reimbursement_list << create_refund(reimbursement, payment, amount, simulate) + unpaid_amount -= amount + end + + return reimbursement_list, unpaid_amount + end + + def create_credits(reimbursement, unpaid_amount, simulate, reimbursement_list = []) + credits = [create_credit(reimbursement, unpaid_amount, simulate)] + unpaid_amount -= credits.sum(&:amount) + reimbursement_list += credits + + return reimbursement_list, unpaid_amount + end + + private + + def create_refund(reimbursement, payment, amount, simulate) + refund = reimbursement.refunds.build({ + payment: payment, + amount: amount, + reason: Spree::RefundReason.return_processing_reason, + }) + + simulate ? refund.readonly! : refund.save! + refund + end + + # If you have multiple methods of crediting a customer, overwrite this method + # Must return an array of objects the respond to #description, #display_amount + def create_credit(reimbursement, unpaid_amount, simulate) + creditable = create_creditable(reimbursement, unpaid_amount) + credit = reimbursement.credits.build(creditable: creditable, amount: unpaid_amount) + simulate ? credit.readonly! : credit.save! + credit + end + + def create_creditable(reimbursement, unpaid_amount) + Spree::Reimbursement::Credit.default_creditable_class.new(reimbursement: reimbursement, amount: unpaid_amount) + end + end +end diff --git a/core/app/models/spree/return_authorization.rb b/core/app/models/spree/return_authorization.rb index 121ae2742b2..2e0ee5b5853 100644 --- a/core/app/models/spree/return_authorization.rb +++ b/core/app/models/spree/return_authorization.rb @@ -1,94 +1,104 @@ module Spree - class ReturnAuthorization < ActiveRecord::Base - belongs_to :order + class ReturnAuthorization < Spree::Base + belongs_to :order, class_name: 'Spree::Order', inverse_of: :return_authorizations - has_many :inventory_units + has_many :return_items, inverse_of: :return_authorization, dependent: :destroy + has_many :inventory_units, through: :return_items + has_many :customer_returns, through: :return_items + + belongs_to :stock_location + belongs_to :reason, class_name: 'Spree::ReturnAuthorizationReason', foreign_key: :return_authorization_reason_id before_create :generate_number - before_save :force_positive_amount - validates :order, :presence => true - validates :amount, :numericality => true - validate :must_have_shipped_units + after_save :generate_expedited_exchange_reimbursements - attr_accessible :amount, :reason + accepts_nested_attributes_for :return_items, allow_destroy: true - state_machine :initial => 'authorized' do - after_transition :to => 'received', :do => :process_return + validates :order, presence: true + validates :reason, presence: true + validates :stock_location, presence: true + validate :must_have_shipped_units, on: :create + + + # These are called prior to generating expedited exchanges shipments. + # Should respond to a "call" method that takes the list of return items + class_attribute :pre_expedited_exchange_hooks + self.pre_expedited_exchange_hooks = [] + + state_machine initial: :authorized do + before_transition to: :canceled, do: :cancel_return_items - event :receive do - transition :to => 'received', :from => 'authorized', :if => :allow_receive? - end event :cancel do - transition :to => 'canceled', :from => 'authorized' + transition to: :canceled, from: :authorized, if: lambda { |return_authorization| return_authorization.can_cancel_return_items? } end - end - def currency - order.nil? ? Spree::Config[:currency] : order.currency end - def display_amount - Spree::Money.new(amount, { :currency => currency }) - end - - def add_variant(variant_id, quantity) - order_units = order.inventory_units.group_by(&:variant_id) - returned_units = inventory_units.group_by(&:variant_id) + self.whitelisted_ransackable_attributes = ['memo'] - count = 0 + def pre_tax_total + return_items.sum(:pre_tax_amount) + end - if returned_units[variant_id].nil? || returned_units[variant_id].size < quantity - count = returned_units[variant_id].nil? ? 0 : returned_units[variant_id].size + def display_pre_tax_total + Spree::Money.new(pre_tax_total, { currency: currency }) + end - order_units[variant_id].each do |inventory_unit| - next unless inventory_unit.return_authorization.nil? && count < quantity + def currency + order.nil? ? Spree::Config[:currency] : order.currency + end - inventory_unit.return_authorization = self - inventory_unit.save! + def refundable_amount + order.pre_tax_item_amount + order.promo_total + end - count += 1 - end - elsif returned_units[variant_id].size > quantity - (returned_units[variant_id].size - quantity).times do |i| - returned_units[variant_id][i].return_authorization_id = nil - returned_units[variant_id][i].save! - end - end + def customer_returned_items? + customer_returns.exists? + end - order.authorize_return! if inventory_units.reload.size > 0 && !order.awaiting_return? + def can_cancel_return_items? + return_items.any?(&:can_cancel?) || return_items.blank? end private + def must_have_shipped_units - errors.add(:order, I18n.t(:has_no_shipped_units)) if order.nil? || !order.inventory_units.any?(&:shipped?) + if order.nil? || order.inventory_units.shipped.none? + errors.add(:order, Spree.t(:has_no_shipped_units)) + end end def generate_number - return if number - - record = true - while record - random = "RMA#{Array.new(9){rand(9)}.join}" - record = self.class.where(:number => random).first + self.number ||= loop do + random = "RA#{Array.new(9){rand(9)}.join}" + break random unless self.class.exists?(number: random) end - self.number = random end - def process_return - inventory_units.each &:return! - credit = Adjustment.new(:amount => amount.abs * -1, :label => I18n.t(:rma_credit)) - credit.source = self - credit.adjustable = order - credit.save - order.return if inventory_units.all?(&:returned?) + def cancel_return_items + return_items.each { |item| item.cancel! if item.can_cancel? } end - def allow_receive? - !inventory_units.empty? - end + def generate_expedited_exchange_reimbursements + return unless Spree::Config[:expedited_exchanges] + + items_to_exchange = return_items.select(&:exchange_required?) + items_to_exchange.each(&:attempt_accept) + items_to_exchange.select!(&:accepted?) + + return if items_to_exchange.blank? + + pre_expedited_exchange_hooks.each { |h| h.call items_to_exchange } + + reimbursement = Reimbursement.new(return_items: items_to_exchange, order: order) + + if reimbursement.save + reimbursement.perform! + else + errors.add(:base, reimbursement.errors.full_messages) + raise ActiveRecord::RecordInvalid.new(self) + end - def force_positive_amount - self.amount = amount.abs end end end diff --git a/core/app/models/spree/return_authorization_reason.rb b/core/app/models/spree/return_authorization_reason.rb new file mode 100644 index 00000000000..25b3482dbb4 --- /dev/null +++ b/core/app/models/spree/return_authorization_reason.rb @@ -0,0 +1,7 @@ +module Spree + class ReturnAuthorizationReason < Spree::Base + include Spree::NamedType + + has_many :return_authorizations + end +end diff --git a/core/app/models/spree/return_item.rb b/core/app/models/spree/return_item.rb new file mode 100644 index 00000000000..3eb1c595d00 --- /dev/null +++ b/core/app/models/spree/return_item.rb @@ -0,0 +1,230 @@ +module Spree + class ReturnItem < Spree::Base + COMPLETED_RECEPTION_STATUSES = %w(received given_to_customer) + + class_attribute :return_eligibility_validator + self.return_eligibility_validator = ReturnItem::EligibilityValidator::Default + + class_attribute :exchange_variant_engine + self.exchange_variant_engine = ReturnItem::ExchangeVariantEligibility::SameProduct + + class_attribute :refund_amount_calculator + self.refund_amount_calculator = Calculator::Returns::DefaultRefundAmount + + belongs_to :return_authorization, inverse_of: :return_items + belongs_to :inventory_unit, inverse_of: :return_items + belongs_to :exchange_variant, class_name: 'Spree::Variant' + belongs_to :exchange_inventory_unit, class_name: 'Spree::InventoryUnit', inverse_of: :original_return_item + belongs_to :customer_return, inverse_of: :return_items + belongs_to :reimbursement, inverse_of: :return_items + belongs_to :preferred_reimbursement_type, class_name: 'Spree::ReimbursementType' + belongs_to :override_reimbursement_type, class_name: 'Spree::ReimbursementType' + + validate :belongs_to_same_customer_order + validate :validate_acceptance_status_for_reimbursement + validates :inventory_unit, presence: true + validate :validate_no_other_completed_return_items, on: :create + + after_create :cancel_others, unless: :cancelled? + + scope :awaiting_return, -> { where(reception_status: 'awaiting') } + scope :not_cancelled, -> { where.not(reception_status: 'cancelled') } + scope :pending, -> { where(acceptance_status: 'pending') } + scope :accepted, -> { where(acceptance_status: 'accepted') } + scope :rejected, -> { where(acceptance_status: 'rejected') } + scope :manual_intervention_required, -> { where(acceptance_status: 'manual_intervention_required') } + scope :undecided, -> { where(acceptance_status: %w(pending manual_intervention_required)) } + scope :decided, -> { where.not(acceptance_status: %w(pending manual_intervention_required)) } + scope :reimbursed, -> { where.not(reimbursement_id: nil) } + scope :not_reimbursed, -> { where(reimbursement_id: nil) } + scope :exchange_requested, -> { where.not(exchange_variant: nil) } + scope :exchange_processed, -> { where.not(exchange_inventory_unit: nil) } + scope :exchange_required, -> { exchange_requested.where(exchange_inventory_unit: nil) } + + serialize :acceptance_status_errors + + delegate :eligible_for_return?, :requires_manual_intervention?, to: :validator + delegate :variant, to: :inventory_unit + delegate :shipment, to: :inventory_unit + + before_create :set_default_pre_tax_amount, unless: :pre_tax_amount_changed? + before_save :set_exchange_pre_tax_amount + + state_machine :reception_status, initial: :awaiting do + before_transition to: :received, do: :process_inventory_unit! + after_transition to: :received, do: :attempt_accept + + event :receive do + transition to: :received, from: :awaiting + end + + event :cancel do + transition to: :cancelled, from: :awaiting + end + + event :give do + transition to: :given_to_customer, from: :awaiting + end + end + + def reception_completed? + COMPLETED_RECEPTION_STATUSES.include?(reception_status) + end + + state_machine :acceptance_status, initial: :pending do + event :attempt_accept do + transition to: :accepted, from: :accepted + transition to: :accepted, from: :pending, if: ->(return_item) { return_item.eligible_for_return? } + transition to: :manual_intervention_required, from: :pending, if: ->(return_item) { return_item.requires_manual_intervention? } + transition to: :rejected, from: :pending + end + + # bypasses eligibility checks + event :accept do + transition to: :accepted, from: [:accepted, :pending, :manual_intervention_required] + end + + # bypasses eligibility checks + event :reject do + transition to: :rejected, from: [:accepted, :pending, :manual_intervention_required] + end + + # bypasses eligibility checks + event :require_manual_intervention do + transition to: :manual_intervention_required, from: [:accepted, :pending, :manual_intervention_required] + end + + after_transition any => any, :do => :persist_acceptance_status_errors + end + + def self.from_inventory_unit(inventory_unit) + not_cancelled.find_by(inventory_unit: inventory_unit) || + new(inventory_unit: inventory_unit).tap(&:set_default_pre_tax_amount) + end + + def exchange_requested? + exchange_variant.present? + end + + def exchange_processed? + exchange_inventory_unit.present? + end + + def exchange_required? + exchange_requested? && !exchange_processed? + end + + def display_pre_tax_amount + Spree::Money.new(pre_tax_amount, { currency: currency }) + end + + def total + pre_tax_amount + included_tax_total + additional_tax_total + end + + def display_total + Spree::Money.new(total, { currency: currency }) + end + + def eligible_exchange_variants + exchange_variant_engine.eligible_variants(variant) + end + + def build_exchange_inventory_unit + # The inventory unit needs to have the new variant + # but it also needs to know the original line item + # for pricing information for if the inventory unit is + # ever returned. This means that the inventory unit's line_item + # will have a different variant than the inventory unit itself + super(variant: exchange_variant, line_item: inventory_unit.line_item, order: inventory_unit.order) if exchange_required? + end + + def exchange_shipment + exchange_inventory_unit.try(:shipment) + end + + def set_default_pre_tax_amount + self.pre_tax_amount = refund_amount_calculator.new.compute(self) + end + + private + + def persist_acceptance_status_errors + self.update_attributes(acceptance_status_errors: validator.errors) + end + + def stock_item + return unless customer_return + + Spree::StockItem.find_by({ + variant_id: inventory_unit.variant_id, + stock_location_id: customer_return.stock_location_id, + }) + end + + def currency + return_authorization.try(:currency) || Spree::Config[:currency] + end + + def process_inventory_unit! + inventory_unit.return! + + Spree::StockMovement.create!(stock_item_id: stock_item.id, quantity: 1) if should_restock? + end + + # This logic is also present in the customer return. The reason for the + # duplication and not having a validates_associated on the customer_return + # is that it would lead to duplicate error messages for the customer return. + # Not specifying a stock location for example would add an error message about + # the mandatory field when validating the customer return and again when saving + # the associated return items. + def belongs_to_same_customer_order + return unless customer_return && inventory_unit + + if customer_return.order_id != inventory_unit.order_id + errors.add(:base, Spree.t(:return_items_cannot_be_associated_with_multiple_orders)) + end + end + + def validator + @validator ||= return_eligibility_validator.new(self) + end + + def validate_acceptance_status_for_reimbursement + if reimbursement && !accepted? + errors.add(:reimbursement, :cannot_be_associated_unless_accepted) + end + end + + def set_exchange_pre_tax_amount + self.pre_tax_amount = 0.0.to_d if exchange_requested? + end + + def validate_no_other_completed_return_items + other_return_item = Spree::ReturnItem.where({ + inventory_unit_id: inventory_unit_id, + reception_status: COMPLETED_RECEPTION_STATUSES, + }).first + + if other_return_item + errors.add(:inventory_unit, :other_completed_return_item_exists, { + inventory_unit_id: inventory_unit_id, + return_item_id: other_return_item.id, + }) + end + end + + def cancel_others + Spree::ReturnItem.where(inventory_unit_id: inventory_unit_id) + .where.not(id: id) + .where.not(reception_status: 'cancelled') + .each do |return_item| + return_item.cancel! + end + end + + def should_restock? + variant.should_track_inventory? && stock_item && Spree::Config[:restock_inventory] + end + end +end diff --git a/core/app/models/spree/return_item/eligibility_validator/base_validator.rb b/core/app/models/spree/return_item/eligibility_validator/base_validator.rb new file mode 100644 index 00000000000..13e2e36350a --- /dev/null +++ b/core/app/models/spree/return_item/eligibility_validator/base_validator.rb @@ -0,0 +1,24 @@ +module Spree + class Spree::ReturnItem::EligibilityValidator::BaseValidator + attr_reader :errors + + def initialize(return_item) + @return_item = return_item + @errors = {} + end + + def eligible_for_return? + raise NotImplementedError, Spree.t(:implement_eligible_for_return) + end + + def requires_manual_intervention? + raise NotImplementedError, Spree.t(:implement_requires_manual_intervention) + end + + private + + def add_error(key, error) + @errors[key] = error + end + end +end diff --git a/core/app/models/spree/return_item/eligibility_validator/default.rb b/core/app/models/spree/return_item/eligibility_validator/default.rb new file mode 100644 index 00000000000..068a8900578 --- /dev/null +++ b/core/app/models/spree/return_item/eligibility_validator/default.rb @@ -0,0 +1,28 @@ +module Spree + class ReturnItem::EligibilityValidator::Default < Spree::ReturnItem::EligibilityValidator::BaseValidator + class_attribute :permitted_eligibility_validators + self.permitted_eligibility_validators = [ + ReturnItem::EligibilityValidator::OrderCompleted, + ReturnItem::EligibilityValidator::TimeSincePurchase, + ReturnItem::EligibilityValidator::RMARequired, + ] + + def eligible_for_return? + validators.all? {|v| v.eligible_for_return? } + end + + def requires_manual_intervention? + validators.any? {|v| v.requires_manual_intervention? } + end + + def errors + validators.map(&:errors).reduce({}, :merge) + end + + private + + def validators + @validators ||= permitted_eligibility_validators.map{|v| v.new(@return_item) } + end + end +end diff --git a/core/app/models/spree/return_item/eligibility_validator/order_completed.rb b/core/app/models/spree/return_item/eligibility_validator/order_completed.rb new file mode 100644 index 00000000000..97dfeb3d16c --- /dev/null +++ b/core/app/models/spree/return_item/eligibility_validator/order_completed.rb @@ -0,0 +1,16 @@ +module Spree + class ReturnItem::EligibilityValidator::OrderCompleted < Spree::ReturnItem::EligibilityValidator::BaseValidator + def eligible_for_return? + if @return_item.inventory_unit.order.completed? + return true + else + add_error(:order_not_completed, Spree.t('return_item_order_not_completed')) + return false + end + end + + def requires_manual_intervention? + false + end + end +end diff --git a/core/app/models/spree/return_item/eligibility_validator/rma_required.rb b/core/app/models/spree/return_item/eligibility_validator/rma_required.rb new file mode 100644 index 00000000000..663834c2123 --- /dev/null +++ b/core/app/models/spree/return_item/eligibility_validator/rma_required.rb @@ -0,0 +1,17 @@ +module Spree + class ReturnItem::EligibilityValidator::RMARequired < Spree::ReturnItem::EligibilityValidator::BaseValidator + def eligible_for_return? + if @return_item.return_authorization.present? + return true + else + add_error(:rma_required, Spree.t('return_item_rma_ineligible')) + return false + end + end + + def requires_manual_intervention? + false + end + end +end + diff --git a/core/app/models/spree/return_item/eligibility_validator/time_since_purchase.rb b/core/app/models/spree/return_item/eligibility_validator/time_since_purchase.rb new file mode 100644 index 00000000000..a7b52a9e005 --- /dev/null +++ b/core/app/models/spree/return_item/eligibility_validator/time_since_purchase.rb @@ -0,0 +1,16 @@ +module Spree + class ReturnItem::EligibilityValidator::TimeSincePurchase < Spree::ReturnItem::EligibilityValidator::BaseValidator + def eligible_for_return? + if (@return_item.inventory_unit.order.completed_at + Spree::Config[:return_eligibility_number_of_days].days) > Time.now + return true + else + add_error(:number_of_days, Spree.t('return_item_time_period_ineligible')) + return false + end + end + + def requires_manual_intervention? + false + end + end +end diff --git a/core/app/models/spree/return_item/exchange_variant_eligibility/same_option_value.rb b/core/app/models/spree/return_item/exchange_variant_eligibility/same_option_value.rb new file mode 100644 index 00000000000..29cecd714a8 --- /dev/null +++ b/core/app/models/spree/return_item/exchange_variant_eligibility/same_option_value.rb @@ -0,0 +1,34 @@ +module Spree + module ReturnItem::ExchangeVariantEligibility + class SameOptionValue + class_attribute :option_type_restrictions + self.option_type_restrictions = [] + # This can be configured in an initializer, e.g.: + # Spree::ReturnItem::ExchangeVariantEligibility::SameOptionValue.option_type_restrictions = ["size", "color"] + # + # This restriction causes only variants that share the same option value for the + # specified option types to be returned. e.g.: + # + # option_type_restrictions = ["color", "waist"] + # Variant: blue pants with 32 waist and 30 inseam + # + # can be exchanged for: + # blue pants with 32 waist and 31 inseam + # + # cannot be exchanged for: + # green pants with 32 waist and 30 inseam + # blue pants with 34 waist and 32 inseam + + def self.eligible_variants(variant) + product_variants = SameProduct.eligible_variants(variant).includes(option_values: :option_type) + + relevant_option_values = variant.option_values.select { |ov| option_type_restrictions.include? ov.option_type.name } + if relevant_option_values.present? + product_variants.select { |v| (relevant_option_values & v.option_values) == relevant_option_values } + else + product_variants + end + end + end + end +end diff --git a/core/app/models/spree/return_item/exchange_variant_eligibility/same_product.rb b/core/app/models/spree/return_item/exchange_variant_eligibility/same_product.rb new file mode 100644 index 00000000000..d3b8555f294 --- /dev/null +++ b/core/app/models/spree/return_item/exchange_variant_eligibility/same_product.rb @@ -0,0 +1,9 @@ +module Spree + module ReturnItem::ExchangeVariantEligibility + class SameProduct + def self.eligible_variants(variant) + Spree::Variant.where(product_id: variant.product_id, is_master: variant.is_master?).in_stock + end + end + end +end diff --git a/core/app/models/spree/returns_calculator.rb b/core/app/models/spree/returns_calculator.rb new file mode 100644 index 00000000000..a24920e3578 --- /dev/null +++ b/core/app/models/spree/returns_calculator.rb @@ -0,0 +1,8 @@ +module Spree + class ReturnsCalculator < Calculator + + def compute(return_item) + raise NotImplementedError, "Please implement 'compute(return_item)' in your calculator: #{self.class.name}" + end + end +end diff --git a/core/app/models/spree/role.rb b/core/app/models/spree/role.rb index 9343d527326..3f1c9b9a9aa 100644 --- a/core/app/models/spree/role.rb +++ b/core/app/models/spree/role.rb @@ -1,7 +1,5 @@ module Spree - class Role < ActiveRecord::Base - attr_accessible :name - - has_and_belongs_to_many :users, :join_table => 'spree_roles_users', :class_name => Spree.user_class.to_s + class Role < Spree::Base + has_and_belongs_to_many :users, join_table: 'spree_roles_users', class_name: Spree.user_class.to_s end end diff --git a/core/app/models/spree/shipment.rb b/core/app/models/spree/shipment.rb index 361b9996fd9..a546170e4a9 100644 --- a/core/app/models/spree/shipment.rb +++ b/core/app/models/spree/shipment.rb @@ -1,96 +1,338 @@ require 'ostruct' module Spree - class Shipment < ActiveRecord::Base - belongs_to :order - belongs_to :shipping_method - belongs_to :address + class Shipment < Spree::Base + belongs_to :address, class_name: 'Spree::Address', inverse_of: :shipments + belongs_to :order, class_name: 'Spree::Order', touch: true, inverse_of: :shipments + belongs_to :stock_location, class_name: 'Spree::StockLocation' - has_many :state_changes, :as => :stateful - has_many :inventory_units, :dependent => :nullify - has_one :adjustment, :as => :source, :dependent => :destroy + has_many :adjustments, as: :adjustable, dependent: :delete_all + has_many :inventory_units, dependent: :delete_all, inverse_of: :shipment + has_many :shipping_rates, -> { order('cost ASC') }, dependent: :delete_all + has_many :shipping_methods, through: :shipping_rates + has_many :state_changes, as: :stateful - before_create :generate_shipment_number - after_save :ensure_correct_adjustment, :update_order + after_save :update_adjustments - attr_accessor :special_instructions + before_validation :set_cost_zero_when_nil + + validates :stock_location, presence: true - attr_accessible :order, :shipping_method, :special_instructions, - :shipping_method_id, :tracking, :address, :inventory_units + attr_accessor :special_instructions accepts_nested_attributes_for :address accepts_nested_attributes_for :inventory_units - validates :inventory_units, :presence => true, :if => :require_inventory - validates :shipping_method, :presence => true + make_permalink field: :number, length: 11, prefix: 'H' - make_permalink :field => :number + scope :pending, -> { with_state('pending') } + scope :ready, -> { with_state('ready') } + scope :shipped, -> { with_state('shipped') } + scope :trackable, -> { where("tracking IS NOT NULL AND tracking != ''") } + scope :with_state, ->(*s) { where(state: s) } + # sort by most recent shipped_at, falling back to created_at. add "id desc" to make specs that involve this scope more deterministic. + scope :reverse_chronological, -> { order('coalesce(spree_shipments.shipped_at, spree_shipments.created_at) desc', id: :desc) } - scope :with_state, lambda { |s| where(:state => s) } - scope :shipped, with_state('shipped') - scope :ready, with_state('ready') - scope :pending, with_state('pending') + # shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details) + state_machine initial: :pending, use_transactions: false do + event :ready do + transition from: :pending, to: :ready, if: lambda { |shipment| + # Fix for #2040 + shipment.determine_state(shipment.order) == 'ready' + } + end - def to_param - number if number - generate_shipment_number unless number - number.to_s.to_url.upcase + event :pend do + transition from: :ready, to: :pending + end + + event :ship do + transition from: [:ready, :canceled], to: :shipped + end + after_transition to: :shipped, do: :after_ship + + event :cancel do + transition to: :canceled, from: [:pending, :ready] + end + after_transition to: :canceled, do: :after_cancel + + event :resume do + transition from: :canceled, to: :ready, if: lambda { |shipment| + shipment.determine_state(shipment.order) == :ready + } + transition from: :canceled, to: :pending, if: lambda { |shipment| + shipment.determine_state(shipment.order) == :ready + } + transition from: :canceled, to: :pending + end + after_transition from: :canceled, to: [:pending, :ready, :shipped], do: :after_resume + + after_transition do |shipment, transition| + shipment.state_changes.create!( + previous_state: transition.from, + next_state: transition.to, + name: 'shipment', + ) + end end - def shipped=(value) - return unless value == '1' && shipped_at.nil? - self.shipped_at = Time.now + self.whitelisted_ransackable_attributes = ['number'] + + def add_shipping_method(shipping_method, selected = false) + shipping_rates.create(shipping_method: shipping_method, selected: selected, cost: cost) + end + + def after_cancel + manifest.each { |item| manifest_restock(item) } + end + + def after_resume + manifest.each { |item| manifest_unstock(item) } + end + + def backordered? + inventory_units.any? { |inventory_unit| inventory_unit.backordered? } end def currency - order.nil? ? Spree::Config[:currency] : order.currency + order ? order.currency : Spree::Config[:currency] + end + + # Determines the appropriate +state+ according to the following logic: + # + # pending unless order is complete and +order.payment_state+ is +paid+ + # shipped if already shipped (ie. does not change the state) + # ready all other cases + def determine_state(order) + return 'canceled' if order.canceled? + return 'pending' unless order.can_ship? + return 'pending' if inventory_units.any? &:backordered? + return 'shipped' if state == 'shipped' + order.paid? || Spree::Config[:auto_capture_on_dispatch] ? 'ready' : 'pending' end - # The adjustment amount associated with this shipment (if any.) Returns only the first adjustment to match - # the shipment but there should never really be more than one. - def cost - adjustment ? adjustment.amount : 0 + def discounted_cost + cost + promo_total end - alias_method :amount, :cost + alias discounted_amount discounted_cost def display_cost - Spree::Money.new(cost, { :currency => currency }) + Spree::Money.new(cost, { currency: currency }) end - alias_method :display_amount, :display_cost + alias display_amount display_cost - # shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details) - state_machine :initial => 'pending', :use_transactions => false do - event :ready do - transition :from => 'pending', :to => 'ready', :if => lambda { |shipment| - # Fix for #2040 - shipment.determine_state(shipment.order) == 'ready' - } - end - event :pend do - transition :from => 'ready', :to => 'pending' - end - event :ship do - transition :from => 'ready', :to => 'shipped' - end + def display_discounted_cost + Spree::Money.new(discounted_cost, { currency: currency }) + end + + def display_final_price + Spree::Money.new(final_price, { currency: currency }) + end - after_transition :to => 'shipped', :do => :after_ship + def display_item_cost + Spree::Money.new(item_cost, { currency: currency }) end def editable_by?(user) !shipped? end + def final_price + cost + adjustment_total + end + + def final_price_with_items + item_cost + final_price + end + + def finalize! + InventoryUnit.finalize_units!(inventory_units) + manifest.each { |item| manifest_unstock(item) } + end + + def include?(variant) + inventory_units_for(variant).present? + end + + def inventory_units_for(variant) + inventory_units.where(variant_id: variant.id) + end + + def inventory_units_for_item(line_item, variant = nil) + inventory_units.where(line_item_id: line_item.id, variant_id: line_item.variant.id || variant.id) + end + + def item_cost + manifest.map { |m| (m.line_item.price + (m.line_item.adjustment_total / m.line_item.quantity)) * m.quantity }.sum + end + + def line_items + inventory_units.includes(:line_item).map(&:line_item).uniq + end + + ManifestItem = Struct.new(:line_item, :variant, :quantity, :states) + def manifest - inventory_units.group_by(&:variant).map do |i| - OpenStruct.new(:variant => i.first, :quantity => i.last.length) + # Grouping by the ID means that we don't have to call out to the association accessor + # This makes the grouping by faster because it results in less SQL cache hits. + inventory_units.group_by(&:variant_id).map do |variant_id, units| + units.group_by(&:line_item_id).map do |line_item_id, units| + + states = {} + units.group_by(&:state).each { |state, iu| states[state] = iu.count } + + line_item = units.first.line_item + variant = units.first.variant + ManifestItem.new(line_item, variant, units.length, states) + end + end.flatten + end + + def process_order_payments + pending_payments = order.pending_payments + .sort_by(&:uncaptured_amount).reverse + + shipment_to_pay = final_price_with_items + payments_amount = 0 + + payments_pool = pending_payments.each_with_object([]) do |payment, pool| + break if payments_amount >= shipment_to_pay + payments_amount += payment.uncaptured_amount + pool << payment + end + + payments_pool.each do |payment| + capturable_amount = if payment.amount >= shipment_to_pay + shipment_to_pay + else + payment.amount + end + + cents = (capturable_amount * 100).to_i + payment.capture!(cents) + shipment_to_pay -= capturable_amount end end - def line_items - if order.complete? and Spree::Config[:track_inventory_levels] - order.line_items.select { |li| inventory_units.map(&:variant_id).include?(li.variant_id) } - else - order.line_items + def ready_or_pending? + self.ready? || self.pending? + end + + def refresh_rates + return shipping_rates if shipped? + return [] unless can_get_rates? + + # StockEstimator.new assigment below will replace the current shipping_method + original_shipping_method_id = shipping_method.try(:id) + + self.shipping_rates = Stock::Estimator.new(order).shipping_rates(to_package) + + if shipping_method + selected_rate = shipping_rates.detect { |rate| + rate.shipping_method_id == original_shipping_method_id + } + self.selected_shipping_rate_id = selected_rate.id if selected_rate + end + + shipping_rates + end + + def selected_shipping_rate + shipping_rates.where(selected: true).first + end + + def selected_shipping_rate_id + selected_shipping_rate.try(:id) + end + + def selected_shipping_rate_id=(id) + shipping_rates.update_all(selected: false) + shipping_rates.update(id, selected: true) + self.save! + end + + def set_up_inventory(state, variant, order, line_item) + self.inventory_units.create( + state: state, + variant_id: variant.id, + order_id: order.id, + line_item_id: line_item.id + ) + end + + def shipped=(value) + return unless value == '1' && shipped_at.nil? + self.shipped_at = Time.now + end + + def shipping_method + selected_shipping_rate.try(:shipping_method) || shipping_rates.first.try(:shipping_method) + end + + def tax_category + selected_shipping_rate.try(:tax_rate).try(:tax_category) + end + + # Only one of either included_tax_total or additional_tax_total is set + # This method returns the total of the two. Saves having to check if + # tax is included or additional. + def tax_total + included_tax_total + additional_tax_total + end + + def to_package + package = Stock::Package.new(stock_location) + inventory_units.includes(:variant).joins(:variant).group_by(&:state).each do |state, state_inventory_units| + package.add_multiple state_inventory_units, state.to_sym + end + package + end + + def to_param + number + end + + def tracking_url + @tracking_url ||= shipping_method.build_tracking_url(tracking) + end + + def update_amounts + if selected_shipping_rate + self.update_columns( + cost: selected_shipping_rate.cost, + adjustment_total: adjustments.additional.map(&:update!).compact.sum, + updated_at: Time.now, + ) + end + end + + # Update Shipment and make sure Order states follow the shipment changes + def update_attributes_and_order(params = {}) + if self.update_attributes params + if params.has_key? :selected_shipping_rate_id + # Changing the selected Shipping Rate won't update the cost (for now) + # so we persist the Shipment#cost before calculating order shipment + # total and updating payment state (given a change in shipment cost + # might change the Order#payment_state) + self.update_amounts + + order.updater.update_shipment_total + order.updater.update_payment_state + + # Update shipment state only after order total is updated because it + # (via Order#paid?) affects the shipment state (YAY) + self.update_columns( + state: determine_state(order), + updated_at: Time.now + ) + + # And then it's time to update shipment states and finally persist + # order changes + order.updater.update_shipment_state + order.updater.persist_totals + end + + true end end @@ -100,73 +342,90 @@ def line_items def update!(order) old_state = state new_state = determine_state(order) - update_column 'state', new_state + update_columns( + state: new_state, + updated_at: Time.now, + ) after_ship if new_state == 'shipped' and old_state != 'shipped' end - # Determines the appropriate +state+ according to the following logic: - # - # pending unless order is complete and +order.payment_state+ is +paid+ - # shipped if already shipped (ie. does not change the state) - # ready all other cases - def determine_state(order) - return 'pending' unless order.complete? - return 'pending' if inventory_units.any? &:backordered? - return 'shipped' if state == 'shipped' - order.paid? ? 'ready' : 'pending' + def transfer_to_location(variant, quantity, stock_location) + if quantity <= 0 + raise ArgumentError + end + + transaction do + new_shipment = order.shipments.create!(stock_location: stock_location) + + order.contents.remove(variant, quantity, {shipment: self}) + order.contents.add(variant, quantity, {shipment: new_shipment}) + + refresh_rates + save! + new_shipment.save! + end + end + + def transfer_to_shipment(variant, quantity, shipment_to_transfer_to) + quantity_already_shipment_to_transfer_to = shipment_to_transfer_to.manifest.find{|mi| mi.line_item.variant == variant}.try(:quantity) || 0 + final_quantity = quantity + quantity_already_shipment_to_transfer_to + + if (quantity <= 0 || self == shipment_to_transfer_to) + raise ArgumentError + end + + transaction do + order.contents.remove(variant, quantity, {shipment: self}) + order.contents.add(variant, quantity, {shipment: shipment_to_transfer_to}) + + refresh_rates + save! + shipment_to_transfer_to.refresh_rates + shipment_to_transfer_to.save! + end end private - def generate_shipment_number - return number unless number.blank? - record = true - while record - random = "H#{Array.new(11){rand(9)}.join}" - record = self.class.where(:number => random).first - end - self.number = random + + def after_ship + ShipmentHandler.factory(self).perform end - def description_for_shipping_charge - "#{I18n.t(:shipping)} (#{shipping_method.name})" + def can_get_rates? + order.ship_address && order.ship_address.valid? end - def validate_shipping_method - unless shipping_method.nil? - errors.add :shipping_method, I18n.t(:is_not_available_to_shipment_address) unless shipping_method.zone.include?(address) + def manifest_restock(item) + if item.states["on_hand"].to_i > 0 + stock_location.restock item.variant, item.states["on_hand"], self + end + + if item.states["backordered"].to_i > 0 + stock_location.restock_backordered item.variant, item.states["backordered"] end end - # Determines whether or not inventory units should be associated with the shipment. This is always +false+ when - # +Spree::Config[:track_inventory_levels]+ is set to +false.+ Otherwise its +true+ whenever the order is completed - # (and not canceled.) - def require_inventory - return false unless Spree::Config[:track_inventory_levels] - order.completed? && !order.canceled? + def manifest_unstock(item) + stock_location.unstock item.variant, item.quantity, self end - def after_ship - inventory_units.each &:ship! - send_shipped_email - touch :shipped_at + def recalculate_adjustments + Spree::ItemAdjustments.new(self).update end def send_shipped_email - ShipmentMailer.shipped_email(self).deliver + ShipmentMailer.shipped_email(self.id).deliver end - def ensure_correct_adjustment - if adjustment - adjustment.originator = shipping_method - adjustment.save - else - shipping_method.create_adjustment(I18n.t(:shipping), order, self, true) - reload #ensure adjustment is present on later saves - end + def set_cost_zero_when_nil + self.cost = 0 unless self.cost end - def update_order - order.update! + def update_adjustments + if cost_changed? && state != 'shipped' + recalculate_adjustments + end end + end end diff --git a/core/app/models/spree/shipment_handler.rb b/core/app/models/spree/shipment_handler.rb new file mode 100644 index 00000000000..dfbef918f71 --- /dev/null +++ b/core/app/models/spree/shipment_handler.rb @@ -0,0 +1,43 @@ +module Spree + class ShipmentHandler + class << self + def factory(shipment) + # Do we have a specialized shipping-method-specific handler? e.g: + # Given shipment.shipping_method = Spree::ShippingMethod::DigitalDownload + # do we have Spree::ShipmentHandler::DigitalDownload? + if sm_handler = "Spree::ShipmentHandler::#{shipment.shipping_method.name.split('::').last}".constantize rescue false + sm_handler.new(shipment) + else + new(shipment) + end + end + end + + def initialize(shipment) + @shipment = shipment + end + + def perform + @shipment.inventory_units.each &:ship! + @shipment.process_order_payments if Spree::Config[:auto_capture_on_dispatch] + send_shipped_email + @shipment.touch :shipped_at + update_order_shipment_state + end + + private + def send_shipped_email + ShipmentMailer.shipped_email(@shipment.id).deliver + end + + def update_order_shipment_state + order = @shipment.order + + new_state = OrderUpdater.new(order).update_shipment_state + order.update_columns( + shipment_state: new_state, + updated_at: Time.now, + ) + end + end +end diff --git a/core/app/models/spree/shipping_calculator.rb b/core/app/models/spree/shipping_calculator.rb new file mode 100644 index 00000000000..44f5191d153 --- /dev/null +++ b/core/app/models/spree/shipping_calculator.rb @@ -0,0 +1,22 @@ +module Spree + class ShippingCalculator < Calculator + + def compute_shipment(shipment) + raise NotImplementedError, "Please implement 'compute_shipment(shipment)' in your calculator: #{self.class.name}" + end + + def compute_package(package) + raise NotImplementedError, "Please implement 'compute_package(package)' in your calculator: #{self.class.name}" + end + + def available?(package) + true + end + + private + def total(content_items) + content_items.map(&:amount).sum + end + end +end + diff --git a/core/app/models/spree/shipping_category.rb b/core/app/models/spree/shipping_category.rb index 2f0d347fd97..62296d4d933 100644 --- a/core/app/models/spree/shipping_category.rb +++ b/core/app/models/spree/shipping_category.rb @@ -1,9 +1,8 @@ module Spree - class ShippingCategory < ActiveRecord::Base - validates :name, :presence => true - has_many :products - has_many :shipping_methods - - attr_accessible :name + class ShippingCategory < Spree::Base + validates :name, presence: true + has_many :products, inverse_of: :shipping_category + has_many :shipping_method_categories, inverse_of: :shipping_category + has_many :shipping_methods, through: :shipping_method_categories end end diff --git a/core/app/models/spree/shipping_method.rb b/core/app/models/spree/shipping_method.rb index e8da2ffca4f..50149ab7420 100644 --- a/core/app/models/spree/shipping_method.rb +++ b/core/app/models/spree/shipping_method.rb @@ -1,63 +1,68 @@ module Spree - class ShippingMethod < ActiveRecord::Base + class ShippingMethod < Spree::Base + acts_as_paranoid + include Spree::CalculatedAdjustments DISPLAY = [:both, :front_end, :back_end] - default_scope where(:deleted_at => nil) + default_scope -> { where(deleted_at: nil) } - has_many :shipments - validates :name, :zone, :presence => true + has_many :shipping_method_categories, :dependent => :destroy + has_many :shipping_categories, through: :shipping_method_categories + has_many :shipping_rates, inverse_of: :shipping_method + has_many :shipments, :through => :shipping_rates - belongs_to :shipping_category - belongs_to :zone + has_and_belongs_to_many :zones, :join_table => 'spree_shipping_methods_zones', + :class_name => 'Spree::Zone', + :foreign_key => 'shipping_method_id' - attr_accessible :name, :zone_id, :display_on, :shipping_category_id, - :match_none, :match_one, :match_all + belongs_to :tax_category, :class_name => 'Spree::TaxCategory' - calculated_adjustments + validates :name, presence: true - def available?(order, display_on = nil) - displayable?(display_on) && calculator.available?(order) - end + validate :at_least_one_shipping_category - def displayable?(display_on) - (self.display_on == display_on.to_s || self.display_on.blank?) + def include?(address) + return false unless address + zones.any? do |zone| + zone.include?(address) + end end - def within_zone?(order) - zone && zone.include?(order.ship_address) + def build_tracking_url(tracking) + return if tracking.blank? || tracking_url.blank? + tracking_url.gsub(/:tracking/, ERB::Util.url_encode(tracking)) # :url_encode exists in 1.8.7 through 2.1.0 end - def available_to_order?(order, display_on= nil) - available?(order, display_on) && - within_zone?(order) && - category_match?(order) && - currency_match?(order) + def self.calculators + spree_calculators.send(model_name_without_spree_namespace).select{ |c| c < Spree::ShippingCalculator } end - # Indicates whether or not the category rules for this shipping method - # are satisfied (if applicable) - def category_match?(order) - return true if shipping_category.nil? - - if match_all - order.products.all? { |p| p.shipping_category == shipping_category } - elsif match_one - order.products.any? { |p| p.shipping_category == shipping_category } - elsif match_none - order.products.all? { |p| p.shipping_category != shipping_category } - end + # Some shipping methods are only meant to be set via backend + def frontend? + self.display_on != "back_end" end - def currency_match?(order) - calculator_currency.nil? || calculator_currency == order.currency + def tax_category + Spree::TaxCategory.unscoped { super } end - def calculator_currency - calculator.preferences[:currency] - end + private + def compute_amount(calculable) + self.calculator.compute(calculable) + end - def self.all_available(order, display_on = nil) - all.select { |method| method.available_to_order?(order,display_on) } - end + def at_least_one_shipping_category + if self.shipping_categories.empty? + self.errors[:base] << "You need to select at least one shipping category" + end + end + + def self.on_backend_query + "#{table_name}.display_on != 'front_end' OR #{table_name}.display_on IS NULL" + end + + def self.on_frontend_query + "#{table_name}.display_on != 'back_end' OR #{table_name}.display_on IS NULL" + end end end diff --git a/core/app/models/spree/shipping_method_category.rb b/core/app/models/spree/shipping_method_category.rb new file mode 100644 index 00000000000..6774b71b0d4 --- /dev/null +++ b/core/app/models/spree/shipping_method_category.rb @@ -0,0 +1,6 @@ +module Spree + class ShippingMethodCategory < Spree::Base + belongs_to :shipping_method, class_name: 'Spree::ShippingMethod' + belongs_to :shipping_category, class_name: 'Spree::ShippingCategory', inverse_of: :shipping_method_categories + end +end diff --git a/core/app/models/spree/shipping_rate.rb b/core/app/models/spree/shipping_rate.rb index 037b9e0adba..1b448e36958 100644 --- a/core/app/models/spree/shipping_rate.rb +++ b/core/app/models/spree/shipping_rate.rb @@ -1,19 +1,57 @@ module Spree - class ShippingRate < Struct.new(:id, :shipping_method, :name, :cost, :currency) - def initialize(attributes = {}) - attributes.each do |k, v| - self.send("#{k}=", v) - end + class ShippingRate < Spree::Base + belongs_to :shipment, class_name: 'Spree::Shipment' + belongs_to :shipping_method, class_name: 'Spree::ShippingMethod', inverse_of: :shipping_rates + belongs_to :tax_rate, class_name: 'Spree::TaxRate' + + delegate :order, :currency, to: :shipment + delegate :name, to: :shipping_method + + def display_base_price + Spree::Money.new(cost, currency: currency) + end + + def calculate_tax_amount + tax_rate.calculator.compute_shipping_rate(self) end def display_price - if Spree::Config[:shipment_inc_vat] - price = (1 + Spree::TaxRate.default) * cost - else - price = cost + price = display_base_price.to_s + if tax_rate + tax_amount = calculate_tax_amount + if tax_amount != 0 + if tax_rate.included_in_price? + if tax_amount > 0 + amount = "#{display_tax_amount(tax_amount)} #{tax_rate.name}" + price += " (#{Spree.t(:incl)} #{amount})" + else + amount = "#{display_tax_amount(tax_amount*-1)} #{tax_rate.name}" + price += " (#{Spree.t(:excl)} #{amount})" + end + else + amount = "#{display_tax_amount(tax_amount)} #{tax_rate.name}" + price += " (+ #{amount})" + end + end end + price + end + alias_method :display_cost, :display_price + + def display_tax_amount(tax_amount) + Spree::Money.new(tax_amount, currency: currency) + end + + def shipping_method + Spree::ShippingMethod.unscoped { super } + end + + def shipping_method_code + shipping_method.code + end - Spree::Money.new(price, { :currency => currency }) + def tax_rate + Spree::TaxRate.unscoped { super } end end end diff --git a/core/app/models/spree/state.rb b/core/app/models/spree/state.rb index 1d6e4393720..449a8bccc39 100644 --- a/core/app/models/spree/state.rb +++ b/core/app/models/spree/state.rb @@ -1,10 +1,11 @@ module Spree - class State < ActiveRecord::Base - belongs_to :country + class State < Spree::Base + belongs_to :country, class_name: 'Spree::Country' + has_many :addresses, dependent: :nullify - validates :country, :name, :presence => true + has_many :zone_members, as: :zoneable, dependent: :destroy - attr_accessible :name, :abbr + validates :country, :name, presence: true def self.find_all_by_name_or_abbr(name_or_abbr) where('name = ? OR abbr = ?', name_or_abbr, name_or_abbr) diff --git a/core/app/models/spree/state_change.rb b/core/app/models/spree/state_change.rb index 92f8054b83a..b6d3c77a0a6 100644 --- a/core/app/models/spree/state_change.rb +++ b/core/app/models/spree/state_change.rb @@ -1,7 +1,7 @@ module Spree - class StateChange < ActiveRecord::Base + class StateChange < Spree::Base belongs_to :user - belongs_to :stateful, :polymorphic => true + belongs_to :stateful, polymorphic: true before_create :assign_user def <=>(other) diff --git a/core/app/models/spree/stock/adjuster.rb b/core/app/models/spree/stock/adjuster.rb new file mode 100644 index 00000000000..a0d56e35b3a --- /dev/null +++ b/core/app/models/spree/stock/adjuster.rb @@ -0,0 +1,27 @@ +# Used by Prioritizer to adjust item quantities +# see prioritizer_spec for use cases +module Spree + module Stock + class Adjuster + attr_accessor :inventory_unit, :status, :fulfilled + + def initialize(inventory_unit, status) + @inventory_unit = inventory_unit + @status = status + @fulfilled = false + end + + def adjust(package) + if fulfilled? + package.remove(inventory_unit) + else + self.fulfilled = true + end + end + + def fulfilled? + fulfilled + end + end + end +end diff --git a/core/app/models/spree/stock/availability_validator.rb b/core/app/models/spree/stock/availability_validator.rb new file mode 100644 index 00000000000..31f86caf9c7 --- /dev/null +++ b/core/app/models/spree/stock/availability_validator.rb @@ -0,0 +1,26 @@ +module Spree + module Stock + class AvailabilityValidator < ActiveModel::Validator + def validate(line_item) + unit_count = line_item.inventory_units.size + return if unit_count >= line_item.quantity + quantity = line_item.quantity - unit_count + return if quantity.zero? + + quantifier = Stock::Quantifier.new(line_item.variant) + + return if quantifier.can_supply?(quantity) + + variant = line_item.variant + display_name = "#{variant.name}" + display_name += " (#{variant.options_text})" unless variant.options_text.blank? + + line_item.errors[:quantity] << Spree.t( + :selected_quantity_not_available, + scope: :order_populator, + item: display_name.inspect + ) + end + end + end +end diff --git a/core/app/models/spree/stock/content_item.rb b/core/app/models/spree/stock/content_item.rb new file mode 100644 index 00000000000..fecc94f118e --- /dev/null +++ b/core/app/models/spree/stock/content_item.rb @@ -0,0 +1,48 @@ +module Spree + module Stock + class ContentItem + attr_accessor :inventory_unit, :state + + def initialize(inventory_unit, state = :on_hand) + @inventory_unit = inventory_unit + @state = state + end + + def variant + inventory_unit.variant + end + + def weight + variant.weight * quantity + end + + def line_item + inventory_unit.line_item + end + + def on_hand? + state.to_s == "on_hand" + end + + def backordered? + state.to_s == "backordered" + end + + def price + variant.price + end + + def amount + price * quantity + end + + def quantity + # Since inventory units don't have a quantity, + # make this always 1 for now, leaving ourselves + # open to a different possibility in the future, + # but this massively simplifies things for now + 1 + end + end + end +end diff --git a/core/app/models/spree/stock/coordinator.rb b/core/app/models/spree/stock/coordinator.rb new file mode 100644 index 00000000000..f52c5a0e099 --- /dev/null +++ b/core/app/models/spree/stock/coordinator.rb @@ -0,0 +1,66 @@ +module Spree + module Stock + class Coordinator + attr_reader :order, :inventory_units + + def initialize(order, inventory_units = nil) + @order = order + @inventory_units = inventory_units || InventoryUnitBuilder.new(order).units + end + + def shipments + packages.map do |package| + package.to_shipment.tap { |s| s.address = order.ship_address } + end + end + + def packages + packages = build_packages + packages = prioritize_packages(packages) + packages = estimate_packages(packages) + end + + # Build packages as per stock location + # + # It needs to check whether each stock location holds at least one stock + # item for the order. In case none is found it wouldn't make any sense + # to build a package because it would be empty. Plus we avoid errors down + # the stack because it would assume the stock location has stock items + # for the given order + # + # Returns an array of Package instances + def build_packages(packages = Array.new) + StockLocation.active.each do |stock_location| + next unless stock_location.stock_items.where(:variant_id => inventory_units.map(&:variant_id)).exists? + + packer = build_packer(stock_location, inventory_units) + packages += packer.packages + end + packages + end + + private + def prioritize_packages(packages) + prioritizer = Prioritizer.new(inventory_units, packages) + prioritizer.prioritized_packages + end + + def estimate_packages(packages) + estimator = Estimator.new(order) + packages.each do |package| + package.shipping_rates = estimator.shipping_rates(package) + end + packages + end + + def build_packer(stock_location, inventory_units) + Packer.new(stock_location, inventory_units, splitters(stock_location)) + end + + def splitters(stock_location) + # extension point to return custom splitters for a location + Rails.application.config.spree.stock_splitters + end + end + end +end diff --git a/core/app/models/spree/stock/differentiator.rb b/core/app/models/spree/stock/differentiator.rb new file mode 100644 index 00000000000..28cd4e6648e --- /dev/null +++ b/core/app/models/spree/stock/differentiator.rb @@ -0,0 +1,44 @@ +module Spree + module Stock + class Differentiator + attr_reader :missing, :packed, :required, :packages, :order + + def initialize(order, packages) + @order = order + @packages = packages + build_packed + build_required + build_missing + end + + def missing? + missing.values.any? { |v| v > 0 } + end + + private + def build_missing + @missing = Hash.new(0) + required.keys.each do |variant| + missing = required[variant] - packed[variant] + @missing[variant] = missing if missing > 0 + end + end + + def build_packed + @packed = Hash.new(0) + packages.each do |package| + package.contents.each do |content_item| + @packed[content_item.variant] += content_item.quantity + end + end + end + + def build_required + @required = Hash.new(0) + order.line_items.each do |line_item| + @required[line_item.variant] = line_item.quantity + end + end + end + end +end diff --git a/core/app/models/spree/stock/estimator.rb b/core/app/models/spree/stock/estimator.rb new file mode 100644 index 00000000000..a1f7f66f2cf --- /dev/null +++ b/core/app/models/spree/stock/estimator.rb @@ -0,0 +1,72 @@ +module Spree + module Stock + class Estimator + attr_reader :order, :currency + + def initialize(order) + @order = order + @currency = order.currency + end + + def shipping_rates(package, frontend_only = true) + rates = calculate_shipping_rates(package) + rates.select! { |rate| rate.shipping_method.frontend? } if frontend_only + choose_default_shipping_rate(rates) + sort_shipping_rates(rates) + end + + private + def choose_default_shipping_rate(shipping_rates) + unless shipping_rates.empty? + shipping_rates.min_by(&:cost).selected = true + end + end + + def sort_shipping_rates(shipping_rates) + shipping_rates.sort_by!(&:cost) + end + + def calculate_shipping_rates(package) + shipping_methods(package).map do |shipping_method| + cost = shipping_method.calculator.compute(package) + tax_category = shipping_method.tax_category + if tax_category + tax_rate = tax_category.tax_rates.detect do |rate| + # If the rate's zone matches the order's zone, a positive adjustment will be applied. + # If the rate is from the default tax zone, then a negative adjustment will be applied. + # See the tests in shipping_rate_spec.rb for an example of this.d + rate.zone == order.tax_zone || rate.zone.default_tax? + end + end + + if cost + rate = shipping_method.shipping_rates.new(cost: cost) + rate.tax_rate = tax_rate if tax_rate + end + + rate + end.compact + end + + def shipping_methods(package) + package.shipping_methods.select do |ship_method| + calculator = ship_method.calculator + begin + ship_method.include?(order.ship_address) && + calculator.available?(package) && + (calculator.preferences[:currency].blank? || + calculator.preferences[:currency] == currency) + rescue Exception => exception + log_calculator_exception(ship_method, exception) + end + end + end + + def log_calculator_exception(ship_method, exception) + Rails.logger.info("Something went wrong calculating rates with the #{ship_method.name} (ID=#{ship_method.id}) shipping method.") + Rails.logger.info("*" * 50) + Rails.logger.info(exception.backtrace.join("\n")) + end + end + end +end diff --git a/core/app/models/spree/stock/inventory_unit_builder.rb b/core/app/models/spree/stock/inventory_unit_builder.rb new file mode 100644 index 00000000000..a33ed3f4600 --- /dev/null +++ b/core/app/models/spree/stock/inventory_unit_builder.rb @@ -0,0 +1,30 @@ +module Spree + module Stock + class InventoryUnitBuilder + def initialize(order) + @order = order + end + + def units + @order.line_items.flat_map do |line_item| + line_item.quantity.times.map do |i| + @order.inventory_units.includes( + variant: { + product: { + shipping_category: { + shipping_methods: [:calculator, { zones: :zone_members }] + } + } + } + ).build( + pending: true, + variant: line_item.variant, + line_item: line_item, + order: @order + ) + end + end + end + end + end +end diff --git a/core/app/models/spree/stock/package.rb b/core/app/models/spree/stock/package.rb new file mode 100644 index 00000000000..0ad976c1728 --- /dev/null +++ b/core/app/models/spree/stock/package.rb @@ -0,0 +1,92 @@ +module Spree + module Stock + class Package + attr_reader :stock_location, :contents + attr_accessor :shipping_rates + + def initialize(stock_location, contents=[]) + @stock_location = stock_location + @contents = contents + @shipping_rates = Array.new + end + + def add(inventory_unit, state = :on_hand) + contents << ContentItem.new(inventory_unit, state) unless find_item(inventory_unit) + end + + def add_multiple(inventory_units, state = :on_hand) + inventory_units.each { |inventory_unit| add(inventory_unit, state) } + end + + def remove(inventory_unit) + item = find_item(inventory_unit) + @contents -= [item] if item + end + + # Fix regression that removed package.order. + # Find it dynamically through an inventory_unit. + def order + contents.detect {|item| !!item.try(:inventory_unit).try(:order) }.try(:inventory_unit).try(:order) + end + + def weight + contents.sum(&:weight) + end + + def on_hand + contents.select(&:on_hand?) + end + + def backordered + contents.select(&:backordered?) + end + + def find_item(inventory_unit, state = nil) + contents.detect do |item| + item.inventory_unit == inventory_unit && + (!state || item.state.to_s == state.to_s) + end + end + + def quantity(state = nil) + matched_contents = state.nil? ? contents : contents.select { |c| c.state.to_s == state.to_s } + matched_contents.map(&:quantity).sum + end + + def empty? + quantity == 0 + end + + def currency + order.currency + end + + def shipping_categories + contents.map { |item| item.variant.shipping_category }.compact.uniq + end + + def shipping_methods + shipping_categories.map(&:shipping_methods).reduce(:&).to_a + end + + def inspect + contents.map do |content_item| + "#{content_item.variant.name} #{content_item.state}" + end.join(' / ') + end + + def to_shipment + # At this point we should only have one content item per inventory unit + # across the entire set of inventory units to be shipped, which has been + # taken care of by the Prioritizer + contents.each { |content_item| content_item.inventory_unit.state = content_item.state.to_s } + + Spree::Shipment.new( + stock_location: stock_location, + shipping_rates: shipping_rates, + inventory_units: contents.map(&:inventory_unit) + ) + end + end + end +end diff --git a/core/app/models/spree/stock/packer.rb b/core/app/models/spree/stock/packer.rb new file mode 100644 index 00000000000..14159c4df9c --- /dev/null +++ b/core/app/models/spree/stock/packer.rb @@ -0,0 +1,48 @@ +module Spree + module Stock + class Packer + attr_reader :stock_location, :inventory_units, :splitters + + def initialize(stock_location, inventory_units, splitters=[Splitter::Base]) + @stock_location = stock_location + @inventory_units = inventory_units + @splitters = splitters + end + + def packages + if splitters.empty? + [default_package] + else + build_splitter.split [default_package] + end + end + + def default_package + package = Package.new(stock_location) + inventory_units.group_by(&:variant).each do |variant, variant_inventory_units| + units = variant_inventory_units.clone + if variant.should_track_inventory? + next unless stock_location.stock_item(variant) + + on_hand, backordered = stock_location.fill_status(variant, units.count) + package.add_multiple units.slice!(0, on_hand), :on_hand if on_hand > 0 + package.add_multiple units.slice!(0, backordered), :backordered if backordered > 0 + else + package.add_multiple units + end + + end + package + end + + private + def build_splitter + splitter = nil + splitters.reverse.each do |klass| + splitter = klass.new(self, splitter) + end + splitter + end + end + end +end diff --git a/core/app/models/spree/stock/prioritizer.rb b/core/app/models/spree/stock/prioritizer.rb new file mode 100644 index 00000000000..6cc900d55d6 --- /dev/null +++ b/core/app/models/spree/stock/prioritizer.rb @@ -0,0 +1,47 @@ +module Spree + module Stock + class Prioritizer + attr_reader :packages, :inventory_units + + def initialize(inventory_units, packages, adjuster_class=Adjuster) + @inventory_units = inventory_units + @packages = packages + @adjuster_class = adjuster_class + end + + def prioritized_packages + sort_packages + adjust_packages + prune_packages + packages + end + + private + def adjust_packages + inventory_units.each do |inventory_unit| + adjuster = @adjuster_class.new(inventory_unit, :on_hand) + + visit_packages(adjuster) + + adjuster.status = :backordered + visit_packages(adjuster) + end + end + + def visit_packages(adjuster) + packages.each do |package| + item = package.find_item adjuster.inventory_unit, adjuster.status + adjuster.adjust(package) if item + end + end + + def sort_packages + # order packages by preferred stock_locations + end + + def prune_packages + packages.reject! { |pkg| pkg.empty? } + end + end + end +end diff --git a/core/app/models/spree/stock/quantifier.rb b/core/app/models/spree/stock/quantifier.rb new file mode 100644 index 00000000000..1c79ac5cd27 --- /dev/null +++ b/core/app/models/spree/stock/quantifier.rb @@ -0,0 +1,29 @@ +module Spree + module Stock + class Quantifier + attr_reader :stock_items + + def initialize(variant) + @variant = variant + @stock_items = Spree::StockItem.joins(:stock_location).where(:variant_id => @variant, Spree::StockLocation.table_name =>{ :active => true}) + end + + def total_on_hand + if @variant.should_track_inventory? + stock_items.sum(:count_on_hand) + else + Float::INFINITY + end + end + + def backorderable? + stock_items.any?(&:backorderable) + end + + def can_supply?(required) + total_on_hand >= required || backorderable? + end + + end + end +end diff --git a/core/app/models/spree/stock/splitter/backordered.rb b/core/app/models/spree/stock/splitter/backordered.rb new file mode 100644 index 00000000000..6fd9be46d51 --- /dev/null +++ b/core/app/models/spree/stock/splitter/backordered.rb @@ -0,0 +1,23 @@ +module Spree + module Stock + module Splitter + class Backordered < Spree::Stock::Splitter::Base + + def split(packages) + split_packages = [] + packages.each do |package| + if package.on_hand.count > 0 + split_packages << build_package(package.on_hand) + end + + if package.backordered.count > 0 + split_packages << build_package(package.backordered) + end + end + return_next split_packages + end + + end + end + end +end diff --git a/core/app/models/spree/stock/splitter/base.rb b/core/app/models/spree/stock/splitter/base.rb new file mode 100644 index 00000000000..a162c63f119 --- /dev/null +++ b/core/app/models/spree/stock/splitter/base.rb @@ -0,0 +1,28 @@ +module Spree + module Stock + module Splitter + class Base + attr_reader :packer, :next_splitter + + def initialize(packer, next_splitter=nil) + @packer = packer + @next_splitter = next_splitter + end + delegate :stock_location, to: :packer + + def split(packages) + return_next(packages) + end + + private + def return_next(packages) + next_splitter ? next_splitter.split(packages) : packages + end + + def build_package(contents=[]) + Spree::Stock::Package.new(stock_location, contents) + end + end + end + end +end diff --git a/core/app/models/spree/stock/splitter/shipping_category.rb b/core/app/models/spree/stock/splitter/shipping_category.rb new file mode 100644 index 00000000000..d5256b78770 --- /dev/null +++ b/core/app/models/spree/stock/splitter/shipping_category.rb @@ -0,0 +1,32 @@ +module Spree + module Stock + module Splitter + class ShippingCategory < Spree::Stock::Splitter::Base + def split(packages) + split_packages = [] + packages.each do |package| + split_packages += split_by_category(package) + end + return_next split_packages + end + + private + def split_by_category(package) + categories = Hash.new { |hash, key| hash[key] = [] } + package.contents.each do |item| + categories[item.variant.shipping_category_id] << item + end + hash_to_packages(categories) + end + + def hash_to_packages(categories) + packages = [] + categories.each do |id, contents| + packages << build_package(contents) + end + packages + end + end + end + end +end diff --git a/core/app/models/spree/stock/splitter/weight.rb b/core/app/models/spree/stock/splitter/weight.rb new file mode 100644 index 00000000000..9fe6b145a1c --- /dev/null +++ b/core/app/models/spree/stock/splitter/weight.rb @@ -0,0 +1,32 @@ +module Spree + module Stock + module Splitter + class Weight < Spree::Stock::Splitter::Base + attr_reader :packer, :next_splitter + + cattr_accessor :threshold do + 150 + end + + def split(packages) + packages.each do |package| + removed_contents = reduce package + packages << build_package(removed_contents) unless removed_contents.empty? + end + return_next packages + end + + private + def reduce(package) + removed = [] + while package.weight > self.threshold + break if package.contents.size == 1 + removed << package.contents.shift + end + removed + end + end + end + end +end + diff --git a/core/app/models/spree/stock_item.rb b/core/app/models/spree/stock_item.rb new file mode 100644 index 00000000000..96edfe5d286 --- /dev/null +++ b/core/app/models/spree/stock_item.rb @@ -0,0 +1,94 @@ +module Spree + class StockItem < Spree::Base + acts_as_paranoid + + belongs_to :stock_location, class_name: 'Spree::StockLocation', inverse_of: :stock_items + belongs_to :variant, class_name: 'Spree::Variant', inverse_of: :stock_items, counter_cache: true + has_many :stock_movements, inverse_of: :stock_item + + validates_presence_of :stock_location, :variant + validates_uniqueness_of :variant_id, scope: [:stock_location_id, :deleted_at] + + validates_numericality_of :count_on_hand, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2**31 - 1, + only_integer: true, if: :verify_count_on_hand? + + delegate :weight, :should_track_inventory?, to: :variant + + after_save :conditional_variant_touch, if: :changed? + after_touch { variant.touch } + + self.whitelisted_ransackable_attributes = ['count_on_hand', 'stock_location_id'] + + def backordered_inventory_units + Spree::InventoryUnit.backordered_for_stock_item(self) + end + + def variant_name + variant.name + end + + def adjust_count_on_hand(value) + self.with_lock do + self.count_on_hand = self.count_on_hand + value + process_backorders(count_on_hand - count_on_hand_was) + + self.save! + end + end + + def set_count_on_hand(value) + self.count_on_hand = value + process_backorders(count_on_hand - count_on_hand_was) + + self.save! + end + + def in_stock? + self.count_on_hand > 0 + end + + # Tells whether it's available to be included in a shipment + def available? + self.in_stock? || self.backorderable? + end + + def variant + Spree::Variant.unscoped { super } + end + + def reduce_count_on_hand_to_zero + self.set_count_on_hand(0) if count_on_hand > 0 + end + + private + def verify_count_on_hand? + count_on_hand_changed? && !backorderable? && (count_on_hand < count_on_hand_was) && (count_on_hand < 0) + end + + def count_on_hand=(value) + write_attribute(:count_on_hand, value) + end + + # Process backorders based on amount of stock received + # If stock was -20 and is now -15 (increase of 5 units), then we should process 5 inventory orders. + # If stock was -20 but then was -25 (decrease of 5 units), do nothing. + def process_backorders(number) + if number > 0 + backordered_inventory_units.first(number).each do |unit| + unit.fill_backorder + end + end + end + + def conditional_variant_touch + # the variant_id changes from nil when a new stock location is added + stock_changed = (count_on_hand_changed? && count_on_hand_change.any?(&:zero?)) || variant_id_changed? + + if !Spree::Config.binary_inventory_cache || stock_changed + variant.touch + end + end + end +end diff --git a/core/app/models/spree/stock_location.rb b/core/app/models/spree/stock_location.rb new file mode 100644 index 00000000000..854b2aec971 --- /dev/null +++ b/core/app/models/spree/stock_location.rb @@ -0,0 +1,122 @@ +module Spree + class StockLocation < Spree::Base + has_many :shipments + has_many :stock_items, dependent: :delete_all, inverse_of: :stock_location + has_many :stock_movements, through: :stock_items + + belongs_to :state, class_name: 'Spree::State' + belongs_to :country, class_name: 'Spree::Country' + + validates_presence_of :name + + scope :active, -> { where(active: true) } + scope :order_default, -> { order(default: :desc, name: :asc) } + + after_create :create_stock_items, :if => "self.propagate_all_variants?" + after_save :ensure_one_default + + def state_text + state.try(:abbr) || state.try(:name) || state_name + end + + # Wrapper for creating a new stock item respecting the backorderable config + def propagate_variant(variant) + self.stock_items.create!(variant: variant, backorderable: self.backorderable_default) + end + + # Return either an existing stock item or create a new one. Useful in + # scenarios where the user might not know whether there is already a stock + # item for a given variant + def set_up_stock_item(variant) + self.stock_item(variant) || propagate_variant(variant) + end + + # Returns an instance of StockItem for the variant id. + # + # @param variant_id [String] The id of a variant. + # + # @return [StockItem] Corresponding StockItem for the StockLocation's variant. + def stock_item(variant_id) + stock_items.where(variant_id: variant_id).order(:id).first + end + + # Attempts to look up StockItem for the variant, and creates one if not found. + # This method accepts an id or instance of the variant since it is used in + # multiple ways. Other methods in this model attempt to pass a variant, + # but controller actions can pass just the variant id as a parameter. + # + # @param variant_or_id [Variant|String] Variant instance or string id of a variant. + # + # @return [StockItem] Corresponding StockItem for the StockLocation's variant. + def stock_item_or_create(variant_or_id) + vid = if variant_or_id.is_a?(Variant) + variant_or_id.id + else + ActiveSupport::Deprecation.warn "Passing a Variant ID is deprecated, and will be removed in Spree 3. Please pass a variant instance instead.", caller + variant_or_id + end + stock_item(vid) || stock_items.create(variant_id: vid) + end + + def count_on_hand(variant) + stock_item(variant).try(:count_on_hand) + end + + def backorderable?(variant) + stock_item(variant).try(:backorderable?) + end + + def restock(variant, quantity, originator = nil) + move(variant, quantity, originator) + end + + def restock_backordered(variant, quantity, originator = nil) + item = stock_item_or_create(variant) + item.update_columns( + count_on_hand: item.count_on_hand + quantity, + updated_at: Time.now + ) + end + + def unstock(variant, quantity, originator = nil) + move(variant, -quantity, originator) + end + + def move(variant, quantity, originator = nil) + stock_item_or_create(variant).stock_movements.create!(quantity: quantity, + originator: originator) + end + + def fill_status(variant, quantity) + if item = stock_item(variant) + + if item.count_on_hand >= quantity + on_hand = quantity + backordered = 0 + else + on_hand = item.count_on_hand + on_hand = 0 if on_hand < 0 + backordered = item.backorderable? ? (quantity - on_hand) : 0 + end + + [on_hand, backordered] + else + [0, 0] + end + end + + private + def create_stock_items + Variant.find_each { |variant| self.propagate_variant(variant) } + end + + def ensure_one_default + if self.default + StockLocation.where(default: true).where.not(id: self.id).each do |stock_location| + stock_location.default = false + stock_location.save! + end + end + end + end +end diff --git a/core/app/models/spree/stock_movement.rb b/core/app/models/spree/stock_movement.rb new file mode 100644 index 00000000000..1def90dbeb4 --- /dev/null +++ b/core/app/models/spree/stock_movement.rb @@ -0,0 +1,33 @@ +module Spree + class StockMovement < Spree::Base + belongs_to :stock_item, class_name: 'Spree::StockItem', inverse_of: :stock_movements + belongs_to :originator, polymorphic: true + + after_create :update_stock_item_quantity + + validates :stock_item, presence: true + validates :quantity, presence: true, numericality: { + greater_than_or_equal_to: -2**31, + less_than_or_equal_to: 2**31-1, + only_integer: true, + allow_nil: true + } + + scope :recent, -> { order('created_at DESC') } + + self.whitelisted_ransackable_attributes = ['quantity'] + + def readonly? + !new_record? + end + + private + + def update_stock_item_quantity + return unless self.stock_item.should_track_inventory? + stock_item.adjust_count_on_hand quantity + end + + end +end + diff --git a/core/app/models/spree/stock_transfer.rb b/core/app/models/spree/stock_transfer.rb new file mode 100644 index 00000000000..4b23ff324d5 --- /dev/null +++ b/core/app/models/spree/stock_transfer.rb @@ -0,0 +1,43 @@ +module Spree + class StockTransfer < Spree::Base + has_many :stock_movements, :as => :originator + + belongs_to :source_location, :class_name => 'StockLocation' + belongs_to :destination_location, :class_name => 'StockLocation' + + make_permalink field: :number, prefix: 'T' + + self.whitelisted_ransackable_attributes = %w[reference source_location_id destination_location_id closed_at created_at number] + + def to_param + number + end + + def source_movements + stock_movements.joins(:stock_item) + .where('spree_stock_items.stock_location_id' => source_location_id) + end + + def destination_movements + stock_movements.joins(:stock_item) + .where('spree_stock_items.stock_location_id' => destination_location_id) + end + + def transfer(source_location, destination_location, variants) + transaction do + variants.each_pair do |variant, quantity| + source_location.unstock(variant, quantity, self) if source_location + destination_location.restock(variant, quantity, self) + + self.source_location = source_location + self.destination_location = destination_location + self.save! + end + end + end + + def receive(destination_location, variants) + transfer(nil, destination_location, variants) + end + end +end diff --git a/core/app/models/spree/store.rb b/core/app/models/spree/store.rb new file mode 100644 index 00000000000..2721abf2704 --- /dev/null +++ b/core/app/models/spree/store.rb @@ -0,0 +1,38 @@ +module Spree + class Store < Spree::Base + validates :code, presence: true, uniqueness: { allow_blank: true } + validates :name, presence: true + validates :url, presence: true + validates :mail_from_address, presence: true + + before_save :ensure_default_exists_and_is_unique + before_destroy :validate_not_default + + scope :by_url, lambda { |url| where("url like ?", "%#{url}%") } + + def self.current(domain = nil) + current_store = domain ? Store.by_url(domain).first : nil + current_store || Store.default + end + + def self.default + where(default: true).first || new + end + + private + + def ensure_default_exists_and_is_unique + if default + Store.where.not(id: id).update_all(default: false) + elsif Store.where(default: true).count == 0 + self.default = true + end + end + + def validate_not_default + if default + errors.add(:base, :cannot_destroy_default_store) + end + end + end +end diff --git a/core/app/models/spree/tax_category.rb b/core/app/models/spree/tax_category.rb index 72c4444f157..cc6dce2382c 100644 --- a/core/app/models/spree/tax_category.rb +++ b/core/app/models/spree/tax_category.rb @@ -1,26 +1,23 @@ module Spree - class TaxCategory < ActiveRecord::Base - validates :name, :presence => true, :uniqueness => { :scope => :deleted_at } + class TaxCategory < Spree::Base + acts_as_paranoid + validates :name, presence: true, uniqueness: { scope: :deleted_at, allow_blank: true } - has_many :tax_rates, :dependent => :destroy - - attr_accessible :name, :description, :is_default + has_many :tax_rates, dependent: :destroy, inverse_of: :tax_category before_save :set_default_category - default_scope where(:deleted_at => nil) - def set_default_category #set existing default tax category to false if this one has been marked as default - if is_default && tax_category = self.class.where(:is_default => true).first - tax_category.update_column(:is_default, false) unless tax_category == self + if is_default && tax_category = self.class.where(is_default: true).first + unless tax_category == self + tax_category.update_columns( + is_default: false, + updated_at: Time.now, + ) + end end end - - def mark_deleted! - self.deleted_at = Time.now - save - end end end diff --git a/core/app/models/spree/tax_rate.rb b/core/app/models/spree/tax_rate.rb index bebe82eda32..d1a3d92465f 100644 --- a/core/app/models/spree/tax_rate.rb +++ b/core/app/models/spree/tax_rate.rb @@ -2,80 +2,200 @@ module Spree class DefaultTaxZoneValidator < ActiveModel::Validator def validate(record) if record.included_in_price - record.errors.add(:included_in_price, I18n.t(:included_price_validation)) unless Zone.default_tax + record.errors.add(:included_in_price, Spree.t(:included_price_validation)) unless Zone.default_tax end end end end module Spree - class TaxRate < ActiveRecord::Base - belongs_to :zone, :class_name => "Spree::Zone" - belongs_to :tax_category, :class_name => "Spree::TaxCategory" + class TaxRate < Spree::Base + acts_as_paranoid - validates :amount, :presence => true, :numericality => true - validates :tax_category_id, :presence => true - validates_with DefaultTaxZoneValidator + # Need to deal with adjustments before calculator is destroyed. + before_destroy :deals_with_adjustments_for_deleted_source + + include Spree::CalculatedAdjustments + include Spree::AdjustmentSource - calculated_adjustments - scope :by_zone, lambda { |zone| where(:zone_id => zone) } + belongs_to :zone, class_name: "Spree::Zone", inverse_of: :tax_rates + belongs_to :tax_category, class_name: "Spree::TaxCategory", inverse_of: :tax_rates - attr_accessible :amount, :tax_category_id, :calculator, :zone_id, :included_in_price, :name, :show_rate_in_label + has_many :adjustments, as: :source + + validates :amount, presence: true, numericality: true + validates :tax_category_id, presence: true + validates_with DefaultTaxZoneValidator + + scope :by_zone, ->(zone) { where(zone_id: zone) } # Gets the array of TaxRates appropriate for the specified order - def self.match(order) - return [] unless order.tax_zone - all.select do |rate| - rate.zone == order.tax_zone || rate.zone.contains?(order.tax_zone) || rate.zone.default_tax + def self.match(order_tax_zone) + return [] unless order_tax_zone + rates = includes(zone: { zone_members: :zoneable }).load.select do |rate| + # Why "potentially"? + # Go see the documentation for that method. + rate.potentially_applicable?(order_tax_zone) + end + + # Imagine with me this scenario: + # You are living in Spain and you have a store which ships to France. + # Spain is therefore your default tax rate. + # When you ship to Spain, you want the Spanish rate to apply. + # When you ship to France, you want the French rate to apply. + # + # Normally, Spree would notice that you have two potentially applicable + # tax rates for one particular item. + # When you ship to Spain, only the Spanish one will apply. + # When you ship to France, you'll see a Spanish refund AND a French tax. + # This little bit of code at the end stops the Spanish refund from appearing. + # + # For further discussion, see #4397 and #4327. + rates.delete_if do |rate| + rate.included_in_price? && + (rates - [rate]).map(&:tax_category).include?(rate.tax_category) end end - def self.adjust(order) - order.clear_adjustments! - self.match(order).each do |rate| - rate.adjust(order) + # Pre-tax amounts must be stored so that we can calculate + # correct rate amounts in the future. For example: + # https://github.com/spree/spree/issues/4318#issuecomment-34723428 + def self.store_pre_tax_amount(item, rates) + pre_tax_amount = case item + when Spree::LineItem then item.discounted_amount + when Spree::Shipment then item.discounted_cost + end + + included_rates = rates.select(&:included_in_price) + if included_rates.any? + pre_tax_amount /= (1 + included_rates.map(&:amount).sum) end + + item.update_column(:pre_tax_amount, pre_tax_amount) end - # For Vat the default rate is the rate that is configured for the default category - # It is needed for every price calculation (as all customer facing prices include vat ) - # The function returns the actual amount, which may be 0 in case of wrong setup, but is never nil - def self.default - category = TaxCategory.includes(:tax_rates).where(:is_default => true).first - return 0 unless category + # This method is best described by the documentation on #potentially_applicable? + def self.adjust(order_tax_zone, items) + rates = self.match(order_tax_zone) + tax_categories = rates.map(&:tax_category) + relevant_items, non_relevant_items = items.partition { |item| tax_categories.include?(item.tax_category) } - address ||= Address.new(:country_id => Spree::Config[:default_country_id]) - rate = category.tax_rates.detect { |rate| rate.zone.include? address }.try(:amount) + if relevant_items.present? + Spree::Adjustment.where(adjustable: relevant_items).tax.destroy_all # using destroy_all to ensure adjustment destroy callback fires. + end + + relevant_items.each do |item| + relevant_rates = rates.select { |rate| rate.tax_category == item.tax_category } + store_pre_tax_amount(item, relevant_rates) + relevant_rates.each do |rate| + rate.adjust(order_tax_zone, item) + end + end + non_relevant_items.each do |item| + if item.adjustments.tax.present? + item.adjustments.tax.destroy_all # using destroy_all to ensure adjustment destroy callback fires. + item.update_columns pre_tax_amount: 0 + end + end + end - rate || 0 + # Tax rates can *potentially* be applicable to an order. + # We do not know if they are/aren't until we attempt to apply these rates to + # the items contained within the Order itself. + # For instance, if a rate passes the criteria outlined in this method, + # but then has a tax category that doesn't match against any of the line items + # inside of the order, then that tax rate will not be applicable to anything. + # For instance: + # + # Zones: + # - Spain (default tax zone) + # - France + # + # Tax rates: (note: amounts below do not actually reflect real VAT rates) + # 21% inclusive - "Clothing" - Spain + # 18% inclusive - "Clothing" - France + # 10% inclusive - "Food" - Spain + # 8% inclusive - "Food" - France + # 5% inclusive - "Hotels" - Spain + # 2% inclusive - "Hotels" - France + # + # Order has: + # Line Item #1 - Tax Category: Clothing + # Line Item #2 - Tax Category: Food + # + # Tax rates that should be selected: + # + # 21% inclusive - "Clothing" - Spain + # 10% inclusive - "Food" - Spain + # + # If the order's address changes to one in France, then the tax will be recalculated: + # + # 18% inclusive - "Clothing" - France + # 8% inclusive - "Food" - France + # + # Note here that the "Hotels" tax rates will not be used at all. + # This is because there are no items which have the tax category of "Hotels". + # + # Under no circumstances should negative adjustments be applied for the Spanish tax rates. + # + # Those rates should never come into play at all and only the French rates should apply. + def potentially_applicable?(order_tax_zone) + # If the rate's zone matches the order's tax zone, then it's applicable. + self.zone == order_tax_zone || + # If the rate's zone *contains* the order's tax zone, then it's applicable. + self.zone.contains?(order_tax_zone) || + # 1) The rate's zone is the default zone, then it's always applicable. + (self.included_in_price? && self.zone.default_tax) end # Creates necessary tax adjustments for the order. - def adjust(order) - label = create_label + def adjust(order_tax_zone, item) + amount = compute_amount(item) + return if amount == 0 + + included = included_in_price && default_zone_or_zone_match?(order_tax_zone) + + if amount < 0 + label = Spree.t(:refund) + ' ' + create_label + end + + self.adjustments.create!({ + :adjustable => item, + :amount => amount, + :order_id => item.order_id, + :label => label || create_label, + :included => included + }) + end + + # This method is used by Adjustment#update to recalculate the cost. + def compute_amount(item) if included_in_price - if Zone.default_tax.contains? order.tax_zone - order.line_items.each { |line_item| create_adjustment(label, line_item, line_item) } + if default_zone_or_zone_match?(item.order.tax_zone) + calculator.compute(item) else - amount = -1 * calculator.compute(order) - label = I18n.t(:refund) + label - order.adjustments.create({ :amount => amount, - :source => order, - :originator => self, - :locked => true, - :label => label }, :without_protection => true) + # In this case, it's a refund. + calculator.compute(item) * - 1 end else - create_adjustment(label, order, order) + calculator.compute(item) end end + def default_zone_or_zone_match?(order_tax_zone) + default_tax = Zone.default_tax + (default_tax && default_tax.contains?(order_tax_zone)) || order_tax_zone == self.zone + end + private def create_label label = "" label << (name.present? ? name : tax_category.name) + " " label << (show_rate_in_label? ? "#{amount * 100}%" : "") + label << " (#{Spree.t(:included_in_price)})" if included_in_price? + label end + end end diff --git a/core/app/models/spree/taxon.rb b/core/app/models/spree/taxon.rb index ecd861da1aa..e1c888f8e9a 100644 --- a/core/app/models/spree/taxon.rb +++ b/core/app/models/spree/taxon.rb @@ -1,27 +1,32 @@ module Spree - class Taxon < ActiveRecord::Base - acts_as_nested_set :dependent => :destroy + class Taxon < Spree::Base + acts_as_nested_set dependent: :destroy - belongs_to :taxonomy - has_and_belongs_to_many :products, :join_table => 'spree_products_taxons' + belongs_to :taxonomy, class_name: 'Spree::Taxonomy', inverse_of: :taxons + has_many :classifications, -> { order(:position) }, dependent: :delete_all, inverse_of: :taxon + has_many :products, through: :classifications + + has_and_belongs_to_many :prototypes, join_table: :spree_taxons_prototypes before_create :set_permalink - attr_accessible :name, :parent_id, :position, :icon, :description, :permalink, :taxonomy_id + validates :name, presence: true + validates :meta_keywords, length: { maximum: 255 } + validates :meta_description, length: { maximum: 255 } + validates :meta_title, length: { maximum: 255 } - validates :name, :presence => true + after_save :touch_ancestors_and_taxonomy + after_touch :touch_ancestors_and_taxonomy has_attached_file :icon, - :styles => { :mini => '32x32>', :normal => '128x128>' }, - :default_style => :mini, - :url => '/spree/taxons/:id/:style/:basename.:extension', - :path => ':rails_root/public/spree/taxons/:id/:style/:basename.:extension', - :default_url => '/assets/default_taxon.png' - - include Spree::Core::S3Support - supports_s3 :icon + styles: { mini: '32x32>', normal: '128x128>' }, + default_style: :mini, + url: '/spree/taxons/:id/:style/:basename.:extension', + path: ':rails_root/public/spree/taxons/:id/:style/:basename.:extension', + default_url: '/assets/default_taxon.png' - include ::Spree::ProductFilters # for detailed defs of filters + validates_attachment :icon, + content_type: { content_type: ["image/jpg", "image/jpeg", "image/png", "image/gif"] } # indicate which filters should be used for a taxon # this method should be customized to your own site @@ -30,25 +35,36 @@ def applicable_filters # fs << ProductFilters.taxons_below(self) ## unless it's a root taxon? left open for demo purposes - fs << ProductFilters.price_filter if ProductFilters.respond_to?(:price_filter) - fs << ProductFilters.brand_filter if ProductFilters.respond_to?(:brand_filter) + fs << Spree::Core::ProductFilters.price_filter if Spree::Core::ProductFilters.respond_to?(:price_filter) + fs << Spree::Core::ProductFilters.brand_filter if Spree::Core::ProductFilters.respond_to?(:brand_filter) fs end + # Return meta_title if set otherwise generates from root name and/or taxon name + def seo_title + unless meta_title.blank? + meta_title + else + root? ? name : "#{root.name} - #{name}" + end + end + # Creates permalink based on Stringex's .to_url method def set_permalink - if parent_id.nil? - self.permalink = name.to_url if permalink.blank? + if parent.present? + self.permalink = [parent.permalink, (permalink.blank? ? name.to_url : permalink.split('/').last)].join('/') else - parent_taxon = Taxon.find(parent_id) - self.permalink = [parent_taxon.permalink, (permalink.blank? ? name.to_url : permalink.split('/').last)].join('/') + self.permalink = name.to_url if permalink.blank? end end + # For #2759 + def to_param + permalink + end + def active_products - scope = products.active - scope = scope.on_hand unless Spree::Config[:show_zero_stock_products] - scope + products.active end def pretty_name @@ -58,5 +74,23 @@ def pretty_name ancestor_chain + "#{name}" end + # awesome_nested_set sorts by :lft and :rgt. This call re-inserts the child + # node so that its resulting position matches the observable 0-indexed position. + # ** Note ** no :position column needed - a_n_s doesn't handle the reordering if + # you bring your own :order_column. + # + # See #3390 for background. + def child_index=(idx) + move_to_child_with_index(parent, idx.to_i) unless self.new_record? + end + + private + + def touch_ancestors_and_taxonomy + # Touches all ancestors at once to avoid recursive taxonomy touch, and reduce queries. + self.class.where(id: ancestors.pluck(:id)).update_all(updated_at: Time.now) + # Have taxonomy touch happen in #touch_ancestors_and_taxonomy rather than association option in order for imports to override. + taxonomy.try!(:touch) + end end end diff --git a/core/app/models/spree/taxonomy.rb b/core/app/models/spree/taxonomy.rb index 21ef50727fc..98403491ba3 100644 --- a/core/app/models/spree/taxonomy.rb +++ b/core/app/models/spree/taxonomy.rb @@ -1,23 +1,25 @@ module Spree - class Taxonomy < ActiveRecord::Base - validates :name, :presence => true + class Taxonomy < Spree::Base + acts_as_list - attr_accessible :name + validates :name, presence: true - has_many :taxons - has_one :root, :conditions => { :parent_id => nil }, :class_name => "Spree::Taxon", - :dependent => :destroy + has_many :taxons, inverse_of: :taxonomy + has_one :root, -> { where parent_id: nil }, class_name: "Spree::Taxon", dependent: :destroy after_save :set_name - default_scope :order => "#{self.table_name}.position" + default_scope -> { order("#{self.table_name}.position") } private def set_name if root - root.update_column(:name, name) + root.update_columns( + name: name, + updated_at: Time.now, + ) else - self.root = Taxon.create!({ :taxonomy_id => id, :name => name }, :without_protection => true) + self.root = Taxon.create!(taxonomy_id: id, name: name) end end diff --git a/core/app/models/spree/tokenized_permission.rb b/core/app/models/spree/tokenized_permission.rb deleted file mode 100644 index 82869703f22..00000000000 --- a/core/app/models/spree/tokenized_permission.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Spree - class TokenizedPermission < ActiveRecord::Base - belongs_to :permissable, :polymorphic => true - end -end - diff --git a/core/app/models/spree/tracker.rb b/core/app/models/spree/tracker.rb index 3a143cb1d2f..ae27d36b6ef 100644 --- a/core/app/models/spree/tracker.rb +++ b/core/app/models/spree/tracker.rb @@ -1,9 +1,7 @@ module Spree - class Tracker < ActiveRecord::Base - attr_accessible :analytics_id, :environment, :active - + class Tracker < Spree::Base def self.current - tracker = where(:active => true, :environment => Rails.env).first + tracker = where(active: true, environment: Rails.env).first tracker.analytics_id.present? ? tracker : nil if tracker end end diff --git a/core/app/models/spree/validations/db_maximum_length_validator.rb b/core/app/models/spree/validations/db_maximum_length_validator.rb new file mode 100644 index 00000000000..1f2a351dae7 --- /dev/null +++ b/core/app/models/spree/validations/db_maximum_length_validator.rb @@ -0,0 +1,22 @@ +module Spree + module Validations + ## + # Validates a field based on the maximum length of the underlying DB field, if there is one. + class DbMaximumLengthValidator < ActiveModel::Validator + + def initialize(options) + super + @field = options[:field].to_s + raise ArgumentError.new("a field must be specified to the validator") if @field.blank? + end + + def validate(record) + limit = record.class.columns_hash[@field].limit + value = record[@field.to_sym] + if value && limit && value.to_s.length > limit + record.errors.add(@field.to_sym, :too_long, count: limit) + end + end + end + end +end diff --git a/core/app/models/spree/variant.rb b/core/app/models/spree/variant.rb index 12b69fcc0d4..70c7c7a9f0a 100644 --- a/core/app/models/spree/variant.rb +++ b/core/app/models/spree/variant.rb @@ -1,69 +1,73 @@ module Spree - class Variant < ActiveRecord::Base - belongs_to :product, :touch => true + class Variant < Spree::Base + acts_as_paranoid + acts_as_list scope: :product - delegate_belongs_to :product, :name, :description, :permalink, :available_on, - :tax_category_id, :shipping_category_id, :meta_description, - :meta_keywords, :tax_category + include Spree::DefaultPrice - attr_accessible :name, :presentation, :cost_price, :lock_version, - :position, :on_demand, :on_hand, :option_value_ids, - :product_id, :option_values_attributes, :price, - :weight, :height, :width, :depth, :sku, :cost_currency + belongs_to :product, touch: true, class_name: 'Spree::Product', inverse_of: :variants + belongs_to :tax_category, class_name: 'Spree::TaxCategory' - has_many :inventory_units - has_many :line_items - has_and_belongs_to_many :option_values, :join_table => :spree_option_values_variants - has_many :images, :as => :viewable, :order => :position, :dependent => :destroy + delegate_belongs_to :product, :name, :description, :slug, :available_on, + :shipping_category_id, :meta_description, :meta_keywords, + :shipping_category - has_one :default_price, - :class_name => 'Spree::Price', - :conditions => proc { { :currency => Spree::Config[:currency] } }, - :dependent => :destroy + has_many :inventory_units, inverse_of: :variant + has_many :line_items, inverse_of: :variant + has_many :orders, through: :line_items - delegate_belongs_to :default_price, :display_price, :display_amount, :price, :price=, :currency if Spree::Price.table_exists? + has_many :stock_items, dependent: :destroy, inverse_of: :variant + has_many :stock_locations, through: :stock_items + has_many :stock_movements, through: :stock_items + + has_and_belongs_to_many :option_values, join_table: :spree_option_values_variants + has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: "Spree::Image" has_many :prices, - :class_name => 'Spree::Price', - :dependent => :destroy + class_name: 'Spree::Price', + dependent: :destroy, + inverse_of: :variant + + before_validation :set_cost_currency validate :check_price - validates :price, :numericality => { :greater_than_or_equal_to => 0 }, :presence => true, :if => proc { Spree::Config[:require_master_price] } - validates :cost_price, :numericality => { :greater_than_or_equal_to => 0, :allow_nil => true } if self.table_exists? && self.column_names.include?('cost_price') - validates :count_on_hand, :numericality => true - before_validation :set_cost_currency - after_save :process_backorders - after_save :save_default_price - after_save :recalculate_product_on_hand, :if => :is_master? + validates :cost_price, numericality: { greater_than_or_equal_to: 0, allow_nil: true } + validates :price, numericality: { greater_than_or_equal_to: 0, allow_nil: true } + validates_uniqueness_of :sku, allow_blank: true, conditions: -> { where(deleted_at: nil) } + + after_create :create_stock_items + after_create :set_master_out_of_stock, unless: :is_master? + + after_touch :clear_in_stock_cache + + scope :in_stock, -> { joins(:stock_items).where('count_on_hand > ? OR track_inventory = ?', 0, false) } - # default variant scope only lists non-deleted variants - scope :deleted, lambda { where('deleted_at IS NOT NULL') } + self.whitelisted_ransackable_associations = %w[option_values product prices default_price] + self.whitelisted_ransackable_attributes = %w[weight sku] def self.active(currency = nil) - joins(:prices).where(:deleted_at => nil).where('spree_prices.currency' => currency || Spree::Config[:currency]).where('spree_prices.amount IS NOT NULL') + joins(:prices).where(deleted_at: nil).where('spree_prices.currency' => currency || Spree::Config[:currency]).where('spree_prices.amount IS NOT NULL') end - # Returns number of inventory units for this variant (new records haven't been saved to database, yet) - def on_hand - if Spree::Config[:track_inventory_levels] && !self.on_demand - count_on_hand - else - (1.0 / 0) # Infinity - end + def self.having_orders + joins(:line_items).distinct end - # set actual attribute - def on_hand=(new_level) - if !Spree::Config[:track_inventory_levels] - raise 'Cannot set on_hand value when Spree::Config[:track_inventory_levels] is false' + def tax_category + if self[:tax_category_id].nil? + product.tax_category else - self.count_on_hand = new_level unless self.on_demand + TaxCategory.find(self[:tax_category_id]) end end def cost_price=(price) - self[:cost_price] = parse_price(price) if price.present? + self[:cost_price] = Spree::LocalizedNumber.parse(price) if price.present? + end + + def weight=(weight) + self[:weight] = Spree::LocalizedNumber.parse(weight) if weight.present? end # returns number of units currently on backorder for this variant. @@ -71,47 +75,56 @@ def on_backorder inventory_units.with_state('backordered').size end - # returns true if at least one inventory unit of this variant is "on_hand" - def in_stock? - if Spree::Config[:track_inventory_levels] && !self.on_demand - on_hand > 0 - else - true - end - end - alias in_stock in_stock? - - # returns true if this variant is allowed to be placed on a new order - def available? - Spree::Config[:track_inventory_levels] ? (Spree::Config[:allow_backorders] || in_stock? || self.on_demand) : true + def is_backorderable? + Spree::Stock::Quantifier.new(self).backorderable? end def options_text - values = self.option_values.sort_by(&:position) + values = self.option_values.sort do |a, b| + a.option_type.position <=> b.option_type.position + end - values.map! do |ov| + values.to_a.map! do |ov| "#{ov.option_type.presentation}: #{ov.presentation}" end - values.to_sentence({ :words_connector => ", ", :two_words_connector => ", " }) + values.to_sentence({ words_connector: ", ", two_words_connector: ", " }) end - def gross_profit - cost_price.nil? ? 0 : (price - cost_price) + # Default to master name + def exchange_name + is_master? ? name : options_text + end + + def descriptive_name + is_master? ? name + ' - Master' : name + ' - ' + options_text end # use deleted? rather than checking the attribute directly. this # allows extensions to override deleted? if they want to provide # their own definition. def deleted? - deleted_at + !!deleted_at + end + + # Product may be created with deleted_at already set, + # which would make AR's default finder return nil. + # This is a stopgap for that little problem. + def product + Spree::Product.unscoped { super } + end + + def options=(options = {}) + options.each do |option| + set_option_value(option[:name], option[:value]) + end end def set_option_value(opt_name, opt_value) # no option values on master return if self.is_master - option_type = Spree::OptionType.where(:name => opt_name).first_or_initialize do |o| + option_type = Spree::OptionType.where(name: opt_name).first_or_initialize do |o| o.presentation = opt_name o.save! end @@ -125,11 +138,10 @@ def set_option_value(opt_name, opt_value) # then we have to check to make sure that the product has the option type unless self.product.option_types.include? option_type self.product.option_types << option_type - self.product.save end end - option_value = Spree::OptionValue.where(:option_type_id => option_type.id, :name => opt_value).first_or_initialize do |o| + option_value = Spree::OptionValue.where(option_type_id: option_type.id, name: opt_value).first_or_initialize do |o| o.presentation = opt_value o.save! end @@ -142,44 +154,74 @@ def option_value(opt_name) self.option_values.detect { |o| o.option_type.name == opt_name }.try(:presentation) end - def on_demand=(on_demand) - self[:on_demand] = on_demand - if on_demand - inventory_units.with_state('backordered').each(&:fill_backorder) - end + def price_in(currency) + prices.select{ |price| price.currency == currency }.first || Spree::Price.new(variant_id: self.id, currency: currency) + end + + def amount_in(currency) + price_in(currency).try(:amount) end - def has_default_price? - !self.default_price.nil? + def price_modifier_amount_in(currency, options = {}) + return 0 unless options.present? + + options.keys.map { |key| + m = "#{key}_price_modifier_amount_in".to_sym + if self.respond_to? m + self.send(m, currency, options[key]) + else + 0 + end + }.sum end - def price_in(currency) - prices.select{ |price| price.currency == currency }.first || Spree::Price.new(:variant_id => self.id, :currency => currency) + def price_modifier_amount(options = {}) + return 0 unless options.present? + + options.keys.map { |key| + m = "#{key}_price_modifier_amount".to_sym + if self.respond_to? m + self.send(m, options[key]) + else + 0 + end + }.sum end - def amount_in(currency) - price_in(currency).try(:amount) + def name_and_sku + "#{name} - #{sku}" end - private + def sku_and_options_text + "#{sku} #{options_text}".strip + end - def process_backorders - if count_changes = changes['count_on_hand'] - new_level = count_changes.last + def in_stock? + Rails.cache.fetch(in_stock_cache_key) do + total_on_hand > 0 + end + end + + def can_supply?(quantity=1) + Spree::Stock::Quantifier.new(self).can_supply?(quantity) + end - if Spree::Config[:track_inventory_levels] && !self.on_demand - new_level = new_level.to_i + def total_on_hand + Spree::Stock::Quantifier.new(self).total_on_hand + end - # update backorders if level is positive - if new_level > 0 - # fill backordered orders before creating new units - backordered_units = inventory_units.with_state('backordered') - backordered_units.slice(0, new_level).each(&:fill_backorder) - new_level -= backordered_units.length - end + # Shortcut method to determine if inventory tracking is enabled for this variant + # This considers both variant tracking flag and site-wide inventory tracking settings + def should_track_inventory? + self.track_inventory? && Spree::Config.track_inventory_levels + end + + private - self.update_attribute_without_callbacks(:count_on_hand, new_level) - end + def set_master_out_of_stock + if product.master && product.master.in_stock? + product.master.stock_items.update_all(:backorderable => false) + product.master.stock_items.each { |item| item.reduce_count_on_hand_to_zero } end end @@ -195,31 +237,22 @@ def check_price end end - # strips all non-price-like characters from the price, taking into account locale settings - def parse_price(price) - return price unless price.is_a?(String) - - separator, delimiter = I18n.t([:'number.currency.format.separator', :'number.currency.format.delimiter']) - non_price_characters = /[^0-9\-#{separator}]/ - price.gsub!(non_price_characters, '') # strip everything else first - price.gsub!(separator, '.') unless separator == '.' # then replace the locale-specific decimal separator with the standard separator if necessary - - price.to_d + def set_cost_currency + self.cost_currency = Spree::Config[:currency] if cost_currency.nil? || cost_currency.empty? end - def recalculate_product_on_hand - on_hand = product.on_hand - if Spree::Config[:track_inventory_levels] && on_hand != (1.0 / 0) # Infinity - product.update_column(:count_on_hand, on_hand) + def create_stock_items + StockLocation.where(propagate_all_variants: true).each do |stock_location| + stock_location.propagate_variant(self) end end - def save_default_price - default_price.save if default_price && (default_price.changed? || default_price.new_record?) + def in_stock_cache_key + "variant-#{id}-in_stock" end - def set_cost_currency - self.cost_currency = Spree::Config[:currency] if cost_currency.nil? || cost_currency.empty? + def clear_in_stock_cache + Rails.cache.delete(in_stock_cache_key) end end end diff --git a/core/app/models/spree/variant/scopes.rb b/core/app/models/spree/variant/scopes.rb index 64962bfe8d1..31cb946e13b 100644 --- a/core/app/models/spree/variant/scopes.rb +++ b/core/app/models/spree/variant/scopes.rb @@ -1,15 +1,19 @@ module Spree - class Variant < ActiveRecord::Base + class Variant < Spree::Base #FIXME WARNING tested only under sqlite and postgresql - scope :descend_by_popularity, order("COALESCE((SELECT COUNT(*) FROM #{LineItem.quoted_table_name} GROUP BY #{LineItem.quoted_table_name}.variant_id HAVING #{LineItem.quoted_table_name}.variant_id = #{Variant.quoted_table_name}.id), 0) DESC") + scope :descend_by_popularity, -> { + ActiveSupport::Deprecation.warn "Variant.descend_by_popularity is deprecated and will be removed from Spree 3.", caller + order("COALESCE((SELECT COUNT(*) FROM #{LineItem.quoted_table_name} GROUP BY #{LineItem.quoted_table_name}.variant_id HAVING #{LineItem.quoted_table_name}.variant_id = #{Variant.quoted_table_name}.id), 0) DESC") + } class << self # Returns variants that match a given option value # # Example: # - # product.variants_including_master.has_option(OptionType.find_by_name("shoe-size"),OptionValue.find_by_name("8")) + # product.variants_including_master.has_option(OptionType.find_by(name: 'shoe-size'), OptionValue.find_by(name: '8')) def has_option(option_type, *option_values) + ActiveSupport::Deprecation.warn "Variant.descend_by_popularity is deprecated and will be removed from Spree 3.", caller option_types = OptionType.table_name option_type_conditions = case option_type diff --git a/core/app/models/spree/zone.rb b/core/app/models/spree/zone.rb index 6eda97df28d..da27a78a246 100644 --- a/core/app/models/spree/zone.rb +++ b/core/app/models/spree/zone.rb @@ -1,21 +1,42 @@ module Spree - class Zone < ActiveRecord::Base - has_many :zone_members, :dependent => :destroy, :class_name => "Spree::ZoneMember" - has_many :tax_rates, :dependent => :destroy - has_many :shipping_methods, :dependent => :nullify + class Zone < Spree::Base + has_many :zone_members, dependent: :destroy, class_name: "Spree::ZoneMember", inverse_of: :zone + has_many :tax_rates, dependent: :destroy, inverse_of: :zone + has_and_belongs_to_many :shipping_methods, :join_table => 'spree_shipping_methods_zones' - validates :name, :presence => true, :uniqueness => true + validates :name, presence: true, uniqueness: { allow_blank: true } after_save :remove_defunct_members after_save :remove_previous_default alias :members :zone_members - accepts_nested_attributes_for :zone_members, :allow_destroy => true, :reject_if => proc { |a| a['zoneable_id'].blank? } + accepts_nested_attributes_for :zone_members, allow_destroy: true, reject_if: proc { |a| a['zoneable_id'].blank? } - attr_accessible :name, :description, :default_tax, :kind, :zone_members, :zone_members_attributes + self.whitelisted_ransackable_attributes = ['description'] + + def self.default_tax + where(default_tax: true).first + end + + # Returns the matching zone with the highest priority zone type (State, Country, Zone.) + # Returns nil in the case of no matches. + def self.match(address) + return unless address and matches = self.includes(:zone_members). + order('spree_zones.zone_members_count', 'spree_zones.created_at'). + where("(spree_zone_members.zoneable_type = 'Spree::Country' AND spree_zone_members.zoneable_id = ?) OR (spree_zone_members.zoneable_type = 'Spree::State' AND spree_zone_members.zoneable_id = ?)", address.country_id, address.state_id). + references(:zones) + + ['state', 'country'].each do |zone_kind| + if match = matches.detect { |zone| zone_kind == zone.kind } + return match + end + end + matches.first + end def kind - return nil if members.empty? || members.any? { |member| member.try(:zoneable_type).nil? } - members.last.zoneable_type.demodulize.downcase + if members.any? && !members.any? { |member| member.try(:zoneable_type).nil? } + members.last.zoneable_type.demodulize.underscore + end end def kind=(value) @@ -25,7 +46,6 @@ def kind=(value) def include?(address) return false unless address - # NOTE: This is complicated by the fact that include? for HMP is broken in Rails 2.1 (so we use awkward index method) members.any? do |zone_member| case zone_member.zoneable_type when 'Spree::Country' @@ -38,30 +58,13 @@ def include?(address) end end - # Returns the matching zone with the highest priority zone type (State, Country, Zone.) - # Returns nil in the case of no matches. - def self.match(address) - return unless matches = self.includes(:zone_members).order('zone_members_count', 'created_at').select { |zone| zone.include? address } - - ['state', 'country'].each do |zone_kind| - if match = matches.detect { |zone| zone_kind == zone.kind } - return match - end - end - matches.first - end - # convenience method for returning the countries contained within a zone def country_list - @countries ||= - case kind - when 'country' - zoneables - when 'state' - zoneables.collect(&:country) - else - nil - end.flatten.compact.uniq + @countries ||= case kind + when 'country' then zoneables + when 'state' then zoneables.collect(&:country) + else [] + end.flatten.compact.uniq end def <=>(other) @@ -71,11 +74,31 @@ def <=>(other) # All zoneables belonging to the zone members. Will be a collection of either # countries or states depending on the zone type. def zoneables - members.collect(&:zoneable) + members.includes(:zoneable).collect(&:zoneable) end - def self.default_tax - where(:default_tax => true).first + def country_ids + if kind == 'country' + members.pluck(:zoneable_id) + else + [] + end + end + + def state_ids + if kind == 'state' + members.pluck(:zoneable_id) + else + [] + end + end + + def country_ids=(ids) + set_zone_members(ids, 'Spree::Country') + end + + def state_ids=(ids) + set_zone_members(ids, 'Spree::State') end # Indicates whether the specified zone falls entirely within the zone performing @@ -85,13 +108,9 @@ def contains?(target) return false if zone_members.empty? || target.zone_members.empty? if kind == target.kind - target.zoneables.each do |target_zoneable| - return false unless zoneables.include?(target_zoneable) - end + return false if (target.zoneables.collect(&:id) - zoneables.collect(&:id)).present? else - target.zoneables.each do |target_state| - return false unless zoneables.include?(target_state.country) - end + return false if (target.zoneables.collect(&:country).collect(&:id) - zoneables.collect(&:id)).present? end true end @@ -99,16 +118,22 @@ def contains?(target) private def remove_defunct_members - zone_members.each do |zone_member| - zone_member.destroy if zone_member.zoneable_id.nil? || zone_member.zoneable_type != "Spree::#{kind.capitalize}" + if zone_members.any? + zone_members.where('zoneable_id IS NULL OR zoneable_type != ?', "Spree::#{kind.classify}").destroy_all end end def remove_previous_default - return unless default_tax + Spree::Zone.where('id != ?', self.id).update_all(default_tax: false) if default_tax + end - self.class.all.each do |zone| - zone.update_column 'default_tax', false unless zone == self + def set_zone_members(ids, type) + zone_members.destroy_all + ids.reject{ |id| id.blank? }.map do |id| + member = ZoneMember.new + member.zoneable_type = type + member.zoneable_id = id + members << member end end end diff --git a/core/app/models/spree/zone_member.rb b/core/app/models/spree/zone_member.rb index 84d6f2693b4..8694c5a85fc 100644 --- a/core/app/models/spree/zone_member.rb +++ b/core/app/models/spree/zone_member.rb @@ -1,9 +1,7 @@ module Spree - class ZoneMember < ActiveRecord::Base - belongs_to :zone, :counter_cache => true - belongs_to :zoneable, :polymorphic => true - - attr_accessible :zone, :zone_id, :zoneable, :zoneable_id, :zoneable_type + class ZoneMember < Spree::Base + belongs_to :zone, class_name: 'Spree::Zone', counter_cache: true, inverse_of: :zone_members + belongs_to :zoneable, polymorphic: true def name return nil if zoneable.nil? diff --git a/core/app/views/layouts/spree/base_mailer.html.erb b/core/app/views/layouts/spree/base_mailer.html.erb new file mode 100644 index 00000000000..21ec88ea0b1 --- /dev/null +++ b/core/app/views/layouts/spree/base_mailer.html.erb @@ -0,0 +1,784 @@ + + + + + + + + + + + + + +
    +
    + <%= render partial: 'spree/shared/base_mailer_header' %> + + + + +
    + + + + +
    + + + + + +
    + <%= yield %> +
    +
    + <%= render partial: 'spree/shared/base_mailer_footer' %> + +
    +
    +
    + + diff --git a/core/app/views/spree/address/_form.html.erb b/core/app/views/spree/address/_form.html.erb deleted file mode 100644 index 9aa4b10e523..00000000000 --- a/core/app/views/spree/address/_form.html.erb +++ /dev/null @@ -1,75 +0,0 @@ -<% address_id = address_type.chars.first %> -
    > -

    > - <%= form.label :firstname, t(:first_name) %>*
    - <%= form.text_field :firstname, :class => 'required' %> -

    -

    > - <%= form.label :lastname, t(:last_name) %>*
    - <%= form.text_field :lastname, :class => 'required' %> -

    - <% if Spree::Config[:company] %> -

    > - <%= form.label :company, t(:company) %>
    - <%= form.text_field :company %> -

    - <% end %> -

    > - <%= form.label :address1, t(:street_address) %>*
    - <%= form.text_field :address1, :class => 'required' %> -

    -

    > - <%= form.label :address2, t(:street_address_2) %>
    - <%= form.text_field :address2 %> -

    -

    > - <%= form.label :city, t(:city) %>*
    - <%= form.text_field :city, :class => 'required' %> -

    -

    > - <%= form.label :country_id, t(:country) %>*
    - > - <%= form.collection_select :country_id, available_countries, :id, :name, {}, {:class => 'required'} %> - -

    - - <% if Spree::Config[:address_requires_state] %> -

    > - <% have_states = !address.country.states.empty? %> - <%= form.label :state, t(:state) %>>*
    - - <% state_elements = [ - form.collection_select(:state_id, address.country.states, - :id, :name, - {:include_blank => true}, - {:class => have_states ? 'required' : 'hidden', - :disabled => !have_states}) + - form.text_field(:state_name, - :class => !have_states ? 'required' : 'hidden', - :disabled => have_states) - ].join.gsub('"', "'").gsub("\n", "") - %> - <%= javascript_tag do -%> - document.write("<%== state_elements %>"); - <% end %> -

    - - <% end %> - -

    > - <%= form.label :zipcode, t(:zip) %>*
    - <%= form.text_field :zipcode, :class => 'required' %> -

    -

    > - <%= form.label :phone, t(:phone) %>*
    - <%= form.phone_field :phone, :class => 'required' %> -

    - <% if Spree::Config[:alternative_shipping_phone] %> -

    > - <%= form.label :alternative_phone, t(:alternative_phone) %>
    - <%= form.phone_field :alternative_phone %> -

    - <% end %> -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/adjustments/_adjustments_table.html.erb b/core/app/views/spree/admin/adjustments/_adjustments_table.html.erb deleted file mode 100644 index c50dcc3ce20..00000000000 --- a/core/app/views/spree/admin/adjustments/_adjustments_table.html.erb +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - <% @order.adjustments.each do |adjustment| - @edit_url = edit_admin_order_adjustment_path(@order, adjustment) - @delete_url = admin_order_adjustment_path(@order, adjustment) - %> - - - - - - - <% end %> - -
    <%= "#{t('spree.date')}/#{t('spree.time')}" %><%= t(:description) %><%= t(:amount) %>
    <%= pretty_time(adjustment.created_at) %><%= adjustment.label %><%= adjustment.display_amount %> - <%= link_to_edit adjustment, :no_text => true %> - <%= link_to_delete adjustment, :no_text => true %> -
    diff --git a/core/app/views/spree/admin/adjustments/_form.html.erb b/core/app/views/spree/admin/adjustments/_form.html.erb deleted file mode 100644 index c7f9c4f9ab0..00000000000 --- a/core/app/views/spree/admin/adjustments/_form.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -
    -
    - <%= f.field_container :amount do %> - <%= f.label :amount, raw(t(:amount) + content_tag(:span, " *", :class => "required")) %> - <%= text_field :adjustment, :amount, :class => 'fullwidth' %> - <%= f.error_message_on :amount %> - <% end %> -
    -
    - <%= f.field_container :label do %> - <%= f.label :label, raw(t(:description) + content_tag(:span, " *", :class => "required")) %> - <%= text_field :adjustment, :label, :class => 'fullwidth' %> - <%= f.error_message_on :label %> - <% end %> -
    -
    diff --git a/core/app/views/spree/admin/adjustments/edit.html.erb b/core/app/views/spree/admin/adjustments/edit.html.erb deleted file mode 100644 index 16d226d8a35..00000000000 --- a/core/app/views/spree/admin/adjustments/edit.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Adjustments' } %> - -<% content_for :page_title do %> - <%= t(:edit) %> <%= t(:adjustment) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_adjustments_list), spree.admin_order_adjustments_url(@order), :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @adjustment } %> -<%= form_for @adjustment, :url => admin_order_adjustment_path(@order, @adjustment), :method => :put do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button t(:continue), 'icon-arrow-right' %> - <%= t(:or) %> - <%= link_to_with_icon 'icon-remove', t('actions.cancel'), admin_order_adjustments_url(@order), :class => 'button' %> -
    -
    -<% end %> diff --git a/core/app/views/spree/admin/adjustments/index.html.erb b/core/app/views/spree/admin/adjustments/index.html.erb deleted file mode 100644 index ed3f6b96b1b..00000000000 --- a/core/app/views/spree/admin/adjustments/index.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Adjustments' } %> - -<% content_for :page_title do %> - <%= t(:adjustments) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:new_adjustment), new_admin_order_adjustment_url(@order), :icon => 'icon-plus' %>
  • -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => 'adjustments_table' %> - -<%= button_link_to t(:continue), @order.cart? ? new_admin_order_payment_url(@order) : admin_orders_url, :icon => 'icon-arrow-right' %> diff --git a/core/app/views/spree/admin/adjustments/new.html.erb b/core/app/views/spree/admin/adjustments/new.html.erb deleted file mode 100644 index 5402bd09764..00000000000 --- a/core/app/views/spree/admin/adjustments/new.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Adjustments' } %> - -<% content_for :page_title do %> - <%= t(:new_adjustment) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_adjustments_list), spree.admin_order_adjustments_url(@order), :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @adjustment } %> - -<%= form_for @adjustment, :url => admin_order_adjustments_path do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button t(:continue), 'icon-arrow-right' %> - <%= t(:or) %> - <%= button_link_to t('actions.cancel'), admin_order_adjustments_url(@order), :icon => 'icon-remove' %> -
    -
    -<% end %> diff --git a/core/app/views/spree/admin/banners/_gateway.html.erb b/core/app/views/spree/admin/banners/_gateway.html.erb deleted file mode 100644 index 5deb7682a64..00000000000 --- a/core/app/views/spree/admin/banners/_gateway.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<% if !try_spree_current_user.try(:dismissed_banner?, :gateway) && - Spree::PaymentMethod.production.where("type != 'Spree::Gateway::Bogus'").empty? %> - - - - - -<% end %> diff --git a/core/app/views/spree/admin/countries/_form.html.erb b/core/app/views/spree/admin/countries/_form.html.erb deleted file mode 100644 index b0e2239f413..00000000000 --- a/core/app/views/spree/admin/countries/_form.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -
    -
    -
    - <%= f.label :name, t(:name) %> - <%= f.text_field :name, :class => 'fullwidth' %> -
    -
    -
    -
    - <%= f.label :iso_name, t(:iso_name) %> - <%= f.text_field :iso_name, :class => 'fullwidth' %> -
    -
    -
    -
    - -
    -
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/countries/edit.html.erb b/core/app/views/spree/admin/countries/edit.html.erb deleted file mode 100644 index bc7f70208e6..00000000000 --- a/core/app/views/spree/admin/countries/edit.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_country) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_countries_list), spree.admin_countries_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @country } %> - -<%= form_for [:admin, @country] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/countries/index.html.erb b/core/app/views/spree/admin/countries/index.html.erb deleted file mode 100644 index cfd4941172a..00000000000 --- a/core/app/views/spree/admin/countries/index.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:listing_countries) %> -<% end %> - - - - - - - - - - - - - - - - - - <% @countries.each do |country| %> - - - - - - <% end %> - -
    <%= t(:name) %><%= t(:iso_name) %><%= t(:states_required) %>
    <%= country.name %><%= country.iso_name %><%= country.states_required.to_s.titleize %> - <%= link_to_edit country, :no_text => true %> -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/general_settings/edit.html.erb b/core/app/views/spree/admin/general_settings/edit.html.erb deleted file mode 100644 index f52144667a9..00000000000 --- a/core/app/views/spree/admin/general_settings/edit.html.erb +++ /dev/null @@ -1,84 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:general_settings) %> -<% end %> - -<%= form_tag admin_general_settings_path, :method => :put do %> -
    - -
    - <% @preferences_general.each do |key| - type = Spree::Config.preference_type(key) %> -
    - <%= label_tag(key, t(key) + ': ') + tag(:br) if type != :boolean %> - <%= preference_field_tag(key, Spree::Config[key], :type => type) %> - <%= label_tag(key, t(key)) + tag(:br) if type == :boolean %> -
    - <% end %> - - -
    -
    -
    - <%= t(:security_settings)%> - <% @preferences_security.each do |key| - type = Spree::Config.preference_type(key) %> -
    - <%= label_tag(key, t(key) + ': ') + tag(:br) if type != :boolean %> - <%= preference_field_tag(key, Spree::Config[key], :type => type) %> - <%= label_tag(key, t(key)) + tag(:br) if type == :boolean %> -
    - <% end %> -
    -
    -
    -
    - <%= t(:currency_settings)%> - <% @preferences_currency.each do |key| - type = Spree::Config.preference_type(key) %> -
    - <%= label_tag(key, t(key) + ': ') + tag(:br) if type != :boolean %> - <%= preference_field_tag(key, Spree::Config[key], :type => type) %> - <%= label_tag(key, t(key)) + tag(:br) if type == :boolean %> -
    - <% end %> -
    - <%= label_tag :currency, t(:choose_currency) %>
    - <%= select_tag :currency, currency_options, :class => 'fullwidth' %> -
    -
    - <%= label_tag t(:currency_symbol_position) %>
    -
    -
      -
    • - <%= radio_button_tag :currency_symbol_position, "before" %> - <%= label_tag :currency_symbol_position_before, Spree::Money.new(10, :symbol_position => "before") %> -
    • -
    • - <%= radio_button_tag :currency_symbol_position, "after" %> - <%= label_tag :currency_symbol_position_after, Spree::Money.new(10, :symbol_position => "after") %> -
    • -
    -
    -
    -
    -
    -
    - -
    - <%= button t(:update), 'icon-refresh' %> - <%= t(:or) %> - <%= link_to_with_icon 'icon-remove', t(:cancel), admin_general_settings_url, :class => 'button' %> -
    - -
    - -
    - - -<% end %> - - diff --git a/core/app/views/spree/admin/image_settings/edit.html.erb b/core/app/views/spree/admin/image_settings/edit.html.erb deleted file mode 100644 index 1fda48e7827..00000000000 --- a/core/app/views/spree/admin/image_settings/edit.html.erb +++ /dev/null @@ -1,118 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:image_settings) %> -<% end %> - -<%= form_tag admin_image_settings_path, :method => :put do %> - -
    -
    - <%= t(:general_settings)%> - -
    -
    <%= t(:image_settings_warning) %>
    -
    - -
    - <%= label_tag 'preferences[attachment_path]', t(:attachment_path) %> - <%= preference_field_tag 'preferences[attachment_path]', Spree::Config[:attachment_path], :type => :string %> -
    - - -
    -
    - <%= label_tag 'preferences[attachment_default_url]', t(:attachment_default_url) %> - <%= preference_field_tag 'preferences[attachment_default_url]', Spree::Config[:attachment_default_url], :type => :string %> -
    -
    -
    -
    - <%= label_tag 'preferences[attachment_default_style]', t(:attachment_default_style) %> - <%= collection_select 'preferences', 'attachment_default_style', @styles, :first, :first, {:selected => Spree::Config[:attachment_default_style] }, :class => 'select2 fullwidth' %> -
    -
    - -
    - -
    - <%= preference_field_tag 'preferences[use_s3]', Spree::Config[:use_s3], :type => :boolean %> - <%= label_tag 'preferences[use_s3]', t(:use_s3) %> -
    - -
    - -
    - <%= t(:attachment_styles) %> - -
    - <% @styles.each_with_index do |(style_name, style_value), index| %> -
    - <%= label_tag "attachment_styles[#{style_name}]", style_name %> - - <%= text_field_tag "attachment_styles[#{style_name}]", style_value, :class => 'fullwidth' %> -
    - <% end %> -
    - -
    - -
    - <%= link_to_with_icon 'icon-plus', t(:add_new_style), '#', :class => 'add_new_style button' %> -
    -
    - -
    - - - -
    - - -
    - <%= button t(:update), 'icon-refresh' %> -
    -
    - -<% end %> diff --git a/core/app/views/spree/admin/images/_form.html.erb b/core/app/views/spree/admin/images/_form.html.erb deleted file mode 100644 index b003e806d38..00000000000 --- a/core/app/views/spree/admin/images/_form.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -
    -
    -
    - <%= f.label t(:filename) %>
    - <%= f.file_field :attachment %> -
    -
    - <%= f.label Spree::Variant.model_name.human %>
    - <%= f.select :viewable_id, @variants, {}, {:class => 'select2 fullwidth'} %> -
    -
    -
    - <%= f.label t(:alt_text) %>
    - <%= f.text_area :alt, :rows => 4, :class => 'fullwidth' %> -
    -
    - -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/images/edit.html.erb b/core/app/views/spree/admin/images/edit.html.erb deleted file mode 100644 index c4035048242..00000000000 --- a/core/app/views/spree/admin/images/edit.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Images' } %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @image } %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_images_list), admin_product_images_url(@product), :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= form_for [:admin, @product, @image], :html => { :multipart => true } do |f| %> -
    - <%= @image.attachment_file_name%> -
    - <%= f.label t(:thumbnail) %>
    - <%= link_to image_tag(@image.attachment.url(:small)), @image.attachment.url(:product) %> -
    -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    -
    -
    - <%= button t(:update), 'icon-refresh' %> - <%= t(:or) %> - <%= link_to t(:cancel), admin_product_images_url(@product), :id => 'cancel_link', :class => 'button icon-remove' %> -
    -
    -<% end %> diff --git a/core/app/views/spree/admin/images/index.html.erb b/core/app/views/spree/admin/images/index.html.erb deleted file mode 100644 index 5299d9ce40f..00000000000 --- a/core/app/views/spree/admin/images/index.html.erb +++ /dev/null @@ -1,81 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => 'Images'} %> - -<% content_for :page_actions do %> -
  • <%= link_to_with_icon('icon-plus', t(:new_image), new_admin_product_image_url(@product), :id => 'new_image_link', :class => 'button') %>
  • -<% end %> - -
    - -<% unless @product.images.any? %> -
    - <%= t(:no_images_found) %>. -
    -<% else %> - - - - - <% if @product.has_variants? %> - - <% end %> - - - - - - - <% if @product.has_variants? %> - - <% end %> - - - - - - - <% @product.images.each do |image| %> - - - - <% if @product.has_variants? %> - - <% end %> - - - - <% end %> - - <% @product.variants.each do |variant| %> - <% variant.images.each do |image| %> - - - - <% if @product.has_variants? %> - - <% end %> - - - - <% end %> - <% end %> - -
    <%= t(:thumbnail) %><%= Spree::Variant.model_name.human %><%= t(:alt_text) %>
    - - - <%= link_to image_tag(image.attachment.url(:mini)), image.attachment.url(:product) %> - <%= t(:all) %><%= image.alt %> - <%= link_to_with_icon 'icon-edit', t(:edit), edit_admin_product_image_url(@product, image), :no_text => true, :data => {:action => 'edit'} %> - <%= link_to_delete image, { :url => admin_product_image_url(@product, image), :no_text => true } %> -
    - - - <%= link_to image_tag(image.attachment.url(:mini)), image.attachment.url(:product) %> - <%= variant.options_text %> - <%= image.alt %> - - <%= link_to_with_icon 'icon-edit', t(:edit), edit_admin_product_image_url(@product, image), :no_text => true, :data => {:action => 'edit'} %> - <%= link_to_delete image, {:url => admin_product_image_url(@product, image), :no_text => true }%> -
    -<% end %> \ No newline at end of file diff --git a/core/app/views/spree/admin/images/new.html.erb b/core/app/views/spree/admin/images/new.html.erb deleted file mode 100644 index e8c3fee0d36..00000000000 --- a/core/app/views/spree/admin/images/new.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= form_for [:admin, @product, @image], :html => { :multipart => true } do |f| %> -
    - <%= t(:new_image) %> - - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button t(:update), 'icon-refresh' %> - <%= t(:or) %> - <%= link_to_with_icon 'icon-remove', t(:cancel), admin_product_images_url(@product), :id => 'cancel_link', :class => 'button' %> -
    -
    -<% end %> - -<%= javascript_include_tag 'admin/images/new.js' %> \ No newline at end of file diff --git a/core/app/views/spree/admin/inventory_settings/edit.html.erb b/core/app/views/spree/admin/inventory_settings/edit.html.erb deleted file mode 100644 index e7989ade21e..00000000000 --- a/core/app/views/spree/admin/inventory_settings/edit.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:inventory_settings) %> -<% end %> - -<%= form_tag admin_inventory_settings_path, :method => :put do %> - -
    - <%= hidden_field_tag 'preferences[show_zero_stock_products]', '0' %> - <%= check_box_tag('preferences[show_zero_stock_products]', "1", Spree::Config[:show_zero_stock_products]) %> - <%= t(:show_out_of_stock_products) %> -
    - -
    - <%= hidden_field_tag 'preferences[allow_backorders]', '0' %> - <%= check_box_tag('preferences[allow_backorders]', "1", Spree::Config[:allow_backorders]) %> - <%= t(:allow_backorders) %> -
    - -
    - <%= button t(:update), 'icon-refresh' %> -
    - -<% end %> diff --git a/core/app/views/spree/admin/inventory_settings/show.html.erb b/core/app/views/spree/admin/inventory_settings/show.html.erb deleted file mode 100644 index 257543794be..00000000000 --- a/core/app/views/spree/admin/inventory_settings/show.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:inventory_settings) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= link_to_with_icon 'icon-edit', t(:edit), edit_admin_inventory_settings_path, :id => 'admin_inventory_settings_link', :class => 'button' %> -
  • -<% end %> - -
    -
    <%= t(:products_with_zero_inventory_display, :not => show_not(Spree::Config[:show_zero_stock_products])) %>
    -
    - -
    <%= t(:backordering_is_allowed, :not => show_not(Spree::Config[:allow_backorders])) %>
    -
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/inventory_units/adjust.html.erb b/core/app/views/spree/admin/inventory_units/adjust.html.erb deleted file mode 100644 index c4e4929b3ce..00000000000 --- a/core/app/views/spree/admin/inventory_units/adjust.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @level } %> - -<%= form_tag do %> -

    - -

    <%= t(:inventory_adjustment) %>

    - - - - - - - - - - - - - - - - - - - - -
    <%= t(:sku) %><%= t(:product) %><%= t(:options) %><%= t(:current) %><%= t(:adjustment) %>
    <%= @variant.sku %><%= @variant.product.name %><%= variant_options @variant %><%= on_hand(@variant) %> - <%= text_field :level, :adjustment, :class => 'quantity' %> -
    -<%= submit_tag t(:update) %> -<%= t(:or) %> -<%= link_to t(:cancel), :controller => 'overview', :action => :index %> -<% end %> diff --git a/core/app/views/spree/admin/mail_methods/_form.html.erb b/core/app/views/spree/admin/mail_methods/_form.html.erb deleted file mode 100644 index cbf48393dac..00000000000 --- a/core/app/views/spree/admin/mail_methods/_form.html.erb +++ /dev/null @@ -1,91 +0,0 @@ -
    -
    - -
    -
    - <%= t(:general) %> -
    - <%= f.label :environment, t(:environment) %>
    - <%= f.collection_select(:environment, - Rails.configuration.database_configuration.keys.sort, - :to_s, :titleize, - {}, {:id => 'gtwy-env', :class => 'select2 fullwidth'}) %> -
    - -
    - <%= f.check_box :preferred_enable_mail_delivery, {:style => "vertical-align:middle;"} %> - <%= f.label :preferred_enable_mail_delivery, t(:enable_mail_delivery) %> -
    - -
    - <%= f.label :preferred_mails_from, t(:send_mails_as) %>
    - <%= f.text_field :preferred_mails_from, :maxlength => 256, :class => 'fullwidth' %> -
    - - <%= t(:smtp_send_all_emails_as_from_following_address) %> - -
    - -
    - <%= f.label :preferred_mail_bcc, t(:send_copy_of_all_mails_to) %>
    - <%= f.text_field :preferred_mail_bcc, :maxlength => 256, :class => 'fullwidth' %> -
    - - <%= t(:smtp_send_copy_to_this_addresses) %> - -
    - -
    - <%= f.label :preferred_intercept_email, t(:intercept_email_address) %>
    - <%= f.text_field :preferred_intercept_email, :maxlength => 256, :class => 'fullwidth' %> -
    - - <%= t(:intercept_email_instructions) %> - -
    -
    -
    - -
    -
    - <%= t(:smtp) %> -
    - <%= f.label :preferred_mail_domain, t(:smtp_domain) %>
    - <%= f.text_field :preferred_mail_domain, :class => 'fullwidth' %> -
    -
    - <%= f.label :preferred_mail_host, t(:smtp_mail_host) %>
    - <%= f.text_field :preferred_mail_host, :class => 'fullwidth' %> -
    -
    - <%= f.label :preferred_mail_port, t(:smtp_port) %>
    - <%= f.text_field :preferred_mail_port, :class => 'fullwidth' %> -
    -
    - <%= f.label :preferred_secure_connection_type, t(:secure_connection_type) %>
    - <%= f.collection_select(:preferred_secure_connection_type, - Spree::MailMethod::SECURE_CONNECTION_TYPES, - :to_s, :to_s, - {}, {'class' => 'select2 fullwidth'}) %> -
    -
    - <%= f.label :preferred_mail_auth_type, t(:smtp_authentication_type) %>
    - <%= f.collection_select(:preferred_mail_auth_type, - Spree::MailMethod::MAIL_AUTH, - :to_s, :to_s, - {}, {'class' => 'select2 fullwidth'}) %> -
    -
    - <%= f.label :preferred_smtp_username, t(:smtp_username) %>
    - <%= f.text_field :preferred_smtp_username, :class => 'fullwidth' %> -
    -
    - <%= f.label :preferred_smtp_password, t(:smtp_password) %>
    - <%= f.password_field :preferred_smtp_password, :class => 'fullwidth' %> -
    -
    -
    - - -
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/mail_methods/edit.html.erb b/core/app/views/spree/admin/mail_methods/edit.html.erb deleted file mode 100644 index 3ffe3deb724..00000000000 --- a/core/app/views/spree/admin/mail_methods/edit.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_mail_method) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= link_to_with_icon 'icon-arrow-left', t(:back_to_mail_methods_list), admin_mail_methods_path, :class => 'button' %>
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @mail_method } %> - -<%= form_for [:admin, @mail_method] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    <%= button t(:update), 'icon-refresh' %>
    -
    -<% end %> diff --git a/core/app/views/spree/admin/mail_methods/index.html.erb b/core/app/views/spree/admin/mail_methods/index.html.erb deleted file mode 100644 index 1c8d06b9670..00000000000 --- a/core/app/views/spree/admin/mail_methods/index.html.erb +++ /dev/null @@ -1,41 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:mail_methods) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_mail_method), new_object_url, :icon => 'icon-plus', :id => 'admin_new_mail_method_link' %> -
  • -<% end %> - -<% unless @mail_methods.any? %> -
    <%= t(:no_mail_methods_defined)%>
    -<% else %> - - - - - - - - - - - - - <% @mail_methods.each do |method|%> - - - - - - <% end %> - -
    <%= t(:environment) %><%= t(:active) %>
    <%= method.environment.to_s.titleize %><%= method.active ? t(:yes) : t(:no) %> - <%= link_to_edit method, :no_text => true%> - <%= link_to_delete method, :no_text => true %> - <%= link_to_with_icon 'icon-envelope-alt', '', testmail_admin_mail_method_path(method), :method => :post, :title => t('admin.mail_methods.send_testmail'), :class => 'send_mail button no-text' %> -
    -<% end%> \ No newline at end of file diff --git a/core/app/views/spree/admin/mail_methods/new.html.erb b/core/app/views/spree/admin/mail_methods/new.html.erb deleted file mode 100644 index 9b39a98f0b1..00000000000 --- a/core/app/views/spree/admin/mail_methods/new.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_mail_method) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= link_to_with_icon 'icon-arrow-left', t(:back_to_mail_methods_list), admin_mail_methods_path, :class => 'button' %>
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @mail_method } %> - -<%= form_for [:admin, @mail_method] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button t(:create), 'icon-ok' %> -
    -
    -<% end %> diff --git a/core/app/views/spree/admin/option_types/_form.html.erb b/core/app/views/spree/admin/option_types/_form.html.erb deleted file mode 100644 index 061f6b9d9ee..00000000000 --- a/core/app/views/spree/admin/option_types/_form.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
    -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> *
    - <%= f.text_field :name, :class => "fullwidth" %> - <%= f.error_message_on :name %> - <% end %> -
    - -
    - <%= f.field_container :presentation do %> - <%= f.label :presentation, t(:presentation) %> *
    - <%= f.text_field :presentation, :class => "fullwidth" %> - <%= f.error_message_on :presentation %> - <% end %> -
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/option_types/_option_value_fields.html.erb b/core/app/views/spree/admin/option_types/_option_value_fields.html.erb deleted file mode 100644 index 7fca1f1a6e2..00000000000 --- a/core/app/views/spree/admin/option_types/_option_value_fields.html.erb +++ /dev/null @@ -1,9 +0,0 @@ - - - - <%= f.hidden_field :id %> - - <%= f.text_field :name %> - <%= f.text_field :presentation %> - <%= link_to_remove_fields t(:remove), f, :no_text => true %> - diff --git a/core/app/views/spree/admin/option_types/edit.html.erb b/core/app/views/spree/admin/option_types/edit.html.erb deleted file mode 100644 index 38d5ee46bde..00000000000 --- a/core/app/views/spree/admin/option_types/edit.html.erb +++ /dev/null @@ -1,50 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_option_type) %> "<%= @option_type.name %>" -<% end %> - -<% content_for :page_actions do %> -
  • - - <%= link_to_add_fields t(:add_option_value), "tbody#option_values", :class => 'button icon-plus' %> - -
  • -
  • - <%= button_link_to t(:back_to_option_types_list), spree.admin_option_types_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @option_type } %> - -<%= form_for [:admin, @option_type] do |f| %> -
    - <%= t(:option_values) %> - - <%= render :partial => 'form', :locals => { :f => f } %> - - - - - - - - - - - <% if @option_type.option_values.empty? %> - - - - - <% else %> - <%= f.fields_for :option_values do |option_value_form| %> - <%= render :partial => 'option_value_fields', :locals => { :f => option_value_form } %> - <% end %> - <% end %> - -
    <%= t(:name) %><%= t(:display) %>
    <%= t(:none) %>
    - - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/option_types/index.html.erb b/core/app/views/spree/admin/option_types/index.html.erb deleted file mode 100644 index 6bbcc357eab..00000000000 --- a/core/app/views/spree/admin/option_types/index.html.erb +++ /dev/null @@ -1,40 +0,0 @@ -<% content_for :page_title do %> - <%= t(:option_types) %> -<% end %> - -<% content_for :page_actions do %> - -<% end %> - -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -
    - - - - - - - - - - - - - - - - <% @option_types.each do |option_type| %> - - - - - - <% end %> - -
    <%= t(:name) %><%= t(:presentation) %>
    <%= option_type.name %><%= option_type.presentation %> - <%= link_to_edit(option_type, :class => 'admin_edit_option_type', :no_text => true) %> - <%= link_to_delete(option_type, :no_text => true) %> -
    diff --git a/core/app/views/spree/admin/option_types/new.html.erb b/core/app/views/spree/admin/option_types/new.html.erb deleted file mode 100644 index 2ffd720e563..00000000000 --- a/core/app/views/spree/admin/option_types/new.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @option_type } %> - -<%= form_for [:admin, @option_type] do |f| %> -
    - <%= t(:new_option_type) %> - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/orders/_add_product.html.erb b/core/app/views/spree/admin/orders/_add_product.html.erb deleted file mode 100644 index 3bd2b3b660b..00000000000 --- a/core/app/views/spree/admin/orders/_add_product.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<%= render :partial => "spree/admin/variants/autocomplete", :formats => :js %> - -
    -
    - <%= t(:add_product) %> - -
    - <%= label_tag :add_product_name, t(:name_or_sku) %> - <%= hidden_field_tag :add_variant_id, "", :class => "variant_autocomplete fullwidth" %> -
    - -
    - <%= label_tag :add_quantity, t(:qty) %> - <%= number_field_tag :add_quantity, 1, :min => 0 %> -
    - -
    - <%= link_to_with_icon 'icon-plus', t(:add), admin_order_line_items_url(@order), - :method => :post, - :id => 'add_line_item_to_order', - :class => 'button fullwidth', - 'data-update' => 'order-form-wrapper' %> -
    - -
    -
    diff --git a/core/app/views/spree/admin/orders/_form.html.erb b/core/app/views/spree/admin/orders/_form.html.erb deleted file mode 100644 index 8964f9b5e6e..00000000000 --- a/core/app/views/spree/admin/orders/_form.html.erb +++ /dev/null @@ -1,67 +0,0 @@ -
    - <% if @line_item.try(:errors).present? %> - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @line_item } %> - <% end %> - - <%= form_for @order, :url => admin_order_url(@order), :method => :put do |f| %> -
    - <%= f.hidden_field :number %> - - - - - - - - - - - - - - - - - - - <%= f.fields_for :line_items do |li_form| %> - <%= render :partial => 'spree/admin/orders/line_item', :locals => { :f => li_form } %> - <% end %> - - - - - - - - - - <% @order.adjustments.eligible.each do |adjustment| %> - - - - - - <% end %> - - - - - - - - -
    <%= t(:item_description) %><%= t(:price) %><%= t(:qty) %><%= t(:total) %>
    <%= t(:subtotal) %>:<%= @order.display_item_total %>
    <%= adjustment.label %><%= adjustment.display_amount %>
    <%= t(:order_total) %>:<%= @order.display_total %>
    - -
    - <%= button t(:update), 'icon-refresh' %> - <%= t(:or) %> - <%= link_to_with_icon 'button icon-arrow-left', t(:back), admin_orders_url %> -
    -
    - <% end %> - - <%= javascript_tag do -%> - <%= render :partial => 'spree/admin/shared/update_order_state', :handlers => [:js] %> - <% end -%> -
    diff --git a/core/app/views/spree/admin/orders/_line_item.html.erb b/core/app/views/spree/admin/orders/_line_item.html.erb deleted file mode 100644 index 774ff41700b..00000000000 --- a/core/app/views/spree/admin/orders/_line_item.html.erb +++ /dev/null @@ -1,9 +0,0 @@ - - <%=f.object.variant.product.name%> <%= "(#{f.object.variant.options_text})" unless f.object.variant.option_values.empty? %> - <%= f.object.variant.display_amount %> - <%= f.number_field :quantity, :min => 0, :class => "qty" %> - <%= f.object.display_amount %> - - <%= link_to_delete f.object, {:url => admin_order_line_item_url(@order.number, f.object), :no_text => true} %> - - diff --git a/core/app/views/spree/admin/orders/customer_details/_autocomplete.js.erb b/core/app/views/spree/admin/orders/customer_details/_autocomplete.js.erb new file mode 100644 index 00000000000..9484fa0dbb3 --- /dev/null +++ b/core/app/views/spree/admin/orders/customer_details/_autocomplete.js.erb @@ -0,0 +1,19 @@ + diff --git a/core/app/views/spree/admin/orders/customer_details/_form.html.erb b/core/app/views/spree/admin/orders/customer_details/_form.html.erb deleted file mode 100644 index 7eb35a72b6e..00000000000 --- a/core/app/views/spree/admin/orders/customer_details/_form.html.erb +++ /dev/null @@ -1,66 +0,0 @@ -
    - -
    - <%= t(:account) %> - -
    -
    -
    - <%= f.label :email, t(:email) + ':' %> - <%= f.email_field :email, :class => 'fullwidth' %> -
    -
    -
    -
    - <%= label_tag nil, t(:guest_checkout) %>: -
      - <% if @order.completed? %> -
    • - <%= @order.user.nil? ? t(:yes) : t(:no) %> -
    • - <% else %> - <% guest = @order.user.nil? || @order.user.anonymous? %> -
    • - <%= radio_button_tag :guest_checkout, true, guest %> - <%= t(:yes) %> -
    • -
    • - <%= radio_button_tag :guest_checkout, false, !guest, :disabled => @order.cart? %> - <%= t(:no) %> -
    • - <%= hidden_field_tag :user_id, @order.user_id %> - <% end %> -
    -
    -
    -
    -
    - -
    -
    - <%= t(:billing_address) %> - <%= f.fields_for :bill_address do |ba_form| %> - <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => ba_form, :name => t(:billing_address), :use_billing => false } %> - <% end %> -
    -
    - -
    -
    - <%= t(:shipping_address) %> - <%= f.fields_for :ship_address do |sa_form| %> - <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => sa_form, :name => t(:shipping_address), :use_billing => true } %> - <% end %> -
    -
    - -
    - -
    - <%= button @order.cart? ? t(:continue) : t(:update), @order.cart? ? 'icon-arrow-right' : 'icon-refresh' %> -
    - - <% content_for :head do %> - <%= javascript_include_tag states_path, 'admin/address_states.js' %> - <% end %> -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/orders/customer_details/edit.html.erb b/core/app/views/spree/admin/orders/customer_details/edit.html.erb deleted file mode 100644 index 19dfdc29198..00000000000 --- a/core/app/views/spree/admin/orders/customer_details/edit.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Customer Details' } %> - -<%= csrf_meta_tags %> - -<% content_for :page_title do %> - <%= t(:customer_details) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<% if @order.cart? %> -
    -
    - <%= t(:customer_search) %> - <%= label_tag :customer_search, t(:enter_at_least_five_letters) %> - <%= text_field_tag :customer_search, nil, :class => 'fullwidth title' %> -
    -
    -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> - -<%= form_for @order, :url => admin_order_customer_url(@order) do |f| %> - <%= render 'form', :f => f %> -<% end %> - diff --git a/core/app/views/spree/admin/orders/edit.html.erb b/core/app/views/spree/admin/orders/edit.html.erb deleted file mode 100644 index b87a1c17b4d..00000000000 --- a/core/app/views/spree/admin/orders/edit.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -<%= csrf_meta_tags %> -<% content_for :page_actions do %> -
  • <%= event_links %>
  • -
  • <%= button_link_to t(:resend), resend_admin_order_url(@order), :method => :post, :icon => 'icon-email' if @order.user %>
  • -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Order Details' } %> - -
    - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> -
    - -<%= render :partial => 'add_product' %> - -
    -
    - <%= render :partial => 'form', :locals => { :order => @order } %> -
    -
    - -<% content_for :head do %> - <%= javascript_tag 'var expand_variants = true;' %> -<% end %> diff --git a/core/app/views/spree/admin/orders/index.html.erb b/core/app/views/spree/admin/orders/index.html.erb deleted file mode 100644 index 5d6b196a58f..00000000000 --- a/core/app/views/spree/admin/orders/index.html.erb +++ /dev/null @@ -1,132 +0,0 @@ -<% content_for :page_title do %> - <%= t(:listing_orders) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_order), new_admin_order_url, :icon => 'icon-plus', :id => 'admin_new_order' %> -
  • -<% end %> - -<% content_for :table_filter_title do %> - <%= t(:search) %> -<% end %> - -<% content_for :table_filter do %> -
    - <%= search_form_for [:admin, @search] do |f| %> -
    -
    - <%= label_tag nil, t(:date_range) %> -
    - <%= f.text_field :created_at_gt, :class => 'datepicker datepicker-from', :value => params[:q][:created_at_gt], :placeholder => t(:start) %> - - - - - - <%= f.text_field :created_at_lt, :class => 'datepicker datepicker-to', :value => params[:q][:created_at_lt], :placeholder => t(:stop) %> -
    -
    - -
    - <%= label_tag nil, t(:status) %> - <%= f.select :state_eq, Spree::Order.state_machines[:state].states.collect {|s| [t("order_state.#{s.name}"), s.value]}, {:include_blank => true}, :class => 'select2' %> -
    -
    - -
    -
    - <%= label_tag nil, t(:order_number) %> - <%= f.text_field :number_cont %> -
    -
    - <%= label_tag nil, t(:email) %> - <%= f.email_field :email_cont %> -
    -
    - -
    -
    - <%= label_tag nil, t(:first_name_begins_with) %> - <%= f.text_field :bill_address_firstname_start, :size => 25 %> -
    -
    - <%= label_tag nil, t(:last_name_begins_with) %> - <%= f.text_field :bill_address_lastname_start, :size => 25%> -
    -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    -
    - <%= button t(:filter_results), 'icon-search' %> -
    -
    - <% end %> -
    -<% end %> - - - - - - - - - - - - - - - <% if @show_only_completed %> - - <% else %> - - <% end %> - - - - - - - - - - - <% @orders.each do |order| %> - - - - - - - - - - - <% end %> - -
    <%= sort_link @search, :completed_at, t(:completed_at, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :created_at, t(:created_at, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :number, t(:number, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :state, t(:state, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :payment_state, t(:payment_state, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :shipment_state, t(:shipment_state, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :email, t(:email, :scope => 'activerecord.attributes.spree/order') %><%= sort_link @search, :total, t(:total, :scope => 'activerecord.attributes.spree/order') %>
    <%= l (@show_only_completed ? order.completed_at : order.created_at).to_date %><%= link_to order.number, admin_order_path(order) %><%= t("order_state.#{order.state.downcase}") %><%= link_to t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) if order.payment_state %><%= link_to t("shipment_states.#{order.shipment_state}"), admin_order_shipments_path(order) if order.shipment_state %><%= mail_to order.email %><%= order.display_total %> - <%= link_to_edit_url edit_admin_order_path(order), :title => "admin_edit_#{dom_id(order)}", :no_text => true %> -
    - -<%= paginate @orders %> diff --git a/core/app/views/spree/admin/orders/new.html.erb b/core/app/views/spree/admin/orders/new.html.erb deleted file mode 100644 index c21e049462a..00000000000 --- a/core/app/views/spree/admin/orders/new.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<% content_for :page_title do %> - <%= t(:new) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_orders_list), spree.admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Order Details' } %> - -<%= csrf_meta_tags %> - -
    - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> -
    - -<%= render :partial => 'add_product' %> - -<% unless @order.line_items.any? %> -
    -
    - <%= render :partial => 'form' %> -
    -
    -<% else %> -<% end %> - -<% content_for :head do %> - <%= javascript_tag 'var expand_variants = true;' %> -<% end %> diff --git a/core/app/views/spree/admin/orders/show.html.erb b/core/app/views/spree/admin/orders/show.html.erb deleted file mode 100644 index 6faedd0a0a9..00000000000 --- a/core/app/views/spree/admin/orders/show.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:cancel), fire_admin_order_url(@order.number, { :e => 'cancel' }), :icon => 'icon-trash', :data => { :confirm => t(:are_you_sure) } if @order.can_cancel? %> -
  • -
  • - <%= button_link_to t(:edit), edit_admin_order_url(@order.number), :icon => 'icon-edit' %> -
  • -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - - -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Order Details' } %> - -
    - <% if @order.bill_address %> -
    - <%= t(:bill_address) %> -
    - <%= render :partial => 'spree/admin/shared/address', :locals => { :address => @order.bill_address } %> -
    -
    - <% end %> - <% if @order.ship_address %> -
    - <%= t(:ship_address) %> -
    - <%= render :partial => 'spree/admin/shared/address', :locals => { :address => @order.ship_address } %> -
    -
    - <% end %> -
    - -
    - <%= render :partial => 'spree/admin/shared/order_details', :locals => { :order => @order } %> -
    diff --git a/core/app/views/spree/admin/payment_methods/_form.html.erb b/core/app/views/spree/admin/payment_methods/_form.html.erb deleted file mode 100644 index aa35d8c891e..00000000000 --- a/core/app/views/spree/admin/payment_methods/_form.html.erb +++ /dev/null @@ -1,53 +0,0 @@ -<% # Usage of old-style form helpers in this file is INTENTIONAL - # For reasons, see commit 3e981c7. %> -
    - -
    - -
    -
    - <%= f.label :type, t(:provider) %> - <%= collection_select(:payment_method, :type, @providers, :to_s, :name, {}, {:id => 'gtwy-type', :class => 'select2 fullwidth'}) %> - - <% unless @object.new_record? %> - <%= preference_fields(@object, f) %> - - <% if @object.respond_to?(:preferences) %> -
    <%= t(:provider_settings_warning) %>
    - <% end %> - <% end %> -
    -
    - <%= label_tag nil, t(:environment) %> - <%= collection_select(:payment_method, :environment, Rails.configuration.database_configuration.keys.sort, :to_s, :titleize, {}, {:id => 'gtwy-env', :class => 'select2 fullwidth'}) %> -
    -
    - <%= label_tag nil, t(:active) %> -
      -
    • - <%= radio_button :payment_method, :active, true %> - <%= label_tag nil, t(:yes) %> -
    • -
    • - <%= radio_button :payment_method, :active, false %> - <%= label_tag nil, t(:no) %> -
    • -
    -
    -
    - -
    -
    - <%= label_tag nil, t(:name) %> - <%= text_field :payment_method, :name, :class => 'fullwidth' %> -
    -
    - <%= label_tag nil, t(:description) %> - <%= text_area :payment_method, :description, {:cols => 60, :rows => 6, :class => 'fullwidth'} %> -
    -
    - -
    -
    - -
    diff --git a/core/app/views/spree/admin/payment_methods/edit.html.erb b/core/app/views/spree/admin/payment_methods/edit.html.erb deleted file mode 100644 index 637c83a7b1b..00000000000 --- a/core/app/views/spree/admin/payment_methods/edit.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_payment_method) %> <%= @payment_method.name %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_payment_methods_list), spree.admin_payment_methods_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @payment_method } %> - -<%= form_for @payment_method, :url => admin_payment_method_path(@payment_method) do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    - <%= button t(:update), 'icon-refresh' %> -
    -
    - -<% end %> diff --git a/core/app/views/spree/admin/payment_methods/index.html.erb b/core/app/views/spree/admin/payment_methods/index.html.erb deleted file mode 100644 index 9eed288fa83..00000000000 --- a/core/app/views/spree/admin/payment_methods/index.html.erb +++ /dev/null @@ -1,45 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:payment_methods) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_payment_method), new_object_url, :icon => 'icon-plus', :id => 'admin_new_payment_methods_link' %> -
  • -<% end %> - - - - - - - - - - - - - - - - - - - - - <% @payment_methods.each do |method|%> - - - - - - - - <% end %> - -
    <%= t(:name) %><%= t(:provider) %><%= t(:environment) %><%= t(:active) %>
    <%= method.name %><%= method.type %><%= method.environment.to_s.titleize %><%= method.active ? t(:yes) : t(:no) %> - <%= link_to_edit method, :no_text => true %> - <%= link_to_delete method, :no_text => true %> -
    diff --git a/core/app/views/spree/admin/payment_methods/new.html.erb b/core/app/views/spree/admin/payment_methods/new.html.erb deleted file mode 100644 index 112234cb1d5..00000000000 --- a/core/app/views/spree/admin/payment_methods/new.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_payment_method) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_payment_methods_list), admin_payment_methods_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @payment_method } %> - -<%= form_for @payment_method, :url => collection_url do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    - <%= button t(:create), 'icon-ok' %> -
    -
    -<% end %> diff --git a/core/app/views/spree/admin/payments/_form.html.erb b/core/app/views/spree/admin/payments/_form.html.erb deleted file mode 100644 index 0fd71fd8aeb..00000000000 --- a/core/app/views/spree/admin/payments/_form.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -
    -
    -
    - <%= f.label :amount, t(:amount) %> - <%= f.text_field :amount, :value => @order.outstanding_balance, :class => 'fullwidth' %> -
    -
    -
    -
    - -
      - <% @payment_methods.each do |method| %> -
    • - -
    • - <% end %> -
    -
    - <% @payment_methods.each do |method| %> - <%= render :partial => "spree/admin/payments/source_forms/#{method.method_type}", :locals => { :payment_method => method } %> - <% end %> -
    -
    -
    -
    diff --git a/core/app/views/spree/admin/payments/_list.html.erb b/core/app/views/spree/admin/payments/_list.html.erb deleted file mode 100644 index 89e2d6b39e5..00000000000 --- a/core/app/views/spree/admin/payments/_list.html.erb +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - <% payments.each do |payment| %> - - - - - - - - <% end %> - -
    <%= "#{t('spree.date')}/#{t('spree.time')}" %><%= t(:amount, :scope => 'activerecord.attributes.spree/payment') %><%= t(:payment_method) %><%= t(:payment_state) %>
    <%= pretty_time(payment.created_at) %><%= payment.display_amount %><%= link_to payment_method_name(payment), spree.admin_order_payment_path(@order, payment) %> <%= t(payment.state, :scope => :payment_states, :default => payment.state.capitalize) %> - <% payment.actions.each do |action| %> - <%= link_to_with_icon "icon-#{action}", t(action), fire_admin_order_payment_path(@order, payment, :e => action), :method => :put, :no_text => true, :data => {:action => action} %> - <% end %> -
    diff --git a/core/app/views/spree/admin/payments/credit.html.erb b/core/app/views/spree/admin/payments/credit.html.erb deleted file mode 100644 index 8c4529dbb1f..00000000000 --- a/core/app/views/spree/admin/payments/credit.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Creditcards' } %> - -<% content_for :page_title do %> - <%= t(:refund) %> -<% end %> - -<%= form_tag do %> -

    <%= t(:refund) %>

    -
    -

    - <%= label_tag :amount, t(:amount) %> - <%= text_field_tag :amount, @payment.amount %> -

    -

    - <%= button t(:make_refund) %> -

    -
    -<% end %> diff --git a/core/app/views/spree/admin/payments/index.html.erb b/core/app/views/spree/admin/payments/index.html.erb deleted file mode 100644 index 29844d66441..00000000000 --- a/core/app/views/spree/admin/payments/index.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Payments' } %> - -<% content_for :page_actions do %> - <% if @order.outstanding_balance? %> -
  • - <%= button_link_to t(:new_payment), new_admin_order_payment_url(@order), :icon => 'icon-plus' %> -
  • - <% end %> -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<% content_for :page_title do %> - <%= t(:payments) %> -<% end %> - -<% if @order.outstanding_balance? %> -
    <%= @order.outstanding_balance < 0 ? t(:credit_owed) : t(:balance_due) %>: <%= @order.display_outstanding_balance %>
    -<% end %> - -<%= render :partial => 'list', :locals => { :payments => @payments } %> - - -<%= button_link_to t(:continue), admin_orders_url, :icon => 'icon-arrow-right' %> diff --git a/core/app/views/spree/admin/payments/new.html.erb b/core/app/views/spree/admin/payments/new.html.erb deleted file mode 100644 index c9d7a0ef68c..00000000000 --- a/core/app/views/spree/admin/payments/new.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Payments' } %> - -<% content_for :page_title do %> - <%= t(:new) %> <%= t("activerecord.models.#{@payment.class.to_s.underscore}.one") %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_payments_list), spree.admin_order_payments_url(@order), :icon => 'icon-arrow-left' %>
  • -<% end %> - -<% if @payment_methods.any? %> - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @payment } %> - - <%= form_for @payment, :url => admin_order_payments_path(@order) do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button @order.cart? ? t(:continue) : t(:update), @order.cart? ? 'icon-arrow-right' : 'icon-refresh' %> -
    -
    - <% end %> -<% else %> - <%= t(:cannot_create_payment_without_payment_methods) %> - <%= link_to t(:please_define_payment_methods), admin_payment_methods_url %> -<% end %> diff --git a/core/app/views/spree/admin/payments/show.html.erb b/core/app/views/spree/admin/payments/show.html.erb deleted file mode 100644 index 3c63eac162e..00000000000 --- a/core/app/views/spree/admin/payments/show.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Payments' } %> - -<% content_for :page_title do %> - - <%= t("activerecord.models.#{@payment.class.to_s.underscore}.one") %> - - <%= payment_method_name(@payment) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_payments_list), spree.admin_order_payments_url(@order), :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => "spree/admin/payments/source_views/#{@payment.payment_method.method_type}", :locals => { :payment => @payment.source.is_a?(Spree::Payment) ? @payment.source : @payment } %> - -
    -
    <%= label_tag nil, t(:amount) %>: <%= @payment.amount %>
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/payments/source_forms/_gateway.html.erb b/core/app/views/spree/admin/payments/source_forms/_gateway.html.erb deleted file mode 100644 index d3e8626ae85..00000000000 --- a/core/app/views/spree/admin/payments/source_forms/_gateway.html.erb +++ /dev/null @@ -1,50 +0,0 @@ -
    -
    - <% @previous_cards.each do |card| %> - - <% end %> - -
    -
    -
    - <%= image_tag 'credit_cards/credit_card.gif', :id => 'credit-card-image' %> -
    - - <% param_prefix = "payment_source[#{payment_method.id}]" %> - -
    - -
    -
    -
    - <%= label_tag nil, raw(t(:card_number) + content_tag(:span, ' *', :class => 'required')) %> - <%= text_field_tag "#{param_prefix}[number]", '', :class => 'required fullwidth', :maxlength => 19 %> - -
    -
    -
    -
    -
    - <%= label_tag nil, raw(t(:expiration) + content_tag(:span, ' *', :class => 'required')) %>
    - <%= select_month(Date.today, :prefix => param_prefix, :field_name => 'month', :use_month_numbers => true, :class => 'required select2') %> - <%= select_year(Date.today, :prefix => param_prefix, :field_name => 'year', :start_year => Date.today.year, :end_year => Date.today.year + 15, :class => 'required select2') %> -
    -
    -
    -
    - <%= label_tag nil, raw(t(:card_code) + content_tag(:span, ' *', :class => "required")) %> - <%= text_field_tag "#{param_prefix}[verification_value]", '', :class => 'required fullwidth', :size => 5 %> - - (<%= t(:whats_this) %>) - -
    -
    - -
    -
    - -
    diff --git a/core/app/views/spree/admin/payments/source_views/_gateway.html.erb b/core/app/views/spree/admin/payments/source_views/_gateway.html.erb deleted file mode 100644 index 21f434c6492..00000000000 --- a/core/app/views/spree/admin/payments/source_views/_gateway.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -
    - <%= t(:credit_card) %> - -
    -
    -
    -
    <%= t(:card_number) %>:
    -
    <%= payment.source.display_number %>
    - -
    <%= t(:expiration) %>:
    -
    <%= payment.source.month %>/<%= payment.source.year %>
    - -
    <%= t(:card_code) %>:
    -
    <%= payment.source.verification_value %>
    -
    -
    - -
    -
    -
    <%= t(:maestro_or_solo_cards) %>:
    -
    <%= payment.source.issue_number %>
    - -
    <%= t(:start_date) %>:
    -
    <%= payment.source.start_month %>/<%= payment.source.start_year %>
    -
    -
    -
    - -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/product_properties/_product_property_fields.html.erb b/core/app/views/spree/admin/product_properties/_product_property_fields.html.erb deleted file mode 100644 index 5fc6bec99f7..00000000000 --- a/core/app/views/spree/admin/product_properties/_product_property_fields.html.erb +++ /dev/null @@ -1,13 +0,0 @@ - - - <%= f.text_field :property_name, :class => 'autocomplete' %> - - - <%= f.text_field :value, :class => 'autocomplete' %> - - - <% unless @product.properties.empty? %> - <%= link_to_remove_fields t(:remove), f, :no_text => true %> - <% end %> - - diff --git a/core/app/views/spree/admin/product_properties/index.html.erb b/core/app/views/spree/admin/product_properties/index.html.erb deleted file mode 100644 index 48dd2e12fba..00000000000 --- a/core/app/views/spree/admin/product_properties/index.html.erb +++ /dev/null @@ -1,59 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Product Properties' } %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @product } %> - -<% content_for :page_actions do %> -
      -
    • - <%= link_to_add_fields t(:add_product_properties), 'tbody#product_properties', :class => 'icon-plus button' %> -
    • -
    • - - <%= link_to t(:select_from_prototype), available_admin_prototypes_url, :remote => true, 'data-update' => 'prototypes', :class => 'button icon-copy' %> - -
    • -
    -<% end %> - -<%= form_for @product, :url => admin_product_url(@product), :method => :put do |f| %> -
    -
    - -
    - <%= image_tag 'spinner.gif', :plugin => 'spree', :style => 'display:none;', :id => 'busy_indicator' %> - - - - - - - - - - - <%= f.fields_for :product_properties do |pp_form| %> - <%= render :partial => 'product_property_fields', :locals => { :f => pp_form } %> - <% end %> - -
    <%= t(:property) %><%= t(:value) %>
    - - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> - - <%= hidden_field_tag 'clear_product_properties', 'true' %> -
    -<% end %> - -<%= javascript_tag do -%> - var properties = <%= raw(@properties.to_json) %>; - - $("#product_properties input.autocomplete").live("keydown", function(){ - already_auto_completed = $(this).is('ac_input'); - if (!already_auto_completed) { - $(this).autocomplete({source: properties}); - $(this).focus(); - } - }); -<% end -%> - diff --git a/core/app/views/spree/admin/products/_form.html.erb b/core/app/views/spree/admin/products/_form.html.erb deleted file mode 100644 index 8987cb3d459..00000000000 --- a/core/app/views/spree/admin/products/_form.html.erb +++ /dev/null @@ -1,154 +0,0 @@ -
    - -
    - <%= f.field_container :name do %> - <%= f.label :name, raw(t(:name) + content_tag(:span, ' *', :class => 'required')) %> - <%= f.text_field :name, :class => 'fullwidth title' %> - <%= f.error_message_on :name %> - <% end %> - - <%= f.field_container :permalink do %> - <%= f.label :permalink, raw(t(:permalink) + content_tag(:span, ' *', :class => "required")) %> - <%= f.text_field :permalink, :class => 'fullwidth title' %> - <%= f.error_message_on :permalink %> - <% end %> - - <%= f.field_container :description do %> - <%= f.label :description, t(:description) %> - <%= f.text_area :description, {:rows => "#{unless @product.has_variants? then '20' else '13' end}", :class => 'fullwidth'} %> - <%= f.error_message_on :description %> - <% end %> -
    - -
    - <%= f.field_container :price do %> - <%= f.label :price, raw(t(:master_price) + content_tag(:span, ' *', :class => "required")) %> - <%= f.text_field :price, :value => number_to_currency(@product.price, :unit => '') %> - <%= f.error_message_on :price %> - <% end %> - -
    - <%= f.field_container :cost_price do %> - <%= f.label :cost_price, t(:cost_price) %> - <%= f.text_field :cost_price, :value => number_to_currency(@product.cost_price, :unit => '') %> - <%= f.error_message_on :cost_price %> - <% end %> -
    -
    - <%= f.field_container :cost_currency do %> - <%= f.label :cost_currency, t(:cost_currency) %> - <%= f.text_field :cost_currency %> - <%= f.error_message_on :cost_currency %> - <% end %> -
    - -
    - - <%= f.field_container :available_on do %> - <%= f.label :available_on, t(:available_on) %> - <%= f.error_message_on :available_on %> - <% if @product.available_on? %> - <% available_on = l(@product.available_on, :format => t('spree.date_picker.format')) %> - <% else %> - <% available_on = nil %> - <% end %> - <%= f.text_field :available_on, :value => available_on, :class => 'datepicker' %> - <% end %> - - <% unless @product.has_variants? %> - <%= f.field_container :sku do %> - <%= f.label :sku, t(:sku) %> - <%= f.text_field :sku, :size => 16 %> - <% end %> - - <% if Spree::Config[:track_inventory_levels] %> -
    - <%= f.field_container :on_hand do %> - <%= f.label :on_hand, t(:on_hand) %> - <%= f.number_field :on_hand, :min => 0 %> - <% end %> -
    -
    - <%= f.field_container :on_demand, :class => ['checkbox'] do %> - - <% end %> -
    - -
    - <% end %> - -
      -
    • - <%= f.label :weight, t(:weight) %> - <%= f.text_field :weight, :size => 4 %> -
    • -
    • - <%= f.label :height, t(:height) %> - <%= f.text_field :height, :size => 4 %> -
    • -
    • - <%= f.label :width, t(:width) %> - <%= f.text_field :width, :size => 4 %> -
    • -
    • - <%= f.label :depth, t(:depth) %> - <%= f.text_field :depth, :size => 4 %> -
    • -
    - <% end %> - - <%= f.field_container :shipping_categories do %> - <%= f.label :shipping_category_id, t(:shipping_categories) %> - <%= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, { :include_blank => 'None' }, { :class => 'select2' }) %> - <%= f.error_message_on :shipping_category %> - <% end %> - - <%= f.field_container :tax_category do %> - <%= f.label :tax_category_id, t(:tax_category) %> - <%= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { :include_blank => 'None' }, { :class => 'select2' }) %> - <%= f.error_message_on :tax_category %> - <% end %> -
    - -
    - <%= f.field_container :taxons do %> - <%= f.label :taxon_ids, t(:taxons) %>
    - <%= f.hidden_field :taxon_ids, :value => @product.taxon_ids.join(',') %> - <% end %> - - <%= f.field_container :option_types do %> - <%= f.label :option_type_ids, t(:option_types) %> - <%= f.select :option_type_ids, option_types_options_for(@product), {}, :class => "select2", :multiple => true %> - <% end %> -
    - - -
    - <%= f.field_container :meta_keywords do %> - <%= f.label :meta_keywords, t(:meta_keywords) %> - <%= f.text_field :meta_keywords, :class => 'fullwidth' %> - <% end %> - - <%= f.field_container :meta_description do %> - <%= f.label :meta_description, t(:meta_description) %> - <%= f.text_field :meta_description, :class => 'fullwidth' %> - <% end %> -
    - -
    - -
    - -
    -
    - -<% unless Rails.env.test? %> - -<% end %> diff --git a/core/app/views/spree/admin/products/edit.html.erb b/core/app/views/spree/admin/products/edit.html.erb deleted file mode 100644 index ad0b30826bb..00000000000 --- a/core/app/views/spree/admin/products/edit.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_products_list), admin_products_url, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Product Details' } %> -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @product } %> - -<%= form_for [:admin, @product], :method => :put, :html => { :multipart => true } do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/products/index.html.erb b/core/app/views/spree/admin/products/index.html.erb deleted file mode 100644 index c12d30f4793..00000000000 --- a/core/app/views/spree/admin/products/index.html.erb +++ /dev/null @@ -1,104 +0,0 @@ -<% content_for :page_title do %> - <%= t(:listing_products) %> -<% end %> - -<% content_for :page_actions do %> -
    - -
    -<% end %> - -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<% content_for :table_filter_title do %> - <%= t(:search) %> -<% end %> - -<% content_for :table_filter do %> -
    - - <%= search_form_for [:admin, @search] do |f| %> - - <%- locals = {:f => f} %> - -
    -
    -
    - <%= f.label :name_cont, t(:name) %> - <%= f.text_field :name_cont, :size => 15 %> -
    -
    -
    -
    - <%= f.label :variants_including_master_sku_cont, t(:sku) %> - <%= f.text_field :variants_including_master_sku_cont, :size => 15 %> -
    -
    -
    -
    - -
    -
    -
    - -
    - -
    - <%= button t(:search), 'icon-search' %> -
    - <% end %> -
    -<% end %> - -
    - -<% if @collection.any? %> - - - - - - - - - - - - - - - - - - <% @collection.each do |product| %> - id="<%= spree_dom_id product %>" data-hook="admin_products_index_rows" class="<%= cycle('odd', 'even') %>"> - - - - - - - <% end %> - -
    <%= t(:sku) %><%= sort_link @search,:name, t(:name), { :default_order => "desc" }, {:title => 'admin_products_listing_name_title'} %><%= sort_link @search,:master_default_price_amount, t(:master_price), {}, {:title => 'admin_products_listing_price_title'} %>
    <%= product.sku rescue '' %><%= mini_image(product) %><%= link_to product.try(:name), edit_admin_product_path(product) %><%= product.display_price rescue '' %> - <%= link_to_edit product, :no_text => true, :class => 'edit' unless product.deleted? %> -   - <%= link_to_clone product, :no_text => true, :class => 'clone' %> -   - <%= link_to_delete product, :no_text => true unless product.deleted? %> -
    -<% else %> -
    - <%= t(:no_results) %> -
    -<% end %> - -<%= paginate @collection %> - diff --git a/core/app/views/spree/admin/products/new.html.erb b/core/app/views/spree/admin/products/new.html.erb deleted file mode 100644 index dad15b70a34..00000000000 --- a/core/app/views/spree/admin/products/new.html.erb +++ /dev/null @@ -1,80 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @product } %> - -<%= form_for [:admin, @product], :html => { :multipart => true } do |f| %> - -
    - - <%= t(:new_product) %> - - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> *
    - <%= f.text_field :name, :class => 'fullwidth title' %> - <%= f.error_message_on :name %> - <% end %> - -
    - <% unless @product.has_variants? %> -
    - <%= f.field_container :sku do %> - <%= f.label :sku, t(:sku) %>
    - <%= f.text_field :sku, :size => 16, :class => 'fullwidth' %> - <%= f.error_message_on :sku %> - <% end %> -
    - <% end %> - -
    - <%= f.field_container :prototype do %> - <%= f.label :prototype_id, t(:prototype) %>
    - <%= f.collection_select :prototype_id, Spree::Prototype.all, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth'} %> - <% end %> -
    - -
    - <%= f.field_container :price do %> - <%= f.label :price, t(:master_price) %> *
    - <%= f.text_field :price, :class => 'fullwidth' %> - <%= f.error_message_on :price %> - <% end %> -
    - -
    - <%= f.field_container :available_on do %> - <%= f.label :available_on, t(:available_on) %> - <%= f.error_message_on :available_on %> - <%= f.text_field :available_on, :class => 'datepicker fullwidth' %> - <% end %> -
    - -
    - -
    - <%= render :file => 'spree/admin/prototypes/show' if @prototype %> -
    - - <%= render :partial => 'spree/admin/shared/new_resource_links' %> - -
    -<% end %> - - diff --git a/core/app/views/spree/admin/products/new.js.erb b/core/app/views/spree/admin/products/new.js.erb deleted file mode 100644 index 3e901f416bb..00000000000 --- a/core/app/views/spree/admin/products/new.js.erb +++ /dev/null @@ -1,7 +0,0 @@ -$("#new_product").html('<%= escape_javascript(render :template => "spree/admin/products/new", :formats => [:html], :handlers => [:erb]) %>'); -handle_date_picker_fields(); -<% unless Rails.env.test? %> - $('.select2').select2(); -<% end %> -$("#table-filter").hide(); -$("#admin_new_product").parent().hide(); diff --git a/core/app/views/spree/admin/properties/_form.html.erb b/core/app/views/spree/admin/properties/_form.html.erb deleted file mode 100644 index ce5134bd566..00000000000 --- a/core/app/views/spree/admin/properties/_form.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -
    -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> *
    - <%= f.text_field :name, :class => 'fullwidth' %> - <%= f.error_message_on :name %> - <% end %> -
    -
    - <%= f.field_container :presentation do %> - <%= f.label :presentation, t(:presentation) %> *
    - <%= f.text_field :presentation, :class => 'fullwidth' %> - <%= f.error_message_on :presentation %> - <% end %> -
    -
    diff --git a/core/app/views/spree/admin/properties/edit.html.erb b/core/app/views/spree/admin/properties/edit.html.erb deleted file mode 100644 index b3524533b10..00000000000 --- a/core/app/views/spree/admin/properties/edit.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_property) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_properties_list), admin_properties_url, :icon => 'icon-arrow-left'%>
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @property } %> - -<%= form_for [:admin, @property] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/properties/index.html.erb b/core/app/views/spree/admin/properties/index.html.erb deleted file mode 100644 index dae9059c004..00000000000 --- a/core/app/views/spree/admin/properties/index.html.erb +++ /dev/null @@ -1,41 +0,0 @@ -<% content_for :page_title do %> - <%= t(:properties) %> -<% end %> - -<% content_for :page_actions do %> - -<% end %> - -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -
    - - - - - - - - - - - - - - - - <% @properties.each do |property| %> - - - - - - <% end %> - -
    <%= t(:name) %><%= t(:presentation) %>
    <%= property.name %><%= property.presentation %> - <%= link_to_edit(property, :no_text => true) %> - <%= link_to_delete(property, :no_text => true) %> -
    - diff --git a/core/app/views/spree/admin/properties/new.html.erb b/core/app/views/spree/admin/properties/new.html.erb deleted file mode 100644 index de943767e8e..00000000000 --- a/core/app/views/spree/admin/properties/new.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @property } %> - -<%= form_for [:admin, @property] do |f| %> -
    - <%= t(:new_property) %> - <%= render :partial => 'form', :locals => { :f => f } %> -
    - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -
    -<% end %> diff --git a/core/app/views/spree/admin/prototypes/_form.html.erb b/core/app/views/spree/admin/prototypes/_form.html.erb deleted file mode 100644 index 4c29af3a836..00000000000 --- a/core/app/views/spree/admin/prototypes/_form.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -
    -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> - <%= f.text_field :name, :class => 'fullwidth' %> - <%= f.error_message_on :name %> - <% end %> -
    - -
    -
    - <%= f.field_container :property_ids do %> - <%= f.label :property_ids, t(:properties) %>
    - <%= f.select :property_ids, Spree::Property.sorted.map { |p| [p.presentation, p.id] }, {}, { :multiple => true, :class => "select2 fullwidth" } %> - <% end %> -
    -
    - -
    - <%= f.field_container :option_type_ids do %> - <%= f.label :option_type_ids, t(:option_types) %>
    - <%= f.select :option_type_ids, Spree::OptionType.all.map { |ot| [ot.presentation, ot.id] }, {}, { :multiple => true, :class => "select2 fullwidth" } %> - <% end %> -
    -
    diff --git a/core/app/views/spree/admin/prototypes/_prototypes.html.erb b/core/app/views/spree/admin/prototypes/_prototypes.html.erb deleted file mode 100644 index 43dc0168928..00000000000 --- a/core/app/views/spree/admin/prototypes/_prototypes.html.erb +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - <% @prototypes.each do |prototype| %> - - - - - <% end %> - <% if @prototypes.empty? %> - - <% end %> - -
    <%= t(:name) %>
    <%= prototype.name %> - <%= link_to t(:select), select_admin_prototype_url(prototype), :class => 'ajax button select_properties_from_prototype icon-ok' %> -
    <% t(:none) %>.
    diff --git a/core/app/views/spree/admin/prototypes/edit.html.erb b/core/app/views/spree/admin/prototypes/edit.html.erb deleted file mode 100644 index 8452953dc98..00000000000 --- a/core/app/views/spree/admin/prototypes/edit.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_prototype) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_prototypes_list), spree.admin_prototypes_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @prototype } %> - -<%= form_for [:admin, @prototype] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/prototypes/index.html.erb b/core/app/views/spree/admin/prototypes/index.html.erb deleted file mode 100644 index 3eab1cf4d4f..00000000000 --- a/core/app/views/spree/admin/prototypes/index.html.erb +++ /dev/null @@ -1,40 +0,0 @@ -<% content_for :page_title do %> - <%= t(:prototypes) %> -<% end %> - -<% content_for :page_actions do %> - -<% end %> - -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= image_tag 'spinner.gif', :plugin => 'spree', :style => 'display: none', :id => 'busy_indicator' %> - -<%# Placeholder for new prototype form %> -
    - - - - - - - - - - - - - - <% @prototypes.each do |prototype| %> - - - - - <% end %> - -
    <%= t(:name) %>
    <%= prototype.name %> - <%= link_to_edit(prototype, :no_text => true, :class => 'admin_edit_prototype') %> - <%= link_to_delete(prototype, :no_text => true) %> -
    diff --git a/core/app/views/spree/admin/prototypes/new.html.erb b/core/app/views/spree/admin/prototypes/new.html.erb deleted file mode 100644 index f54b2e1a554..00000000000 --- a/core/app/views/spree/admin/prototypes/new.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @prototype } %> - -<%= form_for [:admin, @prototype] do |f| %> -
    - <%= t(:new_prototype) %> - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/prototypes/select.js.erb b/core/app/views/spree/admin/prototypes/select.js.erb deleted file mode 100644 index 1d601986349..00000000000 --- a/core/app/views/spree/admin/prototypes/select.js.erb +++ /dev/null @@ -1,4 +0,0 @@ -<% @prototype_properties.each do |prop| %> - $("a.add_fields").click(); - $(".product_property.fields:last input:first").val("<%= prop.name %>"); -<% end %> diff --git a/core/app/views/spree/admin/prototypes/show.html.erb b/core/app/views/spree/admin/prototypes/show.html.erb deleted file mode 100644 index 303ec96396d..00000000000 --- a/core/app/views/spree/admin/prototypes/show.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<% if @prototype.option_types.present? %> -

    Variants

    - -
      - <% @prototype.option_types.each do |ot| %> -
    • - - <%= check_box_tag "option_types[]", ot.id, (params[:option_types] || []).include?(ot.id.to_s), :id => "option_type_#{ot.id}", :class => "option-type" %> - <%= label_tag "option_type_#{ot.id}", ot.presentation %> - -
        - <% ot.option_values.each do |ov| %> -
      • - <%= check_box_tag "product[option_values_hash[#{ot.id}]][]", ov.id, params[:product] && (params[:product][:option_values_hash] || {}).values.flatten.include?(ov.id.to_s), :id => "option_value_#{ov.id}", :class => "option-value" %> - <%= label_tag "option_value_#{ov.id}", ov.presentation %> -
      • - <% end %> -
      -
    • - <% end %> -
    - - -<% end %> diff --git a/core/app/views/spree/admin/reports/index.html.erb b/core/app/views/spree/admin/reports/index.html.erb deleted file mode 100644 index 1ae8b4c17a7..00000000000 --- a/core/app/views/spree/admin/reports/index.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<% content_for :page_title do %> - <%= t(:listing_reports) %> -<% end %> - - - - - - - - - - <% @reports.each do |key, value| %> - - - - - <% end %> - -
    <%= t(:name) %><%= t(:description) %>
    <%= link_to value[:name], send("#{key}_admin_reports_url".to_sym) %><%= value[:description] %>
    - diff --git a/core/app/views/spree/admin/reports/sales_total.html.erb b/core/app/views/spree/admin/reports/sales_total.html.erb deleted file mode 100644 index 2949199485f..00000000000 --- a/core/app/views/spree/admin/reports/sales_total.html.erb +++ /dev/null @@ -1,37 +0,0 @@ -<% content_for :page_title do %> - <%= t(:sales_totals) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= link_to_with_icon 'icon-arrow-left', t(:back_to_reports_list), spree.admin_reports_url, :class => 'button' %>
  • -<% end %> - - -<% content_for :table_filter_title do %> - <%= t(:date_range) %> -<% end %> - -<% content_for :table_filter do %> - <%= render :partial => 'spree/admin/shared/report_criteria', :locals => {} %> -<% end %> - - - - - - - - - - - - <% @totals.each do |key, row| %> - - - - - - - <% end %> - -
    <%= t(:currency) %><%= t(:item_total) %><%= t(:adjustment_total) %><%= t(:sales_total) %>
    <%= key %><%= row[:item_total].format %><%= row[:adjustment_total].format %><%= row[:sales_total].format %>
    diff --git a/core/app/views/spree/admin/return_authorizations/_form.html.erb b/core/app/views/spree/admin/return_authorizations/_form.html.erb deleted file mode 100644 index 64e57a31de5..00000000000 --- a/core/app/views/spree/admin/return_authorizations/_form.html.erb +++ /dev/null @@ -1,72 +0,0 @@ -
    - - - - - - - - - - - <% @return_authorization.order.inventory_units.group_by(&:variant).each do | variant, units| %> - - - - - - - <% end %> - -
    <%= t(:product) %><%= t(:quantity_shipped) %><%= t(:quantity_returned) %><%= t(:return_quantity) %>
    -

    <%= variant.name %>

    - <%= variant.options_text %> -
    <%= units.select(&:shipped?).size %><%= units.select(&:returned?).size %> - <% if @return_authorization.received? %> - <%= @return_authorization.inventory_units.group_by(&:variant)[variant].try(:size) || 0 %> - <% elsif units.select(&:shipped?).empty? %> - 0 - <% else %> - <%= number_field_tag "return_quantity[#{variant.id}]", - @return_authorization.inventory_units.group_by(&:variant)[variant].try(:size) || 0, {:style => 'width:50px;', :min => 0} %> - <% end %> -
    - - <%= f.field_container :amount do %> - <%= f.label :amount, t(:amount) %> *
    - <% if @return_authorization.received? %> - <%= @return_authorization.display_amount %> - <% else %> - <%= f.text_field :amount, {:style => 'width:80px;'} %> <%= t(:rma_value) %>: 0.00 - <%= f.error_message_on :amount %> - <% end %> - <% end %> - - <%= f.field_container :reason do %> - <%= f.label :reason, t(:reason) %> - <%= f.text_area :reason, {:style => 'height:100px;', :class => 'fullwidth'} %> - <%= f.error_message_on :reason %> - <% end %> -
    - - diff --git a/core/app/views/spree/admin/return_authorizations/edit.html.erb b/core/app/views/spree/admin/return_authorizations/edit.html.erb deleted file mode 100644 index 419c8918ba7..00000000000 --- a/core/app/views/spree/admin/return_authorizations/edit.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<% content_for :page_title do %> - <%= t(:return_authorization) %> <%= @return_authorization.number %> (<%= t(@return_authorization.state.downcase) %>) -<% end %> - -<% content_for :page_actions do %> -
  • - <% if @return_authorization.can_receive? %> - <%= button_link_to t(:receive), fire_admin_order_return_authorization_url(@order, @return_authorization, :e => 'receive'), :method => :put, :data => { :confirm => t(:are_you_sure) } %> - <% end %> - <% if @return_authorization.can_cancel? %> - <%= button_link_to t(:cancel), fire_admin_order_return_authorization_url(@order, @return_authorization, :e => 'cancel'), :method => :put, :data => { :confirm => t(:are_you_sure) } %> - <% end %> -
  • -<% end %> - -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Return Authorizations' } %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @return_authorization } %> -<%= form_for [:admin, @order, @return_authorization] do |f| %> - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button t(:update) %> - <%= t(:or) %> <%= link_to t('actions.cancel'), admin_order_return_authorizations_url(@order) %> -
    -<% end %> diff --git a/core/app/views/spree/admin/return_authorizations/index.html.erb b/core/app/views/spree/admin/return_authorizations/index.html.erb deleted file mode 100644 index 35a9725001f..00000000000 --- a/core/app/views/spree/admin/return_authorizations/index.html.erb +++ /dev/null @@ -1,52 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Return Authorizations' } %> - -<% content_for :page_actions do %> - <% if @order.inventory_units.any? &:shipped? %> -
  • - <%= button_link_to t(:new_return_authorization), new_admin_order_return_authorization_url(@order), :icon => 'icon-plus' %> -
  • - <% end %> -
  • <%= button_link_to t(:back_to_orders_list), spree.admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<% content_for :page_title do %> - <%= t(:return_authorizations) %> -<% end %> - -<% if @order.inventory_units.any? &:shipped? %> - - - - - - - - - - - - <% @return_authorizations.each do |return_authorization| %> - - - - - - - - - <% end %> - -
    <%= t(:rma_number) %><%= t(:status) %><%= t(:amount) %><%= "#{t('spree.date')}/#{t('spree.time')}" %>
    <%= return_authorization.number %><%= t(return_authorization.state.downcase) %><%= return_authorization.display_amount %><%= pretty_time(return_authorization.created_at) %> - <%= link_to_edit return_authorization %> - <% unless return_authorization.received? %> -   - <%= link_to_delete return_authorization %> - <% end %> -
    -<% else %> -
    - <%= t(:cannot_create_returns) %> -
    -<% end %> - -<%= button_link_to t(:continue), admin_orders_url, :icon => 'icon-arrow-right' %> diff --git a/core/app/views/spree/admin/return_authorizations/new.html.erb b/core/app/views/spree/admin/return_authorizations/new.html.erb deleted file mode 100644 index 6eaf91ab81a..00000000000 --- a/core/app/views/spree/admin/return_authorizations/new.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Return Authorizations' } %> - -<% content_for :page_title do %> - <%= t(:new_return_authorization) %> -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @return_authorization } %> -<%= form_for [:admin, @order, @return_authorization] do |f| %> - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button t(:continue) %> - <%= t(:or) %> <%= link_to t('actions.cancel'), admin_order_return_authorizations_url(@order) %> -
    -<% end %> diff --git a/core/app/views/spree/admin/search/users.rabl b/core/app/views/spree/admin/search/users.rabl deleted file mode 100644 index 6e6d3932f05..00000000000 --- a/core/app/views/spree/admin/search/users.rabl +++ /dev/null @@ -1,32 +0,0 @@ -object false -child @users => :users do - attributes :email, :id - address_fields = [:firstname, :lastname, - :address1, :address2, - :city, :zipcode, - :phone, :state_name, - :state_id, :country_id, - :company] - - child :ship_address => :ship_address do - attributes *address_fields - child :state do - attributes :name - end - - child :country do - attributes :name - end - end - - child :bill_address => :bill_address do - attributes *address_fields - child :state do - attributes :name - end - - child :country do - attributes :name - end - end -end diff --git a/core/app/views/spree/admin/shared/_address_form.html.erb b/core/app/views/spree/admin/shared/_address_form.html.erb deleted file mode 100644 index 8003bde1640..00000000000 --- a/core/app/views/spree/admin/shared/_address_form.html.erb +++ /dev/null @@ -1,65 +0,0 @@ -<% if use_billing %> -
    - - <%= check_box_tag 'order[use_billing]', '1', (!(@order.bill_address.empty? && @order.ship_address.empty?) && @order.bill_address.same_as?(@order.ship_address)) %> - <%= label_tag 'order[use_billing]', t(:use_billing_address) %> - -
    - -<% end %> - -
    -
    - <%= f.label :firstname, t(:first_name) + ':' %> - <%= f.text_field :firstname, :class => 'fullwidth' %> -
    -
    - <%= f.label :lastname, t(:last_name) + ':' %> - <%= f.text_field :lastname, :class => 'fullwidth' %> -
    - <% if Spree::Config[:company] %> -
    - <%= f.label :company, t(:company) + ':' %> - <%= f.text_field :company, :class => 'fullwidth' %> -
    - <% end %> -
    - <%= f.label :address1, t(:street_address) + ':' %> - <%= f.text_field :address1, :class => "fullwidth" %> -
    -
    - <%= f.label :address2, t(:street_address_2) + ':' %> - <%= f.text_field :address2, :class => "fullwidth" %> -
    -
    - <%= f.label :city, t(:city) + ':' %> - <%= f.text_field :city, :class => 'fullwidth' %> -
    -
    - <%= f.label :zipcode, t(:zip) + ':' %> - <%= f.text_field :zipcode, :class => 'fullwidth' %> -
    -
    - <%= f.label :country_id, t(:country) + ':' %> - <%= f.collection_select :country_id, available_countries, :id, :name, {}, {:class => 'select2 fullwidth'} %> -
    -
    - <%= f.label :state_id, t(:state) + ':' %> - - <%= f.text_field :state_name, :style => "display: #{f.object.country.states.empty? ? 'block' : 'none' };", :disabled => !f.object.country.states.empty?, :class => 'fullwidth' %> - <%= f.collection_select :state_id, f.object.country.states.sort, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth', :style => "display: #{f.object.country.states.empty? ? 'none' : 'block' };", :disabled => f.object.country.states.empty?} %> - -
    -
    - <%= f.label :phone, t(:phone) + ':' %> - <%= f.phone_field :phone, :class => 'fullwidth' %> -
    -
    - -<% content_for :head do %> - <%= javascript_tag do -%> - $(document).ready(function(){ - $('span#<%= name == t(:shipping_address) ? 's' : 'b' %>country .select2').on('change', function() { update_state('<%= name == t(:shipping_address) ? 's' : 'b' %>'); }); - }); - <% end -%> -<% end %> diff --git a/core/app/views/spree/admin/shared/_alert.html.erb b/core/app/views/spree/admin/shared/_alert.html.erb deleted file mode 100644 index 1265411bd54..00000000000 --- a/core/app/views/spree/admin/shared/_alert.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
    - <%= alert.message %> <%= link_to alert.url_name, alert.url if alert.url %> - <%= link_to 'X', dismiss_alert_admin_general_settings_path(:alert_id => alert.id), - :remote => true, :method => :post, :class => 'dismiss' %> -
    diff --git a/core/app/views/spree/admin/shared/_configuration_menu.html.erb b/core/app/views/spree/admin/shared/_configuration_menu.html.erb deleted file mode 100644 index 283b6d443bc..00000000000 --- a/core/app/views/spree/admin/shared/_configuration_menu.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -<% content_for :sidebar_title do %> - <%= t(:configurations) %> -<% end %> - -<% content_for :sidebar do %> - -<% end %> diff --git a/core/app/views/spree/admin/shared/_destroy.js.erb b/core/app/views/spree/admin/shared/_destroy.js.erb deleted file mode 100644 index b939f4b5c3a..00000000000 --- a/core/app/views/spree/admin/shared/_destroy.js.erb +++ /dev/null @@ -1,16 +0,0 @@ -notice_div = $('.flash.notice'); -<% notice = flash.discard(:notice) -if notice %> - if (notice_div.length > 0) { - notice_div.html("<%= notice %>"); - notice_div.show(); - } else { - if ($("#content .toolbar").length > 0) { - $("#content .toolbar").before('
    <%= notice %>
    '); - } else { - $("#content h1").before('
    <%= notice %>
    '); - } - } <% -end %> - -<%= render :partial => '/spree/admin/shared/update_order_state' if @order %> diff --git a/core/app/views/spree/admin/shared/_edit_resource_links.html.erb b/core/app/views/spree/admin/shared/_edit_resource_links.html.erb deleted file mode 100644 index dcf8c367b3a..00000000000 --- a/core/app/views/spree/admin/shared/_edit_resource_links.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
    - <%= button t(:update), 'icon-refresh' %> - <%= t(:or) %> - <%= button_link_to t(:cancel), collection_url, :icon => 'icon-remove' %> -
    diff --git a/core/app/views/spree/admin/shared/_head.html.erb b/core/app/views/spree/admin/shared/_head.html.erb deleted file mode 100644 index c499fc6f8c8..00000000000 --- a/core/app/views/spree/admin/shared/_head.html.erb +++ /dev/null @@ -1,25 +0,0 @@ - - -<%= csrf_meta_tags %> - - - <%= "Spree #{t('administration')}: " %> - <%= t(controller.controller_name, :default => controller.controller_name.titleize) %> - - - - - -<%= stylesheet_link_tag 'admin/all' %> - -<%= javascript_include_tag 'admin/all' %> - -<%= render "spree/admin/shared/translations" %> -<%= render "spree/admin/shared/routes" %> - -<%= javascript_tag do -%> - jQuery.alerts.dialogClass = 'spree'; - <%== "var AUTH_TOKEN = #{form_authenticity_token.inspect};" %> -<% end -%> - -<%= yield :head %> diff --git a/core/app/views/spree/admin/shared/_new_resource_links.html.erb b/core/app/views/spree/admin/shared/_new_resource_links.html.erb deleted file mode 100644 index fecd2acf596..00000000000 --- a/core/app/views/spree/admin/shared/_new_resource_links.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
    - <%= button t('actions.create'), 'icon-ok' %> - <%= t(:or) %> - <%= link_to_with_icon 'icon-remove', t('actions.cancel'), collection_url, :class => 'button' %> -
    diff --git a/core/app/views/spree/admin/shared/_order_details.html.erb b/core/app/views/spree/admin/shared/_order_details.html.erb deleted file mode 100644 index 8592db5ba49..00000000000 --- a/core/app/views/spree/admin/shared/_order_details.html.erb +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - <% @order.line_items.each do |item| %> - - - - - - - <% end %> - - - - - - - - - <% @order.adjustments.eligible.each do |adjustment| %> - <% next if (adjustment.originator_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> - - - - - <% end %> - - - - - - - - <% if order.price_adjustment_totals.present? %> - - <% @order.price_adjustment_totals.keys.each do |key| %> - - - - - <% end %> - - <% end %> -
    <%= t(:item_description) %><%= t(:price) %><%= t(:qty) %><%= t(:total) %>
    <%= item.variant.product.name %> <%= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? %><%= item.variant.display_amount %><%= item.quantity %><%= item.display_amount %>
    <%= t(:subtotal) %>:<%= @order.display_total %>
    <%= adjustment.label %>:<%= adjustment.display_amount %>
    <%= t(:order_total) %>:<%= @order.display_total %>
    <%= key %><%= @order.price_adjustment_totals[key].display_amount %>
    diff --git a/core/app/views/spree/admin/shared/_order_tabs.html.erb b/core/app/views/spree/admin/shared/_order_tabs.html.erb deleted file mode 100644 index 167c89aaee0..00000000000 --- a/core/app/views/spree/admin/shared/_order_tabs.html.erb +++ /dev/null @@ -1,71 +0,0 @@ -<% content_for :page_title do %> - <%= t(:order) %> #<%= @order.number %> -<% end %> - -<% content_for :sidebar_title do %> - <%= t(:order_information) %> -<% end %> - -<% content_for :sidebar do %> -
    -
    -
    <%= t(:status) %>:
    -
    <%= t(@order.state, :scope => :order_state) %>
    -
    <%= t(:total) %>:
    -
    <%= @order.display_total %>
    - - <% if @order.completed? %> -
    <%= t(:shipment) %>:
    -
    <%= t(@order.shipment_state, :scope => :shipment_states, :default => [:missing, "none"]) %>
    -
    <%= t(:payment) %>:
    -
    <%= t(@order.payment_state, :scope => :payment_states, :default => [:missing, "none"]) %>
    -
    <%= t(:date_completed) %>:
    -
    <%= pretty_time(@order.completed? ? @order.completed_at : @order.created_at) %>
    - <% end %> -
    -
    - - - - -<% end %> diff --git a/core/app/views/spree/admin/shared/_product_tabs.html.erb b/core/app/views/spree/admin/shared/_product_tabs.html.erb deleted file mode 100644 index 08d96acce1c..00000000000 --- a/core/app/views/spree/admin/shared/_product_tabs.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<% content_for :page_title do %> - <%= t(:editing_product) %> “<%= @product.name %>” -<% end %> - -<% content_for :sidebar_title do %> - <%= @product.sku %> -<% end %> - -<% content_for :sidebar do %> - - -<% end %> diff --git a/core/app/views/spree/admin/shared/_report_criteria.html.erb b/core/app/views/spree/admin/shared/_report_criteria.html.erb deleted file mode 100644 index 3c396963a74..00000000000 --- a/core/app/views/spree/admin/shared/_report_criteria.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -<%= search_form_for @search, :url => spree.sales_total_admin_reports_path do |s| %> -
    - <%= label_tag nil, t(:start), :class => 'inline' %> - <%= s.text_field :created_at_gt, :class => 'datepicker datepicker-from' %> - - - - - - <%= s.text_field :created_at_lt, :class => 'datepicker datepicker-to' %> - <%= label_tag nil, t(:end), :class => 'inline' %> -
    - -
    - <%= button t(:search), 'icon-search' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/shared/_routes.html.erb b/core/app/views/spree/admin/shared/_routes.html.erb deleted file mode 100644 index f33abb3aace..00000000000 --- a/core/app/views/spree/admin/shared/_routes.html.erb +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/core/app/views/spree/admin/shared/_show_resource_links.html.erb b/core/app/views/spree/admin/shared/_show_resource_links.html.erb deleted file mode 100644 index 8c0e3fa67d4..00000000000 --- a/core/app/views/spree/admin/shared/_show_resource_links.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

    - <%= link_to t(:edit), edit_object_url %> | - <%= link_to t(:back), collection_url %> | - <%= link_to t(:delete), object_url, :method => :delete, :data => { :confirm => t(:are_you_sure_you_want_to_delete_this_record) } %> -

    diff --git a/core/app/views/spree/admin/shared/_tabs.html.erb b/core/app/views/spree/admin/shared/_tabs.html.erb deleted file mode 100644 index 2c1944dba91..00000000000 --- a/core/app/views/spree/admin/shared/_tabs.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<%= tab :overview, :route => :admin, :icon => 'icon-dashboard' %> -<%= tab :orders, :payments, :creditcard_payments, :shipments, :credit_cards, :return_authorizations, :icon => 'icon-shopping-cart' %> -<%= tab :products , :option_types, :properties, :prototypes, :variants, :product_properties, :taxons, :icon => 'icon-th-large' %> -<%= tab :reports, :icon => 'icon-file' %> -<%= tab :configurations, :general_settings, :mail_methods, :tax_categories, :zones, :states, :payment_methods, :inventory_settings, :taxonomies, :shipping_methods, :trackers, :label => 'configuration', :icon => 'icon-wrench', :url => edit_admin_general_settings_path %> diff --git a/core/app/views/spree/admin/shared/_translations.html.erb b/core/app/views/spree/admin/shared/_translations.html.erb deleted file mode 100644 index e2c2f5b8669..00000000000 --- a/core/app/views/spree/admin/shared/_translations.html.erb +++ /dev/null @@ -1,30 +0,0 @@ - diff --git a/core/app/views/spree/admin/shared/_update_order_state.js b/core/app/views/spree/admin/shared/_update_order_state.js deleted file mode 100644 index 6eb62993783..00000000000 --- a/core/app/views/spree/admin/shared/_update_order_state.js +++ /dev/null @@ -1,7 +0,0 @@ -$('#order_tab_summary h5#order_status').html('<%= j t(:status) %>: <%= j t(@order.state, :scope => :order_state) %>'); -$('#order_tab_summary h5#order_total').html('<%= j t(:total) %>: <%= j @order.display_total.to_s %>'); - -<% if @order.completed? %> - $('#order_tab_summary h5#payment_status').html('<%= j t(:payment) %>: <%= j t(@order.payment_state, :scope => :payment_states, :default => [:missing, "none"]) %>'); - $('#order_tab_summary h5#shipment_status').html('<%= j t(:shipment) %>: <%= j t(@order.shipment_state, :scope => :shipment_state, :default => [:missing, "none"]) %>'); -<% end %> diff --git a/core/app/views/spree/admin/shipments/_form.html.erb b/core/app/views/spree/admin/shipments/_form.html.erb deleted file mode 100644 index 85f61c4a7bf..00000000000 --- a/core/app/views/spree/admin/shipments/_form.html.erb +++ /dev/null @@ -1,71 +0,0 @@ -
    - <% unless @shipment.order.cart? %> - - - - - - - - - - - - <% @shipment.order.inventory_units.each do |inventory_unit| %> - - - - - - - - <% end %> - -
    <%= t(:include_in_shipment) %><%= t(:sku) %><%= t(:item_description) %><%= t(:status) %><%= t(:note) %>
    - <%= check_box_tag "inventory_units[#{inventory_unit.id}]", - :true, - (inventory_unit.shipment == @shipment), - { :disabled => %w(shipped backordered returned).include?(inventory_unit.state), - :class => 'inventory_unit'} %> - <%= inventory_unit.variant.sku %> - <%=inventory_unit.variant.product.name %> - <%= '(' + variant_options(inventory_unit.variant) + ')' unless inventory_unit.variant.option_values.empty? %> - <%= t(inventory_unit.state) %> - <% if inventory_unit.shipment == @shipment %> - <%= t(:included_in_this_shipment) %> - <% elsif !inventory_unit.shipment.nil? %> - <%= t(:included_in_other_shipment) %> - <%= link_to inventory_unit.shipment.number, edit_admin_order_shipment_url(inventory_unit.order, inventory_unit.shipment) %> - <% end %> -
    - <% end %> - -
    - <% shipment_form.fields_for 'address' do |sa_form| %> - <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => sa_form, :name => t(:shipping_address), :use_billing => false } %> - <% end %> -
    - -
    - <%= t(:shipment_details) %> - -
    -
    - <%= shipment_form.label :shipping_method_id, t(:shipping_method) + ':' %> - <%= shipment_form.select :shipping_method_id, @shipping_methods.map {|sm| ["#{sm.name} - #{sm.zone.name}", sm.id] }, {}, {:class => 'select2 fullwidth'} %> -
    - -
    - <%= shipment_form.label :tracking, t(:tracking) + ':' %> - <%= shipment_form.text_field :tracking, :class => 'fullwidth' %> -
    - - <% if Spree::Config[:shipping_instructions] %> -
    - <%= shipment_form.label :special_instructions, t(:special_instructions) + ':' %> - <%= shipment_form.text_area :special_instructions, :class => 'fullwidth' %> -
    - <% end %> -
    - -
    -
    diff --git a/core/app/views/spree/admin/shipments/edit.html.erb b/core/app/views/spree/admin/shipments/edit.html.erb deleted file mode 100644 index d85e685c722..00000000000 --- a/core/app/views/spree/admin/shipments/edit.html.erb +++ /dev/null @@ -1,43 +0,0 @@ -<% content_for :page_actions do %> - <% if @shipment.can_ship? %> -
  • - <%= button_link_to t(:ship), fire_admin_order_shipment_path(@order, @shipment, :e => 'ship'), :method => :put, :data => { :confirm => t(:are_you_sure) } %> -
  • - <% end %> -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Shipments' } %> - -<% content_for :page_title do %> - <%= t(:shipments) %> #<%= @shipment.number%> (<%= t(@shipment.state.to_sym, :scope => :state_names, :default => @shipment.state.to_s.humanize) %>) -<% end %> - -
    - - <% if @shipment.cost %> -
    <%= t(:charges) %> <%= @shipment.display_cost %>
    - <% end %> - - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipment } %> -
    - -
    - <%= form_for @shipment, :url => admin_order_shipment_path(@order, @shipment), :method => :put do |shipment_form| %> -
    - <%= render :partial => 'form', :locals => { :shipment_form => shipment_form } %> - -
    -
    - <% if @shipment.editable_by?(try_spree_current_user) %> - <%= button @order.cart? ? t(:continue) : t(:update), @order.cart? ? 'icon-arrow-right' : 'icon-refresh' %> - <%= t(:or) %> - <%= button_link_to t(:cancel), admin_order_shipments_path(@order), :icon => 'icon-remove' %> - <% else %> - <%= button_link_to t(:back), edit_admin_order_shipment_path(@order), :icon => 'icon-arrow-left' %> - <% end %> -
    -
    -
    - <% end %> -
    diff --git a/core/app/views/spree/admin/shipments/index.html.erb b/core/app/views/spree/admin/shipments/index.html.erb deleted file mode 100644 index 4208ef01f4a..00000000000 --- a/core/app/views/spree/admin/shipments/index.html.erb +++ /dev/null @@ -1,45 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Shipments' } %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_shipment), new_admin_order_shipment_url(@order), :icon => 'icon-plus' %> -
  • -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -<% content_for :page_title do %> - <%= t(:shipments) %> -<% end %> - - - - - - - - - - - - - - - <% @shipments.each do |shipment| %> - - - - - - - - - - <% end %> - -
    <%= t(:shipment_number) %><%= t(:shipping_method) %><%= t(:shipping_cost) %><%= t(:tracking) %><%= t(:status) %><%= "#{t('spree.date')}/#{t('spree.time')}" %>
    <%= shipment.number %><%= shipment.shipping_method.name if shipment.shipping_method %><%= shipment.display_cost %><%= shipment.tracking %><%= t(shipment.state.to_sym, :scope => :state_names, :default => shipment.state.to_s.humanize) %><%= shipment.shipped_at.to_s(:date_time24) if shipment.shipped_at %> - <%= link_to_with_icon 'icon-edit', t(:edit), edit_admin_order_shipment_url(@order, shipment), :no_text => true, :data => {:action => 'edit'} %> - <%= link_to_delete shipment, :url => admin_order_shipment_url(@order, shipment), :no_text => true %> -
    -
    - <%= button_link_to t(:continue), admin_orders_url, :icon => 'icon-arrow-right' %> -
    diff --git a/core/app/views/spree/admin/shipments/new.html.erb b/core/app/views/spree/admin/shipments/new.html.erb deleted file mode 100644 index 7ff5d136a66..00000000000 --- a/core/app/views/spree/admin/shipments/new.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%= render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Shipments' } %> - -<% content_for :page_actions do %> -
  • <%= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' %>
  • -<% end %> - -
    -

    <%= t(:new_shipment) %>

    - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipment } %> -
    - -
    - <%= form_for @shipment, :url => admin_order_shipments_path(@order) do |shipment_form| %> - <%= render :partial => 'form', :locals => { :shipment_form => shipment_form } %> - -
    -

    - <%= button t(:create) %> -

    -
    - <% end %> -
    diff --git a/core/app/views/spree/admin/shipping_categories/_form.html.erb b/core/app/views/spree/admin/shipping_categories/_form.html.erb deleted file mode 100644 index bddf8b6884b..00000000000 --- a/core/app/views/spree/admin/shipping_categories/_form.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -
    -
    - <%= label_tag t(:name) %>
    - <%= f.text_field :name %> -
    -
    diff --git a/core/app/views/spree/admin/shipping_categories/edit.html.erb b/core/app/views/spree/admin/shipping_categories/edit.html.erb deleted file mode 100644 index 3894730c0ee..00000000000 --- a/core/app/views/spree/admin/shipping_categories/edit.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_shipping_category) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_shipping_categories), spree.admin_shipping_categories_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_category } %> - -<%= form_for [:admin, @shipping_category] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/shipping_categories/index.html.erb b/core/app/views/spree/admin/shipping_categories/index.html.erb deleted file mode 100644 index a8af9c7ffb3..00000000000 --- a/core/app/views/spree/admin/shipping_categories/index.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:shipping_categories) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_shipping_category), new_object_url, :icon => 'icon-plus' %> -
  • -<% end %> - - - - - - - - - - - - - - <% @shipping_categories.each do |shipping_category|%> - - - - - <% end %> - -
    <%= t(:name) %>
    <%= shipping_category.name %> - <%= link_to_edit shipping_category, :no_text => true %> - <%= link_to_delete shipping_category, :no_text => true %> -
    diff --git a/core/app/views/spree/admin/shipping_categories/new.html.erb b/core/app/views/spree/admin/shipping_categories/new.html.erb deleted file mode 100644 index 0431d5b44ec..00000000000 --- a/core/app/views/spree/admin/shipping_categories/new.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_shipping_category) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_shipping_categories_list), spree.admin_shipping_categories_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_category } %> - -<%= form_for [:admin, @shipping_category] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/shipping_methods/_form.html.erb b/core/app/views/spree/admin/shipping_methods/_form.html.erb deleted file mode 100644 index 10a52081a4d..00000000000 --- a/core/app/views/spree/admin/shipping_methods/_form.html.erb +++ /dev/null @@ -1,50 +0,0 @@ -
    -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %>
    - <%= f.text_field :name, :class => 'fullwidth' %> - <%= error_message_on :shipping_method, :name %> - <% end %> -
    - -
    - <%= f.field_container :zone_id do %> - <%= f.label :zone_id, t(:zone) %>
    - <%= f.collection_select(:zone_id, @available_zones, :id, :name, {}, {:class => 'select2 fullwidth'}) %> - <%= error_message_on :shipping_method, :zone_id %> - <% end %> -
    - -
    - <%= f.field_container :display_on do %> - <%= f.label :display_on, t(:display) %>
    - <%= select(:shipping_method, :display_on, Spree::ShippingMethod::DISPLAY.collect { |display| [t(display), display == :both ? nil : display.to_s] }, {}, {:class => 'select2 fullwidth'}) %> - <%= error_message_on :shipping_method, :display_on %> - <% end %> -
    -
    - -
    -
    - <%= t(:availability) %> - - <%= f.field_container :shipping_category do %> - <%= f.label :shipping_category, t(:shipping_category_choose) %> - <%= select(:shipping_method, :shipping_category_id, Spree::ShippingCategory.all.collect { |s| [s.name, s.id] }, { :include_blank => "None" }, {:class => 'select2 fullwidth'}) %> - <% end %> - -
    - <%= label_tag t(:match_rule) %> -
      -
    • <%= f.check_box :match_none %> <%= f.label :match_none, t('match_choices.none') %>
    • -
    • <%= f.check_box :match_one %> <%= f.label :match_one, t('match_choices.one') %>
    • -
    • <%= f.check_box :match_all %> <%= f.label :match_all, t('match_choices.all') %>
    • -
    -
    - -
    -
    - -
    - <%= render :partial => 'spree/admin/shared/calculator_fields', :locals => { :f => f } %> -
    diff --git a/core/app/views/spree/admin/shipping_methods/edit.html.erb b/core/app/views/spree/admin/shipping_methods/edit.html.erb deleted file mode 100644 index 8866ac612ff..00000000000 --- a/core/app/views/spree/admin/shipping_methods/edit.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_shipping_method) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_shipping_methods_list), spree.admin_shipping_methods_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -
    - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_method } %> -
    - -
    - <%= form_for [:admin, @shipping_method] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - -
    - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -
    - <% end %> -
    diff --git a/core/app/views/spree/admin/shipping_methods/index.html.erb b/core/app/views/spree/admin/shipping_methods/index.html.erb deleted file mode 100644 index 8f10414d612..00000000000 --- a/core/app/views/spree/admin/shipping_methods/index.html.erb +++ /dev/null @@ -1,45 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:shipping_methods) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_shipping_method), new_object_url, :icon => 'icon-plus', :id => 'admin_new_shipping_method_link' %> -
  • -<% end %> - - - - - - - - - - - - - - - - - - - - <% @shipping_methods.each do |shipping_method|%> - - - - - - - - <% end %> - -
    <%= t(:name) %><%= t(:zone) %><%= t(:calculator) %><%= t(:display) %>
    <%= shipping_method.name %><%= shipping_method.zone.name if shipping_method.zone %><%= shipping_method.calculator.description %><%= shipping_method.display_on.blank? ? t(:both) : t(shipping_method.display_on) %> - <%= link_to_edit shipping_method, :no_text => true %> - <%= link_to_delete shipping_method, :no_text => true %> -
    - diff --git a/core/app/views/spree/admin/shipping_methods/new.html.erb b/core/app/views/spree/admin/shipping_methods/new.html.erb deleted file mode 100644 index dedba275b74..00000000000 --- a/core/app/views/spree/admin/shipping_methods/new.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_shipping_method) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_shipping_methods_list), spree.admin_shipping_methods_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -
    - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @shipping_method } %> -
    - -
    - <%= form_for [:admin, @shipping_method] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - -
    - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -
    - <% end %> -
    diff --git a/core/app/views/spree/admin/states/_form.html.erb b/core/app/views/spree/admin/states/_form.html.erb deleted file mode 100644 index 1d2617a9911..00000000000 --- a/core/app/views/spree/admin/states/_form.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -
    -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> - <%= f.text_field :name, :class => 'fullwidth' %> - <% end %> -
    -
    - <%= f.field_container :abbr do %> - <%= f.label :abbr, t(:abbreviation) %> - <%= f.text_field :abbr, :class => 'fullwidth' %> - <% end %> -
    -
    diff --git a/core/app/views/spree/admin/states/_state_list.html.erb b/core/app/views/spree/admin/states/_state_list.html.erb deleted file mode 100644 index 8fbcf426094..00000000000 --- a/core/app/views/spree/admin/states/_state_list.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -
    - - - - - - - - - - - - - - - - <% @states.each do |state| %> - - - - - - <% end %> - <% if @states.empty? %> - - <% end %> - -
    <%= t(:name) %><%= t(:abbreviation) %>
    <%= state.name %><%= state.abbr %> - <%= link_to_with_icon 'icon-edit', t(:edit), edit_admin_country_state_url(@country, state), :no_text => true %> - <%= link_to_delete state, :no_text => true %> -
    <%= t(:none) %>
    diff --git a/core/app/views/spree/admin/states/edit.html.erb b/core/app/views/spree/admin/states/edit.html.erb deleted file mode 100644 index 6aab9f819e0..00000000000 --- a/core/app/views/spree/admin/states/edit.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_state) %> <%= @state.name %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_states_list), spree.admin_country_states_url(@country), :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @state } %> - -<%= form_for [:admin, @country, @state] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/states/index.html.erb b/core/app/views/spree/admin/states/index.html.erb deleted file mode 100644 index 15e1aab664f..00000000000 --- a/core/app/views/spree/admin/states/index.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:states) %> -<% end %> - -<% content_for :page_actions do %> - -<% end %> - -
    - <%= label_tag :country, t(:country) %> - -
    - -<%= image_tag 'spinner.gif', :plugin => 'spree', :style => 'display:none;', :id => 'busy_indicator' %> - -
    - <%= render :partial => 'state_list'%> -
    diff --git a/core/app/views/spree/admin/states/new.html.erb b/core/app/views/spree/admin/states/new.html.erb deleted file mode 100644 index 32ad31fca22..00000000000 --- a/core/app/views/spree/admin/states/new.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @state } %> - -<% content_for :page_title do %> - <%= t(:new_state) %> -<% end %> - -<%= form_for [:admin, @country, @state] do |f| %> -
    - <%= t(:new_state) %> - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/tax_categories/_form.html.erb b/core/app/views/spree/admin/tax_categories/_form.html.erb deleted file mode 100644 index 9edaae4354f..00000000000 --- a/core/app/views/spree/admin/tax_categories/_form.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -
    -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> - <%= f.text_field :name, :class => 'fullwidth' %> - <% end %> -
    - -
    - <%= f.field_container :description do %> - <%= f.label :description, t(:description) %>
    - <%= f.text_field :description, :class => 'fullwidth' %> - <% end %> -
    - -
    - <%= f.field_container :is_default, :class => ['checkbox'] do %> - - <% end %> -
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/tax_categories/edit.html.erb b/core/app/views/spree/admin/tax_categories/edit.html.erb deleted file mode 100644 index 93184640899..00000000000 --- a/core/app/views/spree/admin/tax_categories/edit.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_tax_category) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= link_to_with_icon 'icon-arrow-left', t(:back_to_tax_categories_list), admin_tax_categories_path, :class => 'button' %>
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_category } %> - -<%= form_for [:admin, @tax_category] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/tax_categories/index.html.erb b/core/app/views/spree/admin/tax_categories/index.html.erb deleted file mode 100644 index e659062755b..00000000000 --- a/core/app/views/spree/admin/tax_categories/index.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:listing_tax_categories) %> -<% end %> - -<% content_for :page_actions do %> -
      -
    • - <%= button_link_to t(:new_tax_category), new_object_url, :icon => 'icon-plus', :id => 'admin_new_tax_categories_link' %> -
    • -
    -<% end %> - - - - - - - - - - - - - - - - - - <% @tax_categories.each do |tax_category| - @edit_url = edit_admin_tax_category_path(tax_category) - @delete_url = admin_tax_category_path(tax_category) - %> - - - - - - <% end %> - <% if @tax_categories.empty? %> - - <% end %> - -
    <%= t(:name) %><%= t(:description) %><%= t(:default) %>
    <%= tax_category.name %><%= tax_category.description %><%= tax_category.is_default.to_s.titleize %> - <%= link_to_edit tax_category, :no_text => true %> - <%= link_to_delete tax_category, :no_text => true %> -
    <%= t(:none) %>
    \ No newline at end of file diff --git a/core/app/views/spree/admin/tax_categories/new.html.erb b/core/app/views/spree/admin/tax_categories/new.html.erb deleted file mode 100644 index 38232a9d8b0..00000000000 --- a/core/app/views/spree/admin/tax_categories/new.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_tax_category) %> -<% end %> - -<% content_for :page_actions do %> -
  • <%= link_to_with_icon 'icon-arrow-left', t(:back_to_tax_categories_list), admin_tax_categories_path, :class => 'button' %>
  • -<% end %> - - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_category } %> - -<%= form_for [:admin, @tax_category] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/tax_rates/_form.html.erb b/core/app/views/spree/admin/tax_rates/_form.html.erb deleted file mode 100644 index 87f1a4c3a4b..00000000000 --- a/core/app/views/spree/admin/tax_rates/_form.html.erb +++ /dev/null @@ -1,41 +0,0 @@ -
    -
    -
    - <%= t(:general_settings) %> - -
    -
    - <%= f.label :name, t(:name) %> - <%= f.text_field :name, :class => 'fullwidth' %> -
    -
    - <%= f.label :amount, t(:rate) %> - <%= f.text_field :amount, :class => 'fullwidth' %> -
    -
    - <%= f.check_box :included_in_price %> - <%= f.label :included_in_price, t(:included_in_price) %> -
    -
    - -
    -
    - <%= f.label :zone, t(:zone) %> - <%= f.collection_select(:zone_id, @available_zones, :id, :name, {}, {:class => 'select2 fullwidth'}) %> -
    -
    - <%= f.label :tax_category_id, t(:tax_category) %> - <%= f.collection_select(:tax_category_id, @available_categories,:id, :name, {}, {:class => 'select2 fullwidth'}) %> -
    -
    - <%= f.check_box :show_rate_in_label %> - <%= f.label :show_rate_in_label, t(:show_rate_in_label) %> -
    -
    -
    -
    - -
    - - <%= render :partial => 'spree/admin/shared/calculator_fields', :locals => { :f => f } %> -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/tax_rates/edit.html.erb b/core/app/views/spree/admin/tax_rates/edit.html.erb deleted file mode 100644 index 6c67307ab04..00000000000 --- a/core/app/views/spree/admin/tax_rates/edit.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_tax_rate) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_tax_rates_list), spree.admin_tax_rates_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_rate } %> - -<%= form_for [:admin, @tax_rate] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/tax_rates/index.html.erb b/core/app/views/spree/admin/tax_rates/index.html.erb deleted file mode 100644 index fcd3dd57331..00000000000 --- a/core/app/views/spree/admin/tax_rates/index.html.erb +++ /dev/null @@ -1,59 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:tax_rates) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_tax_rate), new_object_url, :icon => 'icon-plus' %> -
  • -<% end %> - -<% unless @tax_rates.any? %> -
    - <%= t(:no_results) %> -
    -<% else %> - - - - - - - - - - - - - - - - - - - - - - - - - <% @tax_rates.each do |tax_rate|%> - - - - - - - - - - - <% end %> - -
    <%= t(:zone) %><%= t(:name) %><%= t(:category) %><%= t(:amount) %><%= t(:included_in_price) %><%= t(:show_rate_in_label) %><%= t(:calculator) %>
    <%=tax_rate.zone.try(:name) || t(:not_available) %><%=tax_rate.name %><%=tax_rate.tax_category.try(:name) || t(:not_available) %><%=tax_rate.amount %><%=tax_rate.included_in_price %><%=tax_rate.show_rate_in_label %><%=tax_rate.calculator.to_s %> - <%= link_to_edit tax_rate, :no_text => true %> - <%= link_to_delete tax_rate, :no_text => true %> -
    -<% end %> diff --git a/core/app/views/spree/admin/tax_rates/new.html.erb b/core/app/views/spree/admin/tax_rates/new.html.erb deleted file mode 100644 index de4e663ca38..00000000000 --- a/core/app/views/spree/admin/tax_rates/new.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_tax_rate) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_tax_rates_list), spree.admin_tax_rates_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tax_rate } %> - -<%= form_for [:admin, @tax_rate] do |f| %> -
    - - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - - <%= render :partial => 'spree/admin/shared/new_resource_links' %> - -
    -<% end %> diff --git a/core/app/views/spree/admin/tax_settings/edit.html.erb b/core/app/views/spree/admin/tax_settings/edit.html.erb deleted file mode 100644 index 71fbafcbece..00000000000 --- a/core/app/views/spree/admin/tax_settings/edit.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:tax_settings) %> -<% end %> - -<%= form_tag admin_tax_settings_path, :method => :put do %> -
    - <%= check_box_tag 'preferences[shipment_inc_vat]', '1', Spree::Config[:shipment_inc_vat] %> - <%= label_tag nil, t(:shipment_inc_vat) %> - <%= hidden_field_tag 'preferences[shipment_inc_vat]', '0' %> -
    - -
    - <%= button t(:update), 'icon-refresh' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/taxonomies/_form.html.erb b/core/app/views/spree/admin/taxonomies/_form.html.erb deleted file mode 100644 index 7ec0dc1663f..00000000000 --- a/core/app/views/spree/admin/taxonomies/_form.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> *
    - <%= error_message_on :taxonomy, :name, :class => 'fullwidth title' %> - <%= text_field :taxonomy, :name %> - <% end %> -
    diff --git a/core/app/views/spree/admin/taxonomies/_js_head.html.erb b/core/app/views/spree/admin/taxonomies/_js_head.html.erb deleted file mode 100755 index d36e1c46efc..00000000000 --- a/core/app/views/spree/admin/taxonomies/_js_head.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% content_for :head do %> - <%= javascript_tag "var taxonomy_id = #{@taxonomy.id}; - var loading = '#{escape_javascript t(:loading)}'; - var new_taxon = '#{escape_javascript t(:new_taxon)}'; - var server_error = '#{escape_javascript t(:server_error)}'; - var taxonomy_tree_error = '#{escape_javascript t(:taxonomy_tree_error)}';" - %> -<% end %> diff --git a/core/app/views/spree/admin/taxonomies/_list.html.erb b/core/app/views/spree/admin/taxonomies/_list.html.erb deleted file mode 100644 index 8ff5867268e..00000000000 --- a/core/app/views/spree/admin/taxonomies/_list.html.erb +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - <% @taxonomies.each do |taxonomy| %> - - - - - <% end %> - -
    <%= t(:name) %>
    - - <%= taxonomy.name %> - - <%= link_to_edit taxonomy.id, :no_text => true %> - <%= link_to_delete taxonomy, :no_text => true %> -
    diff --git a/core/app/views/spree/admin/taxonomies/edit.erb b/core/app/views/spree/admin/taxonomies/edit.erb deleted file mode 100755 index 84980ce083c..00000000000 --- a/core/app/views/spree/admin/taxonomies/edit.erb +++ /dev/null @@ -1,62 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<%= render :partial => 'js_head' %> - -<% content_for :page_title do %> - <%= t(:taxonomy_edit) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_taxonomies_list), spree.admin_taxonomies_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - - - -<%= form_for [:admin, @taxonomy] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    - <%= label_tag nil, t(:tree) %>
    -
    -
    - - -
    <%= t(:taxonomy_tree_instruction) %>
    - -
    - -
    - <%= button t(:update), 'icon-refresh' %> - <%= t(:or) %> - <%= button_link_to t(:cancel), admin_taxonomies_path, :icon => 'icon-remove' %> -
    -
    -<% end %> - -<% content_for :head do %> - <%= javascript_tag do -%> - var initial = [ - { "attr" : - { "id" : "<%= @taxonomy.root.id %>", "rel" : "root" }, - "data" : "<%= escape_javascript(raw(@taxonomy.root.name)) %>", - "state" : "open", - "children" : [ - <% @taxonomy.root.children.each_with_index do |taxon,i| %> - { - "attr" : - { "id" : "<%= taxon.id %>"}, - "data" : "<%= escape_javascript(raw(taxon.name)) %>" - <% unless taxon.children.empty? %> - ,"state" : "closed" - <% end %> - }<%= ',' if i < (@taxonomy.root.children.size - 1) %> - <% end %> - ] - } - ]; - <% end -%> -<% end %> diff --git a/core/app/views/spree/admin/taxonomies/get_children.json.erb b/core/app/views/spree/admin/taxonomies/get_children.json.erb deleted file mode 100755 index 003aec9d85f..00000000000 --- a/core/app/views/spree/admin/taxonomies/get_children.json.erb +++ /dev/null @@ -1,9 +0,0 @@ -[<% @taxons.each_with_index do |t,i| %> - { "attr" : - { "id" : "<%= t.id %>" }, - "data" : "<%= escape_javascript(raw(t.name)) %>" - <% unless t.children.empty? %> - ,"state" : "closed" - <% end %> - }<%= "," if i < (@taxons.size - 1) %> -<% end %>] diff --git a/core/app/views/spree/admin/taxonomies/index.html.erb b/core/app/views/spree/admin/taxonomies/index.html.erb deleted file mode 100644 index 85684803867..00000000000 --- a/core/app/views/spree/admin/taxonomies/index.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:taxonomies) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_taxonomy), spree.new_admin_taxonomy_url, :icon => 'icon-plus', :id => 'admin_new_taxonomy_link' %>

    -
  • -<% end %> - -
    - <%= render :partial => 'list' %> -
    diff --git a/core/app/views/spree/admin/taxonomies/new.html.erb b/core/app/views/spree/admin/taxonomies/new.html.erb deleted file mode 100644 index dee127a1ba0..00000000000 --- a/core/app/views/spree/admin/taxonomies/new.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_taxonomy) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_taxonomies_list), spree.admin_taxonomies_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= form_for [:admin, @taxonomy] do |f| %> - - <%= render :partial => 'form', :locals => { :f => f } %> -
    -
    -
    - <%= button t(:create), 'icon-ok' %> -
    -
    -<% end %> diff --git a/core/app/views/spree/admin/taxons/_form.html.erb b/core/app/views/spree/admin/taxons/_form.html.erb deleted file mode 100644 index 52c4e2ad1b5..00000000000 --- a/core/app/views/spree/admin/taxons/_form.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -
    -
    - <%= f.field_container :name do %> - <%= f.label :name, t(:name) %> *
    - <%= error_message_on :taxon, :name, :class => 'fullwidth title' %> - <%= text_field :taxon, :name, :class => 'fullwidth' %> - <% end %> - - <%= f.field_container :permalink_part do %> - <%= f.label :permalink_part, t(:permalink) %>*
    - <%= @taxon.permalink.split("/")[0...-1].join("/") + "/" %> - <%= text_field_tag :permalink_part, @permalink_part %> - <% end %> - - <%= f.field_container :icon do %> - <%= f.label :icon, t(:icon) %>
    - <%= f.file_field :icon %> - <% end %> -
    - -
    - <%= f.field_container :description do %> - <%= f.label :description, t(:description) %>
    - <%= f.text_area :description, :class => 'fullwidth', :rows => 6 %> - <% end %> -
    -
    diff --git a/core/app/views/spree/admin/taxons/_taxon_table.html.erb b/core/app/views/spree/admin/taxons/_taxon_table.html.erb deleted file mode 100644 index 863f8e3de21..00000000000 --- a/core/app/views/spree/admin/taxons/_taxon_table.html.erb +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - <% taxons.each do |taxon| %> - - - - - - <% end %> - <% if taxons.empty? %> - - <% end %> - -
    <%= t(:name) %><%= t(:path) %>
    <%= taxon.name %><%= taxon_path taxon %> - <%= link_to_delete taxon, :url => remove_admin_product_taxon_url(@product, taxon), :name => icon('delete') + ' ' + t(:remove) %> -
    <%= t(:none) %>.
    diff --git a/core/app/views/spree/admin/taxons/edit.html.erb b/core/app/views/spree/admin/taxons/edit.html.erb deleted file mode 100644 index d992b3dd8f2..00000000000 --- a/core/app/views/spree/admin/taxons/edit.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:taxon_edit) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_taxonomies_list), spree.admin_taxonomies_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= form_for [:admin, @taxonomy, @taxon], :method => :put, :html => { :multipart => true } do |f| %> - <%= render :partial => 'form', :locals => { :f => f } %> - -
    - <%= button t(:update), 'icon-refresh' %> <%= t(:or) %> <%= button_link_to t(:cancel), edit_admin_taxonomy_url(@taxonomy), :icon => "icon-remove" %> -
    -<% end %> diff --git a/core/app/views/spree/admin/trackers/_form.html.erb b/core/app/views/spree/admin/trackers/_form.html.erb deleted file mode 100644 index d0e7b9db02b..00000000000 --- a/core/app/views/spree/admin/trackers/_form.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -
    -
    -
    - <%= label_tag nil, t(:google_analytics_id) %> - <%= text_field :tracker, :analytics_id, :class => 'fullwidth' %> -
    -
    -
    -
    - <%= label_tag nil, t(:environment) %> - <%= collection_select(:tracker, :environment, Rails.configuration.database_configuration.keys.sort, :to_s, :titleize, {}, {:id => 'tracker-env', :class => 'select2 fullwidth'}) %> -
    -
    -
    -
    - <%= label_tag nil, t(:active) %> -
      -
    • - <%= radio_button(:tracker, :active, true) %> - <%= t(:yes) %> -
    • -
    • - <%= radio_button(:tracker, :active, false) %> - <%= t(:no) %> -
    • -
    -
    -
    - -
    - -
    - -
    diff --git a/core/app/views/spree/admin/trackers/edit.html.erb b/core/app/views/spree/admin/trackers/edit.html.erb deleted file mode 100644 index bbd8c1f4bfa..00000000000 --- a/core/app/views/spree/admin/trackers/edit.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_tracker) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_trackers_list), spree.admin_trackers_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tracker } %> - -<%= form_for [:admin, @tracker] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/trackers/index.html.erb b/core/app/views/spree/admin/trackers/index.html.erb deleted file mode 100644 index a1ab3e2a05d..00000000000 --- a/core/app/views/spree/admin/trackers/index.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:analytics_trackers) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_tracker), new_object_url, :icon => 'icon-plus', :id => 'admin_new_tracker_link' %> -
  • -<% end %> - -<% unless @trackers.any? %> -
    - <%= t(:no_trackers_found)%>, - <%= link_to t(:add_one), new_object_url %>! -
    -<% else %> - - - - - - - - - - - - - - - - - <% @trackers.each do |tracker|%> - - - - - - - <% end %> - -
    <%= t(:google_analytics_id) %><%= t(:environment) %><%= t(:active) %>
    <%= tracker.analytics_id %><%= tracker.environment.to_s.titleize %><%= tracker.active ? t(:yes) : t(:no) %> - <%= link_to_edit tracker, :no_text => true %> - <%= link_to_delete tracker, :no_text => true %> -
    -<% end%> \ No newline at end of file diff --git a/core/app/views/spree/admin/trackers/new.html.erb b/core/app/views/spree/admin/trackers/new.html.erb deleted file mode 100644 index 3c4975ee2f1..00000000000 --- a/core/app/views/spree/admin/trackers/new.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_tracker) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_trackers_list), spree.admin_trackers_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @tracker } %> - -<%= form_for [:admin, @tracker] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/variants/_autocomplete.js.erb b/core/app/views/spree/admin/variants/_autocomplete.js.erb deleted file mode 100644 index 16e86647e33..00000000000 --- a/core/app/views/spree/admin/variants/_autocomplete.js.erb +++ /dev/null @@ -1,30 +0,0 @@ - diff --git a/core/app/views/spree/admin/variants/_form.html.erb b/core/app/views/spree/admin/variants/_form.html.erb deleted file mode 100644 index 2385ffade37..00000000000 --- a/core/app/views/spree/admin/variants/_form.html.erb +++ /dev/null @@ -1,61 +0,0 @@ -
    - -
    - <% @product.options.each do |option| %> -
    - <%= label :new_variant, option.option_type.presentation %> - <% if @variant.new_record? %> - <%= select(:new_variant, option.option_type.presentation, - option.option_type.option_values.collect {|ov| [ ov.presentation, ov.id ] }, - {}, {:class => 'select2 fullwidth'}) - %> - <% else %> - <% if opt = @variant.option_values.detect {|o| o.option_type == option.option_type }.try(:presentation) %> - <%= text_field(:new_variant, option.option_type.presentation, :value => opt, :disabled => 'disabled', :class => 'fullwidth') %> - <% end %> - <% end %> -
    - <% end %> - -
    - <%= f.label :sku, t(:sku) %> - <%= f.text_field :sku, :class => 'fullwidth' %> -
    - -
    - <%= f.label :price, t(:price) %> - <%= f.text_field :price, :value => number_to_currency(@variant.price, :unit => ''), :class => 'fullwidth' %> -
    - -
    - <%= f.label :cost_price, t(:cost_price) %> - <%= f.text_field :cost_price, :value => number_to_currency(@variant.cost_price, :unit => ''), :class => 'fullwidth' %> -
    - - <% if Spree::Config[:track_inventory_levels] %> -
    - -
    -
    - <%= f.label :on_hand, t(:on_hand) %> - <%= f.text_field :on_hand, :class => 'fullwidth' %> -
    - <% end %> -
    -
    - - - -
    - <% [:weight, :height, :width, :depth].each do |field| %> -
    <%= f.label field, t(field) %> - <% value = number_with_precision(@variant.send(field), :precision => 2) %> - <%= f.text_field field, :value => value, :class => 'fullwidth' %> -
    - <% end %> -
    - -
    diff --git a/core/app/views/spree/admin/variants/edit.html.erb b/core/app/views/spree/admin/variants/edit.html.erb deleted file mode 100644 index c45e1cb9afe..00000000000 --- a/core/app/views/spree/admin/variants/edit.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Variants' } %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @variant } %> - -<%= form_for [:admin, @product, @variant] do |f| %> -
    - <%= render :partial => 'form', :locals => { :f => f } %> -
    - - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -<% end %> diff --git a/core/app/views/spree/admin/variants/index.html.erb b/core/app/views/spree/admin/variants/index.html.erb deleted file mode 100644 index 61ccf96e738..00000000000 --- a/core/app/views/spree/admin/variants/index.html.erb +++ /dev/null @@ -1,78 +0,0 @@ -<%= render :partial => 'spree/admin/shared/product_sub_menu' %> - -<%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => 'Variants'} %> - -<%# Place for new variant form %> -
    - -<% if @variants.any? %> - - - - - - - - - - - - - - - - - - - - <% @variants.each do |variant| %> - - <% next if variant.option_values.empty? %> - data-hook="variants_row" class="<%= cycle('odd', 'even')%>"> - - - - - - - - <% end %> - <% unless @product.has_variants? %> - - <% end %> - -
    <%= t(:options) %><%= t(:price) %><%= t(:sku) %><%= t(:on_hand) %>
    - - <%= variant.options_text %><%= variant.display_price %><%= variant.sku %><%= variant.on_hand %> - <%= link_to_edit(variant, :no_text => true) unless variant.deleted? %> -   - <%= link_to_delete(variant, :no_text => true) unless variant.deleted? %> -
    <%= t(:none) %>
    -<% else %> -
    <%= t(:no_results)%>.
    -<% end %> - -<% if @product.empty_option_values? %> -

    - <%= t(:to_add_variants_you_must_first_define) %> - <%= link_to t(:option_types), admin_product_url(@product) %> - <%= t(:and) %> - <%= link_to t(:option_values), admin_option_types_url %> -

    -<% else %> - <% content_for :page_actions do %> -
      - -
    • <%= link_to_with_icon('icon-filter', @deleted.blank? ? t(:show_deleted) : t(:show_active), admin_product_variants_url(@product, :deleted => @deleted.blank? ? "on" : "off"), :class => 'button') %>
    • -
    - <% end %> - -<% end %> diff --git a/core/app/views/spree/admin/variants/new.html.erb b/core/app/views/spree/admin/variants/new.html.erb deleted file mode 100644 index d4c33c5c037..00000000000 --- a/core/app/views/spree/admin/variants/new.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @variant } %> - -<%= form_for [:admin, @product, @variant] do |f| %> -
    - <%= t(:new_variant) %> - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/variants/search.rabl b/core/app/views/spree/admin/variants/search.rabl deleted file mode 100644 index 5d303ef17b2..00000000000 --- a/core/app/views/spree/admin/variants/search.rabl +++ /dev/null @@ -1,15 +0,0 @@ -object false -child(@variants => :variants) do - attributes :sku, :options_text, :count_on_hand, :id, :name - - child(:images => :images) do - attributes :mini_url - end - - child(:option_values => :option_values) do - child(:option_type => :option_type) do - attributes :name, :presentation - end - attributes :name, :presentation - end -end diff --git a/core/app/views/spree/admin/zones/_country_member.html.erb b/core/app/views/spree/admin/zones/_country_member.html.erb deleted file mode 100644 index 8294b5d1518..00000000000 --- a/core/app/views/spree/admin/zones/_country_member.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
  • - <%= f.hidden_field :zoneable_type, :value => 'Spree::Country' %> - <%= f.collection_select(:zoneable_id, @countries, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth'}) %> - <%= remove_nested f %> -
  • diff --git a/core/app/views/spree/admin/zones/_form.html.erb b/core/app/views/spree/admin/zones/_form.html.erb deleted file mode 100644 index b9abbc437c1..00000000000 --- a/core/app/views/spree/admin/zones/_form.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -
    -
    - <%= t(:general_settings)%> - - <%= zone_form.field_container :name do %> - <%= zone_form.label :name, t(:name) %>
    - <%= zone_form.text_field :name, :class => 'fullwidth' %> - <% end %> - - <%= zone_form.field_container :description do %> - <%= zone_form.label :description, t(:description) %>
    - <%= zone_form.text_field :description, :class => 'fullwidth' %> - <% end %> - -
    - <%= zone_form.check_box :default_tax %> - <%= label_tag t(:default_tax_zone) %> -
    - -
    - <%= label_tag t(:type) %> -
      -
    • - <%= zone_form.radio_button('kind', 'country', { :id => 'country_based' }) %> - <%= label_tag t(:country_based) %> -
    • -
    • - <%= zone_form.radio_button('kind', 'state', { :id => 'state_based' }) %> - <%= label_tag t(:state_based) %> -
    • -
    -
    -
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/zones/_member_type.html.erb b/core/app/views/spree/admin/zones/_member_type.html.erb deleted file mode 100644 index b1e66a5d03f..00000000000 --- a/core/app/views/spree/admin/zones/_member_type.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<%= javascript_tag "var #{type}_member='#{generate_template(zone_form, :zone_members, {:partial => type + "_member"})}';" %> - - -
    -
    - <%= t(type) %> - -
      - <% members_of_type = zone_form.object.zone_members.select { |member| member.zoneable_type && member.zoneable_type == "Spree::#{type.capitalize}" } %> - <%= zone_form.fields_for :zone_members, members_of_type do |member_form| %> - <%= render :partial => "#{type}_member", :locals => { :f => member_form } %> - <% end %> -
    - -
    - <%= button_link_to t("add_#{type}"), "##{type}_member", { :icon => 'icon-plus', :id => "nested-#{type}" } %> -
    -
    -
    \ No newline at end of file diff --git a/core/app/views/spree/admin/zones/_state_member.html.erb b/core/app/views/spree/admin/zones/_state_member.html.erb deleted file mode 100644 index 28e2afe3b54..00000000000 --- a/core/app/views/spree/admin/zones/_state_member.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
  • - <%= f.hidden_field :zoneable_type, :value => 'Spree::State' %> - <%= f.collection_select(:zoneable_id, @states, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth'}) %> - <%= remove_nested f %> -
  • diff --git a/core/app/views/spree/admin/zones/edit.html.erb b/core/app/views/spree/admin/zones/edit.html.erb deleted file mode 100644 index a7216185dac..00000000000 --- a/core/app/views/spree/admin/zones/edit.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:editing_zone) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_zones_list), admin_zones_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @zone } %> - -<%= form_for [:admin, @zone] do |zone_form| %> -
    - <%= render :partial => 'form', :locals => { :zone_form => zone_form } %> - <%= render :partial => 'member_type', :locals => { :type => 'country', :zone_form => zone_form }%> - <%= render :partial => 'member_type', :locals => { :type => 'state', :zone_form => zone_form } %> -
    - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
    -<% end %> diff --git a/core/app/views/spree/admin/zones/index.html.erb b/core/app/views/spree/admin/zones/index.html.erb deleted file mode 100644 index 6eb0586e60b..00000000000 --- a/core/app/views/spree/admin/zones/index.html.erb +++ /dev/null @@ -1,51 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:zones) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:new_zone), new_object_url, :icon => 'icon-plus', :id => 'admin_new_zone_link' %> -
  • -<% end %> - - <% if @zones.empty? %> -
    - <%= t(:none) %> -
    -<% else %> - - - - - - - - - - - - - - - - - <% @zones.each do |zone| %> - - - - - - - <% end %> - -
    <%= sort_link @search,:name, t(:name), :title => 'zones_order_by_name_title' %> - <%= sort_link @search,:description, t(:description), {}, {:title => 'zones_order_by_description_title'} %> - <%= t(:default_tax) %>
    <%=zone.name %><%=zone.description %><%=zone.default_tax %> - <%=link_to_edit zone, :no_text => true %> - <%=link_to_delete zone, :no_text => true %> -
    -<% end%> - -<%= paginate @zones %> diff --git a/core/app/views/spree/admin/zones/new.html.erb b/core/app/views/spree/admin/zones/new.html.erb deleted file mode 100644 index 57a6d5e7ba0..00000000000 --- a/core/app/views/spree/admin/zones/new.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<%= render :partial => 'spree/admin/shared/configuration_menu' %> - -<% content_for :page_title do %> - <%= t(:new_zone) %> -<% end %> - -<% content_for :page_actions do %> -
  • - <%= button_link_to t(:back_to_zones_list), spree.admin_zones_path, :icon => 'icon-arrow-left' %> -
  • -<% end %> - -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @zone } %> - -<%= form_for [:admin, @zone] do |zone_form| %> - <%= render :partial => 'form', :locals => { :zone_form => zone_form } %> - -
    - - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -<% end %> diff --git a/core/app/views/spree/checkout/_address.html.erb b/core/app/views/spree/checkout/_address.html.erb deleted file mode 100644 index 0b008b01a4a..00000000000 --- a/core/app/views/spree/checkout/_address.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -
    -
    - <%= form.fields_for :bill_address do |bill_form| %> - <%= t(:billing_address) %> - <%= render :partial => 'spree/address/form', :locals => {:form => bill_form, :address_type => 'billing', :address => @order.bill_address} %> - <% end %> -
    -
    - -
    -
    - <%= form.fields_for :ship_address do |ship_form| %> - <%= t(:shipping_address) %> -

    - <%= check_box_tag 'order[use_billing]', '1', ((@order.bill_address.empty? && @order.ship_address.empty?) || @order.bill_address.same_as?(@order.ship_address)) %> - <%= label_tag :order_use_billing, t(:use_billing_address), :id => 'use_billing' %> -

    - <%= render :partial => 'spree/address/form', :locals => {:form => ship_form, :address_type => 'shipping', :address => @order.ship_address} %> - <% end %> -
    -
    -
    -
    - <%= submit_tag t(:save_and_continue), :class => 'continue button primary' %> -
    diff --git a/core/app/views/spree/checkout/_confirm.html.erb b/core/app/views/spree/checkout/_confirm.html.erb deleted file mode 100644 index 8ba525dd887..00000000000 --- a/core/app/views/spree/checkout/_confirm.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -
    -
    - <%= t(@order.state, :scope => :order_state).titleize %> - <%= render :partial => 'spree/shared/order_details', :locals => { :order => @order } %> -
    - -
    - -
    - <%= submit_tag t(:place_order), :class => 'continue button primary' %> - -
    diff --git a/core/app/views/spree/checkout/_delivery.html.erb b/core/app/views/spree/checkout/_delivery.html.erb deleted file mode 100644 index d5668b08c5f..00000000000 --- a/core/app/views/spree/checkout/_delivery.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -
    - <%= t(:shipping_method) %> -
    -
    -

    - <% @order.rate_hash.each do |shipping_method| %> - - <% end %> -

    -
    - <% if Spree::Config[:shipping_instructions] && @order.rate_hash.present? %> -

    - <%= form.label :special_instructions, t(:shipping_instructions) %>
    - <%= form.text_area :special_instructions, :cols => 40, :rows => 7 %> -

    - <% end %> -
    -
    - -
    - -
    - <%= submit_tag t(:save_and_continue), :class => 'continue button primary' %> -
    diff --git a/core/app/views/spree/checkout/_payment.html.erb b/core/app/views/spree/checkout/_payment.html.erb deleted file mode 100644 index e42138a941d..00000000000 --- a/core/app/views/spree/checkout/_payment.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -
    - <%= t(:payment_information) %> -
    - <% @order.available_payment_methods.each do |method| %> -

    - -

    - <% end %> - -
      - <% @order.available_payment_methods.each do |method| %> -
    • -
      - <%= render :partial => "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } %> -
      -
    • - <% end %> -
    -
    -
    -
    -
    - -
    - -
    - <%= submit_tag t(:save_and_continue), :class => 'continue button primary' %> - -
    diff --git a/core/app/views/spree/checkout/_summary.html.erb b/core/app/views/spree/checkout/_summary.html.erb deleted file mode 100644 index 58cb14a9cec..00000000000 --- a/core/app/views/spree/checkout/_summary.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -

    <%= t(:order_summary) %>

    - - - - - - - - - <% order.adjustments.eligible.each do |adjustment| %> - <% next if (adjustment.originator_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> - - - - - <% end %> - - - - - - <% if order.price_adjustment_totals.present? %> - - <% @order.price_adjustment_totals.keys.each do |key| %> - - - - - <% end %> - - <% end %> - -
    <%= t(:item_total) %>:<%= order.display_item_total %>
    <%= adjustment.label %>: <%= adjustment.display_amount %>
    <%= t(:order_total) %>:<%= @order.display_total %>
    <%= key %><%= @order.price_adjustment_totals[key].display_total %>
    diff --git a/core/app/views/spree/checkout/edit.html.erb b/core/app/views/spree/checkout/edit.html.erb deleted file mode 100644 index 65e6bfeff60..00000000000 --- a/core/app/views/spree/checkout/edit.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<% content_for :head do %> - <%= javascript_include_tag states_url(:format => :js) %> - <%= javascript_include_tag countries_url(:format => :js) %> -<% end %> - -
    - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> - -
    -

    <%= t(:checkout) %>

    -
    <%= checkout_progress %>
    -
    - -
    -
    - <%= form_for @order, :url => update_checkout_path(@order.state), :html => { :id => "checkout_form_#{@order.state}" } do |form| %> - <% unless @order.email? %> -

    - <%= form.label :email %>
    - <%= form.text_field :email %> -

    - <% end %> - <%= render @order.state, :form => form %> - <% end %> -
    - <% if @order.state != 'confirm' %> -
    - <%= render :partial => 'summary', :locals => { :order => @order } %> -
    - <% end %> -
    - -
    diff --git a/core/app/views/spree/checkout/payment/_gateway.html.erb b/core/app/views/spree/checkout/payment/_gateway.html.erb deleted file mode 100644 index d44941178d6..00000000000 --- a/core/app/views/spree/checkout/payment/_gateway.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<%= image_tag 'credit_cards/credit_card.gif', :id => 'credit-card-image' %> -<% param_prefix = "payment_source[#{payment_method.id}]" %> - -

    - <%= label_tag nil, t(:card_number) %>
    - <% options_hash = Rails.env.production? ? {:autocomplete => 'off'} : {} %> - <%= text_field_tag "#{param_prefix}[number]", '', options_hash.merge(:id => 'card_number', :class => 'required', :size => 19, :maxlength => 19, :autocomplete => "off") %> - * -   - -

    -

    - <%= label_tag nil, t(:expiration) %>
    - <%= select_month(Date.today, { :prefix => param_prefix, :field_name => 'month', :use_month_numbers => true }, :class => 'required') %> - <%= select_year(Date.today, { :prefix => param_prefix, :field_name => 'year', :start_year => Date.today.year, :end_year => Date.today.year + 15 }, :class => 'required') %> - * -

    -

    - <%= label_tag nil, t(:card_code) %>
    - <%= text_field_tag "#{param_prefix}[verification_value]", '', options_hash.merge(:id => 'card_code', :class => 'required', :size => 5) %> - * - <%= link_to "(#{t(:whats_this)})", spree.content_path('cvv'), :target => '_blank', :onclick => "window.open(this.href,'cvv_info','left=20,top=20,width=500,height=500,toolbar=0,resizable=0,scrollbars=1');return false", "data-hook" => "cvv_link" %> -

    -<%= hidden_field param_prefix, 'first_name', :value => @order.billing_firstname %> -<%= hidden_field param_prefix, 'last_name', :value => @order.billing_lastname %> diff --git a/core/app/views/spree/checkout/registration.html.erb b/core/app/views/spree/checkout/registration.html.erb deleted file mode 100644 index 5df050a5901..00000000000 --- a/core/app/views/spree/checkout/registration.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @user } %> -

    <%= t(:registration) %>

    -
    -
    - -
    - <% if Spree::Config[:allow_guest_checkout] %> -
    - <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> -
    <%= t(:guest_user_account) %>
    - <%= form_for @order, :url => update_checkout_registration_path, :method => :put, :html => { :id => 'checkout_form_registration' } do |f| %> -

    - <%= f.label :email, t(:email) %>
    - <%= f.email_field :email, :class => 'title' %> -

    -

    <%= f.submit t(:continue), :class => 'button primary' %>

    - <% end %> -
    - <% end %> -
    diff --git a/core/app/views/spree/countries/index.js.erb b/core/app/views/spree/countries/index.js.erb deleted file mode 100644 index 58a5e2c117e..00000000000 --- a/core/app/views/spree/countries/index.js.erb +++ /dev/null @@ -1 +0,0 @@ -states_required_mapper = <%== @states_required %> \ No newline at end of file diff --git a/core/app/views/spree/home/index.html.erb b/core/app/views/spree/home/index.html.erb deleted file mode 100644 index d17a6e4f91a..00000000000 --- a/core/app/views/spree/home/index.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% content_for :sidebar do %> -
    - <%= render :partial => 'spree/shared/taxonomies' %> -
    -<% end %> - -
    - <%= render :partial => 'spree/shared/products', :locals => { :products => @products } %> -
    - diff --git a/core/app/views/spree/layouts/admin.html.erb b/core/app/views/spree/layouts/admin.html.erb deleted file mode 100644 index b11301d975d..00000000000 --- a/core/app/views/spree/layouts/admin.html.erb +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - <%= render :partial => 'spree/admin/shared/head' %> - - - -
    - - <% if flash[:error] %> -
    <%= flash[:error] %>
    - <% end %> - <% if notice %> -
    <%= notice %>
    - <% end %> - <% if flash[:success] %> -
    <%= flash[:success] %>
    - <% end %> - -
    -
    -
    -
    <%= t(:loading) %>...
    -
    -
    - - <%= render :partial => 'spree/admin/shared/alert', :collection => session[:alerts] %> - - - - - - <% if content_for?(:sub_menu) %> - - <% end %> - - <% if content_for?(:page_title) || content_for?(:page_actions) %> -
    -
    - -
    -
    - <% if content_for?(:page_title) %> -
    -

    <%= yield :page_title %>

    -
    - <% end %> - <% if content_for?(:page_actions) %> -
    -
      - <%= yield :page_actions %> -
    -
    - <% end %> -
    -
    - -
    -
    - <% end %> - -
    -
    -
    - - <% if content_for?(:table_filter) %> -
    -
    - <%= yield :table_filter_title %> - <%= yield :table_filter %> -
    -
    - <% end %> - -
    - <%= yield %> -
    - - - <% if content_for?(:sidebar) %> - - <% end %> - -
    -
    - -
    - -
    - -
    - - - - diff --git a/core/app/views/spree/order_mailer/cancel_email.html.erb b/core/app/views/spree/order_mailer/cancel_email.html.erb new file mode 100644 index 00000000000..5fc4f6700a9 --- /dev/null +++ b/core/app/views/spree/order_mailer/cancel_email.html.erb @@ -0,0 +1,45 @@ + + + + + +
    +

    + <%= Spree.t('order_mailer.cancel_email.dear_customer') %> +

    +

    + <%= Spree.t('order_mailer.cancel_email.instructions') %> +

    +

    + <%= Spree.t('order_mailer.cancel_email.order_summary_canceled') %> +

    + + <% @order.line_items.each do |item| %> + + + + + + <% end %> + + + + + + <% @order.adjustments.eligible.each do |adjustment| %> + + + + + + <% end %> + + + + + +
    <%= item.variant.sku %> + <%= raw(item.variant.product.name) %> + <%= raw(item.variant.options_text) -%> + (<%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %>
    <%= Spree.t('order_mailer.cancel_email.subtotal') %><%= @order.display_item_total %>
    <%= raw(adjustment.label) %><%= adjustment.display_amount %>
    <%= Spree.t('order_mailer.cancel_email.total') %><%= @order.display_total %>
    +
    diff --git a/core/app/views/spree/order_mailer/cancel_email.text.erb b/core/app/views/spree/order_mailer/cancel_email.text.erb index a27e1a8624f..08105a23afc 100755 --- a/core/app/views/spree/order_mailer/cancel_email.text.erb +++ b/core/app/views/spree/order_mailer/cancel_email.text.erb @@ -1,16 +1,16 @@ -<%= t('order_mailer.cancel_email.dear_customer') %> +<%= Spree.t('order_mailer.cancel_email.dear_customer') %> -<%= t('order_mailer.cancel_email.instructions') %> +<%= Spree.t('order_mailer.cancel_email.instructions') %> ============================================================ -<%= t('order_mailer.cancel_email.order_summary_canceled') %> +<%= Spree.t('order_mailer.cancel_email.order_summary_canceled') %> ============================================================ <% @order.line_items.each do |item| %> - <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) @ <%= item.variant.display_amount %> = <%= item.display_amount %> + <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %> <% end %> ============================================================ -<%= t('order_mailer.cancel_email.subtotal') %> <%= @order.display_item_total %> +<%= Spree.t('order_mailer.cancel_email.subtotal') %> <%= @order.display_item_total %> <% @order.adjustments.eligible.each do |adjustment| %> <%= raw(adjustment.label) %> <%= adjustment.display_amount %> <% end %> -<%= t('order_mailer.cancel_email.total') %> <%= @order.display_total %> +<%= Spree.t('order_mailer.cancel_email.total') %> <%= @order.display_total %> diff --git a/core/app/views/spree/order_mailer/confirm_email.html.erb b/core/app/views/spree/order_mailer/confirm_email.html.erb new file mode 100644 index 00000000000..f5498dcf5ac --- /dev/null +++ b/core/app/views/spree/order_mailer/confirm_email.html.erb @@ -0,0 +1,84 @@ + + + + + +
    +

    + <%= Spree.t('order_mailer.confirm_email.dear_customer') %> +

    +

    + <%= Spree.t('order_mailer.confirm_email.instructions') %> +

    +

    + <%= Spree.t('order_mailer.confirm_email.order_summary') %> +

    + + <% @order.line_items.each do |item| %> + + + + + + <% end %> + + + + + + <% if @order.line_item_adjustments.exists? %> + <% if @order.all_adjustments.promotion.eligible.exists? %> + <% @order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> + + + + + + <% end %> + <% end %> + <% end %> + <% @order.shipments.group_by { |s| s.selected_shipping_rate.try(:name) }.each do |name, shipments| %> + + + + + + <% end %> + <% if @order.all_adjustments.eligible.tax.exists? %> + <% @order.all_adjustments.eligible.tax.group_by(&:label).each do |label, adjustments| %> + + + + + + <% end %> + <% end %> + <% @order.adjustments.eligible.each do |adjustment| %> + <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> + + + + + + <% end %> + + + + + +
    <%= item.variant.sku %> + <%= raw(item.variant.product.name) %> + <%= raw(item.variant.options_text) -%> + (<%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %>
    + <%= Spree.t('order_mailer.confirm_email.subtotal') %> + + <%= @order.display_item_total %> +
    <%= Spree.t(:promotion) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %>
    <%= Spree.t(:shipping) %> <%= name %>:<%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: @order.currency) %>
    <%= Spree.t(:tax) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %>
    <%= adjustment.label %>:<%= adjustment.display_amount %>
    + <%= Spree.t('order_mailer.confirm_email.total') %> + + <%= @order.display_total %> +
    +

    + <%= Spree.t('order_mailer.confirm_email.thanks') %> +

    +
    diff --git a/core/app/views/spree/order_mailer/confirm_email.text.erb b/core/app/views/spree/order_mailer/confirm_email.text.erb index 687762a0543..8d156b163d3 100644 --- a/core/app/views/spree/order_mailer/confirm_email.text.erb +++ b/core/app/views/spree/order_mailer/confirm_email.text.erb @@ -1,20 +1,38 @@ -<%= t('order_mailer.confirm_email.dear_customer') %> +<%= Spree.t('order_mailer.confirm_email.dear_customer') %> -<%= t('order_mailer.confirm_email.instructions') %> +<%= Spree.t('order_mailer.confirm_email.instructions') %> ============================================================ -<%= t('order_mailer.confirm_email.order_summary') %> +<%= Spree.t('order_mailer.confirm_email.order_summary') %> ============================================================ <% @order.line_items.each do |item| %> - <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) @ <%= item.variant.display_amount %> = <%= item.display_amount %> + <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %> <% end %> ============================================================ -<%= t('order_mailer.confirm_email.subtotal') %>: <%= @order.display_item_total %> +<%= Spree.t('order_mailer.confirm_email.subtotal') %> <%= @order.display_item_total %> +<% if @order.line_item_adjustments.exists? %> + <% if @order.all_adjustments.promotion.eligible.exists? %> + <% @order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> +<%= Spree.t(:promotion) %>: <%= label %> <%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %> + <% end %> + <% end %> +<% end %> -<% @order.adjustments.eligible.each do |adjustment| %> - <%= raw(adjustment.label) %> <%= adjustment.display_amount %> +<% @order.shipments.group_by { |s| s.selected_shipping_rate.try(:name) }.each do |name, shipments| %> +<%= Spree.t(:shipping) %>: <%= name %> <%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: @order.currency) %> +<% end %> + +<% if @order.all_adjustments.eligible.tax.exists? %> + <% @order.all_adjustments.eligible.tax.group_by(&:label).each do |label, adjustments| %> +<%= Spree.t(:tax) %>: <%= label %> <%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %> + <% end %> <% end %> -<%= t('order_mailer.confirm_email.total') %>: <%= @order.display_total %> +<% @order.adjustments.eligible.each do |adjustment| %> + <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> +<%= adjustment.label %> <%= adjustment.display_amount %> +<% end %> +============================================================ +<%= Spree.t('order_mailer.confirm_email.total') %> <%= @order.display_total %> -<%= t('order_mailer.confirm_email.thanks') %> +<%= Spree.t('order_mailer.confirm_email.thanks') %> diff --git a/core/app/views/spree/orders/_adjustments.html.erb b/core/app/views/spree/orders/_adjustments.html.erb deleted file mode 100644 index 79efc04bc44..00000000000 --- a/core/app/views/spree/orders/_adjustments.html.erb +++ /dev/null @@ -1,14 +0,0 @@ - - - <%= t(:order_adjustments) %> - - - - <% @order.adjustments.eligible.each do |adjustment| %> - - <%= adjustment.label %> - <%= adjustment.display_amount %> - - - <% end %> - diff --git a/core/app/views/spree/orders/_form.html.erb b/core/app/views/spree/orders/_form.html.erb deleted file mode 100644 index 3059e639f2f..00000000000 --- a/core/app/views/spree/orders/_form.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> - - - - - - - - - - - - <%= order_form.fields_for :line_items do |item_form| %> - <%= render :partial => 'line_item', :locals => { :variant => item_form.object.variant, :line_item => item_form.object, :item_form => item_form } %> - <% end %> - - <%= render "spree/orders/adjustments" unless @order.adjustments.eligible.blank? %> -
    <%= t(:item) %><%= t(:price) %><%= t(:qty) %><%= t(:total) %>
    diff --git a/core/app/views/spree/orders/_line_item.html.erb b/core/app/views/spree/orders/_line_item.html.erb deleted file mode 100644 index 8b9f78bc932..00000000000 --- a/core/app/views/spree/orders/_line_item.html.erb +++ /dev/null @@ -1,31 +0,0 @@ - - - <% if variant.images.length == 0 %> - <%= link_to small_image(variant.product), variant.product %> - <% else %> - <%= link_to image_tag(variant.images.first.attachment.url(:small)), variant.product %> - <% end %> - - -

    <%= link_to variant.product.name, product_path(variant.product) %>

    - <%= variant.options_text %> - <% if @order.insufficient_stock_lines.include? line_item %> - - <%= variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock) %>
    -
    - <% end %> - <%= line_item_description(variant) %> - - - <%= line_item.single_money %> - - - <%= item_form.number_field :quantity, :min => 0, :class => "line_item_quantity", :size => 5 %> - - - <%= line_item.display_amount unless line_item.quantity.nil? %> - - - <%= link_to image_tag('icons/delete.png'), '#', :class => 'delete', :id => "delete_#{dom_id(line_item)}" %> - - diff --git a/core/app/views/spree/orders/edit.html.erb b/core/app/views/spree/orders/edit.html.erb deleted file mode 100644 index 3e8393fb0eb..00000000000 --- a/core/app/views/spree/orders/edit.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<% @body_id = 'cart' %> - -

    <%= t(:shopping_cart) %>

    - -<% if @order.line_items.empty? %> - -
    -

    <%= t(:your_cart_is_empty) %>

    -

    <%= link_to t(:continue_shopping), products_path, :class => 'button continue' %>

    -
    - -<% else %> -
    - <%= form_for @order, :url => update_cart_path, :html => {:id => 'update-cart'} do |order_form| %> -
    - -
    - <%= render :partial => 'form', :locals => { :order_form => order_form } %> -
    - -
    -
    <%= t(:subtotal) %>: <%= @order.display_total %>
    -
    - - - -
    - <% end %> -
    - -
    - <%= form_tag empty_cart_path, :method => :put do %> - - <% end %> -
    - -<% end %> diff --git a/core/app/views/spree/orders/new.html.erb b/core/app/views/spree/orders/new.html.erb deleted file mode 100644 index 756032ca7f9..00000000000 --- a/core/app/views/spree/orders/new.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% @body_id = 'cart' %> - -

    <%= t(:shopping_cart) %>

    - -<%= form_for @order, :url =>'#', :method => :put do |order_form| %> - <%= render :partial => 'form', :locals => { :order_form => order_form } %> -<% end %> - -

    <%= link_to t(:continue_shopping), products_path %>

    diff --git a/core/app/views/spree/orders/show.html.erb b/core/app/views/spree/orders/show.html.erb deleted file mode 100644 index 4b77a5f913e..00000000000 --- a/core/app/views/spree/orders/show.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -
    - <%= t(:order) + " #" + @order.number %> -

    <%= accurate_title %>

    - -
    - <% if params.has_key? :checkout_complete %> -

    <%= t(:thank_you_for_your_order) %>

    - <% end %> - <%= render :partial => 'spree/shared/order_details', :locals => { :order => @order } %> - -
    - -

    - <%= link_to t(:back_to_store), spree.root_path, :class => "button" %> - <% unless params.has_key? :checkout_complete %> - <% if try_spree_current_user && respond_to?(:spree_account_path) %> - <%= link_to t(:my_account), spree_account_path, :class => "button" %> - <% end %> - <% end %> -

    -
    -
    diff --git a/core/app/views/spree/products/_cart_form.html.erb b/core/app/views/spree/products/_cart_form.html.erb deleted file mode 100644 index 7823f82e9f2..00000000000 --- a/core/app/views/spree/products/_cart_form.html.erb +++ /dev/null @@ -1,53 +0,0 @@ -<%= form_for :order, :url => populate_orders_path do |f| %> -
    - - <% if @product.has_variants? %> -
    -
    <%= t(:variants) %>
    -
      - <% has_checked = false - @product.variants.active(current_currency).each_with_index do |v,index| - next if v.option_values.empty? || (!v.in_stock && !Spree::Config[:show_zero_stock_products]) - checked = !has_checked && (v.in_stock || Spree::Config[:allow_backorders]) - has_checked = true if checked %> -
    • - <%= radio_button_tag "products[#{@product.id}]", v.id, checked, :disabled => !v.in_stock && !Spree::Config[:allow_backorders], 'data-price' => v.price_in(current_currency).display_price %> - -
    • - <% end%> -
    -
    - <% end%> - - <% if @product.price %> -
    - -
    -
    <%= t(:price) %>
    -
    <%= @product.price_in(current_currency).display_price %>
    -
    - -
    - <% if @product.on_sale? %> - <%= number_field_tag (@product.has_variants? ? :quantity : "variants[#{@product.master.id}]"), - 1, :class => 'title', :in => 1..@product.on_hand, :min => 1 %> - <%= button_tag :class => 'large primary', :id => 'add-to-cart-button', :type => :submit do %> - <%= t(:add_to_cart) %> - <% end %> - <% else %> - <%= content_tag('strong', t(:out_of_stock)) %> - <% end %> -
    - -
    - <% end %> - -
    -<% end %> diff --git a/core/app/views/spree/products/_thumbnails.html.erb b/core/app/views/spree/products/_thumbnails.html.erb deleted file mode 100644 index a1d471db7c7..00000000000 --- a/core/app/views/spree/products/_thumbnails.html.erb +++ /dev/null @@ -1,19 +0,0 @@ - -<% if product.images.size > 1 || product.variant_images.size > 0 %> -
      - <% product.images.each do |i| %> -
    • <%= link_to image_tag(i.attachment.url(:mini)), i.attachment.url(:product) %>
    • - <% end %> - <% if @product.has_variants? - @variants.each do |v| - if v.available? - v.images.each do |i| %> -
    • <%= link_to image_tag(i.attachment.url(:mini)), i.attachment.url(:product) %>
    • - <% - end - end - end - end - %> -
    -<% end %> diff --git a/core/app/views/spree/products/index.html.erb b/core/app/views/spree/products/index.html.erb deleted file mode 100644 index 5dcb2a5f79f..00000000000 --- a/core/app/views/spree/products/index.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<% content_for :sidebar do %> -
    - <% if "products" == params[:controller] && @taxon %> - <%= render :partial => 'spree/shared/filters' %> - <% else %> - <%= render :partial => 'spree/shared/taxonomies' %> - <% end %> -
    -<% end %> - - -<% if params[:keywords] %> - -
    - <% if @products.empty? %> -
    <%= t(:no_products_found) %>
    - <% else %> - <%= render :partial => 'spree/shared/products', :locals => { :products => @products, :taxon => @taxon } %> - <% end %> -
    - -<% else %> - -
    - <%= render :partial => 'spree/shared/products', :locals => { :products => @products, :taxon => @taxon } %> -
    -<% end %> diff --git a/core/app/views/spree/products/show.html.erb b/core/app/views/spree/products/show.html.erb deleted file mode 100644 index 07612c3024a..00000000000 --- a/core/app/views/spree/products/show.html.erb +++ /dev/null @@ -1,45 +0,0 @@ -
    - <% @body_id = 'product-details' %> - -
    -
    - -
    -
    - <%= render :partial => 'image' %> -
    -
    - <%= render :partial => 'thumbnails', :locals => { :product => @product } %> -
    -
    - -
    - <%= render :partial => 'properties' %> -
    - -
    -
    - -
    -
    - -
    - -

    <%= accurate_title %>

    - -
    - <%= product_description(@product) rescue t(:product_has_no_description) %> -
    - -
    - <%= render :partial => 'cart_form' %> -
    - -
    - - <%= render :partial => 'taxons' %> - -
    -
    - -
    diff --git a/core/app/views/spree/reimbursement_mailer/reimbursement_email.text.erb b/core/app/views/spree/reimbursement_mailer/reimbursement_email.text.erb new file mode 100644 index 00000000000..02b4a4e5a9e --- /dev/null +++ b/core/app/views/spree/reimbursement_mailer/reimbursement_email.text.erb @@ -0,0 +1,22 @@ +<%= Spree.t('reimbursement_mailer.reimbursement_email.dear_customer') %> + +<%= Spree.t('reimbursement_mailer.reimbursement_email.instructions') %> + +============================================================ +<%= Spree.t('reimbursement_mailer.reimbursement_email.refund_summary') %> +============================================================ +<%= Spree.t('reimbursement_mailer.reimbursement_email.total_refunded', total: @reimbursement.display_total) %> + +<% if @reimbursement.return_items.exchange_requested.present? %> +============================================================ +<%= Spree.t('reimbursement_mailer.reimbursement_email.exchange_summary') %> +============================================================ +<% @reimbursement.return_items.exchange_requested.each do |return_item| %> +<%= return_item.variant.sku %> <%= raw(return_item.variant.product.name) %> <%= "(#{raw(return_item.variant.options_text)})" if return_item.variant.options_text.present? -%> -> <%= return_item.exchange_variant.sku %> <%= raw(return_item.exchange_variant.product.name) if return_item.exchange_variant.options_text.present? %> <%= "(#{raw(return_item.exchange_variant.options_text)})" -%> +<% end %> + + +<% if @reimbursement.return_items.awaiting_return.present? && Spree::Config[:expedited_exchanges] %> +<%= Spree.t('reimbursement_mailer.reimbursement_email.days_to_send', days: Spree::Config[:expedited_exchanges_days_window]) %> +<% end %> +<% end %> diff --git a/core/app/views/spree/shared/_base_mailer_footer.html.erb b/core/app/views/spree/shared/_base_mailer_footer.html.erb new file mode 100644 index 00000000000..653e92164ca --- /dev/null +++ b/core/app/views/spree/shared/_base_mailer_footer.html.erb @@ -0,0 +1,20 @@ + diff --git a/core/app/views/spree/shared/_base_mailer_header.html.erb b/core/app/views/spree/shared/_base_mailer_header.html.erb new file mode 100644 index 00000000000..ff7f4871cc5 --- /dev/null +++ b/core/app/views/spree/shared/_base_mailer_header.html.erb @@ -0,0 +1,31 @@ + diff --git a/core/app/views/spree/shared/_error_messages.html.erb b/core/app/views/spree/shared/_error_messages.html.erb index d10f973ce94..02cdac2dd3e 100644 --- a/core/app/views/spree/shared/_error_messages.html.erb +++ b/core/app/views/spree/shared/_error_messages.html.erb @@ -1,7 +1,7 @@ <% if target && target.errors.any? %>
    -

    <%= t(:errors_prohibited_this_record_from_being_saved, :count => target.errors.count) %>:

    -

    <%= t(:there_were_problems_with_the_following_fields) %>:

    +

    <%= Spree.t(:errors_prohibited_this_record_from_being_saved, :count => target.errors.count) %>:

    +

    <%= Spree.t(:there_were_problems_with_the_following_fields) %>:

      <% target.errors.full_messages.each do |msg| %>
    • <%= msg %>
    • diff --git a/core/app/views/spree/shared/_footer.html.erb b/core/app/views/spree/shared/_footer.html.erb deleted file mode 100644 index b5aa7635c34..00000000000 --- a/core/app/views/spree/shared/_footer.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -
      - - -
      diff --git a/core/app/views/spree/shared/_google_analytics.html.erb b/core/app/views/spree/shared/_google_analytics.html.erb deleted file mode 100644 index 03207eaa50a..00000000000 --- a/core/app/views/spree/shared/_google_analytics.html.erb +++ /dev/null @@ -1,39 +0,0 @@ -<% if tracker = Spree::Tracker.current %> - - <%= javascript_tag do %> - var _gaq = _gaq || []; - _gaq.push(['_setAccount', '<%= tracker.analytics_id %>']); - _gaq.push(['_trackPageview']); - - <% if flash[:commerce_tracking] %> - <%# more info: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiEcommerce %> - _gaq.push(['_addTrans', - "<%= @order.number %>", - "", - "<%= @order.total %>", - "<%= @order.adjustments.tax.sum(:amount) %>", - "<%= @order.adjustments.shipping.sum(:amount) %>", - "<%= @order.bill_address.city %>", - "<%= @order.bill_address.state_text %>", - "<%= @order.bill_address.country.name %>" - ]); - <% @order.line_items.each do |line_item| %> - _gaq.push(['_addItem', - "<%= @order.number %>", - "<%= line_item.variant.sku %>", - "<%= line_item.variant.product.name %>", - "", - "<%= line_item.price %>", - "<%= line_item.quantity %>" - ]); - <% end %> - _gaq.push(['_trackTrans']); - <% end %> - - (function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); - })(); - <% end %> -<% end %> diff --git a/core/app/views/spree/shared/_head.html.erb b/core/app/views/spree/shared/_head.html.erb deleted file mode 100644 index 86d91408538..00000000000 --- a/core/app/views/spree/shared/_head.html.erb +++ /dev/null @@ -1,13 +0,0 @@ - -<%= title %> - - -<%== meta_data_tags %> -<%= favicon_link_tag image_path('favicon.ico') %> -<%= stylesheet_link_tag 'store/all', :media => 'screen' %> -<%= csrf_meta_tags %> -<%= javascript_include_tag 'store/all' %> - -<%= yield :head %> diff --git a/core/app/views/spree/shared/_main_nav_bar.html.erb b/core/app/views/spree/shared/_main_nav_bar.html.erb deleted file mode 100644 index 6d9302d4e36..00000000000 --- a/core/app/views/spree/shared/_main_nav_bar.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/core/app/views/spree/shared/_order_details.html.erb b/core/app/views/spree/shared/_order_details.html.erb deleted file mode 100644 index 84c3f454678..00000000000 --- a/core/app/views/spree/shared/_order_details.html.erb +++ /dev/null @@ -1,119 +0,0 @@ -
      - - <% if order.has_step?("address") %> -
      -
      <%= t(:shipping_address) %> <%= link_to "(#{t(:edit)})", checkout_state_path(:address) unless @order.completed? %>
      -
      - <%= order.ship_address %> -
      -
      - -
      -
      <%= t(:billing_address) %> <%= link_to "(#{t(:edit)})", checkout_state_path(:address) unless @order.completed? %>
      -
      - <%= order.bill_address %> -
      -
      - - <% if @order.has_step?("delivery") %> -
      -
      <%= t(:shipping_method) %> <%= link_to "(#{t(:edit)})", checkout_state_path(:delivery) unless @order.completed? %>
      -
      - <%= order.shipping_method.name %> -
      -
      - <% end %> - <% end %> - -
      -
      <%= t(:payment_information) %> <%= link_to "(#{t(:edit)})", checkout_state_path(:payment) unless @order.completed? %>
      -
      - <% if order.credit_cards.empty? %> - <%= content_tag(:span, order.payment.payment_method.name) if order.payment %> - <% else %> - - <%= image_tag "credit_cards/icons/#{order.credit_cards.first.cc_type}.png" %> - <%= t(:ending_in)%> <%= order.credit_cards.first.last_digits %> - -
      - - <%= order.credit_cards.first.first_name %> - <%= order.credit_cards.first.last_name %> - - <% end %> -
      -
      - -
      - -
      - - - - - - - - - - - - - - - - - - - <% @order.line_items.each do |item| %> - - - - - - - - <% end %> - - - - - - - - <% if order.price_adjustment_totals.present? %> - - <% @order.price_adjustment_totals.keys.each do |key| %> - - - - - <% end %> - - <% end %> - - - - - - - - <% @order.adjustments.eligible.each do |adjustment| %> - <% next if (adjustment.originator_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> - - - - - <% end %> - -
      <%= t(:item) %><%= t(:price) %><%= t(:qty) %><%= t(:total) %>
      - <% if item.variant.images.length == 0 %> - <%= link_to small_image(item.variant.product), item.variant.product %> - <% else %> - <%= link_to image_tag(item.variant.images.first.attachment.url(:small)), item.variant.product %> - <% end %> - -

      <%= item.variant.product.name %>

      - <%= truncate(item.variant.product.description, :length => 100, :omission => "...") %> - <%= "(" + item.variant.options_text + ")" unless item.variant.option_values.empty? %> -
      <%= item.variant.display_amount %><%= item.quantity %><%= item.display_amount %>
      <%= t(:order_total) %>:<%= @order.display_total %>
      <%= key %><%= @order.price_adjustment_totals[key].display_amount %>
      <%= t(:subtotal) %>:<%= @order.display_item_total %>
      <%= adjustment.label %><%= adjustment.display_amount %>
      diff --git a/core/app/views/spree/shared/_products.html.erb b/core/app/views/spree/shared/_products.html.erb deleted file mode 100644 index 5c5831f0886..00000000000 --- a/core/app/views/spree/shared/_products.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<% - paginated_products = @searcher.retrieve_products if params.key?(:keywords) - paginated_products ||= products -%> -<% if products.empty? %> - <%= t(:no_products_found) %> -<% elsif params.key?(:keywords) %> -
      <%= t(:search_results, :keywords => h(params[:keywords])) %>
      -<% end %> - -<% if products.any? %> -
        - <% products.each do |product| %> - <% if product.on_display? %> -
      • "classes") %>" data-hook="products_list_item" itemscope itemtype="http://schema.org/Product"> -
        - <%= link_to small_image(product, :itemprop => "image"), product, :itemprop => 'url' %> -
        - <%= link_to truncate(product.name, :length => 50), product, :class => 'info', :itemprop => "name", :title => product.name %> - <%= product.price_in(current_currency).display_price %> -
      • - <% end %> - <% end %> - <% reset_cycle("classes") %> -
      -<% end %> - -<% if paginated_products.respond_to?(:num_pages) %> - <%= paginate paginated_products %> -<% end %> diff --git a/core/app/views/spree/shared/_search.html.erb b/core/app/views/spree/shared/_search.html.erb deleted file mode 100644 index 797959e8fc5..00000000000 --- a/core/app/views/spree/shared/_search.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% @taxons = @taxon && @taxon.parent ? @taxon.parent.children : Spree::Taxon.roots %> -<%= form_tag products_path, :method => :get do %> - <%= select_tag :taxon, - options_for_select([[t(:all_departments), '']] + - @taxons.map {|t| [t.name, t.id]}, - @taxon ? @taxon.id : params[:taxon]) %> - <%= search_field_tag :keywords, params[:keywords], :placeholder => t(:search) %> - <%= submit_tag t(:search), :name => nil %> -<% end %> diff --git a/core/app/views/spree/shared/_taxonomies.html.erb b/core/app/views/spree/shared/_taxonomies.html.erb deleted file mode 100644 index 512d39fdf68..00000000000 --- a/core/app/views/spree/shared/_taxonomies.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/core/app/views/spree/shipment_mailer/shipped_email.html.erb b/core/app/views/spree/shipment_mailer/shipped_email.html.erb new file mode 100644 index 00000000000..a7a2bbd8af5 --- /dev/null +++ b/core/app/views/spree/shipment_mailer/shipped_email.html.erb @@ -0,0 +1,34 @@ + + + + + +
      +

      + <%= Spree.t('shipment_mailer.shipped_email.dear_customer') %> +

      +

      + <%= Spree.t('shipment_mailer.shipped_email.instructions') %> +

      +

      + <%= Spree.t('shipment_mailer.shipped_email.shipment_summary') %> +

      + + <% @shipment.manifest.each do |item| %> + + + + + + <% end %> +
      <%= item.variant.sku %><%= item.variant.product.name %><%= item.variant.options_text %>
      +

      + <%= Spree.t('shipment_mailer.shipped_email.track_information', :tracking => @shipment.tracking) if @shipment.tracking %> +

      +

      + <%= Spree.t('shipment_mailer.shipped_email.track_link', :url => @shipment.tracking_url) if @shipment.tracking_url %> +

      +

      + <%= Spree.t('shipment_mailer.shipped_email.thanks') %> +

      +
      diff --git a/core/app/views/spree/shipment_mailer/shipped_email.text.erb b/core/app/views/spree/shipment_mailer/shipped_email.text.erb index 76a95577742..425df7aef2e 100644 --- a/core/app/views/spree/shipment_mailer/shipped_email.text.erb +++ b/core/app/views/spree/shipment_mailer/shipped_email.text.erb @@ -1,15 +1,16 @@ -<%= t('shipment_mailer.shipped_email.dear_customer') %> +<%= Spree.t('shipment_mailer.shipped_email.dear_customer') %> -<%= t('shipment_mailer.shipped_email.instructions') %> +<%= Spree.t('shipment_mailer.shipped_email.instructions') %> ============================================================ -<%= t('shipment_mailer.shipped_email.shipment_summary') %> +<%= Spree.t('shipment_mailer.shipped_email.shipment_summary') %> ============================================================ <% @shipment.manifest.each do |item| %> <%= item.variant.sku %> <%= item.variant.product.name %> <%= item.variant.options_text %> <% end %> ============================================================ -<%= t('shipment_mailer.shipped_email.track_information', :tracking => @shipment.tracking) if @shipment.tracking %> +<%= Spree.t('shipment_mailer.shipped_email.track_information', :tracking => @shipment.tracking) if @shipment.tracking %> +<%= Spree.t('shipment_mailer.shipped_email.track_link', :url => @shipment.tracking_url) if @shipment.tracking_url %> -<%= t('shipment_mailer.shipped_email.thanks') %> +<%= Spree.t('shipment_mailer.shipped_email.thanks') %> diff --git a/core/app/views/spree/states/index.js.erb b/core/app/views/spree/states/index.js.erb deleted file mode 100644 index c8b360e2b2f..00000000000 --- a/core/app/views/spree/states/index.js.erb +++ /dev/null @@ -1 +0,0 @@ -state_mapper = <%== @state_info %> \ No newline at end of file diff --git a/core/app/views/spree/taxons/_taxon.html.erb b/core/app/views/spree/taxons/_taxon.html.erb deleted file mode 100644 index 72ee1d02eca..00000000000 --- a/core/app/views/spree/taxons/_taxon.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -
      -
      <%= link_to taxon.name, seo_url(taxon), :class => 'breadcrumbs' %>
      - <%= render :partial => 'spree/shared/products', :locals => { :products => taxon_preview(taxon), :taxon => taxon } %> -
      diff --git a/core/app/views/spree/taxons/show.html.erb b/core/app/views/spree/taxons/show.html.erb deleted file mode 100644 index 04c2aa28473..00000000000 --- a/core/app/views/spree/taxons/show.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -

      <%= accurate_title %>

      - -<% content_for :sidebar do %> -
      - <%= render :partial => 'spree/shared/taxonomies' %> - <%= render :partial => 'spree/shared/filters' if @taxon.children.empty? %> -
      -<% end %> - -
      - <%= render :partial => 'spree/shared/products', :locals => { :products => @products, :taxon => @taxon } %> -
      - -<% unless params[:keyword].present? %> -
      - <%= render :partial => 'taxon', :collection => @taxon.children %> -
      -<% end %> - diff --git a/core/app/views/spree/test_mailer/test_email.html.erb b/core/app/views/spree/test_mailer/test_email.html.erb new file mode 100644 index 00000000000..227b6b7195c --- /dev/null +++ b/core/app/views/spree/test_mailer/test_email.html.erb @@ -0,0 +1,40 @@ + + + + + +
      + +
      +

      + <%= Spree.t('test_mailer.test_email.greeting') %> +

      +
      + +
      + + + + + + +
      + +

      + <%= Spree.t('test_mailer.test_email.message') %> +

      + +

      + Please remember to configure all of the emails that Spree has provided to your needs. + Spree comes shipped with Ink + prepackaged, but you can use your own version. Ink is not placed in the asset pipeline. +

      + +

      + Also take note that Gmail does not support <style> tags. + Therefore, you will need a gem that will be able to remove your <style> + tags and place them inline. Gmail only supports inline styles. We use + Premailer for Rails by default. +

      + +
      diff --git a/core/app/views/spree/test_mailer/test_email.text.erb b/core/app/views/spree/test_mailer/test_email.text.erb index 9944b6faef4..3491d6fbf3f 100644 --- a/core/app/views/spree/test_mailer/test_email.text.erb +++ b/core/app/views/spree/test_mailer/test_email.text.erb @@ -1,4 +1,4 @@ -<%= t('test_mailer.test_email.greeting') %> +<%= Spree.t('test_mailer.test_email.greeting') %> ================ -<%= t('test_mailer.test_email.message') %> +<%= Spree.t('test_mailer.test_email.message') %> diff --git a/core/config/initializers/check_for_orphaned_preferences.rb b/core/config/initializers/check_for_orphaned_preferences.rb deleted file mode 100644 index 5a3204bd249..00000000000 --- a/core/config/initializers/check_for_orphaned_preferences.rb +++ /dev/null @@ -1,6 +0,0 @@ -begin - ActiveRecord::Base.connection.execute("SELECT owner_id, owner_type, name, value FROM spree_preferences WHERE 'key' IS NULL").each do |pref| - warn "[WARNING] Orphaned preference `#{pref[2]}` with value `#{pref[3]}` for #{pref[1]} with id of: #{pref[0]}, you should reset the preference value manually." - end -rescue -end diff --git a/core/config/initializers/friendly_id.rb b/core/config/initializers/friendly_id.rb new file mode 100644 index 00000000000..69ecb376b19 --- /dev/null +++ b/core/config/initializers/friendly_id.rb @@ -0,0 +1,88 @@ +# FriendlyId Global Configuration +# +# Use this to set up shared configuration options for your entire application. +# Any of the configuration options shown here can also be applied to single +# models by passing arguments to the `friendly_id` class method or defining +# methods in your model. +# +# To learn more, check out the guide: +# +# http://norman.github.io/friendly_id/file.Guide.html + +FriendlyId.defaults do |config| + # ## Reserved Words + # + # Some words could conflict with Rails's routes when used as slugs, or are + # undesirable to allow as slugs. Edit this list as needed for your app. + config.use :reserved + + config.reserved_words = %w(new edit index session login logout users admin + stylesheets assets javascripts images) + + # ## Friendly Finders + # + # Uncomment this to use friendly finders in all models. By default, if + # you wish to find a record by its friendly id, you must do: + # + # MyModel.friendly.find('foo') + # + # If you uncomment this, you can do: + # + # MyModel.find('foo') + # + # This is significantly more convenient but may not be appropriate for + # all applications, so you must explicity opt-in to this behavior. You can + # always also configure it on a per-model basis if you prefer. + # + # Something else to consider is that using the :finders addon boosts + # performance because it will avoid Rails-internal code that makes runtime + # calls to `Module.extend`. + # + # config.use :finders + # + # ## Slugs + # + # Most applications will use the :slugged module everywhere. If you wish + # to do so, uncomment the following line. + # + # config.use :slugged + # + # By default, FriendlyId's :slugged addon expects the slug column to be named + # 'slug', but you can change it if you wish. + # + # config.slug_column = 'slug' + # + # When FriendlyId can not generate a unique ID from your base method, it appends + # a UUID, separated by a single dash. You can configure the character used as the + # separator. If you're upgrading from FriendlyId 4, you may wish to replace this + # with two dashes. + # + # config.sequence_separator = '-' + # + # ## Tips and Tricks + # + # ### Controlling when slugs are generated + # + # As of FriendlyId 5.0, new slugs are generated only when the slug field is + # nil, but if you're using a column as your base method can change this + # behavior by overriding the `should_generate_new_friendly_id` method that + # FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave + # more like 4.0. + # + # config.use Module.new { + # def should_generate_new_friendly_id? + # slug.blank? || _changed? + # end + # } + # + # FriendlyId uses Rails's `parameterize` method to generate slugs, but for + # languages that don't use the Roman alphabet, that's not usually suffient. Here + # we use the Babosa library to transliterate Russian Cyrillic slugs to ASCII. If + # you use this, don't forget to add "babosa" to your Gemfile. + # + # config.use Module.new { + # def normalize_friendly_id(text) + # text.to_slug.normalize! :transliterations => [:russian, :latin] + # end + # } +end diff --git a/core/config/initializers/premailer_assets.rb b/core/config/initializers/premailer_assets.rb new file mode 100644 index 00000000000..ea48309b687 --- /dev/null +++ b/core/config/initializers/premailer_assets.rb @@ -0,0 +1 @@ +Rails.application.config.assets.precompile += %w( ink.css ) diff --git a/core/config/initializers/spree.rb b/core/config/initializers/spree.rb deleted file mode 100644 index e1d4845a06f..00000000000 --- a/core/config/initializers/spree.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'mail' - -# Spree Configuration -SESSION_KEY = '_spree_session_id' - -LIKE = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? 'ILIKE' : 'LIKE' diff --git a/core/config/initializers/user_class_extensions.rb b/core/config/initializers/user_class_extensions.rb index a3c002f1253..24fd75fe7bb 100644 --- a/core/config/initializers/user_class_extensions.rb +++ b/core/config/initializers/user_class_extensions.rb @@ -1,24 +1,39 @@ Spree::Core::Engine.config.to_prepare do if Spree.user_class Spree.user_class.class_eval do - include Spree::Core::UserBanners + + include Spree::UserApiAuthentication + include Spree::UserReporting + has_and_belongs_to_many :spree_roles, - :join_table => 'spree_roles_users', - :foreign_key => "user_id", - :class_name => "Spree::Role" + join_table: 'spree_roles_users', + foreign_key: "user_id", + class_name: "Spree::Role" + + has_many :spree_orders, foreign_key: "user_id", class_name: "Spree::Order" + + belongs_to :ship_address, class_name: 'Spree::Address' + belongs_to :bill_address, class_name: 'Spree::Address' - has_many :spree_orders, :foreign_key => "user_id", :class_name => "Spree::Order" + def self.ransackable_associations(auth_object=nil) + %w[bill_address ship_address] + end - belongs_to :ship_address, :class_name => 'Spree::Address' - belongs_to :bill_address, :class_name => 'Spree::Address' + def self.ransackable_attributes(auth_object=nil) + %w[id email] + end # has_spree_role? simply needs to return true or false whether a user has a role or not. def has_spree_role?(role_in_question) - spree_roles.where(:name => role_in_question.to_s).any? + spree_roles.where(name: role_in_question.to_s).any? end def last_incomplete_spree_order - spree_orders.incomplete.order('created_at DESC').last + spree_orders.incomplete.order('created_at DESC').first + end + + def analytics_id + id end end end diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml index c17eabbe539..da6238bee1d 100644 --- a/core/config/locales/en.yml +++ b/core/config/locales/en.yml @@ -1,97 +1,101 @@ ---- en: - no: "No" - yes: "Yes" - a_copy_of_all_mail_will_be_sent_to_the_following_addresses: A copy of all mail be sent to the following addresses - abbreviation: Abbreviation - access_denied: "Access Denied" - account: Account - account_updated: "Account updated!" - action: Action - actions: - create: Create - destroy: Destroy - list: List - listing: Listing - new: New - update: Update - cancel: Cancel - active: "Active" - activate: "Activate" activerecord: attributes: spree/address: address1: Address - address2: "Address (contd.)" + address2: Address (contd.) city: City - country: "Country" - firstname: "First Name" - lastname: "Last Name" + country: Country + firstname: First Name + lastname: Last Name phone: Phone - state: "State" - zipcode: "Zip Code" + state: State + zipcode: Zip Code + spree/calculator/tiered_flat_rate: + preferred_base_amount: Base Amount + preferred_tiers: Tiers + spree/calculator/tiered_percent: + preferred_base_percent: Base Percent + preferred_tiers: Tiers spree/country: iso: ISO iso3: ISO3 - iso_name: "ISO Name" + iso_name: ISO Name name: Name - numcode: "ISO Code" + numcode: ISO Code spree/credit_card: + base: '' cc_type: Type month: Month number: Number - verification_value: "Verification Value" + verification_value: Verification Value year: Year + name: Name spree/inventory_unit: state: State spree/line_item: price: Price quantity: Quantity + spree/option_type: + name: Name + presentation: Presentation spree/order: - checkout_complete: "Checkout Complete" - completed_at: "Completed At" - ip_address: "IP Address" - item_total: "Item Total" - number: Number - special_instructions: "Special Instructions" - state: State - total: Total + checkout_complete: Checkout Complete + completed_at: Completed At + coupon_code: Coupon Code created_at: Order Date + email: Customer E-Mail + ip_address: IP Address + item_total: Item Total + number: Number payment_state: Payment State shipment_state: Shipment State - email: Customer E-Mail + special_instructions: Special Instructions + state: State + total: Total + considered_risky: Risky spree/order/bill_address: - address1: "Billing address street" - city: "Billing address city" - firstname: "Billing address first name" - lastname: "Billing address last name" - phone: "Billing address phone" - state: "Billing address state" - zipcode: "Billing address zipcode" + address1: Billing address street + city: Billing address city + firstname: Billing address first name + lastname: Billing address last name + phone: Billing address phone + state: Billing address state + zipcode: Billing address zipcode spree/order/ship_address: - address1: "Shipping address street" - city: "Shipping address city" - firstname: "Shipping address first name" - lastname: "Shipping address last name" - phone: "Shipping address phone" - state: "Shipping address state" - zipcode: "Shipping address zipcode" - spree/option_type: - name: Name - presentation: Presentation + address1: Shipping address street + city: Shipping address city + firstname: Shipping address first name + lastname: Shipping address last name + phone: Shipping address phone + state: Shipping address state + zipcode: Shipping address zipcode + spree/payment: + amount: Amount spree/payment_method: name: Name spree/product: - available_on: "Available On" - cost_currency: "Cost Currency" - cost_price: "Cost Price" + available_on: Available On + cost_currency: Cost Currency + cost_price: Cost Price + description: Description + master_price: Master Price + name: Name + on_hand: On Hand + shipping_category: Shipping Category + tax_category: Tax Category + spree/promotion: + advertise: Advertise + code: Code description: Description - master_price: "Master Price" + event_name: Event Name + expires_at: Expires At + name: Name + path: Path + starts_at: Starts At + usage_limit: Usage Limit + spree/promotion_category: name: Name - on_hand: "On Hand" - on_demand: "On Demand" - shipping_category: "Shipping Category" - tax_category: "Tax Category" spree/property: name: Name presentation: Presentation @@ -104,6 +108,13 @@ en: spree/state: abbr: Abbreviation name: Name + spree/store: + url: Site URL + meta_description: Meta Description + meta_keywords: Meta Keywords + seo_title: Seo Title + name: Site Name + mail_from_address: Mail From Address spree/tax_category: description: Description name: Name @@ -119,11 +130,11 @@ en: name: Name spree/user: email: Email - password: "Password" - password_confirmation: "Password Confirmation" + password: Password + password_confirmation: Password Confirmation spree/variant: - cost_currency: "Cost Currency" - cost_price: "Cost Price" + cost_currency: Cost Currency + cost_price: Cost Price depth: Depth height: Height price: Price @@ -137,45 +148,66 @@ en: spree/address: one: Address other: Addresses - spree/cheque_payment: - one: Cheque Payment - other: Cheque Payments spree/country: one: Country other: Countries spree/credit_card: - one: "Credit Card" - other: "Credit Cards" - spree/creditcard_payment: - one: "Credit Card Payment" - other: "Credit Card Payments" - spree/creditcard_txn: - one: "Credit Card Transaction" - other: "Credit Card Transactions" + one: Credit Card + other: Credit Cards + spree/customer_return: + one: Customer Return + other: Customer Returns spree/inventory_unit: - one: "Inventory Unit" - other: "Inventory Units" + one: Inventory Unit + other: Inventory Units spree/line_item: - one: "Line Item" - other: "Line Items" + one: Line Item + other: Line Items + spree/option_type: + one: Option Type + other: Option Types + spree/option_value: + one: Option Value + other: Option Values spree/order: one: Order other: Orders spree/payment: one: Payment other: Payments + spree/payment_method: + one: Payment Method + other: Payment Methods spree/product: one: Product other: Products + spree/promotion: + one: Promotion + other: Promotions + spree/promotion_category: + one: Promotion Category + other: Promotion Categories spree/property: one: Property other: Properties spree/prototype: one: Prototype other: Prototypes + spree/refund_reason: + one: Refund Reason + other: Refund Reasons + spree/reimbursement: + one: Reimbursement + other: Reimbursements + spree/reimbursement_type: + one: Reimbursement Type + other: Reimbursement Types spree/return_authorization: one: Return Authorization other: Return Authorizations + spree/return_authorization_reason: + one: Return Authorization Reason + other: Return Authorization Reasons spree/role: one: Roles other: Roles @@ -183,23 +215,38 @@ en: one: Shipment other: Shipments spree/shipping_category: - one: "Shipping Category" - other: "Shipping Categories" + one: Shipping Category + other: Shipping Categories + spree/shipping_method: + one: Shipping Method + other: Shipping Methods spree/state: one: State other: States + spree/stock_movement: + one: Stock Movement + other: Stock Movements + spree/stock_location: + one: Stock Location + other: Stock Locations + spree/stock_transfer: + one: Stock Transfer + other: Stock Transfers spree/tax_category: - one: "Tax Category" - other: "Tax Categories" + one: Tax Category + other: Tax Categories spree/tax_rate: - one: "Tax Rate" - other: "Tax Rates" + one: Tax Rate + other: Tax Rates spree/taxon: one: Taxon other: Taxons spree/taxonomy: one: Taxonomy other: Taxonomies + spree/tracker: + one: Tracker + other: Trackers spree/user: one: User other: Users @@ -209,920 +256,1132 @@ en: spree/zone: one: Zone other: Zones - add: Add - add_category: "Add Category" - add_country: "Add Country" - add_new_header: "Add New Header" - add_new_style: "Add New Style" - add_option_type: "Add Option Type" - add_option_types: "Add Option Types" - add_option_value: "Add Option Value" - add_product: "Add Product" - add_product_properties: "Add Product Properties" - add_scope: "Add a scope" - add_state: "Add State" - add_to_cart: "Add To Cart" - add_zone: "Add Zone" - additional_item: Additional Item Cost - address: Address - address_information: "Address Information" - adjustment: Adjustment - adjustment_total: Adjustment Total - adjustments: Adjustments - administration: Administration - admin: - mail_methods: - send_testmail: 'Send Testmail' - testmail: - delivery_error: 'Testmail delivery error' - delivery_success: 'Testmail sent successfully' - error: 'Testmail error: %{e}' - all: "All" - all_departments: All departments - allow_backorders: "Allow Backorders" - allow_ssl_in_development_and_test: Allow SSL to be used when in development and test modes - allow_ssl_in_staging: Allow SSL to be used in staging mode - allow_ssl_in_production: Allow SSL to be used in production mode - allowed_ssl_in_production_mode: "SSL will %{not} be used in production" - already_registered: Already Registered? - alt_text: Alternative Text - alternative_phone: Alternative Phone - amount: Amount - analytics_trackers: Analytics Trackers - and: and - apply: "Apply" - are_you_sure: "Are you sure?" - are_you_sure_category: "Are you sure you want to delete this category?" - are_you_sure_delete: "Are you sure you want to delete this record?" - are_you_sure_delete_image: "Are you sure you want to delete this image?" - are_you_sure_option_type: "Are you sure you want to delete this option type?" - are_you_sure_you_want_to_capture: "Are you sure you want to capture?" - assign_taxon: "Assign Taxon" - assign_taxons: "Assign Taxons" - attachment_default_style: "Attachments Style" - attachment_default_url: "Attachments URL" - attachment_path: "Attachments Path" - attachment_styles: "Paperclip Styles" - authorization_failure: "Authorization Failure" - authorized: Authorized - availability: "Availability" - available_on: "Available On" - available_taxons: "Available Taxons" - awaiting_return: Awaiting Return - back: Back - back_end: Back End - back_to_adjustments_list: "Back To Adjustments List" - back_to_images_list: "Back To Images List" - back_to_mail_methods_list: "Back To Mail Methods List" - back_to_option_types_list: "Back To Option Types List" - back_to_payment_methods_list: "Back To Payment Methods List" - back_to_payments_list: "Back To Payments List" - back_to_products_list: "Back To Products List" - back_to_properties_list: "Back To Products List" - back_to_prototypes_list: "Back To Prototypes List" - back_to_reports_list: "Back To Reports List" - back_to_shipping_categories: "Back To Shipping Categories" - back_to_shipping_methods_list: "Back To Shipping Methods List" - back_to_states_list: "Back To States List" - back_to_store: "Go Back To Store" - back_to_tax_categories_list: "Back To Tax Categories List" - back_to_taxonomies_list: "Back To Taxonomies List" - back_to_trackers_list: "Back To Trackers List" - back_to_zones_list: "Back To Zones List" - backordered: Backordered - backordering_is_allowed: "Backordering %{not} allowed" - balance_due: "Balance Due" - bill_address: "Bill Address" - billing: Billing - billing_address: "Billing Address" - both: Both - calculator: Calculator - calculator_settings_warning: "If you are changing the calculator type, you must save first before you can edit the calculator settings" - cancel: cancel - cancel_my_account: Cancel my account - cancel_my_account_description: "Unhappy?" - canceled: Canceled - cannot_create_payment_without_payment_methods: You cannot create a payment for an order without any payment methods defined. - cannot_create_returns: Cannot create returns as this order has no shipped units. - cannot_perform_operation: "Cannot perform requested operation" - capture: Capture - card_code: "Card Code" - card_details: "Card details" - card_number: "Card Number" - card_type_is: Card type is - cart: Cart - categories: Categories - category: Category - change: Change - change_language: "Change Language" - change_my_password: "Change my password" - charge_total: Charge Total - charged: Charged - charges: Charges - checkout: Checkout - cheque: Cheque - city: City - clone: Clone - code: Code - combine: Combine - complete: complete - complete_list: "Complete List" - configuration: Configuration - configuration_options: "Configuration Options" - configurations: Configurations - configure_s3: "Configure S3" - configured: Configured - confirm: Confirm - confirm_delete: "Confirm Deletion" - confirm_password: "Password Confirmation" - continue: Continue - continue_shopping: "Continue shopping" - copy_all_mails_to: Copy All Mails To - cost_currency: "Cost Currency" - cost_price: "Cost Price" - count_of_reduced_by: "count of '%{name}' reduced by %{count}" - country: Country - country_based: "Country Based" - create: Create - create_a_new_account: "Create a new account" - create_user_account: Create User Account - created_successfully: "Created Successfully" - credit: Credit - credit_card: "Credit Card" - credit_card_capture_complete: "Credit Card Was Captured" - credit_card_payment: "Credit Card Payment" - credit_owed: "Credit Owed" - credit_total: Credit Total - credit_card: Credit Card - credit_cards: Credit Cards - credits: Credits - current: Current - currency: Currency - currency_symbol_position: "Put currency symbol before or after dollar amount?" - currency_settings: "Currency Settings" - customer: Customer - customer_details: "Customer Details" - customer_details_updated: "The customer's details have been updated." - customer_search: "Customer Search" - cut: Cut - date_created: Date created - date_completed: Date Completed - date_range: "Date Range" - debit: Debit - default: Default - default_meta_description: Default Meta Description - default_meta_keywords: Default Meta Keywords - default_seo_title: Default Seo Title - default_tax: Default Tax - default_tax_zone: Default Tax Zone - defined_paperclip_styles: Defined Paperclip Styles - delete: Delete - delivery: Delivery - depth: Depth - description: Description - destroy: Destroy - didnt_receive_confirmation_instructions: "Didn't receive confirmation instructions?" - didnt_receive_unlock_instructions: "Didn't receive unlock instructions?" - discount_amount: "Discount Amount" - display: Display - display_currency: "Display currency" - dismiss_banner: "No. Thanks! I'm not interested, do not display this message again" - dollar_amounts_displayed_as: "Dollar amounts displayed as %{example}" - edit: Edit - editing_billing_integration: Editing Billing Integration - editing_category: "Editing Category" - editing_mail_method: Editing Mail Method - editing_option_type: "Editing Option Type" - editing_option_types: "Editing Option Types" - editing_payment_method: Editing Payment Method - editing_product: "Editing Product" - editing_product_group: "Editing Product Group" - editing_property: "Editing Property" - editing_prototype: "Editing Prototype" - editing_shipping_category: "Editing Shipping Category" - editing_shipping_method: "Editing Shipping Method" - editing_state: "Editing State" - editing_tax_category: "Editing Tax Category" - editing_tax_rate: "Editing Tax Rate" - editing_tracker: Editing Tracker - editing_user: "Editing User" - editing_zone: "Editing Zone" - email: Email - email_address: "Email Address" - email_server_settings_description: "Set email server settings." - empty: "Empty" - empty_cart: "Empty Cart" - enable_login_via_login_password: "Use standard email/password" - enable_login_via_openid: "Use OpenID instead" - enable_mail_delivery: Enable Mail Delivery - ending_in: "Ending in" - enter_exactly_as_shown_on_card: Please enter exactly as shown on the card - enter_at_least_five_letters: Enter at least five letters of customer name - enter_password_to_confirm: "(we need your current password to confirm your changes)" - enter_token: Enter Token - environment: "Environment" - error: error + errors: + models: + spree/calculator/tiered_flat_rate: + attributes: + base: + keys_should_be_positive_number: "Tier keys should all be numbers larger than 0" + preferred_tiers: + should_be_hash: "should be a hash" + spree/calculator/tiered_percent: + attributes: + base: + keys_should_be_positive_number: "Tier keys should all be numbers larger than 0" + values_should_be_percent: "Tier values should all be percentages between 0% and 100%" + preferred_tiers: + should_be_hash: "should be a hash" + spree/classification: + attributes: + taxon_id: + already_linked: "is already linked to this product" + spree/credit_card: + attributes: + base: + card_expired: "Card has expired" + expiry_invalid: "Card expiration is invalid" + spree/line_item: + attributes: + currency: + must_match_order_currency: "Must match order currency" + spree/refund: + attributes: + amount: + greater_than_allowed: is greater than the allowed amount. + spree/reimbursement: + attributes: + base: + return_items_order_id_does_not_match: One or more of the return items specified do not belong to the same order as the reimbursement. + spree/return_item: + attributes: + reimbursement: + cannot_be_associated_unless_accepted: cannot be associated to a return item that is not accepted. + inventory_unit: + other_completed_return_item_exists: "%{inventory_unit_id} has already been taken by return item %{return_item_id}" + spree/store: + attributes: + base: + cannot_destroy_default_store: Cannot destroy the default Store. + + + devise: + confirmations: + confirmed: Your account was successfully confirmed. You are now signed in. + send_instructions: You will receive an email with instructions about how to confirm your account in a few minutes. + failure: + inactive: Your account was not activated yet. + invalid: Invalid email or password. + invalid_token: Invalid authentication token. + locked: Your account is locked. + timeout: Your session expired, please sign in again to continue. + unauthenticated: You need to sign in or sign up before continuing. + unconfirmed: You have to confirm your account before continuing. + mailer: + confirmation_instructions: + subject: Confirmation instructions + reset_password_instructions: + subject: Reset password instructions + unlock_instructions: + subject: Unlock Instructions + oauth_callbacks: + failure: Could not authorize you from %{kind} because "%{reason}". + success: Successfully authorized from %{kind} account. + unlocks: + send_instructions: You will receive an email with instructions about how to unlock your account in a few minutes. + unlocked: Your account was successfully unlocked. You are now signed in. + user_passwords: + user: + cannot_be_blank: Your password cannot be blank. + send_instructions: You will receive an email with instructions about how to reset your password in a few minutes. + updated: Your password was changed successfully. You are now signed in. + user_registrations: + destroyed: Bye! Your account was successfully cancelled. We hope to see you again soon. + inactive_signed_up: You have signed up successfully. However, we could not sign you in because your account is %{reason}. + signed_up: Welcome! You have signed up successfully. + updated: You updated your account successfully. + user_sessions: + signed_in: Signed in successfully. + signed_out: Signed out successfully. errors: messages: - could_not_create_taxon: "Could not create taxon" - no_shipping_methods_available: "No shipping methods available for selected location, please change your address and try again." - no_payment_methods_available: "No payment methods are configured for this environment" - errors_prohibited_this_record_from_being_saved: - one: "1 error prohibited this record from being saved" - other: "%{count} errors prohibited this record from being saved" - error_user_destroy_with_orders: "Users with completed orders may not be deleted" - event: Event - events: - spree: - cart: - add: 'Add to cart' - order: - contents_changed: "Order contents changed" + already_confirmed: was already confirmed + not_found: not found + not_locked: was not locked + not_saved: + one: ! '1 error prohibited this %{resource} from being saved:' + other: ! '%{count} errors prohibited this %{resource} from being saved:' + spree: + abbreviation: Abbreviation + accept: Accept + acceptance_status: Acceptance status + acceptance_errors: Acceptance errors + accepted: Accepted + account: Account + account_updated: Account updated + action: Action + actions: + cancel: Cancel + continue: Continue + create: Create + destroy: Destroy + edit: Edit + list: List + listing: Listing + new: New + refund: Refund + save: Save + update: Update + activate: Activate + active: Active + add: Add + add_action_of_type: Add action of type + add_country: Add Country + add_coupon_code: Add Coupon Code + add_new_header: Add New Header + add_new_style: Add New Style + add_one: Add One + add_option_value: Add Option Value + add_product: Add Product + add_product_properties: Add Product Properties + add_rule_of_type: Add rule of type + add_state: Add State + add_stock: Add Stock + add_stock_management: Add Stock Management + add_to_cart: Add To Cart + add_variant: Add Variant + additional_item: Additional Item + address1: Address + address2: Address (contd.) + adjustable: Adjustable + adjustment: Adjustment + adjustment_amount: Amount + adjustment_successfully_closed: Adjustment has been successfully closed! + adjustment_successfully_opened: Adjustment has been successfully opened! + adjustment_total: Adjustment Total + adjustments: Adjustments + admin: + tab: + configuration: Configuration + option_types: Option Types + orders: Orders + overview: Overview + products: Products + promotions: Promotions + properties: Properties + prototypes: Prototypes + reports: Reports + taxonomies: Taxonomies + taxons: Taxons + users: Users user: - signup: 'User signup' - page_view: "Static page viewed" - existing_customer: "Existing Customer" - expiration: "Expiration" - expiration_month: "Expiration Month" - expiration_year: "Expiration Year" - extension: Extension - extensions: Extensions - filename: Filename - final_confirmation: "Final Confirmation" - finalize: Finalize - finalized_payments: Finalized Payments - first_item: First Item Cost - first_name: "First Name" - first_name_begins_with: "First Name Begins With" - flat_percent: "Flat Percent" - flat_rate_amount: Amount - flat_rate_per_item: "Flat Rate (per item)" - flat_rate_per_order: "Flat Rate (per order)" - flexible_rate: "Flexible Rate" - forgot_password: "Forgot Password?" - from_state: From State - front_end: Front End - full_name: "Full Name" - gateway: Gateway - gateway_configuration: "Gateway configuration" - gateway_config_unavailable: "Gateway unavailable for environment" - gateway_error: "Gateway Error" - gateway_setting_description: "Select a payment gateway and configure its settings." - gateway_settings_warning: "If you are changing the gateway type, you must save first before you can edit the gateway settings" - general: "General" - general_settings: "General Settings" - edit_general_settings: "Edit General Settings" - general_settings_description: "Configure general Spree settings." - google_analytics: "Google Analytics" - google_analytics_active: "Active" - google_analytics_create: "Create New Google Analytics Account" - google_analytics_id: "Analytics ID" - google_analytics_new: "New Google Analytics Account" - google_analytics_setting_description: "Manage Google Analytics ID." - guest_checkout: Guest Checkout - guest_user_account: Checkout as a Guest - has_no_shipped_units: has no shipped units - height: Height - hello_user: "Hello User" - hide_cents: "Hide cents" - history: History - home: "Home" - icon: "Icon" - icons_by: "Icons by" - image: Image - images: Images - images_for: "Images for" - image_settings: "Image Settings" - image_settings_description: "Image Settings Description" - image_settings_updated: "Image Settings successfully updated." - image_settings_warning: "You will need to regenerate thumbnails if you update the paperclip styles. Use rake paperclip:refresh:thumbnails to do this." - in_progress: "In Progress" - include_in_shipment: Include in Shipment - included_in_other_shipment: Included in another Shipment - included_in_price: Included in Price - included_in_this_shipment: Included in this Shipment - included_price_validation: "cannot be selected unless you have set a Default Tax Zone" - instructions_to_reset_password: "Fill out the form below and instructions to reset your password will be emailed to you:" - insufficient_stock: "Insufficient stock available, only %{on_hand} remaining" - integration_settings_warning: "If you are changing the billing integration, you must save first before you can edit the integration settings" - intercept_email_address: Intercept Email Address - intercept_email_instructions: "Override email recipient and replace with this address." - invalid_search: "Invalid search criteria." - inventory: Inventory - inventory_adjustment: "Inventory Adjustment" - inventory_setting_description: "Inventory Configuration, Backordering, Zero-Stock Display." - inventory_settings: "Inventory Settings" - is_not_available_to_shipment_address: is not available to shipment address - issue_number: Issue Number - item: Item - item_description: "Item Description" - item_total: "Item Total" - last_name: "Last Name" - last_name_begins_with: "Last Name Begins With" - learn_more: Learn More - leave_blank_to_not_change: "(leave blank if you don't want to change it)" - list: List - listing_categories: "Listing Categories" - listing_option_types: "Listing Option Types" - listing_orders: "Listing Orders" - listing_product_groups: "Listing Product Groups" - listing_products: "Listing Products" - listing_reports: "Listing Reports" - listing_tax_categories: "Listing Tax Categories" - listing_users: "Listing Users" - live: "Live" - loading: Loading - locale_changed: "Locale Changed" - logged_in_as: "Logged in as" - logged_in_succesfully: "Logged in successfully" - logged_out: "You have been logged out." - login: Login - login_as_existing: "Login as Existing Customer" - login_failed: "Login authentication failed." - login_name: Login - logout: Logout - look_for_similar_items: Look for similar items - maestro_or_solo_cards: Maestro/Solo cards - mail_delivery_enabled: "Mail delivery is enabled" - mail_delivery_not_enabled: "Mail delivery is not enabled" - mail_methods: Mail Methods - mail_server_preferences: Mail Server Preferences - make_refund: Make refund - mark_shipped: "Mark Shipped" - master_price: "Master Price" - match_choices: - none: "None" - one: "One" - all: "All" - match_rule: "Products That Must Match:" - max_items: Max Items - meta_description: "Meta Description" - meta_keywords: "Meta Keywords" - metadata: "Metadata" - missing_required_information: "Missing Required Information" - minimal_amount: "Minimal Amount" - month: "Month" - more: More - my_account: "My Account" - my_orders: "My Orders" - name: Name - name_or_sku: "Name or SKU (enter at least first 4 characters of product name)" - new: New - new_adjustment: "New Adjustment" - new_billing_integration: New Billing Integration - new_category: "New category" - new_customer: "New Customer" - new_group: New Group - new_image: "New Image" - new_mail_method: New Mail Method - new_option_type: "New Option Type" - new_option_value: "New Option Value" - new_order: "New Order" - new_order_completed: "New Order Completed" - new_payment: "New Payment" - new_payment_method: New Payment Method - new_product: "New Product" - new_product_group: New Product Group - new_property: "New Property" - new_prototype: "New Prototype" - new_return_authorization: New Return Authorization - new_shipment: "New Shipment" - new_shipping_category: "New Shipping Category" - new_shipping_method: "New Shipping Method" - new_state: "New State" - new_tax_category: "New Tax Category" - new_tax_rate: "New Tax Rate" - new_taxon: "New Taxon" - new_taxonomy: "New Taxonomy" - new_tracker: New Tracker - new_user: "New User" - new_variant: "New Variant" - new_zone: "New Zone" - next: Next - no_items_in_cart: "" - no_match_found: "No Match Found" - no_products_found: "No products found" - no_results: "No results" - no_user_found: "No user was found with that email address" - none: None - none_available: "None Available" - normal_amount: "Normal Amount" - not: not - not_available: "N/A" - not_found: "%{resource} is not found" - not_shown: "Not Shown" - note: Note - notice_messages: - option_type_removed: "Succesfully removed option type." - product_cloned: "Product has been cloned" - product_deleted: "Product has been deleted" - product_not_cloned: "Product could not be cloned" - product_not_deleted: "Product could not be deleted" - variant_deleted: "Variant has been deleted" - variant_not_deleted: "Variant could not be deleted" - on_hand: "On Hand" - one_default_category_with_default_tax_rate: "You should configure exactly one default category with your countries default tax rate" - operation: Operation - option_type: "Option Type" - option_types: "Option Types" - option_value: "Option Value" - option_values: "Option Values" - options: Options - or: or - or_over_price: "%{price} or over" - order: Order - order_adjustments: "Order adjustments" - order_confirmation_note: "" - order_date: "Order Date" - order_details: "Order Details" - order_email_resent: "Order Email Resent" - order_mailer: - confirm_email: - subject: "Order Confirmation" - dear_customer: "Dear Customer," - instructions: "Please review and retain the following order information for your records." - order_summary: "Order Summary" - subtotal: "Subtotal:" - total: "Order Total:" - thanks: "Thank you for your business." - cancel_email: - subject: "Cancellation of Order" - dear_customer: "Dear Customer," - instructions: "Your order has been CANCELED. Please retain this cancellation information for your records." - order_summary_canceled: "Order Summary [CANCELED]" - subtotal: "Subtotal:" - total: "Order Total:" - order_not_in_system: That order number is not valid on this site. - order_number: Order - order_operation_authorize: Authorize - order_processed_but_following_items_are_out_of_stock: "Your order has been processed, but following items are out of stock:" - order_processed_successfully: "Your order has been processed successfully" - order_state: - # keys correspond to Checkout state names: - address: address - adjustments: adjustments - awaiting_return: awaiting return - canceled: canceled - cart: cart + account: Account + addresses: Addresses + items: Items + items_purchased: Items Purchased + order_history: Order History + order_num: "Order #" + orders: Orders + user_information: User Information + administration: Administration + agree_to_privacy_policy: Agree to Privacy Policy + agree_to_terms_of_service: Agree to Terms of Service + all: All + all_adjustments_closed: All adjustments successfully closed! + all_adjustments_opened: All adjustments successfully opened! + all_departments: All departments + all_items_have_been_returned: All items have been returned + allow_ssl_in_development_and_test: Allow SSL to be used when in development and test modes + allow_ssl_in_production: Allow SSL to be used in production mode + allow_ssl_in_staging: Allow SSL to be used in staging mode + already_signed_up_for_analytics: You have already signed up for Spree Analytics + alt_text: Alternative Text + alternative_phone: Alternative Phone + amount: Amount + analytics_desc_header_1: Spree Analytics + analytics_desc_header_2: Live analytics integrated into your Spree dashboard + analytics_desc_list_1: Get live sales information as it happens + analytics_desc_list_2: Requires only a free Spree account to activate + analytics_desc_list_3: Absolutely no code to install + analytics_desc_list_4: It's completely free! + analytics_trackers: Analytics Trackers + and: and + approve: approve + approver: Approver + approved_at: Approved at + are_you_sure: Are you sure? + are_you_sure_delete: Are you sure you want to delete this record? + associated_adjustment_closed: The associated adjustment is closed, and will not be recalculated. Do you want to open it? + authorization_failure: Authorization Failure + authorized: Authorized + auto_capture: Auto Capture + available_on: Available On + average_order_value: Average Order Value + avs_response: AVS Response + back: Back + back_end: Backend + backordered: Backordered + back_to_adjustments_list: Back To Adjustments List + back_to_customer_return: Back To Customer Return + back_to_customer_return_list: Back To Customer Return List + back_to_images_list: Back To Images List + back_to_option_types_list: Back To Option Types List + back_to_orders_list: Back To Orders List + back_to_payment: Back To Payment + back_to_payment_methods_list: Back To Payment Methods List + back_to_payments_list: Back To Payments List + back_to_products_list: Back To Products List + back_to_promotions_list: Back To Promotions List + back_to_promotion_categories_list: Back To Promotions Categories List + back_to_properties_list: Back To Properties List + back_to_prototypes_list: Back To Prototypes List + back_to_reports_list: Back To Reports List + back_to_refund_reason_list: Back To Refund Reason List + back_to_reimbursement_type_list: Back To Reimbursement Type List + back_to_return_authorizations_list: Back To Return Authorizations List + back_to_rma_reason_list: Back To RMA Reason List + back_to_shipping_categories: Back To Shipping Categories + back_to_shipping_categories_list: Back To Shipping Categories List + back_to_shipping_methods_list: Back To Shipping Methods List + back_to_states_list: Back To States List + back_to_stock_locations_list: Back to Stock Locations List + back_to_stock_movements_list: Back to Stock Movements List + back_to_stock_transfers_list: Back to Stock Transfers List + back_to_store: Go Back To Store + back_to_tax_categories_list: Back To Tax Categories List + back_to_taxonomies_list: Back To Taxonomies List + back_to_trackers_list: Back To Trackers List + back_to_users_list: Back To Users List + back_to_zones_list: Back To Zones List + backorderable: Backorderable + backorderable_default: Backorderable default + backorders_allowed: backorders allowed + balance_due: Balance Due + base_amount: Base Amount + base_percent: Base Percent + bill_address: Bill Address + billing: Billing + billing_address: Billing Address + both: Both + calculated_reimbursements: Calculated Reimbursements + calculator: Calculator + calculator_settings_warning: If you are changing the calculator type, you must save first before you can edit the calculator settings + cancel: cancel + canceler: Canceler + canceled_at: Canceled at + cannot_create_payment_without_payment_methods: You cannot create a payment for an order without any payment methods defined. + cannot_create_customer_returns: Cannot create customer returns as this order has no shipped units. + cannot_create_returns: Cannot create returns as this order has no shipped units. + cannot_perform_operation: Cannot perform requested operation + cannot_set_shipping_method_without_address: Cannot set shipping method until customer details are provided. + capture: Capture + capture_events: Capture events + card_code: Card Code + card_number: Card Number + card_type: Brand + card_type_is: Card type is + cart: Cart + cart_subtotal: + one: 'Subtotal (1 item)' + other: 'Subtotal (%{count} items)' + categories: Categories + category: Category + charged: Charged + checkout: Checkout + choose_a_customer: Choose a customer + choose_a_taxon_to_sort_products_for: "Choose a taxon to sort products for" + choose_currency: Choose Currency + choose_dashboard_locale: Choose Dashboard Locale + choose_location: Choose location + city: City + clear_cache: Clear Cache + clear_cache_ok: Cache was flushed + clear_cache_warning: Clearing cache will temporarily reduce the performance of your store. + click_and_drag_on_the_products_to_sort_them: Click and drag on the products to sort them. + clone: Clone + close: Close + close_all_adjustments: Close All Adjustments + code: Code + company: Company complete: complete - confirm: confirm - delivery: delivery - payment: payment - resumed: resumed - returned: returned - skrill: skrill - order_summary: Order Summary - order_sure_want_to: "Are you sure you want to %{event} this order?" - order_total: "Order Total" - order_total_message: "The total amount charged to your card will be" - order_updated: "Order Updated" - orders: Orders - other_payment_options: Other Payment Options - out_of_stock: "Out of Stock" - over_paid: "Over Paid" - overview: Overview - page_only_viewable_when_logged_in: You attempted to visit a page which can only be viewed when you are logged in - page_only_viewable_when_logged_out: You attempted to visit a page which can only be viewed when you are logged out - pagination: - previous_page: "« previous page" - next_page: "next page »" - truncate: "…" - paid: Paid - parent_category: "Parent Category" - password: Password - password_reset_instructions: "Password Reset Instructions" - password_reset_instructions_are_mailed: "Instructions to reset your password have been emailed to you. Please check your email." - password_reset_token_not_found: "We're sorry, but we could not locate your account. If you are having issues try copying and pasting the URL from your email into your browser or restarting the reset password process." - password_updated: "Password successfully updated" - paste: Paste - path: Path - pay: pay - payment: Payment - payment_actions: "Actions" - payment_gateway: "Payment Gateway" - payment_information: "Payment Information" - payment_method: Payment Method - payment_methods: Payment Methods - payment_methods_setting_description: Configure methods customers can use to pay. - payment_processing_failed: "Payment could not be processed, please check the details you entered" - payment_processor_choose_banner_text: "If you need help choosing a payment processor, please visit" - payment_processor_choose_link: "our payments page" - payment_state: Payment State - payment_states: - balance_due: balance due - completed: completed - checkout: checkout - credit_owed: credit owed - failed: failed - paid: paid - pending: pending - processing: processing - void: void - payment_updated: Payment Updated - payments: Payments - pending_payments: Pending Payments - permalink: Permalink - phone: Phone - place_order: Place Order - please_create_user: "Please create a user account" - please_define_payment_methods: "Please define some payment methods first." - powered_by: "Powered by" - populate_get_error: "Something went wrong. Please try adding the item again." - presentation: Presentation - preview: Preview - previous: Previous - price: Price - price_sack: Price Sack - price_range: Price Range - problem_authorizing_card: "Problem authorizing credit card" - problem_capturing_card: "Problem capturing credit card" - problems_processing_order: "We had problems processing your order" - proceed_as_guest: "No Thanks, Proceed as Guest" - process: Process - product: Product - product_details: "Product Details" - product_group: Product Group - product_group_invalid: Product Group has invalid scopes - product_groups: Product Groups - product_has_no_description: This product has no description - product_properties: "Product Properties" - product_scopes: - groups: - price: - description: "Scopes for selecting products based on Price" - name: Price - search: - description: "Scopes for selecting products based on name, keywords and description of product" - name: "Text search" - taxon: - description: "Scopes for selecting products based on Taxons" - name: Taxon - values: - description: "Scopes for selecting products based on option and property values" - name: Values - scopes: - ascend_by_name: - name: Ascend by product name - ascend_by_updated_at: - name: Ascend by actualization date - descend_by_name: - name: Descend by product name - descend_by_updated_at: - name: Descend by actualization date - in_name: - args: - words: Words - description: "(separated by space or comma)" - name: "Product name have following" - sentence: product name contain %s - in_name_or_description: - args: - words: Words - description: "(separated by space or comma)" - name: "Product name or description have following" - sentence: name or description contain %s - in_name_or_keywords: - args: - words: Words - description: "(separated by space or comma)" - name: "Product name or meta keywords have following" - sentence: name or keywords contain %s - in_taxons: - args: - "taxon_names": "Taxon names" - description: "Taxon names have to be separated by comma or space(eg. adidas,shoes)" - name: "In taxons and all their descendants" - sentence: in %s and all their descendants - master_price_gte: - args: - amount: Amount - description: "" - name: "Master price greater or equal to" - sentence: price greater or equal to %.2f - master_price_lte: - args: - amount: Amount - description: "" - name: "Master price lesser or equal to" - sentence: price less or equal to %.2f - price_between: - args: - high: High - low: Low - description: "" - name: "Price between" - sentence: price between %.2f and %.2f - taxons_name_eq: - args: - taxon_name: "Taxon name" - description: "In specific taxon - without descendants" - name: "In Taxon(without descendants)" - sentence: in %s - with: - args: - value: Value - description: "Selects all products that have at least one variant that have specified value as either option or property (eg. red)" - name: With value - sentence: with value %s - with_ids: - args: - ids: IDs - description: "Select specific products" - name: Products with IDs - sentence: with IDs %s - with_option: - args: - option: Option - description: "Selects all products that have specified option(eg. color)" - name: "With option" - sentence: with option %s - with_option_value: - args: - option: Option - value: Value - description: "Selects all products that have at least one variant with specified option and value(eg. color:red)" - name: "With option and value" - sentence: with option %s and value %s - with_property: - args: - property: Property - description: "Selects all products that have specified property(eg. weight)" - name: "With property" - sentence: with property %s - with_property_value: - args: - property: Property - value: Value - description: "Selects all products that have at least one variant with specified property and value(eg. weight:10kg)" - name: "With property value" - sentence: with property %s and value %s - products: Products - products_with_zero_inventory_display: "Products with a zero inventory will %{not} be displayed" - properties: Properties - property: Property - prototype: Prototype - prototypes: Prototypes - provider: "Provider" - provider_settings_warning: "If you are changing the provider type, you must save first before you can edit the provider settings" - qty: Qty - quantity_shipped: Quantity Shipped - quantity_returned: Quantity Returned - range: "Range" - rate: Rate - reason: Reason - recalculate_order_total: "Recalculate order total" - receive: receive - received: Received - refund: Refund - register: Register as a New User - register_or_guest: Checkout as Guest or Register - registration: Registration - remember_me: "Remember me" - remove: Remove - rename: Rename - reports: Reports - required_for_solo_and_maestro: Required for Solo and Maestro cards. - resend: Resend - resend_confirmation_instructions: "Resend confirmation instructions" - resend_unlock_instructions: "Resend unlock instructions" - reset_password: "Reset my password" - resource_controller: - member_object_not_found: "Member object not found." - successfully_created: "Successfully created!" - successfully_removed: "Successfully removed!" - successfully_updated: "Successfully updated!" - response_code: "Response Code" - resume: "resume" - resumed: Resumed - return: return - return_authorization: Return Authorization - return_authorization_updated: Return authorization updated - return_authorizations: Return Authorizations - return_quantity: Return Quantity - returned: Returned - review: Review - rma_credit: RMA Credit - rma_number: RMA Number - rma_value: RMA Value - roles: Roles - s3_access_key: "Access Key" - s3_bucket: "Bucket" - s3_headers: "S3 Headers" - s3_secret: "Secret Key" - s3_protocol: "S3 Protocol" - s3_used_for_product_images: "S3 is being used for product images" - s3_not_used_for_product_images: "S3 is not being used for product images" - sales_tax: "Sales Tax" - sales_total: "Sales Total" - sales_total_description: "Sales Total For All Orders" - save_and_continue: Save and Continue - save_preferences: Save Preferences - scope: Scope - scopes: Scopes - search: Search - search_results: "Search results for '%{keywords}'" - searching: Searching - secure_connection_type: Secure Connection Type - secure_credit_card: Secure Credit Card - security_settings: "Security Settings" - select: Select - select_from_prototype: "Select From Prototype" - select_preferred_shipping_option: "Select preferred shipping option" - send_copy_of_all_mails_to: Send Copy of All Mails To - send_copy_of_orders_mails_to: Send Copy of Order Mails To - send_mails_as: Send Mails As - send_me_reset_password_instructions: "Send me reset password instructions" - send_order_mails_as: Send Order Mails As - server: Server - server_error: "The server returned an error" - settings: Settings - ship: ship - ship_address: "Ship Address" - shipment: Shipment - shipment_details: Shipment Details - shipment_inc_vat: "Shipment including VAT" - shipment_mailer: - shipped_email: - subject: "Shipment Notification" - dear_customer: "Dear Customer," - instructions: "Your order has been shipped" - shipment_summary: "Shipment Summary" - track_information: "Tracking Information: %{tracking}" - thanks: "Thank you for your business." - shipment_number: "Shipment #" - shipment_state: Shipment State - shipment_states: - backorder: backorder - partial: partial - pending: pending - ready: ready - shipped: shipped - shipment_updated: Shipment Updated - shipments: "Shipments" - shipped: Shipped - shipping: Shipping - shipping_address: "Shipping Address" - shipping_categories: "Shipping Categories" - shipping_categories_description: "Manage shipping categories to identify which products can be shipped via which method." - shipping_category: Shipping Category - shipping_category_choose: "Shipping Category" - shipping_cost: Cost - shipping_error: "Shipping Error" - shipping_instructions: "Shipping Instructions" - shipping_method: "Shipping Method" - shipping_methods: "Shipping Methods" - shipping_methods_description: "Manage shipping methods." - shipping_total: "Shipping Total" - shop_by_taxonomy: "Shop by %{taxonomy}" - shopping_cart: "Shopping Cart" - short_description: "Short description" - show: Show - show_active: "Show Active" - show_deleted: "Show Deleted" - show_incomplete_orders: "Show Incomplete Orders" - show_only_complete_orders: "Only show complete orders" - show_out_of_stock_products: "Show out-of-stock products" - show_only_unfulfilled_orders: "Show only unfulfilled orders" - showing_first_n: "Showing first %{n}" - sign_up: "Sign up" - site_name: "Site Name" - site_url: "Site URL" - sku: SKU - smtp: SMTP - smtp_authentication_type: SMTP Authentication Type - smtp_domain: SMTP Domain - smtp_mail_host: SMTP Mail Host - smtp_password: SMTP Password - smtp_port: SMTP Port - smtp_send_all_emails_as_from_following_address: "Send all mails as from the following address." - smtp_send_copy_to_this_addresses: "Sends a copy of all outgoing mails to this address. For multiple addresses, separate with commas." - smtp_username: SMTP Username - sold: Sold - sort_ordering: "Sort ordering" - special_instructions: "Special Instructions" - spree: + configuration: Configuration + configurations: Configurations + confirm: Confirm + confirm_delete: Confirm Deletion + confirm_password: Password Confirmation + continue: Continue + continue_shopping: Continue shopping + cost_currency: Cost Currency + cost_price: Cost Price + could_not_connect_to_jirafe: Could not connect to Jirafe to sync data. This will be automatically retried later. + could_not_create_customer_return: Could not create customer return + could_not_create_stock_movement: There was a problem saving this stock movement. Please try again. + count_on_hand: Count On Hand + countries: Countries + country: Country + country_based: Country Based + country_name: Name + country_names: + CA: Canada + FRA: France + ITA: Italy + US: United States of America + coupon: Coupon + coupon_code: Coupon code + coupon_code_already_applied: The coupon code has already been applied to this order + coupon_code_applied: The coupon code was successfully applied to your order. + coupon_code_better_exists: The previously applied coupon code results in a better deal + coupon_code_expired: The coupon code is expired + coupon_code_max_usage: Coupon code usage limit exceeded + coupon_code_not_eligible: This coupon code is not eligible for this order + coupon_code_not_found: The coupon code you entered doesn't exist. Please try again. + coupon_code_unknown_error: This coupon code could not be applied to the cart at this time. + customer_return: Customer Return + customer_returns: Customer Returns + create: Create + create_a_new_account: Create a new account + create_new_order: Create new order + create_reimbursement: Create reimbursement + created_at: Created At + credit: Credit + credits: Credits + credit_card: Credit Card + credit_cards: Credit Cards + credit_owed: Credit Owed + currency: Currency + currency_decimal_mark: Currency decimal mark + currency_settings: Currency Settings + currency_symbol_position: Put currency symbol before or after dollar amount? + currency_thousands_separator: Currency thousands separator + current: Current + current_promotion_usage: ! 'Current Usage: %{count}' + customer: Customer + customer_details: Customer Details + customer_details_updated: Customer Details Updated + customer_search: Customer Search + cut: Cut + cvv_response: CVV Response + dash: + jirafe: + app_id: App ID + app_token: App Token + currently_unavailable: Jirafe is currently unavailable. Spree will automatically connect to Jirafe once it is available. + explanation: The fields below may already be populated if you chose to register with Jirafe from the admin dashboard. + header: Jirafe Analytics Settings + site_id: Site ID + token: Token + jirafe_settings_updated: Jirafe Settings have been updated. date: Date + date_completed: Date Completed date_picker: + first_day: 0 format: ! '%Y/%m/%d' - js_format: 'yy/mm/dd' + js_format: yy/mm/dd + date_range: Date Range + default: Default + default_refund_amount: Default Refund Amount + default_tax: Default Tax + default_tax_zone: Default Tax Zone + delete: Delete + deleted_variants_present: Some line items in this order have products that are no longer available. + delivery: Delivery + depth: Depth + description: Description + destination: Destination + destroy: Destroy + discount_amount: Discount Amount + dismiss_banner: No. Thanks! I'm not interested, do not display this message again + display: Display + display_currency: Display currency + edit: Edit + editing_country: Editing Country + editing_option_type: Editing Option Type + editing_payment_method: Editing Payment Method + editing_product: Editing Product + editing_promotion: Editing Promotion + editing_promotion_category: Editing Promotion Category + editing_property: Editing Property + editing_prototype: Editing Prototype + edit_refund_reason: Edit Refund Reason + editing_refund_reason: Editing Refund Reason + editing_reimbursement: Editing Reimbursement + editing_reimbursement_type: Editing Reimbursement Type + editing_rma_reason: Editing RMA Reason + editing_shipping_category: Editing Shipping Category + editing_shipping_method: Editing Shipping Method + editing_state: Editing State + editing_stock_location: Editing Stock Location + editing_stock_movement: Editing Stock Movement + editing_tax_category: Editing Tax Category + editing_tax_rate: Editing Tax Rate + editing_tracker: Editing Tracker + editing_user: Editing User + editing_zone: Editing Zone + eligibility_errors: + messages: + has_excluded_product: Your cart contains a product that prevents this coupon code from being applied. + item_total_less_than: This coupon code can't be applied to orders less than %{amount}. + item_total_less_than_or_equal: This coupon code can't be applied to orders less than or equal to %{amount}. + item_total_more_than: This coupon code can't be applied to orders higher than %{amount}. + item_total_more_than_or_equal: This coupon code can't be applied to orders higher than or equal to %{amount}. + limit_once_per_user: This coupon code can only be used once per user. + missing_product: This coupon code can't be applied because you don't have all of the necessary products in your cart. + missing_taxon: You need to add a product from all applicable categories before applying this coupon code. + no_applicable_products: You need to add an applicable product before applying this coupon code. + no_matching_taxons: You need to add a product from an applicable category before applying this coupon code. + no_user_or_email_specified: You need to login or provide your email before applying this coupon code. + no_user_specified: You need to login before applying this coupon code. + not_first_order: This coupon code can only be applied to your first order. + email: Email + empty: Empty + empty_cart: Empty Cart + enable_mail_delivery: Enable Mail Delivery + end: End + ending_in: Ending in + environment: Environment + error: error + errors: + messages: + could_not_create_taxon: Could not create taxon + no_payment_methods_available: No payment methods are configured for this environment + no_shipping_methods_available: No shipping methods available for selected location, please change your address and try again. + errors_prohibited_this_record_from_being_saved: + one: 1 error prohibited this record from being saved + other: ! '%{count} errors prohibited this record from being saved' + event: Event + events: + spree: + cart: + add: Add to cart + checkout: + coupon_code_added: Coupon code added + content: + visited: Visit static content page + order: + contents_changed: Order contents changed + page_view: Static page viewed + user: + signup: User signup + exceptions: + count_on_hand_setter: Cannot set count_on_hand manually, as it is set automatically by the recalculate_count_on_hand callback. Please use `update_column(:count_on_hand, value)` instead. + exchange_for: Exchange for + expedited_exchanges_warning: "Any specified exchanges will ship to the customer immediately upon saving. The customer will be charged the full amount of the item if they do not return the original item within %{days_window} days." + excl: excl. + expiration: Expiration + extension: Extension + existing_shipments: Existing shipments + failed_payment_attempts: Failed Payment Attempts + filename: Filename + fill_in_customer_info: Please fill in customer info + filter_results: Filter Results + finalize: Finalize + find_a_taxon: Find a Taxon + finalized: Finalized + first_item: First Item + first_name: First Name + first_name_begins_with: First Name Begins With + flat_percent: Flat Percent + flat_rate_per_order: Flat Rate + flexible_rate: Flexible Rate + forgot_password: Forgot Password? + free_shipping: Free Shipping + free_shipping_amount: "-" + front_end: Front End + gateway: Gateway + gateway_config_unavailable: Gateway unavailable for environment + gateway_error: Gateway Error + general: General + general_settings: General Settings + google_analytics: Google Analytics + google_analytics_id: Analytics ID + guest_checkout: Guest Checkout + guest_user_account: Checkout as a Guest + has_no_shipped_units: has no shipped units + height: Height + hide_cents: Hide cents + home: Home + i18n: + available_locales: Available Locales + fields: Fields + language: Language + localization_settings: Localization Settings + only_incomplete: Only incomplete + only_complete: Only complete + select_locale: Select locale + show_only: Show only + supported_locales: Supported Locales + this_file_language: English (US) + translations: Translations + icon: Icon + identifier: Identifier + image: Image + images: Images + implement_eligible_for_return: "Must implement #eligible_for_return? for your EligibilityValidator." + implement_requires_manual_intervention: "Must implement #requires_manual_intervention? for your EligibilityValidator." + inactive: Inactive + incl: incl. + included_in_price: Included in Price + included_price_validation: cannot be selected unless you have set a Default Tax Zone + incomplete: Incomplete + info_product_has_multiple_skus: "This product has %{count} variants:" + info_number_of_skus_not_shown: + one: "and one other" + other: "and %{count} others" + instructions_to_reset_password: Please enter your email on the form below + insufficient_stock: Insufficient stock available, only %{on_hand} remaining + insufficient_stock_lines_present: Some line items in this order have insufficient quantity. + intercept_email_address: Intercept Email Address + intercept_email_instructions: Override email recipient and replace with this address. + internal_name: Internal Name + invalid_credit_card: Invalid credit card. + invalid_payment_provider: Invalid payment provider. + invalid_promotion_action: Invalid promotion action. + invalid_promotion_rule: Invalid promotion rule. + inventory: Inventory + inventory_adjustment: Inventory Adjustment + inventory_error_flash_for_insufficient_quantity: An item in your cart has become unavailable. + inventory_state: Inventory State + is_not_available_to_shipment_address: is not available to shipment address + iso_name: Iso Name + item: Item + item_description: Item Description + item_total: Item Total + item_total_rule: + operators: + gt: greater than + gte: greater than or equal to + lt: less than + lte: less than or equal to + items_cannot_be_shipped: We are unable to calculate shipping rates for the selected items. + items_in_rmas: Items in Return Authorizations + items_to_be_reimbursed: Items to be reimbursed + items_reimbursed: Items reimbursed + jirafe: Jirafe + landing_page_rule: + path: Path + last_name: Last Name + last_name_begins_with: Last Name Begins With + learn_more: Learn More + lifetime_stats: Lifetime Stats + line_item_adjustments: "Line item adjustments" + list: List + listing_countries: Listing Countries + listing_orders: Listing Orders + listing_products: Listing Products + listing_reports: Listing Reports + listing_tax_categories: Listing Tax Categories + listing_users: Listing Users + loading: Loading + locale_changed: Locale Changed + location: Location + lock: Lock + log_entries: "Log Entries" + logs: "Logs" + logged_in_as: Logged in as + logged_in_succesfully: Logged in successfully + logged_out: You have been logged out. + login: Login + login_as_existing: Login as Existing Customer + login_failed: Login authentication failed. + login_name: Login + logout: Logout + look_for_similar_items: Look for similar items + make_refund: Make refund + make_sure_the_above_reimbursement_amount_is_correct: Make sure the above reimbursement amount is correct + manage_promotion_categories: Manage Promotion Categories + manual_intervention_required: Manual intervention required + manage_variants: Manage Variants + master_price: Master Price + match_choices: + all: All + none: None + max_items: Max Items + member_since: Member Since + memo: Memo + meta_description: Meta Description + meta_keywords: Meta Keywords + meta_title: Meta Title + metadata: Metadata + minimal_amount: Minimal Amount + missing_return_authorization: ! 'Missing Return Authorization for %{item_name}.' + month: Month + more: More + move_stock_between_locations: Move Stock Between Locations + my_account: My Account + my_orders: My Orders + name: Name + name_on_card: Name on card + name_or_sku: Name or SKU (enter at least first 4 characters of product name) + new: New + new_adjustment: New Adjustment + new_customer: New Customer + new_customer_return: New Customer Return + new_country: New Country + new_image: New Image + new_option_type: New Option Type + new_order: New Order + new_order_completed: New Order Completed + new_payment: New Payment + new_payment_method: New Payment Method + new_product: New Product + new_promotion: New Promotion + new_promotion_category: New Promotion Category + new_property: New Property + new_prototype: New Prototype + new_refund: New Refund + new_refund_reason: New Refund Reason + new_rma_reason: New RMA Reason + new_return_authorization: New Return Authorization + new_shipping_category: New Shipping Category + new_shipping_method: New Shipping Method + new_shipment_at_location: New shipment at location + new_state: New State + new_stock_location: New Stock Location + new_stock_movement: New Stock Movement + new_stock_transfer: New Stock Transfer + new_tax_category: New Tax Category + new_tax_rate: New Tax Rate + new_taxon: New Taxon + new_taxonomy: New Taxonomy + new_tracker: New Tracker + new_user: New User + new_variant: New Variant + new_zone: New Zone + next: Next + no_actions_added: No actions added + no_images_found: No images found + no_orders_found: No orders found + no_payment_methods_found: No payment methods found + no_payment_found: No payment found + no_pending_payments: No pending payments + no_products_found: No products found + no_promotions_found: No promotions found + no_results: No results + no_rules_added: No rules added + no_resource_found: ! 'No %{resource} found' + no_shipping_methods_found: No shipping methods found + no_shipping_method_selected: No shipping method selected. + no_trackers_found: No Trackers Found + no_stock_locations_found: No stock locations found + no_tracking_present: No tracking details provided. + none: None + none_selected: None Selected + normal_amount: Normal Amount + not: not + not_available: N/A + not_enough_stock: There is not enough inventory at the source location to complete this transfer. + not_found: ! '%{resource} is not found' + note: Note + notice_messages: + product_cloned: Product has been cloned + product_deleted: Product has been deleted + product_not_cloned: Product could not be cloned + product_not_deleted: Product could not be deleted + variant_deleted: Variant has been deleted + variant_not_deleted: Variant could not be deleted + num_orders: "# Orders" + on_hand: On Hand + open: Open + open_all_adjustments: Open All Adjustments + option_type: Option Type + option_type_placeholder: Choose an option type + option_types: Option Types + option_value: Option Value + option_values: Option Values + optional: Optional + options: Options + or: or + or_over_price: ! '%{price} or over' + order: Order + order_adjustments: Order adjustments + order_already_updated: The order has already been updated. + order_approved: Order approved + order_canceled: Order canceled + order_details: Order Details + order_email_resent: Order Email Resent + order_information: Order Information + order_mailer: + cancel_email: + dear_customer: Dear Customer, + instructions: Your order has been CANCELED. Please retain this cancellation information for your records. + order_summary_canceled: Order Summary [CANCELED] + subject: Cancellation of Order + subtotal: ! 'Subtotal:' + total: ! 'Order Total:' + confirm_email: + dear_customer: Dear Customer, + instructions: Please review and retain the following order information for your records. + order_summary: Order Summary + subject: Order Confirmation + subtotal: ! 'Subtotal:' + thanks: Thank you for your business. + total: ! 'Order Total:' + order_not_found: We couldn't find your order. Please try that action again. + order_number: Order %{number} + order_populator: + out_of_stock: ! '%{item} is out of stock.' + selected_quantity_not_available: ! 'selected of %{item} is not available.' + please_enter_reasonable_quantity: Please enter a reasonable quantity. + order_processed_successfully: Your order has been processed successfully + order_resumed: Order resumed + order_state: + address: address + awaiting_return: awaiting return + canceled: canceled + cart: cart + considered_risky: considered risky + complete: complete + confirm: confirm + delivery: delivery + payment: payment + resumed: resumed + returned: returned + order_summary: Order Summary + order_sure_want_to: Are you sure you want to %{event} this order? + order_total: Order Total + order_updated: Order Updated + orders: Orders + out_of_stock: Out of Stock + overview: Overview + package_from: package from + pagination: + next_page: next page » + previous_page: ! '« previous page' + truncate: ! '…' + password: Password + paste: Paste + path: Path + pay: pay + payment: Payment + payment_could_not_be_created: Payment could not be created. + payment_identifier: Payment Identifier + payment_information: Payment Information + payment_method: Payment Method + payment_methods: Payment Methods + payment_method_not_supported: That payment method is unsupported. Please choose another one. + payment_processing_failed: Payment could not be processed, please check the details you entered + payment_processor_choose_banner_text: If you need help choosing a payment processor, please visit + payment_processor_choose_link: our payments page + payment_state: Payment State + payment_states: + balance_due: balance due + checkout: checkout + completed: completed + credit_owed: credit owed + failed: failed + paid: paid + pending: pending + processing: processing + void: void + payment_updated: Payment Updated + payments: Payments + percent: Percent + percent_per_item: Percent Per Item + permalink: Permalink + pending: Pending + phone: Phone + place_order: Place Order + please_define_payment_methods: Please define some payment methods first. + populate_get_error: Something went wrong. Please try adding the item again. + powered_by: Powered by + pre_tax_refund_amount: Pre-Tax Refund Amount + pre_tax_amount: Pre-Tax Amount + pre_tax_total: Pre-Tax Total + preferred_reimbursement_type: Preferred Reimbursement Type + presentation: Presentation + previous: Previous + price: Price + price_range: Price Range + price_sack: Price Sack + process: Process + product: Product + product_details: Product Details + product_has_no_description: This product has no description + product_not_available_in_this_currency: This product is not available in the selected currency. + product_properties: Product Properties + product_rule: + choose_products: Choose products + label: Order must contain %{select} of these products + match_all: all + match_any: at least one + match_none: none + product_source: + group: From product group + manual: Manually choose + products: Products + promotion: Promotion + promotionable: Promotable + promotion_action: Promotion Action + promotion_action_types: + create_adjustment: + description: Creates a promotion credit adjustment on the order + name: Create whole-order adjustment + create_item_adjustments: + description: Creates a promotion credit adjustment on a line item + name: Create per-line-item adjustment + create_line_items: + description: Populates the cart with the specified quantity of variant + name: Create line items + free_shipping: + description: Makes all shipments for the order free + name: Free shipping + promotion_actions: Actions + promotion_form: + match_policies: + all: Match all of these rules + any: Match any of these rules + promotion_rule: Promotion Rule + promotion_rule_types: + first_order: + description: Must be the customer's first order + name: First order + item_total: + description: Order total meets these criteria + name: Item total + landing_page: + description: Customer must have visited the specified page + name: Landing Page + one_use_per_user: + description: Only One Use Per User + name: One Use Per User + product: + description: Order includes specified product(s) + name: Product(s) + user: + description: Available only to the specified users + name: User + user_logged_in: + description: Available only to logged in users + name: User Logged In + taxon: + description: Order includes products with specified taxon(s) + name: Taxon(s) + promotions: Promotions + promotion_uses: Promotion uses + propagate_all_variants: Propagate all variants + properties: Properties + property: Property + prototype: Prototype + prototypes: Prototypes + provider: Provider + provider_settings_warning: If you are changing the provider type, you must save first before you can edit the provider settings + qty: Qty + quantity: Quantity + quantity_returned: Quantity Returned + quantity_shipped: Quantity Shipped + rate: Rate + reason: Reason + receive: receive + receive_stock: Receive Stock + received: Received + reception_status: Reception Status + reference: Reference + refund: Refund + refund_amount_must_be_greater_than_zero: Refund amount must be greater than zero + refund_reasons: Refund Reasons + refunded_amount: Refunded Amount + refunds: Refunds + refund_amount_must_be_greater_than_zero: Refund amount must be greater than zero + register: Register + registration: Registration + reimburse: Reimburse + reimbursed: Reimbursed + reimbursement: Reimbursement + reimbursement_perform_failed: "Reimbursement could not be performed. Error: %{error}" + reimbursement_status: Reimbursement status + reimbursement_type: Reimbursement type + reimbursement_type_override: Reimbursement Type Override + reimbursement_types: Reimbursement Types + reimbursements: Reimbursements + reject: Reject + rejected: Rejected + remember_me: Remember me + remove: Remove + rename: Rename + reports: Reports + resend: Resend + reset_password: Reset my password + response_code: Response Code + resume: resume + resumed: Resumed + return: return + return_authorization: Return Authorization + return_authorization_reasons: Return Authorization Reasons + return_authorization_updated: Return authorization updated + return_authorizations: Return Authorizations + return_item_inventory_unit_ineligible: Return item's inventory unit must be shipped + return_item_inventory_unit_reimbursed: Return item's inventory unit is already reimbursed + return_item_order_not_completed: Return item's order must be completed + return_item_rma_ineligible: Return item requires an RMA + return_item_time_period_ineligible: Return item is outside the eligible time period + return_items: Return Items + return_items_cannot_be_associated_with_multiple_orders: Return items cannot be associated with multiple orders. + reimbursement_mailer: + reimbursement_email: + days_to_send: ! 'You have %{days} days to send back any items awaiting exchange.' + dear_customer: Dear Customer, + exchange_summary: Exchange Summary + for: for + instructions: Your reimbursement has been processed. + refund_summary: Refund Summary + subject: Reimbursement Notification + total_refunded: ! 'Total refunded: %{total}' + return_number: Return Number + return_quantity: Return Quantity + returned: Returned + review: Review + risk: Risk + risk_analysis: Risk Analysis + risky: Risky + rma_credit: RMA Credit + rma_number: RMA Number + rma_value: RMA Value + roles: Roles + rules: Rules + sales_total: Sales Total + sales_total_description: Sales Total For All Orders + sales_totals: Sales Totals + save_and_continue: Save and Continue + save_my_address: Save my address + say_no: 'No' + say_yes: 'Yes' + scope: Scope + search: Search + search_results: Search results for '%{keywords}' + searching: Searching + secure_connection_type: Secure Connection Type + security_settings: Security Settings + select: Select + select_from_prototype: Select From Prototype + select_a_return_authorization_reason: Select a reason for the return authorization + select_a_stock_location: Select a stock location + select_stock: Select stock + send_copy_of_all_mails_to: Send Copy of All Mails To + send_mails_as: Send Mails As + server: Server + server_error: The server returned an error + settings: Settings + ship: ship + ship_address: Ship Address + ship_total: Ship Total + shipment: Shipment + shipment_adjustments: "Shipment adjustments" + shipment_details: "From %{stock_location} via %{shipping_method}" + shipment_mailer: + shipped_email: + dear_customer: Dear Customer, + instructions: Your order has been shipped + shipment_summary: Shipment Summary + subject: Shipment Notification + thanks: Thank you for your business. + track_information: ! 'Tracking Information: %{tracking}' + track_link: ! 'Tracking Link: %{url}' + shipment_state: Shipment State + shipment_states: + backorder: backorder + canceled: canceled + partial: partial + pending: pending + ready: ready + shipped: shipped + shipment_transfer_success: 'Variants successfully transferred' + shipment_transfer_error: 'There was an error transferring variants' + shipments: Shipments + shipped: Shipped + shipping: Shipping + shipping_address: Shipping Address + shipping_categories: Shipping Categories + shipping_category: Shipping Category + shipping_flat_rate_per_item: Flat rate per package item + shipping_flat_rate_per_order: Flat rate + shipping_flexible_rate: Flexible Rate per package item + shipping_instructions: Shipping Instructions + shipping_method: Shipping Method + shipping_methods: Shipping Methods + shipping_price_sack: Price sack + shipping_total: Shipping total + shop_by_taxonomy: Shop by %{taxonomy} + shopping_cart: Shopping Cart + show: Show + show_active: Show Active + show_deleted: Show Deleted + show_only_complete_orders: Only show complete orders + show_only_considered_risky: Only show risky orders + show_rate_in_label: Show rate in label + sku: SKU + skus: SKUs + slug: Slug + source: Source + special_instructions: Special Instructions + split: Split + spree_gateway_error_flash_for_checkout: There was a problem with your payment information. Please check your information and try again. + ssl: + change_protocol: "Please switch to using HTTP (rather than HTTPS) and retry this request." + start: Start + state: State + state_based: State Based + states: States + states_required: States Required + status: Status + stock_location: Stock Location + stock_location_info: Stock location info + stock_locations: Stock Locations + stock_locations_need_a_default_country: You must create a default country before creating a stock location. + stock_management: Stock Management + stock_management_requires_a_stock_location: Please create a stock location in order to manage stock. + stock_movements: Stock Movements + stock_movements_for_stock_location: Stock Movements for %{stock_location_name} + stock_successfully_transferred: Stock was successfully transferred between locations. + stock_transfer: Stock Transfer + stock_transfers: Stock Transfers + stop: Stop + store: Store + street_address: Street Address + street_address_2: Street Address (cont'd) + subtotal: Subtotal + subtract: Subtract + success: Success + successfully_created: ! '%{resource} has been successfully created!' + successfully_refunded: ! '%{resource} has been successfully refunded!' + successfully_removed: ! '%{resource} has been successfully removed!' + successfully_signed_up_for_analytics: Successfully signed up for Spree Analytics + successfully_updated: ! '%{resource} has been successfully updated!' + tax: Tax + tax_included: "Tax (incl.)" + tax_categories: Tax Categories + tax_category: Tax Category + tax_code: Tax Code + tax_rate_amount_explanation: Tax rates are a decimal amount to aid in calculations, (i.e. if the tax rate is 5% then enter 0.05) + tax_rates: Tax Rates + taxon: Taxon + taxon_edit: Edit Taxon + taxon_placeholder: Add a Taxon + taxon_rule: + choose_taxons: Choose taxons + label: Order must contain %{select} of these taxons + match_all: all + match_any: at least one + taxonomies: Taxonomies + taxonomy: Taxonomy + taxonomy_edit: Edit taxonomy + taxonomy_tree_error: The requested change has not been accepted and the tree has been returned to its previous state, please try again. + taxonomy_tree_instruction: ! '* Right click a child in the tree to access the menu for adding, deleting or sorting a child.' + taxons: Taxons + test: Test + test_mailer: + test_email: + greeting: Congratulations! + message: If you have received this email, then your email settings are correct. + subject: Test Mail + test_mode: Test Mode + thank_you_for_your_order: Thank you for your business. Please print out a copy of this confirmation page for your records. + there_are_no_items_for_this_order: There are no items for this order. Please add an item to the order to continue. + there_were_problems_with_the_following_fields: There were problems with the following fields + this_order_has_already_received_a_refund: This order has already received a refund + thumbnail: Thumbnail + tiers: Tiers + tiered_flat_rate: Tiered Flat Rate + tiered_percent: Tiered Percent time: Time - spree_gateway_error_flash_for_checkout: "There was a problem with your payment information. Please check your information and try again." - spree_inventory_error_flash_for_insufficient_quantity: "An item in your cart has become unavailable." - ssl_will_be_used_in_development_and_test_modes: "SSL will be used in development and test mode if necessary." - ssl_will_be_used_in_production_mode: "SSL will be used in production mode" - ssl_will_be_used_in_staging_mode: "SSL will be used in staging mode" - ssl_will_not_be_used_in_development_and_test_modes: "SSL will not be used in development and test mode if necessary." - ssl_will_not_be_used_in_production_mode: "SSL will not be used in production mode" - ssl_will_not_be_used_in_staging_mode: "SSL will not be used in staging mode" - spree_alert_checking: "Check for Spree security and release alerts" - spree_alert_not_checking: "Not checking for Spree security and release alerts" - start: Start - start_date: Valid from - state: State - state_based: "State Based" - state_setting_description: "Administer the list of states/provinces associated with each country." - states: States - status: Status - stop: Stop - store: Store - street_address: "Street Address" - street_address_2: "Street Address (cont'd)" - subtotal: Subtotal - subtract: Subtract - successfully_created: "%{resource} has been successfully created!" - successfully_removed: "%{resource} has been successfully removed!" - successfully_updated: "%{resource} has been successfully updated!" - system: System - tax: Tax - tax_categories: "Tax Categories" - tax_categories_setting_description: "Set up tax categories to identify which products should be taxable." - tax_category: "Tax Category" - tax_rates: "Tax Rates" - tax_rates_description: Tax rates setup and configuration. - tax_settings: "Tax Settings" - tax_settings_description: Basic tax settings. - tax_total: "Tax Total" - tax_type: "Tax Type" - taxon: Taxon - taxon_edit: Edit Taxon - taxonomy: Taxonomy - taxonomies: Taxonomies - taxonomies_setting_description: "Create and manage taxonomies." - taxonomy_edit: "Edit taxonomy" - taxonomy_tree_error: "The requested change has not been accepted and the tree has been returned to its previous state, please try again." - taxonomy_tree_instruction: "* Right click a child in the tree to access the menu for adding, deleting or sorting a child." - taxons: Taxons - test: "Test" - test_mailer: - test_email: - greeting: 'Congratulations!' - message: 'If you have received this email, then your email settings are correct.' - subject: 'Testmail' - test_mode: Test Mode - thank_you_for_your_order: "Thank you for your business. Please print out a copy of this confirmation page for your records." - there_were_problems_with_the_following_fields: "There were problems with the following fields" - this_file_language: "English (US)" - thumbnail: "Thumbnail" - to_add_variants_you_must_first_define: "To add variants, you must first define" - to_state: "To State" - total: Total - tracking: Tracking - transaction: Transaction - transactions: Transactions - tree: Tree - try_again: "Try Again" - type: Type - type_to_search: Type to search - unable_ship_method: "Unable to generate shipping methods due to a server error." - unable_to_authorize_credit_card: "Unable to Authorize Credit Card" - unable_to_capture_credit_card: "Unable to Capture Credit Card" - unable_to_connect_to_gateway: "Unable to connect to gateway." - unable_to_save_order: "Unable to Save Order" - under_price: "Under %{price}" - under_paid: "Under Paid" - unrecognized_card_type: Unrecognized card type - update: Update - update_password: "Update my password and log me in" - updated_successfully: "Updated Successfully" - updating: Updating - usage_limit: Usage Limit - use_as_shipping_address: Use as Shipping Address - use_billing_address: Use Billing Address - use_different_shipping_address: "Use Different Shipping Address" - use_new_cc: "Use a new card" - use_s3: "Use Amazon S3 For Images" - user: User - user_account: User Account - user_created_successfully: "User created successfully" - users: Users - validate_on_profile_create: Validate on profile create - validation: - cannot_be_greater_than_available_stock: "cannot be greater than available stock." - cannot_be_less_than_shipped_units: "cannot be less than the number of shipped units." - cannot_destory_line_item_as_inventory_units_have_shipped: "Cannot destory line item as some inventory units have shipped." - is_too_large: "is too large -- stock on hand cannot cover requested quantity!" - must_be_int: "must be an integer" - must_be_non_negative: "must be a non-negative value" - value: Value - variant: Variant - variants: Variants - vat: "VAT" - version: Version - view_shipping_options: "View shipping options" - void: Void - website: Website - weight: Weight - welcome_to_sample_store: "Welcome to the sample store" - what_is_a_cvv: "What is a (CVV) Credit Card Code?" - what_is_this: "What's This?" - whats_this: "What's this" - width: Width - year: "Year" - you_have_been_logged_out: "You have been logged out." - you_have_no_orders_yet: "You have no orders yet." - your_cart_is_empty: "Your cart is empty" - zip: Zip - zone: Zone - zone_based: "Zone Based" - zone_setting_description: "Collections of countries, states or other zones to be used in various calculations." - zones: Zones + to_add_variants_you_must_first_define: To add variants, you must first define + total: Total + total_per_item: Total per item + total_pre_tax_refund: Total Pre-Tax Refund + total_price: Total price + total_sales: Total Sales + track_inventory: Track Inventory + tracking: Tracking + tracking_number: Tracking Number + tracking_url: Tracking URL + tracking_url_placeholder: e.g. http://quickship.com/package?num=:tracking + transaction_id: Transaction ID + transfer_from_location: Transfer From + transfer_stock: Transfer Stock + transfer_to_location: Transfer To + tree: Tree + type: Type + type_to_search: Type to search + unable_to_connect_to_gateway: Unable to connect to gateway. + unable_to_create_reimbursements: Unable to create reimbursements because there are items pending manual intervention. + under_price: Under %{price} + unlock: Unlock + unrecognized_card_type: Unrecognized card type + unshippable_items: Unshippable Items + update: Update + updating: Updating + usage_limit: Usage Limit + use_app_default: Use App Default + use_billing_address: Use Billing Address + use_existing_cc: Use an existing card on file + use_new_cc: Use a new card + use_new_cc_or_payment_method: Use a new card / payment method + use_s3: Use Amazon S3 For Images + user: User + user_rule: + choose_users: Choose users + users: Users + validation: + unpaid_amount_not_zero: "Amount was not fully reimbursed. Still due: %{amount}" + cannot_be_less_than_shipped_units: cannot be less than the number of shipped units. + cannot_destroy_line_item_as_inventory_units_have_shipped: Cannot destroy line item as some inventory units have shipped. + exceeds_available_stock: exceeds available stock. Please ensure line items have a valid quantity. + is_too_large: is too large -- stock on hand cannot cover requested quantity! + must_be_int: must be an integer + must_be_non_negative: must be a non-negative value + value: Value + variant: Variant + variant_placeholder: Choose a variant + variants: Variants + version: Version + void: Void + weight: Weight + what_is_a_cvv: What is a (CVV) Credit Card Code? + what_is_this: What's This? + width: Width + year: Year + you_have_no_orders_yet: You have no orders yet + your_cart_is_empty: Your cart is empty + your_order_is_empty_add_product: Your order is empty, please search for and add a product above + zip: Zip + zipcode: Zip Code + zone: Zone + zones: Zones diff --git a/core/config/routes.rb b/core/config/routes.rb old mode 100755 new mode 100644 index 29b0629c056..d673dd5c6af --- a/core/config/routes.rb +++ b/core/config/routes.rb @@ -1,191 +1 @@ -Spree::Core::Engine.routes.draw do - - root :to => 'home#index' - - resources :products - - match '/locale/set', :to => 'locale#set' - - resources :tax_categories - - resources :states, :only => :index - resources :countries, :only => :index - - # non-restful checkout stuff - put '/checkout/update/:state', :to => 'checkout#update', :as => :update_checkout - get '/checkout/:state', :to => 'checkout#edit', :as => :checkout_state - get '/checkout', :to => 'checkout#edit' , :as => :checkout - - populate_redirect = redirect do |params, request| - request.flash[:error] = I18n.t(:populate_get_error) - request.referer || '/cart' - end - - get '/orders/populate', :via => :get, :to => populate_redirect - - resources :orders do - post :populate, :on => :collection - - resources :line_items - - resources :shipments do - member do - get :shipping_method - end - end - - end - get '/cart', :to => 'orders#edit', :as => :cart - put '/cart', :to => 'orders#update', :as => :update_cart - put '/cart/empty', :to => 'orders#empty', :as => :empty_cart - - resources :shipments do - member do - get :shipping_method - put :shipping_method - end - end - - # route globbing for pretty nested taxon and product paths - match '/t/*id', :to => 'taxons#show', :as => :nested_taxons - - namespace :admin do - get '/search/users', :to => "search#users", :as => :search_users - - resources :adjustments - resources :zones - resources :banners do - member do - post :dismiss - end - end - resources :countries do - resources :states - end - resources :states - resources :tax_categories - resources :products do - resources :product_properties - resources :images do - collection do - post :update_positions - end - end - member do - get :clone - end - resources :variants do - collection do - post :update_positions - end - end - end - - get '/variants/search', :to => "variants#search", :as => :search_variants - - resources :option_types do - collection do - post :update_positions - post :update_values_positions - end - end - - resources :properties do - collection do - get :filtered - end - end - - resources :prototypes do - member do - get :select - end - - collection do - get :available - end - end - - resource :inventory_settings - resource :image_settings - resources :google_analytics - - resources :orders do - member do - put :fire - get :fire - post :resend - end - - resource :customer, :controller => "orders/customer_details" - - resources :adjustments - resources :line_items - resources :shipments do - member do - put :fire - end - end - resources :return_authorizations do - member do - put :fire - end - end - resources :payments do - member do - put :fire - end - end - end - - resource :general_settings do - collection do - post :dismiss_alert - end - end - - resources :taxonomies do - collection do - post :update_positions - end - member do - get :get_children - end - - resources :taxons - end - - resources :taxons, :only => [] do - collection do - get :search - end - end - - resources :reports, :only => [:index, :show] do - collection do - get :sales_total - post :sales_total - end - end - - resources :shipping_methods - resources :shipping_categories - resources :tax_rates - resource :tax_settings - resources :calculators - - resources :trackers - resources :payment_methods - resources :mail_methods do - member do - post :testmail - end - end - end - - match '/admin', :to => 'admin/orders#index', :as => :admin - - match '/unauthorized', :to => 'home#unauthorized', :as => :unauthorized - match '/content/cvv', :to => 'content#cvv', :as => :cvv - match '/content/*path', :to => 'content#show', :via => :get, :as => :content -end +Spree::Core::Engine.draw_routes \ No newline at end of file diff --git a/core/db/default/spree/countries.rb b/core/db/default/spree/countries.rb new file mode 100644 index 00000000000..e1b86de29a2 --- /dev/null +++ b/core/db/default/spree/countries.rb @@ -0,0 +1,19 @@ +require 'carmen' + +countries = [] +Carmen::Country.all.each do |country| + countries << { + name: country.name, + iso3: country.alpha_3_code, + iso: country.alpha_2_code, + iso_name: country.name.upcase, + numcode: country.numeric_code, + states_required: country.subregions? + } +end + +ActiveRecord::Base.transaction do + Spree::Country.create!(countries) +end + +Spree::Config[:default_country_id] = Spree::Country.find_by(name: "United States").id diff --git a/core/db/default/spree/countries.yml b/core/db/default/spree/countries.yml deleted file mode 100644 index 875cca816e0..00000000000 --- a/core/db/default/spree/countries.yml +++ /dev/null @@ -1,1589 +0,0 @@ ---- -countries_039: - name: Chad - iso3: TCD - iso: TD - iso_name: CHAD - id: "39" - numcode: "148" -countries_065: - name: Faroe Islands - iso3: FRO - iso: FO - iso_name: FAROE ISLANDS - id: "65" - numcode: "234" -countries_092: - name: India - iso3: IND - iso: IN - iso_name: INDIA - id: "92" - numcode: "356" -countries_146: - name: Nicaragua - iso3: NIC - iso: NI - iso_name: NICARAGUA - id: "146" - numcode: "558" -countries_172: - name: Saint Lucia - iso3: LCA - iso: LC - iso_name: SAINT LUCIA - id: "172" - numcode: "662" -countries_066: - name: Fiji - iso3: FJI - iso: FJ - iso_name: FIJI - id: "66" - numcode: "242" -countries_093: - name: Indonesia - iso3: IDN - iso: ID - iso_name: INDONESIA - id: "93" - numcode: "360" -countries_147: - name: Niger - iso3: NER - iso: NE - iso_name: NIGER - id: "147" - numcode: "562" -countries_173: - name: Saint Pierre and Miquelon - iso3: SPM - iso: PM - iso_name: SAINT PIERRE AND MIQUELON - id: "173" - numcode: "666" -countries_067: - name: Finland - iso3: FIN - iso: FI - iso_name: FINLAND - id: "67" - numcode: "246" -countries_148: - name: Nigeria - iso3: NGA - iso: NG - iso_name: NIGERIA - id: "148" - numcode: "566" -countries_174: - name: Saint Vincent and the Grenadines - iso3: VCT - iso: VC - iso_name: SAINT VINCENT AND THE GRENADINES - id: "174" - numcode: "670" -countries_068: - name: France - iso3: FRA - iso: FR - iso_name: FRANCE - id: "68" - numcode: "250" -countries_094: - name: Iran, Islamic Republic of - iso3: IRN - iso: IR - iso_name: IRAN, ISLAMIC REPUBLIC OF - id: "94" - numcode: "364" -countries_149: - name: Niue - iso3: NIU - iso: NU - iso_name: NIUE - id: "149" - numcode: "570" -countries_175: - name: Samoa - iso3: WSM - iso: WS - iso_name: SAMOA - id: "175" - numcode: "882" -countries_069: - name: French Guiana - iso3: GUF - iso: GF - iso_name: FRENCH GUIANA - id: "69" - numcode: "254" -countries_095: - name: Iraq - iso3: IRQ - iso: IQ - iso_name: IRAQ - id: "95" - numcode: "368" -countries_176: - name: San Marino - iso3: SMR - iso: SM - iso_name: SAN MARINO - id: "176" - numcode: "674" -countries_096: - name: Ireland - iso3: IRL - iso: IE - iso_name: IRELAND - id: "96" - numcode: "372" -countries_177: - name: Sao Tome and Principe - iso3: STP - iso: ST - iso_name: SAO TOME AND PRINCIPE - id: "177" - numcode: "678" -countries_097: - name: Israel - iso3: ISR - iso: IL - iso_name: ISRAEL - id: "97" - numcode: "376" -countries_178: - name: Saudi Arabia - iso3: SAU - iso: SA - iso_name: SAUDI ARABIA - id: "178" - numcode: "682" -countries_098: - name: Italy - iso3: ITA - iso: IT - iso_name: ITALY - id: "98" - numcode: "380" -countries_179: - name: Senegal - iso3: SEN - iso: SN - iso_name: SENEGAL - id: "179" - numcode: "686" -countries_099: - name: Jamaica - iso3: JAM - iso: JM - iso_name: JAMAICA - id: "99" - numcode: "388" -countries_100: - name: Japan - iso3: JPN - iso: JP - iso_name: JAPAN - id: "100" - numcode: "392" -countries_101: - name: Jordan - iso3: JOR - iso: JO - iso_name: JORDAN - id: "101" - numcode: "400" -countries_020: - name: Belgium - iso3: BEL - iso: BE - iso_name: BELGIUM - id: "20" - numcode: "56" -countries_021: - name: Belize - iso3: BLZ - iso: BZ - iso_name: BELIZE - id: "21" - numcode: "84" -countries_102: - name: Kazakhstan - iso3: KAZ - iso: KZ - iso_name: KAZAKHSTAN - id: "102" - numcode: "398" -countries_210: - name: Uganda - iso3: UGA - iso: UG - iso_name: UGANDA - id: "210" - numcode: "800" -countries_022: - name: Benin - iso3: BEN - iso: BJ - iso_name: BENIN - id: "22" - numcode: "204" -countries_103: - name: Kenya - iso3: KEN - iso: KE - iso_name: KENYA - id: "103" - numcode: "404" -countries_211: - name: Ukraine - iso3: UKR - iso: UA - iso_name: UKRAINE - id: "211" - numcode: "804" -countries_023: - name: Bermuda - iso3: BMU - iso: BM - iso_name: BERMUDA - id: "23" - numcode: "60" -countries_104: - name: Kiribati - iso3: KIR - iso: KI - iso_name: KIRIBATI - id: "104" - numcode: "296" -countries_130: - name: Mexico - iso3: MEX - iso: MX - iso_name: MEXICO - id: "130" - numcode: "484" -countries_212: - name: United Arab Emirates - iso3: ARE - iso: AE - iso_name: UNITED ARAB EMIRATES - id: "212" - numcode: "784" -countries_024: - name: Bhutan - iso3: BTN - iso: BT - iso_name: BHUTAN - id: "24" - numcode: "64" -countries_050: - name: Cuba - iso3: CUB - iso: CU - iso_name: CUBA - id: "50" - numcode: "192" -countries_105: - name: North Korea - iso3: PRK - iso: KP - iso_name: KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF - id: "105" - numcode: "408" -countries_131: - name: Micronesia, Federated States of - iso3: FSM - iso: FM - iso_name: MICRONESIA, FEDERATED STATES OF - id: "131" - numcode: "583" -countries_213: - name: United Kingdom - iso3: GBR - iso: GB - iso_name: UNITED KINGDOM - id: "213" - numcode: "826" -countries_025: - name: Bolivia - iso3: BOL - iso: BO - iso_name: BOLIVIA - id: "25" - numcode: "68" -countries_051: - name: Cyprus - iso3: CYP - iso: CY - iso_name: CYPRUS - id: "51" - numcode: "196" -countries_106: - name: South Korea - iso3: KOR - iso: KR - iso_name: KOREA, REPUBLIC OF - id: "106" - numcode: "410" -countries_132: - name: Moldova, Republic of - iso3: MDA - iso: MD - iso_name: MOLDOVA, REPUBLIC OF - id: "132" - numcode: "498" -countries_214: - name: United States - iso3: USA - iso: US - iso_name: UNITED STATES - id: "214" - numcode: "840" -countries_026: - name: Bosnia and Herzegovina - iso3: BIH - iso: BA - iso_name: BOSNIA AND HERZEGOVINA - id: "26" - numcode: "70" -countries_052: - name: Czech Republic - iso3: CZE - iso: CZ - iso_name: CZECH REPUBLIC - id: "52" - numcode: "203" -countries_107: - name: Kuwait - iso3: KWT - iso: KW - iso_name: KUWAIT - id: "107" - numcode: "414" -countries_133: - name: Monaco - iso3: MCO - iso: MC - iso_name: MONACO - id: "133" - numcode: "492" -countries_215: - name: Uruguay - iso3: URY - iso: UY - iso_name: URUGUAY - id: "215" - numcode: "858" -countries_027: - name: Botswana - iso3: BWA - iso: BW - iso_name: BOTSWANA - id: "27" - numcode: "72" -countries_053: - name: Denmark - iso3: DNK - iso: DK - iso_name: DENMARK - id: "53" - numcode: "208" -countries_080: - name: Guadeloupe - iso3: GLP - iso: GP - iso_name: GUADELOUPE - id: "80" - numcode: "312" -countries_108: - name: Kyrgyzstan - iso3: KGZ - iso: KG - iso_name: KYRGYZSTAN - id: "108" - numcode: "417" -countries_134: - name: Mongolia - iso3: MNG - iso: MN - iso_name: MONGOLIA - id: "134" - numcode: "496" -countries_160: - name: Philippines - iso3: PHL - iso: PH - iso_name: PHILIPPINES - id: "160" - numcode: "608" -countries_028: - name: Brazil - iso3: BRA - iso: BR - iso_name: BRAZIL - id: "28" - numcode: "76" -countries_054: - name: Djibouti - iso3: DJI - iso: DJ - iso_name: DJIBOUTI - id: "54" - numcode: "262" -countries_081: - name: Guam - iso3: GUM - iso: GU - iso_name: GUAM - id: "81" - numcode: "316" -countries_109: - name: Lao People's Democratic Republic - iso3: LAO - iso: LA - iso_name: LAO PEOPLE'S DEMOCRATIC REPUBLIC - id: "109" - numcode: "418" -countries_135: - name: Montserrat - iso3: MSR - iso: MS - iso_name: MONTSERRAT - id: "135" - numcode: "500" -countries_161: - name: Pitcairn - iso3: PCN - iso: PN - iso_name: PITCAIRN - id: "161" - numcode: "612" -countries_216: - name: Uzbekistan - iso3: UZB - iso: UZ - iso_name: UZBEKISTAN - id: "216" - numcode: "860" -countries_029: - name: Brunei Darussalam - iso3: BRN - iso: BN - iso_name: BRUNEI DARUSSALAM - id: "29" - numcode: "96" -countries_055: - name: Dominica - iso3: DMA - iso: DM - iso_name: DOMINICA - id: "55" - numcode: "212" -countries_082: - name: Guatemala - iso3: GTM - iso: GT - iso_name: GUATEMALA - id: "82" - numcode: "320" -countries_136: - name: Morocco - iso3: MAR - iso: MA - iso_name: MOROCCO - id: "136" - numcode: "504" -countries_162: - name: Poland - iso3: POL - iso: PL - iso_name: POLAND - id: "162" - numcode: "616" -countries_217: - name: Vanuatu - iso3: VUT - iso: VU - iso_name: VANUATU - id: "217" - numcode: "548" -countries_056: - name: Dominican Republic - iso3: DOM - iso: DO - iso_name: DOMINICAN REPUBLIC - id: "56" - numcode: "214" -countries_137: - name: Mozambique - iso3: MOZ - iso: MZ - iso_name: MOZAMBIQUE - id: "137" - numcode: "508" -countries_163: - name: Portugal - iso3: PRT - iso: PT - iso_name: PORTUGAL - id: "163" - numcode: "620" -countries_190: - name: Sudan - iso3: SDN - iso: SD - iso_name: SUDAN - id: "190" - numcode: "736" -countries_218: - name: Venezuela - iso3: VEN - iso: VE - iso_name: VENEZUELA - id: "218" - numcode: "862" -countries_057: - name: Ecuador - iso3: ECU - iso: EC - iso_name: ECUADOR - id: "57" - numcode: "218" -countries_083: - name: Guinea - iso3: GIN - iso: GN - iso_name: GUINEA - id: "83" - numcode: "324" -countries_138: - name: Myanmar - iso3: MMR - iso: MM - iso_name: MYANMAR - id: "138" - numcode: "104" -countries_164: - name: Puerto Rico - iso3: PRI - iso: PR - iso_name: PUERTO RICO - id: "164" - numcode: "630" -countries_191: - name: Suriname - iso3: SUR - iso: SR - iso_name: SURINAME - id: "191" - numcode: "740" -countries_219: - name: Viet Nam - iso3: VNM - iso: VN - iso_name: VIET NAM - id: "219" - numcode: "704" -countries_058: - name: Egypt - iso3: EGY - iso: EG - iso_name: EGYPT - id: "58" - numcode: "818" -countries_084: - name: Guinea-Bissau - iso3: GNB - iso: GW - iso_name: GUINEA-BISSAU - id: "84" - numcode: "624" -countries_139: - name: Namibia - iso3: NAM - iso: NA - iso_name: NAMIBIA - id: "139" - numcode: "516" -countries_165: - name: Qatar - iso3: QAT - iso: QA - iso_name: QATAR - id: "165" - numcode: "634" -countries_192: - name: Svalbard and Jan Mayen - iso3: SJM - iso: SJ - iso_name: SVALBARD AND JAN MAYEN - id: "192" - numcode: "744" -countries_059: - name: El Salvador - iso3: SLV - iso: SV - iso_name: EL SALVADOR - id: "59" - numcode: "222" -countries_085: - name: Guyana - iso3: GUY - iso: GY - iso_name: GUYANA - id: "85" - numcode: "328" -countries_166: - name: Reunion - iso3: REU - iso: RE - iso_name: REUNION - id: "166" - numcode: "638" -countries_086: - name: Haiti - iso3: HTI - iso: HT - iso_name: HAITI - id: "86" - numcode: "332" -countries_167: - name: Romania - iso3: ROM - iso: RO - iso_name: ROMANIA - id: "167" - numcode: "642" -countries_193: - name: Swaziland - iso3: SWZ - iso: SZ - iso_name: SWAZILAND - id: "193" - numcode: "748" -countries_087: - name: Holy See (Vatican City State) - iso3: VAT - iso: VA - iso_name: HOLY SEE (VATICAN CITY STATE) - id: "87" - numcode: "336" -countries_168: - name: Russian Federation - iso3: RUS - iso: RU - iso_name: RUSSIAN FEDERATION - id: "168" - numcode: "643" -countries_194: - name: Sweden - iso3: SWE - iso: SE - iso_name: SWEDEN - id: "194" - numcode: "752" -countries_088: - name: Honduras - iso3: HND - iso: HN - iso_name: HONDURAS - id: "88" - numcode: "340" -countries_169: - name: Rwanda - iso3: RWA - iso: RW - iso_name: RWANDA - id: "169" - numcode: "646" -countries_195: - name: Switzerland - iso3: CHE - iso: CH - iso_name: SWITZERLAND - id: "195" - numcode: "756" -countries_089: - name: Hong Kong - iso3: HKG - iso: HK - iso_name: HONG KONG - id: "89" - numcode: "344" -countries_196: - name: Syrian Arab Republic - iso3: SYR - iso: SY - iso_name: SYRIAN ARAB REPUBLIC - id: "196" - numcode: "760" -countries_197: - name: Taiwan - iso3: TWN - iso: TW - iso_name: TAIWAN, PROVINCE OF CHINA - id: "197" - numcode: "158" -countries_198: - name: Tajikistan - iso3: TJK - iso: TJ - iso_name: TAJIKISTAN - id: "198" - numcode: "762" -countries_199: - name: Tanzania, United Republic of - iso3: TZA - iso: TZ - iso_name: TANZANIA, UNITED REPUBLIC OF - id: "199" - numcode: "834" -countries_010: - name: Armenia - iso3: ARM - iso: AM - iso_name: ARMENIA - id: "10" - numcode: "51" -countries_011: - name: Aruba - iso3: ABW - iso: AW - iso_name: ARUBA - id: "11" - numcode: "533" -countries_012: - name: Australia - iso3: AUS - iso: AU - iso_name: AUSTRALIA - id: "12" - numcode: "36" -countries_200: - name: Thailand - iso3: THA - iso: TH - iso_name: THAILAND - id: "200" - numcode: "764" -countries_013: - name: Austria - iso3: AUT - iso: AT - iso_name: AUSTRIA - id: "13" - numcode: "40" -countries_120: - name: Madagascar - iso3: MDG - iso: MG - iso_name: MADAGASCAR - id: "120" - numcode: "450" -countries_201: - name: Togo - iso3: TGO - iso: TG - iso_name: TOGO - id: "201" - numcode: "768" -countries_014: - name: Azerbaijan - iso3: AZE - iso: AZ - iso_name: AZERBAIJAN - id: "14" - numcode: "31" -countries_040: - name: Chile - iso3: CHL - iso: CL - iso_name: CHILE - id: "40" - numcode: "152" -countries_121: - name: Malawi - iso3: MWI - iso: MW - iso_name: MALAWI - id: "121" - numcode: "454" -countries_202: - name: Tokelau - iso3: TKL - iso: TK - iso_name: TOKELAU - id: "202" - numcode: "772" -countries_015: - name: Bahamas - iso3: BHS - iso: BS - iso_name: BAHAMAS - id: "15" - numcode: "44" -countries_041: - name: China - iso3: CHN - iso: CN - iso_name: CHINA - id: "41" - numcode: "156" -countries_122: - name: Malaysia - iso3: MYS - iso: MY - iso_name: MALAYSIA - id: "122" - numcode: "458" -countries_203: - name: Tonga - iso3: TON - iso: TO - iso_name: TONGA - id: "203" - numcode: "776" -countries_016: - name: Bahrain - iso3: BHR - iso: BH - iso_name: BAHRAIN - id: "16" - numcode: "48" -countries_042: - name: Colombia - iso3: COL - iso: CO - iso_name: COLOMBIA - id: "42" - numcode: "170" -countries_123: - name: Maldives - iso3: MDV - iso: MV - iso_name: MALDIVES - id: "123" - numcode: "462" -countries_204: - name: Trinidad and Tobago - iso3: TTO - iso: TT - iso_name: TRINIDAD AND TOBAGO - id: "204" - numcode: "780" -countries_017: - name: Bangladesh - iso3: BGD - iso: BD - iso_name: BANGLADESH - id: "17" - numcode: "50" -countries_043: - name: Comoros - iso3: COM - iso: KM - iso_name: COMOROS - id: "43" - numcode: "174" -countries_070: - name: French Polynesia - iso3: PYF - iso: PF - iso_name: FRENCH POLYNESIA - id: "70" - numcode: "258" -countries_124: - name: Mali - iso3: MLI - iso: ML - iso_name: MALI - id: "124" - numcode: "466" -countries_150: - name: Norfolk Island - iso3: NFK - iso: NF - iso_name: NORFOLK ISLAND - id: "150" - numcode: "574" -countries_205: - name: Tunisia - iso3: TUN - iso: TN - iso_name: TUNISIA - id: "205" - numcode: "788" -countries_018: - name: Barbados - iso3: BRB - iso: BB - iso_name: BARBADOS - id: "18" - numcode: "52" -countries_044: - name: Congo - iso3: COG - iso: CG - iso_name: CONGO - id: "44" - numcode: "178" -countries_071: - name: Gabon - iso3: GAB - iso: GA - iso_name: GABON - id: "71" - numcode: "266" -countries_125: - name: Malta - iso3: MLT - iso: MT - iso_name: MALTA - id: "125" - numcode: "470" -countries_151: - name: Northern Mariana Islands - iso3: MNP - iso: MP - iso_name: NORTHERN MARIANA ISLANDS - id: "151" - numcode: "580" -countries_206: - name: Turkey - iso3: TUR - iso: TR - iso_name: TURKEY - id: "206" - numcode: "792" -countries_045: - name: Congo, the Democratic Republic of the - iso3: COD - iso: CD - iso_name: CONGO, THE DEMOCRATIC REPUBLIC OF THE - id: "45" - numcode: "180" -countries_126: - name: Marshall Islands - iso3: MHL - iso: MH - iso_name: MARSHALL ISLANDS - id: "126" - numcode: "584" -countries_152: - name: Norway - iso3: NOR - iso: "NO" - iso_name: NORWAY - id: "152" - numcode: "578" -countries_207: - name: Turkmenistan - iso3: TKM - iso: TM - iso_name: TURKMENISTAN - id: "207" - numcode: "795" -countries_019: - name: Belarus - iso3: BLR - iso: BY - iso_name: BELARUS - id: "19" - numcode: "112" -countries_046: - name: Cook Islands - iso3: COK - iso: CK - iso_name: COOK ISLANDS - id: "46" - numcode: "184" -countries_072: - name: Gambia - iso3: GMB - iso: GM - iso_name: GAMBIA - id: "72" - numcode: "270" -countries_127: - name: Martinique - iso3: MTQ - iso: MQ - iso_name: MARTINIQUE - id: "127" - numcode: "474" -countries_153: - name: Oman - iso3: OMN - iso: OM - iso_name: OMAN - id: "153" - numcode: "512" -countries_180: - name: Seychelles - iso3: SYC - iso: SC - iso_name: SEYCHELLES - id: "180" - numcode: "690" -countries_208: - name: Turks and Caicos Islands - iso3: TCA - iso: TC - iso_name: TURKS AND CAICOS ISLANDS - id: "208" - numcode: "796" -countries_073: - name: Georgia - iso3: GEO - iso: GE - iso_name: GEORGIA - id: "73" - numcode: "268" -countries_128: - name: Mauritania - iso3: MRT - iso: MR - iso_name: MAURITANIA - id: "128" - numcode: "478" -countries_154: - name: Pakistan - iso3: PAK - iso: PK - iso_name: PAKISTAN - id: "154" - numcode: "586" -countries_181: - name: Sierra Leone - iso3: SLE - iso: SL - iso_name: SIERRA LEONE - id: "181" - numcode: "694" -countries_209: - name: Tuvalu - iso3: TUV - iso: TV - iso_name: TUVALU - id: "209" - numcode: "798" -countries_047: - name: Costa Rica - iso3: CRI - iso: CR - iso_name: COSTA RICA - id: "47" - numcode: "188" -countries_074: - name: Germany - iso3: DEU - iso: DE - iso_name: GERMANY - id: "74" - numcode: "276" -countries_129: - name: Mauritius - iso3: MUS - iso: MU - iso_name: MAURITIUS - id: "129" - numcode: "480" -countries_155: - name: Palau - iso3: PLW - iso: PW - iso_name: PALAU - id: "155" - numcode: "585" -countries_048: - name: Cote D'Ivoire - iso3: CIV - iso: CI - iso_name: COTE D'IVOIRE - id: "48" - numcode: "384" -countries_156: - name: Panama - iso3: PAN - iso: PA - iso_name: PANAMA - id: "156" - numcode: "591" -countries_182: - name: Singapore - iso3: SGP - iso: SG - iso_name: SINGAPORE - id: "182" - numcode: "702" -countries_049: - name: Croatia - iso3: HRV - iso: HR - iso_name: CROATIA - id: "49" - numcode: "191" -countries_075: - name: Ghana - iso3: GHA - iso: GH - iso_name: GHANA - id: "75" - numcode: "288" -countries_157: - name: Papua New Guinea - iso3: PNG - iso: PG - iso_name: PAPUA NEW GUINEA - id: "157" - numcode: "598" -countries_183: - name: Slovakia - iso3: SVK - iso: SK - iso_name: SLOVAKIA - id: "183" - numcode: "703" -countries_076: - name: Gibraltar - iso3: GIB - iso: GI - iso_name: GIBRALTAR - id: "76" - numcode: "292" -countries_158: - name: Paraguay - iso3: PRY - iso: PY - iso_name: PARAGUAY - id: "158" - numcode: "600" -countries_184: - name: Slovenia - iso3: SVN - iso: SI - iso_name: SLOVENIA - id: "184" - numcode: "705" -countries_077: - name: Greece - iso3: GRC - iso: GR - iso_name: GREECE - id: "77" - numcode: "300" -countries_159: - name: Peru - iso3: PER - iso: PE - iso_name: PERU - id: "159" - numcode: "604" -countries_185: - name: Solomon Islands - iso3: SLB - iso: SB - iso_name: SOLOMON ISLANDS - id: "185" - numcode: "90" -countries_078: - name: Greenland - iso3: GRL - iso: GL - iso_name: GREENLAND - id: "78" - numcode: "304" -countries_186: - name: Somalia - iso3: SOM - iso: SO - iso_name: SOMALIA - id: "186" - numcode: "706" -countries_079: - name: Grenada - iso3: GRD - iso: GD - iso_name: GRENADA - id: "79" - numcode: "308" -countries_187: - name: South Africa - iso3: ZAF - iso: ZA - iso_name: SOUTH AFRICA - id: "187" - numcode: "710" -countries_188: - name: Spain - iso3: ESP - iso: ES - iso_name: SPAIN - id: "188" - numcode: "724" -countries_189: - name: Sri Lanka - iso3: LKA - iso: LK - iso_name: SRI LANKA - id: "189" - numcode: "144" -countries_001: - name: Afghanistan - iso3: AFG - iso: AF - iso_name: AFGHANISTAN - id: "1" - numcode: "4" -countries_002: - name: Albania - iso3: ALB - iso: AL - iso_name: ALBANIA - id: "2" - numcode: "8" -countries_003: - name: Algeria - iso3: DZA - iso: DZ - iso_name: ALGERIA - id: "3" - numcode: "12" -countries_110: - name: Latvia - iso3: LVA - iso: LV - iso_name: LATVIA - id: "110" - numcode: "428" -countries_004: - name: American Samoa - iso3: ASM - iso: AS - iso_name: AMERICAN SAMOA - id: "4" - numcode: "16" -countries_030: - name: Bulgaria - iso3: BGR - iso: BG - iso_name: BULGARIA - id: "30" - numcode: "100" -countries_111: - name: Lebanon - iso3: LBN - iso: LB - iso_name: LEBANON - id: "111" - numcode: "422" -countries_005: - name: Andorra - iso3: AND - iso: AD - iso_name: ANDORRA - id: "5" - numcode: "20" -countries_031: - name: Burkina Faso - iso3: BFA - iso: BF - iso_name: BURKINA FASO - id: "31" - numcode: "854" -countries_112: - name: Lesotho - iso3: LSO - iso: LS - iso_name: LESOTHO - id: "112" - numcode: "426" -countries_006: - name: Angola - iso3: AGO - iso: AO - iso_name: ANGOLA - id: "6" - numcode: "24" -countries_032: - name: Burundi - iso3: BDI - iso: BI - iso_name: BURUNDI - id: "32" - numcode: "108" -countries_113: - name: Liberia - iso3: LBR - iso: LR - iso_name: LIBERIA - id: "113" - numcode: "430" -countries_220: - name: Virgin Islands, British - iso3: VGB - iso: VG - iso_name: VIRGIN ISLANDS, BRITISH - id: "220" - numcode: "92" -countries_007: - name: Anguilla - iso3: AIA - iso: AI - iso_name: ANGUILLA - id: "7" - numcode: "660" -countries_033: - name: Cambodia - iso3: KHM - iso: KH - iso_name: CAMBODIA - id: "33" - numcode: "116" -countries_060: - name: Equatorial Guinea - iso3: GNQ - iso: GQ - iso_name: EQUATORIAL GUINEA - id: "60" - numcode: "226" -countries_114: - name: Libyan Arab Jamahiriya - iso3: LBY - iso: LY - iso_name: LIBYAN ARAB JAMAHIRIYA - id: "114" - numcode: "434" -countries_140: - name: Nauru - iso3: NRU - iso: NR - iso_name: NAURU - id: "140" - numcode: "520" -countries_221: - name: Virgin Islands, U.S. - iso3: VIR - iso: VI - iso_name: VIRGIN ISLANDS, U.S. - id: "221" - numcode: "850" -countries_008: - name: Antigua and Barbuda - iso3: ATG - iso: AG - iso_name: ANTIGUA AND BARBUDA - id: "8" - numcode: "28" -countries_034: - name: Cameroon - iso3: CMR - iso: CM - iso_name: CAMEROON - id: "34" - numcode: "120" -countries_115: - name: Liechtenstein - iso3: LIE - iso: LI - iso_name: LIECHTENSTEIN - id: "115" - numcode: "438" -countries_141: - name: Nepal - iso3: NPL - iso: NP - iso_name: NEPAL - id: "141" - numcode: "524" -countries_222: - name: Wallis and Futuna - iso3: WLF - iso: WF - iso_name: WALLIS AND FUTUNA - id: "222" - numcode: "876" -countries_223: - name: Western Sahara - iso3: ESH - iso: EH - iso_name: WESTERN SAHARA - id: "223" - numcode: "732" -countries_009: - name: Argentina - iso3: ARG - iso: AR - iso_name: ARGENTINA - id: "9" - numcode: "32" -countries_035: - name: Canada - iso3: CAN - iso: CA - iso_name: CANADA - id: "35" - numcode: "124" -countries_061: - name: Eritrea - iso3: ERI - iso: ER - iso_name: ERITREA - id: "61" - numcode: "232" -countries_116: - name: Lithuania - iso3: LTU - iso: LT - iso_name: LITHUANIA - id: "116" - numcode: "440" -countries_142: - name: Netherlands - iso3: NLD - iso: NL - iso_name: NETHERLANDS - id: "142" - numcode: "528" -countries_224: - name: Yemen - iso3: YEM - iso: YE - iso_name: YEMEN - id: "224" - numcode: "887" -countries_036: - name: Cape Verde - iso3: CPV - iso: CV - iso_name: CAPE VERDE - id: "36" - numcode: "132" -countries_062: - name: Estonia - iso3: EST - iso: EE - iso_name: ESTONIA - id: "62" - numcode: "233" -countries_117: - name: Luxembourg - iso3: LUX - iso: LU - iso_name: LUXEMBOURG - id: "117" - numcode: "442" -countries_143: - name: Netherlands Antilles - iso3: ANT - iso: AN - iso_name: NETHERLANDS ANTILLES - id: "143" - numcode: "530" -countries_170: - name: Saint Helena - iso3: SHN - iso: SH - iso_name: SAINT HELENA - id: "170" - numcode: "654" -countries_225: - name: Zambia - iso3: ZMB - iso: ZM - iso_name: ZAMBIA - id: "225" - numcode: "894" -countries_037: - name: Cayman Islands - iso3: CYM - iso: KY - iso_name: CAYMAN ISLANDS - id: "37" - numcode: "136" -countries_063: - name: Ethiopia - iso3: ETH - iso: ET - iso_name: ETHIOPIA - id: "63" - numcode: "231" -countries_090: - name: Hungary - iso3: HUN - iso: HU - iso_name: HUNGARY - id: "90" - numcode: "348" -countries_118: - name: Macao - iso3: MAC - iso: MO - iso_name: MACAO - id: "118" - numcode: "446" -countries_144: - name: New Caledonia - iso3: NCL - iso: NC - iso_name: NEW CALEDONIA - id: "144" - numcode: "540" -countries_226: - name: Zimbabwe - iso3: ZWE - iso: ZW - iso_name: ZIMBABWE - id: "226" - numcode: "716" -countries_038: - name: Central African Republic - iso3: CAF - iso: CF - iso_name: CENTRAL AFRICAN REPUBLIC - id: "38" - numcode: "140" -countries_064: - name: Falkland Islands (Malvinas) - iso3: FLK - iso: FK - iso_name: FALKLAND ISLANDS (MALVINAS) - id: "64" - numcode: "238" -countries_091: - name: Iceland - iso3: ISL - iso: IS - iso_name: ICELAND - id: "91" - numcode: "352" -countries_119: - name: Macedonia - iso3: MKD - iso: MK - iso_name: MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF - id: "119" - numcode: "807" -countries_145: - name: New Zealand - iso3: NZL - iso: NZ - iso_name: NEW ZEALAND - id: "145" - numcode: "554" -countries_171: - name: Saint Kitts and Nevis - iso3: KNA - iso: KN - iso_name: SAINT KITTS AND NEVIS - id: "171" - numcode: "659" -countries_998: - name: Serbia - iso3: SRB - iso: RS - id: "998" - numcode: "999" \ No newline at end of file diff --git a/core/db/default/spree/roles.rb b/core/db/default/spree/roles.rb new file mode 100644 index 00000000000..0b6f77ef4ca --- /dev/null +++ b/core/db/default/spree/roles.rb @@ -0,0 +1,2 @@ +Spree::Role.where(:name => "admin").first_or_create +Spree::Role.where(:name => "user").first_or_create diff --git a/core/db/default/spree/roles.yml b/core/db/default/spree/roles.yml deleted file mode 100644 index b0b3ffc5487..00000000000 --- a/core/db/default/spree/roles.yml +++ /dev/null @@ -1,5 +0,0 @@ -admin_role: - name: admin - -user_role: - name: user diff --git a/core/db/default/spree/states.rb b/core/db/default/spree/states.rb new file mode 100644 index 00000000000..27f26a9aef6 --- /dev/null +++ b/core/db/default/spree/states.rb @@ -0,0 +1,16 @@ +ActiveRecord::Base.transaction do + Spree::Country.all.each do |country| + carmen_country = Carmen::Country.named(country.name) + @states ||= [] + if carmen_country.subregions? + carmen_country.subregions.each do |subregion| + @states << { + name: subregion.name, + abbr: subregion.code, + country: country + } + end + end + end + Spree::State.create!(@states) +end diff --git a/core/db/default/spree/states.yml b/core/db/default/spree/states.yml deleted file mode 100644 index 02b838d8bcb..00000000000 --- a/core/db/default/spree/states.yml +++ /dev/null @@ -1,256 +0,0 @@ ---- -states_043: - name: Michigan - country_id: "214" - id: "931624400" - abbr: MI -states_032: - name: South Dakota - country_id: "214" - id: "615306087" - abbr: SD -states_021: - name: Washington - country_id: "214" - id: "414569975" - abbr: WA -states_010: - name: Wisconsin - country_id: "214" - id: "103680699" - abbr: WI -states_044: - name: Arizona - country_id: "214" - id: "948208802" - abbr: AZ -states_033: - name: Illinois - country_id: "214" - id: "625629523" - abbr: IL -states_022: - name: New Hampshire - country_id: "214" - id: "426832442" - abbr: NH -states_011: - name: North Carolina - country_id: "214" - id: "177087202" - abbr: NC -states_045: - name: Kansas - country_id: "214" - id: "969722173" - abbr: KS -states_034: - name: Missouri - country_id: "214" - id: "653576146" - abbr: MO -states_023: - name: Arkansas - country_id: "214" - id: "471470972" - abbr: AR -states_012: - name: Nevada - country_id: "214" - id: "179539703" - abbr: NV -states_001: - name: District of Columbia - country_id: "214" - id: "6764998" - abbr: DC -states_046: - name: Idaho - country_id: "214" - id: "982433740" - abbr: ID -states_035: - name: Nebraska - country_id: "214" - id: "673350891" - abbr: NE -states_024: - name: Pennsylvania - country_id: "214" - id: "471711976" - abbr: PA -states_013: - name: Hawaii - country_id: "214" - id: "199950338" - abbr: HI -states_002: - name: Utah - country_id: "214" - id: "17199670" - abbr: UT -states_047: - name: Vermont - country_id: "214" - id: "989115415" - abbr: VT -states_036: - name: Delaware - country_id: "214" - id: "721598219" - abbr: DE -states_025: - name: Rhode Island - country_id: "214" - id: "474001862" - abbr: RI -states_014: - name: Oklahoma - country_id: "214" - id: "248548169" - abbr: OK -states_003: - name: Louisiana - country_id: "214" - id: "37199952" - abbr: LA -states_048: - name: Montana - country_id: "214" - id: "999156632" - abbr: MT -states_037: - name: Tennessee - country_id: "214" - id: "726305632" - abbr: TN -states_026: - name: Maryland - country_id: "214" - id: "480368357" - abbr: MD -states_015: - name: Florida - country_id: "214" - id: "267271847" - abbr: FL -states_004: - name: Virginia - country_id: "214" - id: "41111624" - abbr: VA -states_049: - name: Minnesota - country_id: "214" - id: "1032288924" - abbr: MN -states_038: - name: New Jersey - country_id: "214" - id: "750950030" - abbr: NJ -states_027: - name: Ohio - country_id: "214" - id: "485193526" - abbr: OH -states_016: - name: California - country_id: "214" - id: "276110813" - abbr: CA -states_005: - name: North Dakota - country_id: "214" - id: "51943165" - abbr: ND -states_050: - name: Maine - country_id: "214" - id: "1055056709" - abbr: ME -states_039: - name: Indiana - country_id: "214" - id: "769938586" - abbr: IN -states_028: - name: Texas - country_id: "214" - id: "525212995" - abbr: TX -states_017: - name: Oregon - country_id: "214" - id: "298914262" - abbr: OR -states_006: - name: Wyoming - country_id: "214" - id: "66390489" - abbr: WY -states_051: - name: Alabama - country_id: "214" - id: "1061493585" - abbr: AL -states_040: - name: Iowa - country_id: "214" - id: "825306985" - abbr: IA -states_029: - name: Mississippi - country_id: "214" - id: "532363768" - abbr: MS -states_018: - name: Kentucky - country_id: "214" - id: "308473843" - abbr: KY -states_007: - name: New Mexico - country_id: "214" - id: "69729944" - abbr: NM -states_041: - name: Georgia - country_id: "214" - id: "876916760" - abbr: GA -states_030: - name: Colorado - country_id: "214" - id: "536031023" - abbr: CO -states_019: - name: Massachusetts - country_id: "214" - id: "385551075" - abbr: MA -states_008: - name: Connecticut - country_id: "214" - id: "69870734" - abbr: CT -states_042: - name: New York - country_id: "214" - id: "889445952" - abbr: NY -states_031: - name: South Carolina - country_id: "214" - id: "597434151" - abbr: SC -states_020: - name: Alaska - country_id: "214" - id: "403740659" - abbr: AK -states_009: - name: West Virginia - country_id: "214" - id: "91367981" - abbr: WV diff --git a/core/db/default/spree/stores.rb b/core/db/default/spree/stores.rb new file mode 100644 index 00000000000..8500e212999 --- /dev/null +++ b/core/db/default/spree/stores.rb @@ -0,0 +1,9 @@ +# Possibly already created by a migration. +unless Spree::Store.where(code: 'spree').exists? + Spree::Store.new do |s| + s.code = 'spree' + s.name = 'Spree Demo Site' + s.url = 'demo.spreecommerce.com' + s.mail_from_address = 'spree@example.com' + end.save! +end \ No newline at end of file diff --git a/core/db/default/spree/zone_members.yml b/core/db/default/spree/zone_members.yml deleted file mode 100644 index a184460be39..00000000000 --- a/core/db/default/spree/zone_members.yml +++ /dev/null @@ -1,169 +0,0 @@ ---- -zone_members_019: - zoneable_id: "162" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_008: - zoneable_id: "67" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_020: - zoneable_id: "163" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_021: - zoneable_id: "167" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_010: - zoneable_id: "74" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_009: - zoneable_id: "68" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_022: - zoneable_id: "183" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_011: - zoneable_id: "90" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_023: - zoneable_id: "184" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_012: - zoneable_id: "96" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_001: - zoneable_id: "13" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_024: - zoneable_id: "188" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_013: - zoneable_id: "98" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_002: - zoneable_id: "20" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_025: - zoneable_id: "194" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_014: - zoneable_id: "110" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_003: - zoneable_id: "30" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_026: - zoneable_id: "213" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_015: - zoneable_id: "116" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_004: - zoneable_id: "51" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_016: - zoneable_id: "117" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_005: - zoneable_id: "52" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_028: - zoneable_id: "214" - created_at: 2009-06-04 17:22:41 - updated_at: 2009-06-04 17:22:41 - zone_id: "2" - zoneable_type: Spree::Country -zone_members_017: - zoneable_id: "125" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_006: - zoneable_id: "53" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_029: - zoneable_id: "35" - created_at: 2009-06-04 17:22:41 - updated_at: 2009-06-04 17:22:41 - zone_id: "2" - zoneable_type: Spree::Country -zone_members_018: - zoneable_id: "142" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country -zone_members_007: - zoneable_id: "62" - created_at: 2009-06-04 13:22:26 - updated_at: 2009-06-04 13:22:26 - zone_id: "1" - zoneable_type: Spree::Country diff --git a/core/db/default/spree/zones.rb b/core/db/default/spree/zones.rb new file mode 100644 index 00000000000..52f0c2efacd --- /dev/null +++ b/core/db/default/spree/zones.rb @@ -0,0 +1,17 @@ +eu_vat = Spree::Zone.create!(name: "EU_VAT", description: "Countries that make up the EU VAT zone.") +north_america = Spree::Zone.create!(name: "North America", description: "USA + Canada") + +["Poland", "Finland", "Portugal", "Romania", "Germany", "France", + "Slovakia", "Hungary", "Slovenia", "Ireland", "Austria", "Spain", + "Italy", "Belgium", "Sweden", "Latvia", "Bulgaria", "United Kingdom", + "Lithuania", "Cyprus", "Luxembourg", "Malta", "Denmark", "Netherlands", + "Estonia"]. +each do |name| + eu_vat.zone_members.create!(zoneable: Spree::Country.find_by!(name: name)) +end + +["United States", "Canada"].each do |name| + north_america.zone_members.create!(zoneable: Spree::Country.find_by!(name: name)) +end + + diff --git a/core/db/default/spree/zones.yml b/core/db/default/spree/zones.yml deleted file mode 100644 index ed771d96351..00000000000 --- a/core/db/default/spree/zones.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -zones_001: - name: EU_VAT - created_at: 2009-06-04 17:22:26 - updated_at: 2009-06-04 17:22:26 - id: "1" - description: Countries that make up the EU VAT zone. -zones_002: - name: North America - created_at: 2009-06-04 17:22:41 - updated_at: 2009-06-04 17:22:41 - id: "2" - description: USA + Canada diff --git a/promo/db/migrate/20120831092359_spree_promo_one_two.rb b/core/db/migrate/20120831092359_spree_promo_one_two.rb similarity index 100% rename from promo/db/migrate/20120831092359_spree_promo_one_two.rb rename to core/db/migrate/20120831092359_spree_promo_one_two.rb diff --git a/core/db/migrate/20121031162139_split_prices_from_variants.rb b/core/db/migrate/20121031162139_split_prices_from_variants.rb index 538c2c683a8..27aa83fa8e5 100644 --- a/core/db/migrate/20121031162139_split_prices_from_variants.rb +++ b/core/db/migrate/20121031162139_split_prices_from_variants.rb @@ -9,7 +9,7 @@ def up Spree::Variant.all.each do |variant| Spree::Price.create!( :variant_id => variant.id, - :amount => variant.price, + :amount => variant[:price], :currency => Spree::Config[:currency] ) end @@ -18,13 +18,14 @@ def up end def down - add_column :spree_variants, :price, :decimal, :after => :sku, :scale => 8, :precision => 2 + prices = ActiveRecord::Base.connection.execute("select variant_id, amount from spree_prices") + add_column :spree_variants, :price, :decimal, :after => :sku, :scale => 2, :precision => 8 - Spree::Variant.all.each do |variant| - variant.price = variant.default_price.amount - variant.save! + prices.each do |price| + ActiveRecord::Base.connection.execute("update spree_variants set price = #{price['amount']} where id = #{price['variant_id']}") end - + + change_column :spree_variants, :price, :decimal, :after => :sku, :scale => 2, :precision => 8, :null => false drop_table :spree_prices end end diff --git a/core/db/migrate/20121126040517_add_last_ip_to_spree_orders.rb b/core/db/migrate/20121126040517_add_last_ip_to_spree_orders.rb new file mode 100644 index 00000000000..662f231069d --- /dev/null +++ b/core/db/migrate/20121126040517_add_last_ip_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddLastIpToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :last_ip_address, :string + end +end diff --git a/core/db/migrate/20121213162028_add_state_to_spree_adjustments.rb b/core/db/migrate/20121213162028_add_state_to_spree_adjustments.rb new file mode 100644 index 00000000000..f06a77c7414 --- /dev/null +++ b/core/db/migrate/20121213162028_add_state_to_spree_adjustments.rb @@ -0,0 +1,6 @@ +class AddStateToSpreeAdjustments < ActiveRecord::Migration + def change + add_column :spree_adjustments, :state, :string + remove_column :spree_adjustments, :locked + end +end diff --git a/core/db/migrate/20130114053446_add_display_on_to_spree_payment_methods.rb b/core/db/migrate/20130114053446_add_display_on_to_spree_payment_methods.rb new file mode 100644 index 00000000000..f89d725fbf8 --- /dev/null +++ b/core/db/migrate/20130114053446_add_display_on_to_spree_payment_methods.rb @@ -0,0 +1,9 @@ +class AddDisplayOnToSpreePaymentMethods < ActiveRecord::Migration + def self.up + add_column :spree_payment_methods, :display_on, :string + end + + def self.down + remove_column :spree_payment_methods, :display_on + end +end diff --git a/core/db/migrate/20130120201805_add_position_to_product_properties.spree.rb b/core/db/migrate/20130120201805_add_position_to_product_properties.spree.rb new file mode 100644 index 00000000000..05bc1270190 --- /dev/null +++ b/core/db/migrate/20130120201805_add_position_to_product_properties.spree.rb @@ -0,0 +1,6 @@ +class AddPositionToProductProperties < ActiveRecord::Migration + def change + add_column :spree_product_properties, :position, :integer, :default => 0 + end +end + diff --git a/core/db/migrate/20130203232234_add_identifier_to_spree_payments.rb b/core/db/migrate/20130203232234_add_identifier_to_spree_payments.rb new file mode 100644 index 00000000000..224043ed0fa --- /dev/null +++ b/core/db/migrate/20130203232234_add_identifier_to_spree_payments.rb @@ -0,0 +1,5 @@ +class AddIdentifierToSpreePayments < ActiveRecord::Migration + def change + add_column :spree_payments, :identifier, :string + end +end diff --git a/core/db/migrate/20130207155350_add_order_id_index_to_payments.rb b/core/db/migrate/20130207155350_add_order_id_index_to_payments.rb new file mode 100644 index 00000000000..a77f572b024 --- /dev/null +++ b/core/db/migrate/20130207155350_add_order_id_index_to_payments.rb @@ -0,0 +1,9 @@ +class AddOrderIdIndexToPayments < ActiveRecord::Migration + def self.up + add_index :spree_payments, :order_id + end + + def self.down + remove_index :spree_payments, :order_id + end +end diff --git a/core/db/migrate/20130208032954_add_primary_to_spree_products_taxons.rb b/core/db/migrate/20130208032954_add_primary_to_spree_products_taxons.rb new file mode 100644 index 00000000000..016e1d1553c --- /dev/null +++ b/core/db/migrate/20130208032954_add_primary_to_spree_products_taxons.rb @@ -0,0 +1,5 @@ +class AddPrimaryToSpreeProductsTaxons < ActiveRecord::Migration + def change + add_column :spree_products_taxons, :id, :primary_key + end +end diff --git a/core/db/migrate/20130211190146_create_spree_stock_items.rb b/core/db/migrate/20130211190146_create_spree_stock_items.rb new file mode 100644 index 00000000000..c0f4c5e7a5e --- /dev/null +++ b/core/db/migrate/20130211190146_create_spree_stock_items.rb @@ -0,0 +1,14 @@ +class CreateSpreeStockItems < ActiveRecord::Migration + def change + create_table :spree_stock_items do |t| + t.belongs_to :stock_location + t.belongs_to :variant + t.integer :count_on_hand, null: false, default: 0 + t.integer :lock_version + + t.timestamps + end + add_index :spree_stock_items, :stock_location_id + add_index :spree_stock_items, [:stock_location_id, :variant_id], :name => 'stock_item_by_loc_and_var_id' + end +end diff --git a/core/db/migrate/20130211191120_create_spree_stock_locations.rb b/core/db/migrate/20130211191120_create_spree_stock_locations.rb new file mode 100644 index 00000000000..345e5350751 --- /dev/null +++ b/core/db/migrate/20130211191120_create_spree_stock_locations.rb @@ -0,0 +1,11 @@ +class CreateSpreeStockLocations < ActiveRecord::Migration + def change + create_table :spree_stock_locations do |t| + t.string :name + t.belongs_to :address + + t.timestamps + end + add_index :spree_stock_locations, :address_id + end +end diff --git a/core/db/migrate/20130213191427_create_default_stock.rb b/core/db/migrate/20130213191427_create_default_stock.rb new file mode 100644 index 00000000000..8369edcca62 --- /dev/null +++ b/core/db/migrate/20130213191427_create_default_stock.rb @@ -0,0 +1,34 @@ +class CreateDefaultStock < ActiveRecord::Migration + def up + unless column_exists? :spree_stock_locations, :default + add_column :spree_stock_locations, :default, :boolean, null: false, default: false + end + + Spree::StockLocation.skip_callback(:create, :after, :create_stock_items) + Spree::StockLocation.skip_callback(:save, :after, :ensure_one_default) + Spree::StockItem.skip_callback(:save, :after, :process_backorders) + location = Spree::StockLocation.new(name: 'default') + location.save(validate: false) + + Spree::Variant.find_each do |variant| + stock_item = Spree::StockItem.unscoped.build(stock_location: location, variant: variant) + stock_item.send(:count_on_hand=, variant.count_on_hand) + # Avoid running default_scope defined by acts_as_paranoid, related to #3805, + # validations would run a query with a delete_at column that might not be present yet + stock_item.save! validate: false + end + + remove_column :spree_variants, :count_on_hand + end + + def down + add_column :spree_variants, :count_on_hand, :integer + + Spree::StockItem.find_each do |stock_item| + stock_item.variant.update_column :count_on_hand, stock_item.count_on_hand + end + + Spree::StockLocation.delete_all + Spree::StockItem.delete_all + end +end diff --git a/core/db/migrate/20130222032153_add_order_id_index_to_shipments.rb b/core/db/migrate/20130222032153_add_order_id_index_to_shipments.rb new file mode 100644 index 00000000000..d7788d3313e --- /dev/null +++ b/core/db/migrate/20130222032153_add_order_id_index_to_shipments.rb @@ -0,0 +1,5 @@ +class AddOrderIdIndexToShipments < ActiveRecord::Migration + def change + add_index :spree_shipments, :order_id + end +end diff --git a/core/db/migrate/20130226032817_change_meta_description_on_spree_products_to_text.rb b/core/db/migrate/20130226032817_change_meta_description_on_spree_products_to_text.rb new file mode 100644 index 00000000000..a362deb32ff --- /dev/null +++ b/core/db/migrate/20130226032817_change_meta_description_on_spree_products_to_text.rb @@ -0,0 +1,5 @@ +class ChangeMetaDescriptionOnSpreeProductsToText < ActiveRecord::Migration + def change + change_column :spree_products, :meta_description, :text, :limit => nil + end +end diff --git a/core/db/migrate/20130226191231_add_stock_location_id_to_spree_shipments.rb b/core/db/migrate/20130226191231_add_stock_location_id_to_spree_shipments.rb new file mode 100644 index 00000000000..e744775ed7a --- /dev/null +++ b/core/db/migrate/20130226191231_add_stock_location_id_to_spree_shipments.rb @@ -0,0 +1,5 @@ +class AddStockLocationIdToSpreeShipments < ActiveRecord::Migration + def change + add_column :spree_shipments, :stock_location_id, :integer + end +end diff --git a/core/db/migrate/20130227143905_add_pending_to_inventory_unit.rb b/core/db/migrate/20130227143905_add_pending_to_inventory_unit.rb new file mode 100644 index 00000000000..ea17f507a4a --- /dev/null +++ b/core/db/migrate/20130227143905_add_pending_to_inventory_unit.rb @@ -0,0 +1,6 @@ +class AddPendingToInventoryUnit < ActiveRecord::Migration + def change + add_column :spree_inventory_units, :pending, :boolean, :default => true + Spree::InventoryUnit.update_all(:pending => false) + end +end diff --git a/core/db/migrate/20130228164411_remove_on_demand_from_product_and_variant.rb b/core/db/migrate/20130228164411_remove_on_demand_from_product_and_variant.rb new file mode 100644 index 00000000000..a3d1300b22e --- /dev/null +++ b/core/db/migrate/20130228164411_remove_on_demand_from_product_and_variant.rb @@ -0,0 +1,6 @@ +class RemoveOnDemandFromProductAndVariant < ActiveRecord::Migration + def change + remove_column :spree_products, :on_demand + remove_column :spree_variants, :on_demand + end +end diff --git a/core/db/migrate/20130228210442_create_shipping_method_zone.rb b/core/db/migrate/20130228210442_create_shipping_method_zone.rb new file mode 100644 index 00000000000..7f20bd4718b --- /dev/null +++ b/core/db/migrate/20130228210442_create_shipping_method_zone.rb @@ -0,0 +1,22 @@ +class CreateShippingMethodZone < ActiveRecord::Migration + def up + create_table :shipping_methods_zones, :id => false do |t| + t.integer :shipping_method_id + t.integer :zone_id + end + # This association has been corrected in a latter migration + # but when this database migration runs, the table is still incorrectly named + # 'shipping_methods_zones' instead of 'spre_shipping_methods_zones' + Spree::ShippingMethod.has_and_belongs_to_many :zones, :join_table => 'shipping_methods_zones', + :class_name => 'Spree::Zone', + :foreign_key => 'shipping_method_id' + Spree::ShippingMethod.all.each{|sm| sm.zones << Spree::Zone.find(sm.zone_id)} + + remove_column :spree_shipping_methods, :zone_id + end + + def down + drop_table :shipping_methods_zones + add_column :spree_shipping_methods, :zone_id, :integer + end +end diff --git a/core/db/migrate/20130301162745_remove_shipping_category_id_from_shipping_method.rb b/core/db/migrate/20130301162745_remove_shipping_category_id_from_shipping_method.rb new file mode 100644 index 00000000000..3b0e8195944 --- /dev/null +++ b/core/db/migrate/20130301162745_remove_shipping_category_id_from_shipping_method.rb @@ -0,0 +1,5 @@ +class RemoveShippingCategoryIdFromShippingMethod < ActiveRecord::Migration + def change + remove_column :spree_shipping_methods, :shipping_category_id + end +end diff --git a/core/db/migrate/20130301162924_create_shipping_method_categories.rb b/core/db/migrate/20130301162924_create_shipping_method_categories.rb new file mode 100644 index 00000000000..72b70b8ceea --- /dev/null +++ b/core/db/migrate/20130301162924_create_shipping_method_categories.rb @@ -0,0 +1,13 @@ +class CreateShippingMethodCategories < ActiveRecord::Migration + def change + create_table :spree_shipping_method_categories do |t| + t.integer :shipping_method_id, :null => false + t.integer :shipping_category_id, :null => false + + t.timestamps + end + + add_index :spree_shipping_method_categories, :shipping_method_id + add_index :spree_shipping_method_categories, :shipping_category_id + end +end diff --git a/core/db/migrate/20130301205200_add_tracking_url_to_spree_shipping_methods.rb b/core/db/migrate/20130301205200_add_tracking_url_to_spree_shipping_methods.rb new file mode 100644 index 00000000000..1206e383656 --- /dev/null +++ b/core/db/migrate/20130301205200_add_tracking_url_to_spree_shipping_methods.rb @@ -0,0 +1,5 @@ +class AddTrackingUrlToSpreeShippingMethods < ActiveRecord::Migration + def change + add_column :spree_shipping_methods, :tracking_url, :string + end +end diff --git a/core/db/migrate/20130304162240_create_spree_shipping_rates.rb b/core/db/migrate/20130304162240_create_spree_shipping_rates.rb new file mode 100644 index 00000000000..34aa0d7ac6d --- /dev/null +++ b/core/db/migrate/20130304162240_create_spree_shipping_rates.rb @@ -0,0 +1,24 @@ +class CreateSpreeShippingRates < ActiveRecord::Migration + def up + create_table :spree_shipping_rates do |t| + t.belongs_to :shipment + t.belongs_to :shipping_method + t.boolean :selected, :default => false + t.decimal :cost, :precision => 8, :scale => 2 + t.timestamps + end + add_index(:spree_shipping_rates, [:shipment_id, :shipping_method_id], + :name => 'spree_shipping_rates_join_index', + :unique => true) + + # Spree::Shipment.all.each do |shipment| + # shipping_method = Spree::ShippingMethod.find(shipment.shipment_method_id) + # shipment.add_shipping_method(shipping_method, true) + # end + end + + def down + # add_column :spree_shipments, :shipping_method_id, :integer + drop_table :spree_shipping_rates + end +end diff --git a/core/db/migrate/20130304192936_remove_category_match_attributes_from_shipping_method.rb b/core/db/migrate/20130304192936_remove_category_match_attributes_from_shipping_method.rb new file mode 100644 index 00000000000..3950ddc726a --- /dev/null +++ b/core/db/migrate/20130304192936_remove_category_match_attributes_from_shipping_method.rb @@ -0,0 +1,7 @@ +class RemoveCategoryMatchAttributesFromShippingMethod < ActiveRecord::Migration + def change + remove_column :spree_shipping_methods, :match_none + remove_column :spree_shipping_methods, :match_one + remove_column :spree_shipping_methods, :match_all + end +end diff --git a/core/db/migrate/20130305143310_create_stock_movements.rb b/core/db/migrate/20130305143310_create_stock_movements.rb new file mode 100644 index 00000000000..32df4cdf28f --- /dev/null +++ b/core/db/migrate/20130305143310_create_stock_movements.rb @@ -0,0 +1,12 @@ +class CreateStockMovements < ActiveRecord::Migration + def change + create_table :spree_stock_movements do |t| + t.belongs_to :stock_item + t.integer :quantity + t.string :action + + t.timestamps + end + add_index :spree_stock_movements, :stock_item_id + end +end diff --git a/core/db/migrate/20130306181701_add_address_fields_to_stock_location.rb b/core/db/migrate/20130306181701_add_address_fields_to_stock_location.rb new file mode 100644 index 00000000000..68974817a46 --- /dev/null +++ b/core/db/migrate/20130306181701_add_address_fields_to_stock_location.rb @@ -0,0 +1,22 @@ +class AddAddressFieldsToStockLocation < ActiveRecord::Migration + def change + remove_column :spree_stock_locations, :address_id + + add_column :spree_stock_locations, :address1, :string + add_column :spree_stock_locations, :address2, :string + add_column :spree_stock_locations, :city, :string + add_column :spree_stock_locations, :state_id, :integer + add_column :spree_stock_locations, :state_name, :string + add_column :spree_stock_locations, :country_id, :integer + add_column :spree_stock_locations, :zipcode, :string + add_column :spree_stock_locations, :phone, :string + + + usa = Spree::Country.where(:iso => 'US').first + # In case USA isn't found. + # See #3115 + country = usa || Spree::Country.first + Spree::Country.reset_column_information + Spree::StockLocation.update_all(:country_id => country) + end +end diff --git a/core/db/migrate/20130306191917_add_active_field_to_stock_locations.rb b/core/db/migrate/20130306191917_add_active_field_to_stock_locations.rb new file mode 100644 index 00000000000..a5dbd91113b --- /dev/null +++ b/core/db/migrate/20130306191917_add_active_field_to_stock_locations.rb @@ -0,0 +1,5 @@ +class AddActiveFieldToStockLocations < ActiveRecord::Migration + def change + add_column :spree_stock_locations, :active, :boolean, :default => true + end +end diff --git a/core/db/migrate/20130306195650_add_backorderable_to_stock_item.rb b/core/db/migrate/20130306195650_add_backorderable_to_stock_item.rb new file mode 100644 index 00000000000..5465ec80254 --- /dev/null +++ b/core/db/migrate/20130306195650_add_backorderable_to_stock_item.rb @@ -0,0 +1,5 @@ +class AddBackorderableToStockItem < ActiveRecord::Migration + def change + add_column :spree_stock_items, :backorderable, :boolean, :default => true + end +end diff --git a/core/db/migrate/20130307161754_add_default_quantity_to_stock_movement.rb b/core/db/migrate/20130307161754_add_default_quantity_to_stock_movement.rb new file mode 100644 index 00000000000..0c6c73ad00a --- /dev/null +++ b/core/db/migrate/20130307161754_add_default_quantity_to_stock_movement.rb @@ -0,0 +1,5 @@ +class AddDefaultQuantityToStockMovement < ActiveRecord::Migration + def change + change_column :spree_stock_movements, :quantity, :integer, :default => 0 + end +end diff --git a/core/db/migrate/20130318151756_add_source_and_destination_to_stock_movements.rb b/core/db/migrate/20130318151756_add_source_and_destination_to_stock_movements.rb new file mode 100644 index 00000000000..49bc67b550c --- /dev/null +++ b/core/db/migrate/20130318151756_add_source_and_destination_to_stock_movements.rb @@ -0,0 +1,8 @@ +class AddSourceAndDestinationToStockMovements < ActiveRecord::Migration + def change + change_table :spree_stock_movements do |t| + t.references :source, polymorphic: true + t.references :destination, polymorphic: true + end + end +end diff --git a/core/db/migrate/20130319062004_change_orders_total_precision.rb b/core/db/migrate/20130319062004_change_orders_total_precision.rb new file mode 100644 index 00000000000..b98b7b1d1b5 --- /dev/null +++ b/core/db/migrate/20130319062004_change_orders_total_precision.rb @@ -0,0 +1,8 @@ +class ChangeOrdersTotalPrecision < ActiveRecord::Migration + def change + change_column :spree_orders, :item_total, :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false + change_column :spree_orders, :total, :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false + change_column :spree_orders, :adjustment_total, :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false + change_column :spree_orders, :payment_total, :decimal, :precision => 10, :scale => 2, :default => 0.0 + end +end diff --git a/core/db/migrate/20130319063911_change_spree_payments_amount_precision.rb b/core/db/migrate/20130319063911_change_spree_payments_amount_precision.rb new file mode 100644 index 00000000000..a643c59d0fb --- /dev/null +++ b/core/db/migrate/20130319063911_change_spree_payments_amount_precision.rb @@ -0,0 +1,7 @@ +class ChangeSpreePaymentsAmountPrecision < ActiveRecord::Migration + def change + + change_column :spree_payments, :amount, :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false + + end +end diff --git a/core/db/migrate/20130319064308_change_spree_return_authorization_amount_precision.rb b/core/db/migrate/20130319064308_change_spree_return_authorization_amount_precision.rb new file mode 100644 index 00000000000..450640b4feb --- /dev/null +++ b/core/db/migrate/20130319064308_change_spree_return_authorization_amount_precision.rb @@ -0,0 +1,7 @@ +class ChangeSpreeReturnAuthorizationAmountPrecision < ActiveRecord::Migration + def change + + change_column :spree_return_authorizations, :amount, :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false + + end +end diff --git a/core/db/migrate/20130319082943_change_adjustments_amount_precision.rb b/core/db/migrate/20130319082943_change_adjustments_amount_precision.rb new file mode 100644 index 00000000000..534a9720715 --- /dev/null +++ b/core/db/migrate/20130319082943_change_adjustments_amount_precision.rb @@ -0,0 +1,7 @@ +class ChangeAdjustmentsAmountPrecision < ActiveRecord::Migration + def change + + change_column :spree_adjustments, :amount, :decimal, :precision => 10, :scale => 2 + + end +end diff --git a/core/db/migrate/20130319183250_add_originator_to_stock_movement.rb b/core/db/migrate/20130319183250_add_originator_to_stock_movement.rb new file mode 100644 index 00000000000..c5b6798dc5d --- /dev/null +++ b/core/db/migrate/20130319183250_add_originator_to_stock_movement.rb @@ -0,0 +1,7 @@ +class AddOriginatorToStockMovement < ActiveRecord::Migration + def change + change_table :spree_stock_movements do |t| + t.references :originator, polymorphic: true + end + end +end diff --git a/core/db/migrate/20130319190507_drop_source_and_destination_from_stock_movement.rb b/core/db/migrate/20130319190507_drop_source_and_destination_from_stock_movement.rb new file mode 100644 index 00000000000..3b316e32e59 --- /dev/null +++ b/core/db/migrate/20130319190507_drop_source_and_destination_from_stock_movement.rb @@ -0,0 +1,15 @@ +class DropSourceAndDestinationFromStockMovement < ActiveRecord::Migration + def up + change_table :spree_stock_movements do |t| + t.remove_references :source, :polymorphic => true + t.remove_references :destination, :polymorphic => true + end + end + + def down + change_table :spree_stock_movements do |t| + t.references :source, polymorphic: true + t.references :destination, polymorphic: true + end + end +end diff --git a/core/db/migrate/20130325163316_migrate_inventory_unit_sold_to_on_hand.rb b/core/db/migrate/20130325163316_migrate_inventory_unit_sold_to_on_hand.rb new file mode 100644 index 00000000000..6325ca0134b --- /dev/null +++ b/core/db/migrate/20130325163316_migrate_inventory_unit_sold_to_on_hand.rb @@ -0,0 +1,9 @@ +class MigrateInventoryUnitSoldToOnHand < ActiveRecord::Migration + def up + Spree::InventoryUnit.where(:state => 'sold').update_all(:state => 'on_hand') + end + + def down + Spree::InventoryUnit.where(:state => 'on_hand').update_all(:state => 'sold') + end +end diff --git a/core/db/migrate/20130326175857_add_stock_location_to_rma.rb b/core/db/migrate/20130326175857_add_stock_location_to_rma.rb new file mode 100644 index 00000000000..700b354e506 --- /dev/null +++ b/core/db/migrate/20130326175857_add_stock_location_to_rma.rb @@ -0,0 +1,5 @@ +class AddStockLocationToRma < ActiveRecord::Migration + def change + add_column :spree_return_authorizations, :stock_location_id, :integer + end +end diff --git a/core/db/migrate/20130328130308_update_shipment_state_for_canceled_orders.rb b/core/db/migrate/20130328130308_update_shipment_state_for_canceled_orders.rb new file mode 100644 index 00000000000..282fc619542 --- /dev/null +++ b/core/db/migrate/20130328130308_update_shipment_state_for_canceled_orders.rb @@ -0,0 +1,15 @@ +class UpdateShipmentStateForCanceledOrders < ActiveRecord::Migration + def up + shipments = Spree::Shipment.joins(:order). + where("spree_orders.state = 'canceled'") + case Spree::Shipment.connection.adapter_name + when "SQLite3" + shipments.update_all("state = 'cancelled'") + when "MySQL" || "PostgreSQL" + shipments.update_all("spree_shipments.state = 'cancelled'") + end + end + + def down + end +end diff --git a/core/db/migrate/20130328195253_add_seo_metas_to_taxons.rb b/core/db/migrate/20130328195253_add_seo_metas_to_taxons.rb new file mode 100644 index 00000000000..fa27170d4a7 --- /dev/null +++ b/core/db/migrate/20130328195253_add_seo_metas_to_taxons.rb @@ -0,0 +1,9 @@ +class AddSeoMetasToTaxons < ActiveRecord::Migration + def change + change_table :spree_taxons do |t| + t.string :meta_title + t.string :meta_description + t.string :meta_keywords + end + end +end diff --git a/core/db/migrate/20130329134939_remove_stock_item_and_variant_lock.rb b/core/db/migrate/20130329134939_remove_stock_item_and_variant_lock.rb new file mode 100644 index 00000000000..932e9f7c4d9 --- /dev/null +++ b/core/db/migrate/20130329134939_remove_stock_item_and_variant_lock.rb @@ -0,0 +1,14 @@ +class RemoveStockItemAndVariantLock < ActiveRecord::Migration + def up + # we are moving to pessimistic locking on stock_items + remove_column :spree_stock_items, :lock_version + + # variants no longer manage their count_on_hand so we are removing their lock + remove_column :spree_variants, :lock_version + end + + def down + add_column :spree_stock_items, :lock_version, :integer + add_column :spree_variants, :lock_version, :integer + end +end diff --git a/core/db/migrate/20130413230529_add_name_to_spree_credit_cards.rb b/core/db/migrate/20130413230529_add_name_to_spree_credit_cards.rb new file mode 100644 index 00000000000..79fe404aa79 --- /dev/null +++ b/core/db/migrate/20130413230529_add_name_to_spree_credit_cards.rb @@ -0,0 +1,5 @@ +class AddNameToSpreeCreditCards < ActiveRecord::Migration + def change + add_column :spree_credit_cards, :name, :string + end +end diff --git a/core/db/migrate/20130414000512_update_name_fields_on_spree_credit_cards.rb b/core/db/migrate/20130414000512_update_name_fields_on_spree_credit_cards.rb new file mode 100644 index 00000000000..18f6596f5de --- /dev/null +++ b/core/db/migrate/20130414000512_update_name_fields_on_spree_credit_cards.rb @@ -0,0 +1,13 @@ +class UpdateNameFieldsOnSpreeCreditCards < ActiveRecord::Migration + def up + if ActiveRecord::Base.connection.adapter_name.downcase.include? "mysql" + execute "UPDATE spree_credit_cards SET name = CONCAT(first_name, ' ', last_name)" + else + execute "UPDATE spree_credit_cards SET name = first_name || ' ' || last_name" + end + end + + def down + execute "UPDATE spree_credit_cards SET name = NULL" + end +end diff --git a/core/db/migrate/20130417120034_add_index_to_source_columns_on_adjustments.rb b/core/db/migrate/20130417120034_add_index_to_source_columns_on_adjustments.rb new file mode 100644 index 00000000000..2b9dadd0be9 --- /dev/null +++ b/core/db/migrate/20130417120034_add_index_to_source_columns_on_adjustments.rb @@ -0,0 +1,5 @@ +class AddIndexToSourceColumnsOnAdjustments < ActiveRecord::Migration + def change + add_index :spree_adjustments, [:source_type, :source_id] + end +end diff --git a/core/db/migrate/20130417120035_update_adjustment_states.rb b/core/db/migrate/20130417120035_update_adjustment_states.rb new file mode 100644 index 00000000000..2dae59d352c --- /dev/null +++ b/core/db/migrate/20130417120035_update_adjustment_states.rb @@ -0,0 +1,16 @@ +class UpdateAdjustmentStates < ActiveRecord::Migration + def up + Spree::Order.complete.find_each do |order| + order.adjustments.update_all(:state => 'closed') + end + + Spree::Shipment.shipped.includes(:adjustment).find_each do |shipment| + shipment.adjustment.update_column(:state, 'finalized') if shipment.adjustment + end + + Spree::Adjustment.where(:state => nil).update_all(:state => 'open') + end + + def down + end +end diff --git a/core/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb b/core/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb new file mode 100644 index 00000000000..8e1c187a58f --- /dev/null +++ b/core/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb @@ -0,0 +1,15 @@ +class AddShippingRatesToShipments < ActiveRecord::Migration + def up + Spree::Shipment.find_each do |shipment| + shipment.shipping_rates.create(:shipping_method_id => shipment.shipping_method_id, + :cost => shipment.cost, + :selected => true) + end + + remove_column :spree_shipments, :shipping_method_id + end + + def down + add_column :spree_shipments, :shipping_method_id, :integer + end +end diff --git a/core/db/migrate/20130418125341_create_spree_stock_transfers.rb b/core/db/migrate/20130418125341_create_spree_stock_transfers.rb new file mode 100644 index 00000000000..9fd80891012 --- /dev/null +++ b/core/db/migrate/20130418125341_create_spree_stock_transfers.rb @@ -0,0 +1,14 @@ +class CreateSpreeStockTransfers < ActiveRecord::Migration + def change + create_table :spree_stock_transfers do |t| + t.string :type + t.string :reference_number + t.integer :source_location_id + t.integer :destination_location_id + t.timestamps + end + + add_index :spree_stock_transfers, :source_location_id + add_index :spree_stock_transfers, :destination_location_id + end +end diff --git a/core/db/migrate/20130423110707_drop_products_count_on_hand.rb b/core/db/migrate/20130423110707_drop_products_count_on_hand.rb new file mode 100644 index 00000000000..8580948c733 --- /dev/null +++ b/core/db/migrate/20130423110707_drop_products_count_on_hand.rb @@ -0,0 +1,5 @@ +class DropProductsCountOnHand < ActiveRecord::Migration + def up + remove_column :spree_products, :count_on_hand + end +end diff --git a/core/db/migrate/20130423223847_set_default_shipping_rate_cost.rb b/core/db/migrate/20130423223847_set_default_shipping_rate_cost.rb new file mode 100644 index 00000000000..8950dee1ae8 --- /dev/null +++ b/core/db/migrate/20130423223847_set_default_shipping_rate_cost.rb @@ -0,0 +1,5 @@ +class SetDefaultShippingRateCost < ActiveRecord::Migration + def change + change_column :spree_shipping_rates, :cost, :decimal, default: 0, precision: 8, scale: 2 + end +end diff --git a/core/db/migrate/20130509115210_add_number_to_stock_transfer.rb b/core/db/migrate/20130509115210_add_number_to_stock_transfer.rb new file mode 100644 index 00000000000..83f66474070 --- /dev/null +++ b/core/db/migrate/20130509115210_add_number_to_stock_transfer.rb @@ -0,0 +1,23 @@ +class AddNumberToStockTransfer < ActiveRecord::Migration + def up + remove_index :spree_stock_transfers, :source_location_id + remove_index :spree_stock_transfers, :destination_location_id + + rename_column :spree_stock_transfers, :reference_number, :reference + add_column :spree_stock_transfers, :number, :string + + Spree::StockTransfer.find_each do |transfer| + transfer.send(:generate_stock_transfer_number) + transfer.save! + end + + add_index :spree_stock_transfers, :number + add_index :spree_stock_transfers, :source_location_id + add_index :spree_stock_transfers, :destination_location_id + end + + def down + rename_column :spree_stock_transfers, :reference, :reference_number + remove_column :spree_stock_transfers, :number, :string + end +end diff --git a/core/db/migrate/20130514151929_add_sku_index_to_spree_variants.rb b/core/db/migrate/20130514151929_add_sku_index_to_spree_variants.rb new file mode 100644 index 00000000000..b028839e11c --- /dev/null +++ b/core/db/migrate/20130514151929_add_sku_index_to_spree_variants.rb @@ -0,0 +1,5 @@ +class AddSkuIndexToSpreeVariants < ActiveRecord::Migration + def change + add_index :spree_variants, :sku + end +end diff --git a/core/db/migrate/20130515180736_add_backorderable_default_to_spree_stock_location.rb b/core/db/migrate/20130515180736_add_backorderable_default_to_spree_stock_location.rb new file mode 100644 index 00000000000..37e3ee4d218 --- /dev/null +++ b/core/db/migrate/20130515180736_add_backorderable_default_to_spree_stock_location.rb @@ -0,0 +1,5 @@ +class AddBackorderableDefaultToSpreeStockLocation < ActiveRecord::Migration + def change + add_column :spree_stock_locations, :backorderable_default, :boolean, default: true + end +end diff --git a/core/db/migrate/20130516151222_add_propage_all_variants_to_spree_stock_location.rb b/core/db/migrate/20130516151222_add_propage_all_variants_to_spree_stock_location.rb new file mode 100644 index 00000000000..7ac6ecec18b --- /dev/null +++ b/core/db/migrate/20130516151222_add_propage_all_variants_to_spree_stock_location.rb @@ -0,0 +1,5 @@ +class AddPropageAllVariantsToSpreeStockLocation < ActiveRecord::Migration + def change + add_column :spree_stock_locations, :propagate_all_variants, :boolean, default: true + end +end diff --git a/core/db/migrate/20130611054351_rename_shipping_methods_zones_to_spree_shipping_methods_zones.rb b/core/db/migrate/20130611054351_rename_shipping_methods_zones_to_spree_shipping_methods_zones.rb new file mode 100644 index 00000000000..e78abca4536 --- /dev/null +++ b/core/db/migrate/20130611054351_rename_shipping_methods_zones_to_spree_shipping_methods_zones.rb @@ -0,0 +1,10 @@ +class RenameShippingMethodsZonesToSpreeShippingMethodsZones < ActiveRecord::Migration + def change + rename_table :shipping_methods_zones, :spree_shipping_methods_zones + # If Spree::ShippingMethod zones association was patched in + # CreateShippingMethodZone migrations, it needs to be patched back + Spree::ShippingMethod.has_and_belongs_to_many :zones, :join_table => 'spree_shipping_methods_zones', + :class_name => 'Spree::Zone', + :foreign_key => 'shipping_method_id' + end +end diff --git a/core/db/migrate/20130611185927_add_user_id_index_to_spree_orders.rb b/core/db/migrate/20130611185927_add_user_id_index_to_spree_orders.rb new file mode 100644 index 00000000000..cb8ef67b0e5 --- /dev/null +++ b/core/db/migrate/20130611185927_add_user_id_index_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddUserIdIndexToSpreeOrders < ActiveRecord::Migration + def change + add_index :spree_orders, :user_id + end +end diff --git a/core/db/migrate/20130618041418_add_updated_at_to_spree_countries.rb b/core/db/migrate/20130618041418_add_updated_at_to_spree_countries.rb new file mode 100644 index 00000000000..fe41413df26 --- /dev/null +++ b/core/db/migrate/20130618041418_add_updated_at_to_spree_countries.rb @@ -0,0 +1,9 @@ +class AddUpdatedAtToSpreeCountries < ActiveRecord::Migration + def up + add_column :spree_countries, :updated_at, :datetime + end + + def down + remove_column :spree_countries, :updated_at + end +end diff --git a/core/db/migrate/20130619012236_add_updated_at_to_spree_states.rb b/core/db/migrate/20130619012236_add_updated_at_to_spree_states.rb new file mode 100644 index 00000000000..1d5d971ee46 --- /dev/null +++ b/core/db/migrate/20130619012236_add_updated_at_to_spree_states.rb @@ -0,0 +1,9 @@ +class AddUpdatedAtToSpreeStates < ActiveRecord::Migration + def up + add_column :spree_states, :updated_at, :datetime + end + + def down + remove_column :spree_states, :updated_at + end +end diff --git a/core/db/migrate/20130626232741_add_cvv_result_code_and_cvv_result_message_to_spree_payments.rb b/core/db/migrate/20130626232741_add_cvv_result_code_and_cvv_result_message_to_spree_payments.rb new file mode 100644 index 00000000000..b8c2d79a680 --- /dev/null +++ b/core/db/migrate/20130626232741_add_cvv_result_code_and_cvv_result_message_to_spree_payments.rb @@ -0,0 +1,6 @@ +class AddCvvResultCodeAndCvvResultMessageToSpreePayments < ActiveRecord::Migration + def change + add_column :spree_payments, :cvv_response_code, :string + add_column :spree_payments, :cvv_response_message, :string + end +end diff --git a/core/db/migrate/20130628021056_add_unique_index_to_permalink_on_spree_products.rb b/core/db/migrate/20130628021056_add_unique_index_to_permalink_on_spree_products.rb new file mode 100644 index 00000000000..9e2a54b7426 --- /dev/null +++ b/core/db/migrate/20130628021056_add_unique_index_to_permalink_on_spree_products.rb @@ -0,0 +1,5 @@ +class AddUniqueIndexToPermalinkOnSpreeProducts < ActiveRecord::Migration + def change + add_index "spree_products", ["permalink"], :name => "permalink_idx_unique", :unique => true + end +end diff --git a/core/db/migrate/20130628022817_add_unique_index_to_orders_shipments_and_stock_transfers.rb b/core/db/migrate/20130628022817_add_unique_index_to_orders_shipments_and_stock_transfers.rb new file mode 100644 index 00000000000..2dbc6b2c13e --- /dev/null +++ b/core/db/migrate/20130628022817_add_unique_index_to_orders_shipments_and_stock_transfers.rb @@ -0,0 +1,7 @@ +class AddUniqueIndexToOrdersShipmentsAndStockTransfers < ActiveRecord::Migration + def add + add_index "spree_orders", ["number"], :name => "number_idx_unique", :unique => true + add_index "spree_shipments", ["number"], :name => "number_idx_unique", :unique => true + add_index "spree_stock_transfers", ["number"], :name => "number_idx_unique", :unique => true + end +end diff --git a/core/db/migrate/20130708052307_add_deleted_at_to_spree_tax_rates.rb b/core/db/migrate/20130708052307_add_deleted_at_to_spree_tax_rates.rb new file mode 100644 index 00000000000..607148512fe --- /dev/null +++ b/core/db/migrate/20130708052307_add_deleted_at_to_spree_tax_rates.rb @@ -0,0 +1,5 @@ +class AddDeletedAtToSpreeTaxRates < ActiveRecord::Migration + def change + add_column :spree_tax_rates, :deleted_at, :datetime + end +end diff --git a/core/db/migrate/20130711200933_remove_lock_version_from_inventory_units.rb b/core/db/migrate/20130711200933_remove_lock_version_from_inventory_units.rb new file mode 100644 index 00000000000..a4cb43a1850 --- /dev/null +++ b/core/db/migrate/20130711200933_remove_lock_version_from_inventory_units.rb @@ -0,0 +1,6 @@ +class RemoveLockVersionFromInventoryUnits < ActiveRecord::Migration + def change + # we are moving to pessimistic locking on stock_items + remove_column :spree_inventory_units, :lock_version + end +end diff --git a/core/db/migrate/20130718042445_add_cost_price_to_line_item.rb b/core/db/migrate/20130718042445_add_cost_price_to_line_item.rb new file mode 100644 index 00000000000..f67fa1a0a37 --- /dev/null +++ b/core/db/migrate/20130718042445_add_cost_price_to_line_item.rb @@ -0,0 +1,5 @@ +class AddCostPriceToLineItem < ActiveRecord::Migration + def change + add_column :spree_line_items, :cost_price, :decimal, :precision => 8, :scale => 2 + end +end diff --git a/core/db/migrate/20130718233855_set_backorderable_to_default_to_false.rb b/core/db/migrate/20130718233855_set_backorderable_to_default_to_false.rb new file mode 100644 index 00000000000..c05cd19426a --- /dev/null +++ b/core/db/migrate/20130718233855_set_backorderable_to_default_to_false.rb @@ -0,0 +1,6 @@ +class SetBackorderableToDefaultToFalse < ActiveRecord::Migration + def change + change_column :spree_stock_items, :backorderable, :boolean, :default => false + change_column :spree_stock_locations, :backorderable_default, :boolean, :default => false + end +end diff --git a/core/db/migrate/20130725031716_add_created_by_id_to_spree_orders.rb b/core/db/migrate/20130725031716_add_created_by_id_to_spree_orders.rb new file mode 100644 index 00000000000..e3277cf3493 --- /dev/null +++ b/core/db/migrate/20130725031716_add_created_by_id_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddCreatedByIdToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :created_by_id, :integer + end +end diff --git a/core/db/migrate/20130729214043_index_completed_at_on_spree_orders.rb b/core/db/migrate/20130729214043_index_completed_at_on_spree_orders.rb new file mode 100644 index 00000000000..0ecfcf35d46 --- /dev/null +++ b/core/db/migrate/20130729214043_index_completed_at_on_spree_orders.rb @@ -0,0 +1,5 @@ +class IndexCompletedAtOnSpreeOrders < ActiveRecord::Migration + def change + add_index :spree_orders, :completed_at + end +end diff --git a/core/db/migrate/20130802014537_add_tax_category_id_to_spree_line_items.rb b/core/db/migrate/20130802014537_add_tax_category_id_to_spree_line_items.rb new file mode 100644 index 00000000000..953ceeb9076 --- /dev/null +++ b/core/db/migrate/20130802014537_add_tax_category_id_to_spree_line_items.rb @@ -0,0 +1,5 @@ +class AddTaxCategoryIdToSpreeLineItems < ActiveRecord::Migration + def change + add_column :spree_line_items, :tax_category_id, :integer + end +end diff --git a/core/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb b/core/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb new file mode 100644 index 00000000000..641b19def2b --- /dev/null +++ b/core/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb @@ -0,0 +1,10 @@ +class MigrateTaxCategoriesToLineItems < ActiveRecord::Migration + def change + Spree::LineItem.find_each do |line_item| + next if line_item.variant.nil? + next if line_item.variant.product.nil? + next if line_item.product.nil? + line_item.update_column(:tax_category_id, line_item.product.tax_category_id) + end + end +end diff --git a/core/db/migrate/20130806022521_drop_spree_mail_methods.rb b/core/db/migrate/20130806022521_drop_spree_mail_methods.rb new file mode 100644 index 00000000000..7e1e9769b64 --- /dev/null +++ b/core/db/migrate/20130806022521_drop_spree_mail_methods.rb @@ -0,0 +1,12 @@ +class DropSpreeMailMethods < ActiveRecord::Migration + def up + drop_table :spree_mail_methods + end + + def down + create_table(:spree_mail_methods) do |t| + t.string :environment + t.boolean :active + end + end +end diff --git a/core/db/migrate/20130806145853_set_default_stock_location_on_shipments.rb b/core/db/migrate/20130806145853_set_default_stock_location_on_shipments.rb new file mode 100644 index 00000000000..7c149446813 --- /dev/null +++ b/core/db/migrate/20130806145853_set_default_stock_location_on_shipments.rb @@ -0,0 +1,8 @@ +class SetDefaultStockLocationOnShipments < ActiveRecord::Migration + def change + if Spree::Shipment.where('stock_location_id IS NULL').count > 0 + location = Spree::StockLocation.find_by(name: 'default') || Spree::StockLocation.first + Spree::Shipment.where('stock_location_id IS NULL').update_all(stock_location_id: location.id) + end + end +end diff --git a/core/db/migrate/20130807024301_upgrade_adjustments.rb b/core/db/migrate/20130807024301_upgrade_adjustments.rb new file mode 100644 index 00000000000..6a4a68e1bde --- /dev/null +++ b/core/db/migrate/20130807024301_upgrade_adjustments.rb @@ -0,0 +1,40 @@ +class UpgradeAdjustments < ActiveRecord::Migration + def up + # Temporarily make originator association available + Spree::Adjustment.class_eval do + belongs_to :originator, polymorphic: true + end + # Shipping adjustments are now tracked as fields on the object + Spree::Adjustment.where(:source_type => "Spree::Shipment").find_each do |adjustment| + # Account for possible invalid data + next if adjustment.source.nil? + adjustment.source.update_column(:cost, adjustment.amount) + adjustment.destroy! + end + + # Tax adjustments have their sources altered + Spree::Adjustment.where(:originator_type => "Spree::TaxRate").find_each do |adjustment| + adjustment.source_id = adjustment.originator_id + adjustment.source_type = "Spree::TaxRate" + adjustment.save! + end + + # Promotion adjustments have their source altered also + Spree::Adjustment.where(:originator_type => "Spree::PromotionAction").find_each do |adjustment| + next if adjustment.originator.nil? + adjustment.source = adjustment.originator + begin + if adjustment.source.calculator_type == "Spree::Calculator::FreeShipping" + # Previously this was a Spree::Promotion::Actions::CreateAdjustment + # And it had a calculator to work out FreeShipping + # In Spree 2.2, the "calculator" is now the action itself. + adjustment.source.becomes(Spree::Promotion::Actions::FreeShipping) + end + rescue + # Fail silently. This is primarily in instances where the calculator no longer exists + end + + adjustment.save! + end + end +end diff --git a/core/db/migrate/20130807024302_rename_adjustment_fields.rb b/core/db/migrate/20130807024302_rename_adjustment_fields.rb new file mode 100644 index 00000000000..d4eaeb2a725 --- /dev/null +++ b/core/db/migrate/20130807024302_rename_adjustment_fields.rb @@ -0,0 +1,14 @@ +class RenameAdjustmentFields < ActiveRecord::Migration + def up + remove_column :spree_adjustments, :originator_id + remove_column :spree_adjustments, :originator_type + + add_column :spree_adjustments, :order_id, :integer unless column_exists?(:spree_adjustments, :order_id) + + # This enables the Spree::Order#all_adjustments association to work correctly + Spree::Adjustment.reset_column_information + Spree::Adjustment.where(adjustable_type: "Spree::Order").find_each do |adjustment| + adjustment.update_column(:order_id, adjustment.adjustable_id) + end + end +end diff --git a/core/db/migrate/20130809164245_add_admin_name_column_to_spree_shipping_methods.rb b/core/db/migrate/20130809164245_add_admin_name_column_to_spree_shipping_methods.rb new file mode 100644 index 00000000000..35a51ed4c34 --- /dev/null +++ b/core/db/migrate/20130809164245_add_admin_name_column_to_spree_shipping_methods.rb @@ -0,0 +1,5 @@ +class AddAdminNameColumnToSpreeShippingMethods < ActiveRecord::Migration + def change + add_column :spree_shipping_methods, :admin_name, :string + end +end diff --git a/core/db/migrate/20130809164330_add_admin_name_column_to_spree_stock_locations.rb b/core/db/migrate/20130809164330_add_admin_name_column_to_spree_stock_locations.rb new file mode 100644 index 00000000000..01657867cb7 --- /dev/null +++ b/core/db/migrate/20130809164330_add_admin_name_column_to_spree_stock_locations.rb @@ -0,0 +1,5 @@ +class AddAdminNameColumnToSpreeStockLocations < ActiveRecord::Migration + def change + add_column :spree_stock_locations, :admin_name, :string + end +end diff --git a/core/db/migrate/20130813004002_add_shipment_total_to_spree_orders.rb b/core/db/migrate/20130813004002_add_shipment_total_to_spree_orders.rb new file mode 100644 index 00000000000..9cafc576367 --- /dev/null +++ b/core/db/migrate/20130813004002_add_shipment_total_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddShipmentTotalToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :shipment_total, :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false + end +end diff --git a/core/db/migrate/20130813140619_expand_order_number_size.rb b/core/db/migrate/20130813140619_expand_order_number_size.rb new file mode 100644 index 00000000000..6963289718e --- /dev/null +++ b/core/db/migrate/20130813140619_expand_order_number_size.rb @@ -0,0 +1,9 @@ +class ExpandOrderNumberSize < ActiveRecord::Migration + def up + change_column :spree_orders, :number, :string, :limit => 32 + end + + def down + change_column :spree_orders, :number, :string, :limit => 15 + end +end diff --git a/core/db/migrate/20130813232134_rename_activators_to_promotions.rb b/core/db/migrate/20130813232134_rename_activators_to_promotions.rb new file mode 100644 index 00000000000..90d62029bfa --- /dev/null +++ b/core/db/migrate/20130813232134_rename_activators_to_promotions.rb @@ -0,0 +1,5 @@ +class RenameActivatorsToPromotions < ActiveRecord::Migration + def change + rename_table :spree_activators, :spree_promotions + end +end diff --git a/core/db/migrate/20130815000406_add_adjustment_total_to_line_items.rb b/core/db/migrate/20130815000406_add_adjustment_total_to_line_items.rb new file mode 100644 index 00000000000..2c653ec38f9 --- /dev/null +++ b/core/db/migrate/20130815000406_add_adjustment_total_to_line_items.rb @@ -0,0 +1,5 @@ +class AddAdjustmentTotalToLineItems < ActiveRecord::Migration + def change + add_column :spree_line_items, :adjustment_total, :decimal, :precision => 10, :scale => 2, :default => 0.0 + end +end diff --git a/core/db/migrate/20130815024413_add_adjustment_total_to_shipments.rb b/core/db/migrate/20130815024413_add_adjustment_total_to_shipments.rb new file mode 100644 index 00000000000..7c9bc0f07ee --- /dev/null +++ b/core/db/migrate/20130815024413_add_adjustment_total_to_shipments.rb @@ -0,0 +1,5 @@ +class AddAdjustmentTotalToShipments < ActiveRecord::Migration + def change + add_column :spree_shipments, :adjustment_total, :decimal, :precision => 10, :scale => 2, :default => 0.0 + end +end diff --git a/core/db/migrate/20130826062534_add_depth_to_spree_taxons.rb b/core/db/migrate/20130826062534_add_depth_to_spree_taxons.rb new file mode 100644 index 00000000000..84d33081a42 --- /dev/null +++ b/core/db/migrate/20130826062534_add_depth_to_spree_taxons.rb @@ -0,0 +1,16 @@ +class AddDepthToSpreeTaxons < ActiveRecord::Migration + def up + if !Spree::Taxon.column_names.include?('depth') + add_column :spree_taxons, :depth, :integer + + say_with_time 'Update depth on all taxons' do + Spree::Taxon.reset_column_information + Spree::Taxon.all.each { |t| t.save } + end + end + end + + def down + remove_column :spree_taxons, :depth + end +end diff --git a/core/db/migrate/20130828234942_add_tax_total_to_line_items_shipments_and_orders.rb b/core/db/migrate/20130828234942_add_tax_total_to_line_items_shipments_and_orders.rb new file mode 100644 index 00000000000..69a7a5f65fe --- /dev/null +++ b/core/db/migrate/20130828234942_add_tax_total_to_line_items_shipments_and_orders.rb @@ -0,0 +1,8 @@ +class AddTaxTotalToLineItemsShipmentsAndOrders < ActiveRecord::Migration + def change + add_column :spree_line_items, :tax_total, :decimal, precision: 10, scale: 2, default: 0.0 + add_column :spree_shipments, :tax_total, :decimal, precision: 10, scale: 2, default: 0.0 + # This column may already be here from a 2.1.x migration + add_column :spree_orders, :tax_total, :decimal, precision: 10, scale: 2, default: 0.0 unless Spree::Order.column_names.include?("tax_total") + end +end diff --git a/core/db/migrate/20130830001033_add_shipping_category_to_shipping_methods_and_products.rb b/core/db/migrate/20130830001033_add_shipping_category_to_shipping_methods_and_products.rb new file mode 100644 index 00000000000..6dab377defe --- /dev/null +++ b/core/db/migrate/20130830001033_add_shipping_category_to_shipping_methods_and_products.rb @@ -0,0 +1,15 @@ +class AddShippingCategoryToShippingMethodsAndProducts < ActiveRecord::Migration + def up + default_category = Spree::ShippingCategory.first + default_category ||= Spree::ShippingCategory.create!(:name => "Default") + + Spree::ShippingMethod.all.each do |method| + method.shipping_categories << default_category if method.shipping_categories.blank? + end + + Spree::Product.where(shipping_category_id: nil).update_all(shipping_category_id: default_category.id) + end + + def down + end +end diff --git a/core/db/migrate/20130830001159_migrate_old_shipping_calculators.rb b/core/db/migrate/20130830001159_migrate_old_shipping_calculators.rb new file mode 100644 index 00000000000..ea53b0816c2 --- /dev/null +++ b/core/db/migrate/20130830001159_migrate_old_shipping_calculators.rb @@ -0,0 +1,19 @@ +class MigrateOldShippingCalculators < ActiveRecord::Migration + def up + Spree::ShippingMethod.all.each do |shipping_method| + old_calculator = shipping_method.calculator + next if old_calculator.class < Spree::ShippingCalculator # We don't want to mess with new shipping calculators + new_calculator = eval(old_calculator.class.name.sub("::Calculator::", "::Calculator::Shipping::")).new + new_calculator.preferences.keys.each do |pref| + # Preferences can't be read/set by name, you have to prefix preferred_ + pref_method = "preferred_#{pref}" + new_calculator.send("#{pref_method}=", old_calculator.send(pref_method)) + end + new_calculator.calculable = old_calculator.calculable + new_calculator.save + end + end + + def down + end +end diff --git a/core/db/migrate/20130903183026_add_code_to_spree_promotion_rules.rb b/core/db/migrate/20130903183026_add_code_to_spree_promotion_rules.rb new file mode 100644 index 00000000000..308a61b0169 --- /dev/null +++ b/core/db/migrate/20130903183026_add_code_to_spree_promotion_rules.rb @@ -0,0 +1,5 @@ +class AddCodeToSpreePromotionRules < ActiveRecord::Migration + def change + add_column :spree_promotion_rules, :code, :string + end +end diff --git a/core/db/migrate/20130909115621_change_states_required_for_countries.rb b/core/db/migrate/20130909115621_change_states_required_for_countries.rb new file mode 100644 index 00000000000..b887e49bf54 --- /dev/null +++ b/core/db/migrate/20130909115621_change_states_required_for_countries.rb @@ -0,0 +1,9 @@ +class ChangeStatesRequiredForCountries < ActiveRecord::Migration + def up + change_column_default :spree_countries, :states_required, false + end + + def down + change_column_default :spree_countries, :states_required, true + end +end diff --git a/core/db/migrate/20130915032339_add_deleted_at_to_spree_stock_items.rb b/core/db/migrate/20130915032339_add_deleted_at_to_spree_stock_items.rb new file mode 100644 index 00000000000..a1eceebee6f --- /dev/null +++ b/core/db/migrate/20130915032339_add_deleted_at_to_spree_stock_items.rb @@ -0,0 +1,5 @@ +class AddDeletedAtToSpreeStockItems < ActiveRecord::Migration + def change + add_column :spree_stock_items, :deleted_at, :datetime + end +end diff --git a/core/db/migrate/20130917024658_remove_promotions_event_name_field.rb b/core/db/migrate/20130917024658_remove_promotions_event_name_field.rb new file mode 100644 index 00000000000..d0f64031e70 --- /dev/null +++ b/core/db/migrate/20130917024658_remove_promotions_event_name_field.rb @@ -0,0 +1,5 @@ +class RemovePromotionsEventNameField < ActiveRecord::Migration + def change + remove_column :spree_promotions, :event_name + end +end diff --git a/core/db/migrate/20130924040529_add_promo_total_to_line_items_and_shipments_and_orders.rb b/core/db/migrate/20130924040529_add_promo_total_to_line_items_and_shipments_and_orders.rb new file mode 100644 index 00000000000..74e749462b2 --- /dev/null +++ b/core/db/migrate/20130924040529_add_promo_total_to_line_items_and_shipments_and_orders.rb @@ -0,0 +1,7 @@ +class AddPromoTotalToLineItemsAndShipmentsAndOrders < ActiveRecord::Migration + def change + add_column :spree_line_items, :promo_total, :decimal, precision: 10, scale: 2, default: 0.0 + add_column :spree_shipments, :promo_total, :decimal, precision: 10, scale: 2, default: 0.0 + add_column :spree_orders, :promo_total, :decimal, precision: 10, scale: 2, default: 0.0 + end +end diff --git a/core/db/migrate/20131001013410_remove_unused_credit_card_fields.rb b/core/db/migrate/20131001013410_remove_unused_credit_card_fields.rb new file mode 100644 index 00000000000..5dac86c5153 --- /dev/null +++ b/core/db/migrate/20131001013410_remove_unused_credit_card_fields.rb @@ -0,0 +1,16 @@ +class RemoveUnusedCreditCardFields < ActiveRecord::Migration + def up + remove_column :spree_credit_cards, :start_month if column_exists?(:spree_credit_cards, :start_month) + remove_column :spree_credit_cards, :start_year if column_exists?(:spree_credit_cards, :start_year) + remove_column :spree_credit_cards, :issue_number if column_exists?(:spree_credit_cards, :issue_number) + end + def down + add_column :spree_credit_cards, :start_month, :string + add_column :spree_credit_cards, :start_year, :string + add_column :spree_credit_cards, :issue_number, :string + end + + def column_exists?(table, column) + ActiveRecord::Base.connection.column_exists?(table, column) + end +end diff --git a/core/db/migrate/20131026154747_add_track_inventory_to_variant.rb b/core/db/migrate/20131026154747_add_track_inventory_to_variant.rb new file mode 100644 index 00000000000..c25bd4abd66 --- /dev/null +++ b/core/db/migrate/20131026154747_add_track_inventory_to_variant.rb @@ -0,0 +1,5 @@ +class AddTrackInventoryToVariant < ActiveRecord::Migration + def change + add_column :spree_variants, :track_inventory, :boolean, :default => true + end +end diff --git a/core/db/migrate/20131107132123_add_tax_category_to_variants.rb b/core/db/migrate/20131107132123_add_tax_category_to_variants.rb new file mode 100644 index 00000000000..83262e65137 --- /dev/null +++ b/core/db/migrate/20131107132123_add_tax_category_to_variants.rb @@ -0,0 +1,6 @@ +class AddTaxCategoryToVariants < ActiveRecord::Migration + def change + add_column :spree_variants, :tax_category_id, :integer + add_index :spree_variants, :tax_category_id + end +end diff --git a/core/db/migrate/20131113035136_add_channel_to_spree_orders.rb b/core/db/migrate/20131113035136_add_channel_to_spree_orders.rb new file mode 100644 index 00000000000..53d932a0ce7 --- /dev/null +++ b/core/db/migrate/20131113035136_add_channel_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddChannelToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :channel, :string, default: "spree" + end +end diff --git a/core/db/migrate/20131118043959_add_included_to_adjustments.rb b/core/db/migrate/20131118043959_add_included_to_adjustments.rb new file mode 100644 index 00000000000..8fc88d6826f --- /dev/null +++ b/core/db/migrate/20131118043959_add_included_to_adjustments.rb @@ -0,0 +1,5 @@ +class AddIncludedToAdjustments < ActiveRecord::Migration + def change + add_column :spree_adjustments, :included, :boolean, :default => false unless Spree::Adjustment.column_names.include?("included") + end +end diff --git a/core/db/migrate/20131118050234_rename_tax_total_fields.rb b/core/db/migrate/20131118050234_rename_tax_total_fields.rb new file mode 100644 index 00000000000..57f3c475ca7 --- /dev/null +++ b/core/db/migrate/20131118050234_rename_tax_total_fields.rb @@ -0,0 +1,11 @@ +class RenameTaxTotalFields < ActiveRecord::Migration + def change + rename_column :spree_line_items, :tax_total, :additional_tax_total + rename_column :spree_shipments, :tax_total, :additional_tax_total + rename_column :spree_orders, :tax_total, :additional_tax_total + + add_column :spree_line_items, :included_tax_total, :decimal, precision: 10, scale: 2, null: false, default: 0.0 + add_column :spree_shipments, :included_tax_total, :decimal, precision: 10, scale: 2, null: false, default: 0.0 + add_column :spree_orders, :included_tax_total, :decimal, precision: 10, scale: 2, null: false, default: 0.0 + end +end diff --git a/core/db/migrate/20131118183431_add_line_item_id_to_spree_inventory_units.rb b/core/db/migrate/20131118183431_add_line_item_id_to_spree_inventory_units.rb new file mode 100644 index 00000000000..478ee9318b7 --- /dev/null +++ b/core/db/migrate/20131118183431_add_line_item_id_to_spree_inventory_units.rb @@ -0,0 +1,21 @@ +class AddLineItemIdToSpreeInventoryUnits < ActiveRecord::Migration + def change + # Stores running the product-assembly extension already have a line_item_id column + unless column_exists? Spree::InventoryUnit.table_name, :line_item_id + add_column :spree_inventory_units, :line_item_id, :integer + add_index :spree_inventory_units, :line_item_id + + shipments = Spree::Shipment.includes(:inventory_units, :order) + + shipments.find_each do |shipment| + shipment.inventory_units.group_by(&:variant_id).each do |variant_id, units| + + line_item = shipment.order.line_items.find_by(variant_id: variant_id) + next unless line_item + + Spree::InventoryUnit.where(id: units.map(&:id)).update_all(line_item_id: line_item.id) + end + end + end + end +end diff --git a/core/db/migrate/20131120234456_add_updated_at_to_variants.rb b/core/db/migrate/20131120234456_add_updated_at_to_variants.rb new file mode 100644 index 00000000000..a60077897b0 --- /dev/null +++ b/core/db/migrate/20131120234456_add_updated_at_to_variants.rb @@ -0,0 +1,5 @@ +class AddUpdatedAtToVariants < ActiveRecord::Migration + def change + add_column :spree_variants, :updated_at, :datetime + end +end diff --git a/core/db/migrate/20131127001002_add_position_to_classifications.rb b/core/db/migrate/20131127001002_add_position_to_classifications.rb new file mode 100644 index 00000000000..b4b5deb6d10 --- /dev/null +++ b/core/db/migrate/20131127001002_add_position_to_classifications.rb @@ -0,0 +1,5 @@ +class AddPositionToClassifications < ActiveRecord::Migration + def change + add_column :spree_products_taxons, :position, :integer + end +end diff --git a/core/db/migrate/20131211112807_create_spree_orders_promotions.rb b/core/db/migrate/20131211112807_create_spree_orders_promotions.rb new file mode 100644 index 00000000000..2deb04d0aff --- /dev/null +++ b/core/db/migrate/20131211112807_create_spree_orders_promotions.rb @@ -0,0 +1,8 @@ +class CreateSpreeOrdersPromotions < ActiveRecord::Migration + def change + create_table :spree_orders_promotions, :id => false do |t| + t.references :order + t.references :promotion + end + end +end diff --git a/core/db/migrate/20131211192741_unique_shipping_method_categories.rb b/core/db/migrate/20131211192741_unique_shipping_method_categories.rb new file mode 100644 index 00000000000..b6dd115aa78 --- /dev/null +++ b/core/db/migrate/20131211192741_unique_shipping_method_categories.rb @@ -0,0 +1,24 @@ +class UniqueShippingMethodCategories < ActiveRecord::Migration + def change + klass = Spree::ShippingMethodCategory + columns = %w[shipping_category_id shipping_method_id] + + say "Find duplicate #{klass} records" + duplicates = klass. + select((columns + %w[COUNT(*)]).join(',')). + group(columns.join(',')). + having('COUNT(*) > 1'). + map { |row| row.attributes.slice(*columns) } + + say "Delete all but the oldest duplicate #{klass} record" + duplicates.each do |conditions| + klass.where(conditions).order(:created_at).drop(1).each(&:destroy) + end + + say "Add unique index to #{klass.table_name} for #{columns.inspect}" + add_index klass.table_name, columns, unique: true, name: 'unique_spree_shipping_method_categories' + + say "Remove redundant simple index on #{klass.table_name}" + remove_index klass.table_name, name: 'index_spree_shipping_method_categories_on_shipping_category_id' + end +end diff --git a/core/db/migrate/20131218054603_add_item_count_to_spree_orders.rb b/core/db/migrate/20131218054603_add_item_count_to_spree_orders.rb new file mode 100644 index 00000000000..92115af1965 --- /dev/null +++ b/core/db/migrate/20131218054603_add_item_count_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddItemCountToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :item_count, :integer, :default => 0 + end +end diff --git a/core/db/migrate/20140106065820_remove_value_type_from_spree_preferences.rb b/core/db/migrate/20140106065820_remove_value_type_from_spree_preferences.rb new file mode 100644 index 00000000000..895f8af985b --- /dev/null +++ b/core/db/migrate/20140106065820_remove_value_type_from_spree_preferences.rb @@ -0,0 +1,8 @@ +class RemoveValueTypeFromSpreePreferences < ActiveRecord::Migration + def up + remove_column :spree_preferences, :value_type + end + def down + raise ActiveRecord::IrreversableMigration + end +end diff --git a/core/db/migrate/20140106224208_rename_permalink_to_slug_for_products.rb b/core/db/migrate/20140106224208_rename_permalink_to_slug_for_products.rb new file mode 100644 index 00000000000..9bbb8273b1c --- /dev/null +++ b/core/db/migrate/20140106224208_rename_permalink_to_slug_for_products.rb @@ -0,0 +1,5 @@ +class RenamePermalinkToSlugForProducts < ActiveRecord::Migration + def change + rename_column :spree_products, :permalink, :slug + end +end diff --git a/core/db/migrate/20140120160805_add_index_to_variant_id_and_currency_on_prices.rb b/core/db/migrate/20140120160805_add_index_to_variant_id_and_currency_on_prices.rb new file mode 100644 index 00000000000..656fb6b44eb --- /dev/null +++ b/core/db/migrate/20140120160805_add_index_to_variant_id_and_currency_on_prices.rb @@ -0,0 +1,5 @@ +class AddIndexToVariantIdAndCurrencyOnPrices < ActiveRecord::Migration + def change + add_index :spree_prices, [:variant_id, :currency] + end +end diff --git a/core/db/migrate/20140124023232_rename_activator_id_in_rules_and_actions_to_promotion_id.rb b/core/db/migrate/20140124023232_rename_activator_id_in_rules_and_actions_to_promotion_id.rb new file mode 100644 index 00000000000..7fe31b0b002 --- /dev/null +++ b/core/db/migrate/20140124023232_rename_activator_id_in_rules_and_actions_to_promotion_id.rb @@ -0,0 +1,6 @@ +class RenameActivatorIdInRulesAndActionsToPromotionId < ActiveRecord::Migration + def change + rename_column :spree_promotion_rules, :activator_id, :promotion_id + rename_column :spree_promotion_actions, :activator_id, :promotion_id + end +end diff --git a/core/db/migrate/20140129024326_add_deleted_at_to_spree_prices.rb b/core/db/migrate/20140129024326_add_deleted_at_to_spree_prices.rb new file mode 100644 index 00000000000..949b7c11d92 --- /dev/null +++ b/core/db/migrate/20140129024326_add_deleted_at_to_spree_prices.rb @@ -0,0 +1,5 @@ +class AddDeletedAtToSpreePrices < ActiveRecord::Migration + def change + add_column :spree_prices, :deleted_at, :datetime + end +end diff --git a/core/db/migrate/20140203161722_add_approver_id_and_approved_at_to_orders.rb b/core/db/migrate/20140203161722_add_approver_id_and_approved_at_to_orders.rb new file mode 100644 index 00000000000..b22f2c3e7a0 --- /dev/null +++ b/core/db/migrate/20140203161722_add_approver_id_and_approved_at_to_orders.rb @@ -0,0 +1,6 @@ +class AddApproverIdAndApprovedAtToOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :approver_id, :integer + add_column :spree_orders, :approved_at, :datetime + end +end diff --git a/core/db/migrate/20140204115338_add_confirmation_delivered_to_spree_orders.rb b/core/db/migrate/20140204115338_add_confirmation_delivered_to_spree_orders.rb new file mode 100644 index 00000000000..243d95b57c6 --- /dev/null +++ b/core/db/migrate/20140204115338_add_confirmation_delivered_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddConfirmationDeliveredToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :confirmation_delivered, :boolean, default: false + end +end diff --git a/core/db/migrate/20140204192230_add_auto_capture_to_payment_methods.rb b/core/db/migrate/20140204192230_add_auto_capture_to_payment_methods.rb new file mode 100644 index 00000000000..bb5b912860d --- /dev/null +++ b/core/db/migrate/20140204192230_add_auto_capture_to_payment_methods.rb @@ -0,0 +1,5 @@ +class AddAutoCaptureToPaymentMethods < ActiveRecord::Migration + def change + add_column :spree_payment_methods, :auto_capture, :boolean + end +end diff --git a/core/db/migrate/20140205120320_create_spree_payment_capture_events.rb b/core/db/migrate/20140205120320_create_spree_payment_capture_events.rb new file mode 100644 index 00000000000..587de419851 --- /dev/null +++ b/core/db/migrate/20140205120320_create_spree_payment_capture_events.rb @@ -0,0 +1,12 @@ +class CreateSpreePaymentCaptureEvents < ActiveRecord::Migration + def change + create_table :spree_payment_capture_events do |t| + t.decimal :amount, precision: 10, scale: 2, default: 0.0 + t.integer :payment_id + + t.timestamps + end + + add_index :spree_payment_capture_events, :payment_id + end +end diff --git a/core/db/migrate/20140205144710_add_uncaptured_amount_to_payments.rb b/core/db/migrate/20140205144710_add_uncaptured_amount_to_payments.rb new file mode 100644 index 00000000000..9964dcf4e4d --- /dev/null +++ b/core/db/migrate/20140205144710_add_uncaptured_amount_to_payments.rb @@ -0,0 +1,5 @@ +class AddUncapturedAmountToPayments < ActiveRecord::Migration + def change + add_column :spree_payments, :uncaptured_amount, :decimal, precision: 10, scale: 2, default: 0.0 + end +end diff --git a/core/db/migrate/20140205181631_default_variant_weight_to_zero.rb b/core/db/migrate/20140205181631_default_variant_weight_to_zero.rb new file mode 100644 index 00000000000..b5710e74abb --- /dev/null +++ b/core/db/migrate/20140205181631_default_variant_weight_to_zero.rb @@ -0,0 +1,11 @@ +class DefaultVariantWeightToZero < ActiveRecord::Migration + def up + Spree::Variant.unscoped.where(weight: nil).update_all("weight = 0.0") + + change_column :spree_variants, :weight, :decimal, precision: 8, scale: 2, default: 0.0 + end + + def down + change_column :spree_variants, :weight, :decimal, precision: 8, scale: 2 + end +end diff --git a/core/db/migrate/20140207085910_add_tax_category_id_to_shipping_methods.rb b/core/db/migrate/20140207085910_add_tax_category_id_to_shipping_methods.rb new file mode 100644 index 00000000000..b330c066e86 --- /dev/null +++ b/core/db/migrate/20140207085910_add_tax_category_id_to_shipping_methods.rb @@ -0,0 +1,5 @@ +class AddTaxCategoryIdToShippingMethods < ActiveRecord::Migration + def change + add_column :spree_shipping_methods, :tax_category_id, :integer + end +end diff --git a/core/db/migrate/20140207093021_add_tax_rate_id_to_shipping_rates.rb b/core/db/migrate/20140207093021_add_tax_rate_id_to_shipping_rates.rb new file mode 100644 index 00000000000..8b584d032c0 --- /dev/null +++ b/core/db/migrate/20140207093021_add_tax_rate_id_to_shipping_rates.rb @@ -0,0 +1,5 @@ +class AddTaxRateIdToShippingRates < ActiveRecord::Migration + def change + add_column :spree_shipping_rates, :tax_rate_id, :integer + end +end diff --git a/core/db/migrate/20140211040159_add_pre_tax_amount_to_line_items_and_shipments.rb b/core/db/migrate/20140211040159_add_pre_tax_amount_to_line_items_and_shipments.rb new file mode 100644 index 00000000000..c2de36b5f21 --- /dev/null +++ b/core/db/migrate/20140211040159_add_pre_tax_amount_to_line_items_and_shipments.rb @@ -0,0 +1,6 @@ +class AddPreTaxAmountToLineItemsAndShipments < ActiveRecord::Migration + def change + add_column :spree_line_items, :pre_tax_amount, :decimal, precision: 8, scale: 2 + add_column :spree_shipments, :pre_tax_amount, :decimal, precision: 8, scale: 2 + end +end diff --git a/core/db/migrate/20140213184916_add_more_indexes.rb b/core/db/migrate/20140213184916_add_more_indexes.rb new file mode 100644 index 00000000000..e23d0d57d49 --- /dev/null +++ b/core/db/migrate/20140213184916_add_more_indexes.rb @@ -0,0 +1,13 @@ +class AddMoreIndexes < ActiveRecord::Migration + def change + add_index :spree_payment_methods, [:id, :type] + add_index :spree_calculators, [:id, :type] + add_index :spree_calculators, [:calculable_id, :calculable_type] + add_index :spree_payments, :payment_method_id + add_index :spree_promotion_actions, [:id, :type] + add_index :spree_promotion_actions, :promotion_id + add_index :spree_promotions, [:id, :type] + add_index :spree_option_values, :option_type_id + add_index :spree_shipments, :stock_location_id + end +end diff --git a/core/db/migrate/20140219060952_add_considered_risky_to_orders.rb b/core/db/migrate/20140219060952_add_considered_risky_to_orders.rb new file mode 100644 index 00000000000..7b4c4161097 --- /dev/null +++ b/core/db/migrate/20140219060952_add_considered_risky_to_orders.rb @@ -0,0 +1,5 @@ +class AddConsideredRiskyToOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :considered_risky, :boolean, :default => false + end +end diff --git a/core/db/migrate/20140227112348_add_preference_store_to_everything.rb b/core/db/migrate/20140227112348_add_preference_store_to_everything.rb new file mode 100644 index 00000000000..5381e32693c --- /dev/null +++ b/core/db/migrate/20140227112348_add_preference_store_to_everything.rb @@ -0,0 +1,8 @@ +class AddPreferenceStoreToEverything < ActiveRecord::Migration + def change + add_column :spree_calculators, :preferences, :text + add_column :spree_gateways, :preferences, :text + add_column :spree_payment_methods, :preferences, :text + add_column :spree_promotion_rules, :preferences, :text + end +end diff --git a/core/db/migrate/20140307235515_add_user_id_to_spree_credit_cards.rb b/core/db/migrate/20140307235515_add_user_id_to_spree_credit_cards.rb new file mode 100644 index 00000000000..837343be7df --- /dev/null +++ b/core/db/migrate/20140307235515_add_user_id_to_spree_credit_cards.rb @@ -0,0 +1,13 @@ +class AddUserIdToSpreeCreditCards < ActiveRecord::Migration + def change + unless Spree::CreditCard.column_names.include? "user_id" + add_column :spree_credit_cards, :user_id, :integer + add_index :spree_credit_cards, :user_id + end + + unless Spree::CreditCard.column_names.include? "payment_method_id" + add_column :spree_credit_cards, :payment_method_id, :integer + add_index :spree_credit_cards, :payment_method_id + end + end +end diff --git a/core/db/migrate/20140309023735_migrate_old_preferences.rb b/core/db/migrate/20140309023735_migrate_old_preferences.rb new file mode 100644 index 00000000000..fe3028c8f2f --- /dev/null +++ b/core/db/migrate/20140309023735_migrate_old_preferences.rb @@ -0,0 +1,23 @@ +class MigrateOldPreferences < ActiveRecord::Migration + def up + migrate_preferences(Spree::Calculator) + migrate_preferences(Spree::PaymentMethod) + migrate_preferences(Spree::PromotionRule) + end + + def down + end + + private + def migrate_preferences klass + klass.reset_column_information + klass.find_each do |record| + store = Spree::Preferences::ScopedStore.new(record.class.name.underscore, record.id) + record.defined_preferences.each do |key| + value = store.fetch(key){} + record.preferences[key] = value unless value.nil? + end + record.save! + end + end +end diff --git a/core/db/migrate/20140309024355_create_spree_stores.rb b/core/db/migrate/20140309024355_create_spree_stores.rb new file mode 100644 index 00000000000..cf1eb37b240 --- /dev/null +++ b/core/db/migrate/20140309024355_create_spree_stores.rb @@ -0,0 +1,25 @@ +class CreateSpreeStores < ActiveRecord::Migration + def change + if table_exists?(:spree_stores) + rename_column :spree_stores, :domains, :url + rename_column :spree_stores, :email, :mail_from_address + add_column :spree_stores, :meta_description, :text + add_column :spree_stores, :meta_keywords, :text + add_column :spree_stores, :seo_title, :string + else + create_table :spree_stores do |t| + t.string :name + t.string :url + t.text :meta_description + t.text :meta_keywords + t.string :seo_title + t.string :mail_from_address + t.string :default_currency + t.string :code + t.boolean :default, default: false, null: false + + t.timestamps + end + end + end +end diff --git a/core/db/migrate/20140309033438_create_store_from_preferences.rb b/core/db/migrate/20140309033438_create_store_from_preferences.rb new file mode 100644 index 00000000000..6267cc4b73d --- /dev/null +++ b/core/db/migrate/20140309033438_create_store_from_preferences.rb @@ -0,0 +1,37 @@ +class CreateStoreFromPreferences < ActiveRecord::Migration + def change + # workaround for spree_i18n and Store translations + Spree::Store.class_eval do + def self.translated?(name) + false + end + end + + preference_store = Spree::Preferences::Store.instance + if store = Spree::Store.where(default: true).first + store.meta_description = preference_store.get('spree/app_configuration/default_meta_description') {} + store.meta_keywords = preference_store.get('spree/app_configuration/default_meta_keywords') {} + store.seo_title = preference_store.get('spree/app_configuration/default_seo_title') {} + store.save! + else + # we set defaults for the things we now require + Spree::Store.new do |s| + s.name = preference_store.get 'spree/app_configuration/site_name' do + 'Spree Demo Site' + end + s.url = preference_store.get 'spree/app_configuration/site_url' do + 'demo.spreecommerce.com' + end + s.mail_from_address = preference_store.get 'spree/app_configuration/mails_from' do + 'spree@example.com' + end + + s.meta_description = preference_store.get('spree/app_configuration/default_meta_description') {} + s.meta_keywords = preference_store.get('spree/app_configuration/default_meta_keywords') {} + s.seo_title = preference_store.get('spree/app_configuration/default_seo_title') {} + s.default_currency = preference_store.get('spree/app_configuration/currency') {} + s.code = 'spree' + end.save! + end + end +end diff --git a/core/db/migrate/20140315053743_add_timestamps_to_spree_assets.rb b/core/db/migrate/20140315053743_add_timestamps_to_spree_assets.rb new file mode 100644 index 00000000000..d636eac65d6 --- /dev/null +++ b/core/db/migrate/20140315053743_add_timestamps_to_spree_assets.rb @@ -0,0 +1,6 @@ +class AddTimestampsToSpreeAssets < ActiveRecord::Migration + def change + add_column :spree_assets, :created_at, :datetime + add_column :spree_assets, :updated_at, :datetime + end +end diff --git a/core/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb b/core/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb new file mode 100644 index 00000000000..e8abb860ecb --- /dev/null +++ b/core/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb @@ -0,0 +1,8 @@ +class CreateSpreeTaxonsPromotionRules < ActiveRecord::Migration + def change + create_table :spree_taxons_promotion_rules do |t| + t.references :taxon, index: true + t.references :promotion_rule, index: true + end + end +end diff --git a/core/db/migrate/20140331100557_add_additional_store_fields.rb b/core/db/migrate/20140331100557_add_additional_store_fields.rb new file mode 100644 index 00000000000..cd9841101ef --- /dev/null +++ b/core/db/migrate/20140331100557_add_additional_store_fields.rb @@ -0,0 +1,8 @@ +class AddAdditionalStoreFields < ActiveRecord::Migration + def change + add_column :spree_stores, :code, :string unless column_exists?(:spree_stores, :code) + add_column :spree_stores, :default, :boolean, default: false, null: false unless column_exists?(:spree_stores, :default) + add_index :spree_stores, :code + add_index :spree_stores, :default + end +end diff --git a/core/db/migrate/20140410141842_add_many_missing_indexes.rb b/core/db/migrate/20140410141842_add_many_missing_indexes.rb new file mode 100644 index 00000000000..b55a7dd1981 --- /dev/null +++ b/core/db/migrate/20140410141842_add_many_missing_indexes.rb @@ -0,0 +1,18 @@ +class AddManyMissingIndexes < ActiveRecord::Migration + def change + add_index :spree_adjustments, [:adjustable_id, :adjustable_type] + add_index :spree_adjustments, :eligible + add_index :spree_adjustments, :order_id + add_index :spree_promotions, :code + add_index :spree_promotions, :expires_at + add_index :spree_states, :country_id + add_index :spree_stock_items, :deleted_at + add_index :spree_option_types, :position + add_index :spree_option_values, :position + add_index :spree_product_option_types, :option_type_id + add_index :spree_product_option_types, :product_id + add_index :spree_products_taxons, :position + add_index :spree_promotions, :starts_at + add_index :spree_stores, :url + end +end diff --git a/core/db/migrate/20140410150358_correct_some_polymorphic_index_and_add_more_missing.rb b/core/db/migrate/20140410150358_correct_some_polymorphic_index_and_add_more_missing.rb new file mode 100644 index 00000000000..06082b0c6a7 --- /dev/null +++ b/core/db/migrate/20140410150358_correct_some_polymorphic_index_and_add_more_missing.rb @@ -0,0 +1,66 @@ +class CorrectSomePolymorphicIndexAndAddMoreMissing < ActiveRecord::Migration + def change + add_index :spree_addresses, :country_id + add_index :spree_addresses, :state_id + remove_index :spree_adjustments, [:source_type, :source_id] + add_index :spree_adjustments, [:source_id, :source_type] + add_index :spree_credit_cards, :address_id + add_index :spree_gateways, :active + add_index :spree_gateways, :test_mode + add_index :spree_inventory_units, :return_authorization_id + add_index :spree_line_items, :tax_category_id + add_index :spree_log_entries, [:source_id, :source_type] + add_index :spree_orders, :approver_id + add_index :spree_orders, :bill_address_id + add_index :spree_orders, :confirmation_delivered + add_index :spree_orders, :considered_risky + add_index :spree_orders, :created_by_id + add_index :spree_orders, :ship_address_id + add_index :spree_orders, :shipping_method_id + add_index :spree_orders_promotions, [:order_id, :promotion_id] + add_index :spree_payments, [:source_id, :source_type] + add_index :spree_prices, :deleted_at + add_index :spree_product_option_types, :position + add_index :spree_product_properties, :position + add_index :spree_product_properties, :property_id + add_index :spree_products, :shipping_category_id + add_index :spree_products, :tax_category_id + add_index :spree_promotion_action_line_items, :promotion_action_id + add_index :spree_promotion_action_line_items, :variant_id + add_index :spree_promotion_rules, :promotion_id + add_index :spree_promotions, :advertise + add_index :spree_return_authorizations, :number + add_index :spree_return_authorizations, :order_id + add_index :spree_return_authorizations, :stock_location_id + add_index :spree_shipments, :address_id + add_index :spree_shipping_methods, :deleted_at + add_index :spree_shipping_methods, :tax_category_id + add_index :spree_shipping_rates, :selected + add_index :spree_shipping_rates, :tax_rate_id + add_index :spree_state_changes, [:stateful_id, :stateful_type] + add_index :spree_state_changes, :user_id + add_index :spree_stock_items, :backorderable + add_index :spree_stock_locations, :active + add_index :spree_stock_locations, :backorderable_default + add_index :spree_stock_locations, :country_id + add_index :spree_stock_locations, :propagate_all_variants + add_index :spree_stock_locations, :state_id + add_index :spree_tax_categories, :deleted_at + add_index :spree_tax_categories, :is_default + add_index :spree_tax_rates, :deleted_at + add_index :spree_tax_rates, :included_in_price + add_index :spree_tax_rates, :show_rate_in_label + add_index :spree_tax_rates, :tax_category_id + add_index :spree_tax_rates, :zone_id + add_index :spree_taxonomies, :position + add_index :spree_taxons, :position + add_index :spree_trackers, :active + add_index :spree_variants, :deleted_at + add_index :spree_variants, :is_master + add_index :spree_variants, :position + add_index :spree_variants, :track_inventory + add_index :spree_zone_members, :zone_id + add_index :spree_zone_members, [:zoneable_id, :zoneable_type] + add_index :spree_zones, :default_tax + end +end diff --git a/core/db/migrate/20140415041315_add_user_id_created_by_id_index_to_order.rb b/core/db/migrate/20140415041315_add_user_id_created_by_id_index_to_order.rb new file mode 100644 index 00000000000..e7cf4a53a7d --- /dev/null +++ b/core/db/migrate/20140415041315_add_user_id_created_by_id_index_to_order.rb @@ -0,0 +1,5 @@ +class AddUserIdCreatedByIdIndexToOrder < ActiveRecord::Migration + def change + add_index :spree_orders, [:user_id, :created_by_id] + end +end diff --git a/core/db/migrate/20140508151342_change_spree_price_amount_precision.rb b/core/db/migrate/20140508151342_change_spree_price_amount_precision.rb new file mode 100644 index 00000000000..f68e0961bb3 --- /dev/null +++ b/core/db/migrate/20140508151342_change_spree_price_amount_precision.rb @@ -0,0 +1,8 @@ +class ChangeSpreePriceAmountPrecision < ActiveRecord::Migration + def change + change_column :spree_prices, :amount, :decimal, :precision => 10, :scale => 2 + change_column :spree_line_items, :price, :decimal, :precision => 10, :scale => 2 + change_column :spree_line_items, :cost_price, :decimal, :precision => 10, :scale => 2 + change_column :spree_variants, :cost_price, :decimal, :precision => 10, :scale => 2 + end +end diff --git a/core/db/migrate/20140518174634_add_token_to_spree_orders.rb b/core/db/migrate/20140518174634_add_token_to_spree_orders.rb new file mode 100644 index 00000000000..7af7e4a0088 --- /dev/null +++ b/core/db/migrate/20140518174634_add_token_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddTokenToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :guest_token, :string + end +end diff --git a/core/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb b/core/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb new file mode 100644 index 00000000000..0e304d04c1b --- /dev/null +++ b/core/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb @@ -0,0 +1,29 @@ +class MoveOrderTokenFromTokenizedPermission < ActiveRecord::Migration + class Spree::TokenizedPermission < Spree::Base + belongs_to :permissable, polymorphic: true + end + + def up + case Spree::Order.connection.adapter_name + when 'SQLite' + Spree::Order.has_one :tokenized_permission, :as => :permissable + Spree::Order.includes(:tokenized_permission).each do |o| + o.update_column :guest_token, o.tokenized_permission.token + end + when 'Mysql2', 'MySQL' + execute "UPDATE spree_orders, spree_tokenized_permissions + SET spree_orders.guest_token = spree_tokenized_permissions.token + WHERE spree_tokenized_permissions.permissable_id = spree_orders.id + AND spree_tokenized_permissions.permissable_type = 'Spree::Order'" + else + execute "UPDATE spree_orders + SET guest_token = spree_tokenized_permissions.token + FROM spree_tokenized_permissions + WHERE spree_tokenized_permissions.permissable_id = spree_orders.id + AND spree_tokenized_permissions.permissable_type = 'Spree::Order'" + end + end + + def down + end +end diff --git a/core/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb b/core/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb new file mode 100644 index 00000000000..451b5fb41b5 --- /dev/null +++ b/core/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb @@ -0,0 +1,10 @@ +class SetShipmentTotalForUsersUpgrading < ActiveRecord::Migration + def up + # NOTE You might not need this at all unless you're upgrading from Spree 2.1.x + # or below. For those upgrading this should populate the Order#shipment_total + # for legacy orders + Spree::Order.complete.where('shipment_total = ?', 0).includes(:shipments).find_each do |order| + order.update_column(:shipment_total, order.shipments.sum(:cost)) + end + end +end diff --git a/core/db/migrate/20140604135309_drop_credit_card_first_name_and_last_name.rb b/core/db/migrate/20140604135309_drop_credit_card_first_name_and_last_name.rb new file mode 100644 index 00000000000..c77f648da53 --- /dev/null +++ b/core/db/migrate/20140604135309_drop_credit_card_first_name_and_last_name.rb @@ -0,0 +1,6 @@ +class DropCreditCardFirstNameAndLastName < ActiveRecord::Migration + def change + remove_column :spree_credit_cards, :first_name, :string + remove_column :spree_credit_cards, :last_name, :string + end +end diff --git a/core/db/migrate/20140609201656_add_deleted_at_to_spree_promotion_actions.rb b/core/db/migrate/20140609201656_add_deleted_at_to_spree_promotion_actions.rb new file mode 100644 index 00000000000..c8e9a353659 --- /dev/null +++ b/core/db/migrate/20140609201656_add_deleted_at_to_spree_promotion_actions.rb @@ -0,0 +1,6 @@ +class AddDeletedAtToSpreePromotionActions < ActiveRecord::Migration + def change + add_column :spree_promotion_actions, :deleted_at, :datetime + add_index :spree_promotion_actions, :deleted_at + end +end diff --git a/core/db/migrate/20140616202624_remove_uncaptured_amount_from_spree_payments.rb b/core/db/migrate/20140616202624_remove_uncaptured_amount_from_spree_payments.rb new file mode 100644 index 00000000000..bd73332338e --- /dev/null +++ b/core/db/migrate/20140616202624_remove_uncaptured_amount_from_spree_payments.rb @@ -0,0 +1,5 @@ +class RemoveUncapturedAmountFromSpreePayments < ActiveRecord::Migration + def change + remove_column :spree_payments, :uncaptured_amount + end +end diff --git a/core/db/migrate/20140625214618_create_spree_refunds.rb b/core/db/migrate/20140625214618_create_spree_refunds.rb new file mode 100644 index 00000000000..deec7b4bb6a --- /dev/null +++ b/core/db/migrate/20140625214618_create_spree_refunds.rb @@ -0,0 +1,12 @@ +class CreateSpreeRefunds < ActiveRecord::Migration + def change + create_table :spree_refunds do |t| + t.integer :payment_id + t.integer :return_authorization_id + t.decimal :amount, precision: 10, scale: 2, default: 0.0, null: false + t.string :transaction_id + + t.timestamps + end + end +end diff --git a/core/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb b/core/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb new file mode 100644 index 00000000000..6a4981d996f --- /dev/null +++ b/core/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb @@ -0,0 +1,12 @@ +class CreateSpreeReturnAuthorizationInventoryUnit < ActiveRecord::Migration + def change + create_table :spree_return_authorization_inventory_units do |t| + t.integer :return_authorization_id + t.integer :inventory_unit_id + t.integer :exchange_variant_id + t.datetime :received_at + + t.timestamps + end + end +end diff --git a/core/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb b/core/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb new file mode 100644 index 00000000000..2c23ab106bc --- /dev/null +++ b/core/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb @@ -0,0 +1,5 @@ +class RenameReturnAuthorizationInventoryUnitToReturnItems < ActiveRecord::Migration + def change + rename_table :spree_return_authorization_inventory_units, :spree_return_items + end +end diff --git a/core/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb b/core/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb new file mode 100644 index 00000000000..9ec679db28d --- /dev/null +++ b/core/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb @@ -0,0 +1,10 @@ +class BackfillLineItemPreTaxAmount < ActiveRecord::Migration + def change + # set pre_tax_amount to discounted_amount - included_tax_total + execute(<<-SQL) + UPDATE spree_line_items + SET pre_tax_amount = ((price * quantity) + promo_total) - included_tax_total + WHERE pre_tax_amount IS NULL; + SQL + end +end diff --git a/core/db/migrate/20140710041921_recreate_spree_return_authorizations.rb b/core/db/migrate/20140710041921_recreate_spree_return_authorizations.rb new file mode 100644 index 00000000000..ec2b98971c1 --- /dev/null +++ b/core/db/migrate/20140710041921_recreate_spree_return_authorizations.rb @@ -0,0 +1,55 @@ +class RecreateSpreeReturnAuthorizations < ActiveRecord::Migration + def up + # If the app has any legacy return authorizations then rename the table & columns and leave them there + # for the spree_legacy_return_authorizations extension to pick up with. + # Otherwise just drop the tables/columns as they are no longer used in stock spree. The spree_legacy_return_authorizations + # extension will recreate these tables for dev environments & etc as needed. + if Spree::ReturnAuthorization.exists? + rename_table :spree_return_authorizations, :spree_legacy_return_authorizations + rename_column :spree_inventory_units, :return_authorization_id, :legacy_return_authorization_id + else + drop_table :spree_return_authorizations + remove_column :spree_inventory_units, :return_authorization_id + end + + Spree::Adjustment.where(source_type: 'Spree::ReturnAuthorization').update_all(source_type: 'Spree::LegacyReturnAuthorization') + + # For now just recreate the table as it was. Future changes to the schema (including dropping "amount") will be coming in a + # separate commit. + create_table :spree_return_authorizations do |t| + t.string "number" + t.string "state" + t.decimal "amount", precision: 10, scale: 2, default: 0.0, null: false + t.integer "order_id" + t.text "reason" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "stock_location_id" + end + + end + + def down + drop_table :spree_return_authorizations + + Spree::Adjustment.where(source_type: 'Spree::LegacyReturnAuthorization').update_all(source_type: 'Spree::ReturnAuthorization') + + if table_exists?(:spree_legacy_return_authorizations) + rename_table :spree_legacy_return_authorizations, :spree_return_authorizations + rename_column :spree_inventory_units, :legacy_return_authorization_id, :return_authorization_id + else + create_table :spree_return_authorizations do |t| + t.string "number" + t.string "state" + t.decimal "amount", precision: 10, scale: 2, default: 0.0, null: false + t.integer "order_id" + t.text "reason" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "stock_location_id" + end + add_column :spree_inventory_units, :return_authorization_id, :integer, after: :shipment_id + add_index :spree_inventory_units, :return_authorization_id + end + end +end diff --git a/core/db/migrate/20140710181204_add_amount_fields_to_return_items.rb b/core/db/migrate/20140710181204_add_amount_fields_to_return_items.rb new file mode 100644 index 00000000000..6a12c8810f6 --- /dev/null +++ b/core/db/migrate/20140710181204_add_amount_fields_to_return_items.rb @@ -0,0 +1,7 @@ +class AddAmountFieldsToReturnItems < ActiveRecord::Migration + def change + add_column :spree_return_items, :pre_tax_amount, :decimal, precision: 10, scale: 2, default: 0.0, null: false + add_column :spree_return_items, :included_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false + add_column :spree_return_items, :additional_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false + end +end diff --git a/core/db/migrate/20140710190048_drop_return_authorization_amount.rb b/core/db/migrate/20140710190048_drop_return_authorization_amount.rb new file mode 100644 index 00000000000..42764c9486e --- /dev/null +++ b/core/db/migrate/20140710190048_drop_return_authorization_amount.rb @@ -0,0 +1,5 @@ +class DropReturnAuthorizationAmount < ActiveRecord::Migration + def change + remove_column :spree_return_authorizations, :amount + end +end diff --git a/core/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb b/core/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb new file mode 100644 index 00000000000..a6da1d62259 --- /dev/null +++ b/core/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb @@ -0,0 +1,28 @@ +class CreateSpreeReturnAuthorizationReasons < ActiveRecord::Migration + def change + create_table :spree_return_authorization_reasons do |t| + t.string :name + t.boolean :active, default: true + t.boolean :mutable, default: true + + t.timestamps + end + + reversible do |direction| + direction.up do + Spree::ReturnAuthorizationReason.create!(name: 'Better price available') + Spree::ReturnAuthorizationReason.create!(name: 'Missed estimated delivery date') + Spree::ReturnAuthorizationReason.create!(name: 'Missing parts or accessories') + Spree::ReturnAuthorizationReason.create!(name: 'Damaged/Defective') + Spree::ReturnAuthorizationReason.create!(name: 'Different from what was ordered') + Spree::ReturnAuthorizationReason.create!(name: 'Different from description') + Spree::ReturnAuthorizationReason.create!(name: 'No longer needed/wanted') + Spree::ReturnAuthorizationReason.create!(name: 'Accidental order') + Spree::ReturnAuthorizationReason.create!(name: 'Unauthorized purchase') + end + end + + add_column :spree_return_authorizations, :return_authorization_reason_id, :integer + add_index :spree_return_authorizations, :return_authorization_reason_id, name: 'index_return_authorizations_on_return_authorization_reason_id' + end +end diff --git a/core/db/migrate/20140713140527_create_spree_refund_reasons.rb b/core/db/migrate/20140713140527_create_spree_refund_reasons.rb new file mode 100644 index 00000000000..1d62143d381 --- /dev/null +++ b/core/db/migrate/20140713140527_create_spree_refund_reasons.rb @@ -0,0 +1,14 @@ +class CreateSpreeRefundReasons < ActiveRecord::Migration + def change + create_table :spree_refund_reasons do |t| + t.string :name + t.boolean :active, default: true + t.boolean :mutable, default: true + + t.timestamps + end + + add_column :spree_refunds, :refund_reason_id, :integer + add_index :spree_refunds, :refund_reason_id, name: 'index_refunds_on_refund_reason_id' + end +end diff --git a/core/db/migrate/20140713142214_rename_return_authorization_reason.rb b/core/db/migrate/20140713142214_rename_return_authorization_reason.rb new file mode 100644 index 00000000000..06dc3a066e9 --- /dev/null +++ b/core/db/migrate/20140713142214_rename_return_authorization_reason.rb @@ -0,0 +1,5 @@ +class RenameReturnAuthorizationReason < ActiveRecord::Migration + def change + rename_column :spree_return_authorizations, :reason, :memo + end +end diff --git a/core/db/migrate/20140715182625_create_spree_promotion_categories.rb b/core/db/migrate/20140715182625_create_spree_promotion_categories.rb new file mode 100644 index 00000000000..8441394480e --- /dev/null +++ b/core/db/migrate/20140715182625_create_spree_promotion_categories.rb @@ -0,0 +1,11 @@ +class CreateSpreePromotionCategories < ActiveRecord::Migration + def change + create_table :spree_promotion_categories do |t| + t.string :name + t.timestamps + end + + add_column :spree_promotions, :promotion_category_id, :integer + add_index :spree_promotions, :promotion_category_id + end +end diff --git a/core/db/migrate/20140716204111_drop_received_at_on_return_items.rb b/core/db/migrate/20140716204111_drop_received_at_on_return_items.rb new file mode 100644 index 00000000000..3388173aea5 --- /dev/null +++ b/core/db/migrate/20140716204111_drop_received_at_on_return_items.rb @@ -0,0 +1,9 @@ +class DropReceivedAtOnReturnItems < ActiveRecord::Migration + def up + remove_column :spree_return_items, :received_at + end + + def down + add_column :spree_return_items, :received_at, :datetime + end +end diff --git a/core/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb b/core/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb new file mode 100644 index 00000000000..6e2a1a17e2c --- /dev/null +++ b/core/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb @@ -0,0 +1,6 @@ +class AddReceptionAndAcceptanceStatusToReturnItems < ActiveRecord::Migration + def change + add_column :spree_return_items, :reception_status, :string + add_column :spree_return_items, :acceptance_status, :string + end +end diff --git a/core/db/migrate/20140717155155_create_default_refund_reason.rb b/core/db/migrate/20140717155155_create_default_refund_reason.rb new file mode 100644 index 00000000000..76aecf9d6d8 --- /dev/null +++ b/core/db/migrate/20140717155155_create_default_refund_reason.rb @@ -0,0 +1,9 @@ +class CreateDefaultRefundReason < ActiveRecord::Migration + def up + Spree::RefundReason.create!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) + end + + def down + Spree::RefundReason.find_by(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false).destroy + end +end diff --git a/core/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb b/core/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb new file mode 100644 index 00000000000..71f5ca13cd8 --- /dev/null +++ b/core/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb @@ -0,0 +1,7 @@ +class AddDefaultToSpreeStockLocations < ActiveRecord::Migration + def change + unless column_exists? :spree_stock_locations, :default + add_column :spree_stock_locations, :default, :boolean, null: false, default: false + end + end +end diff --git a/core/db/migrate/20140718133010_create_spree_customer_returns.rb b/core/db/migrate/20140718133010_create_spree_customer_returns.rb new file mode 100644 index 00000000000..6caf95ba9c1 --- /dev/null +++ b/core/db/migrate/20140718133010_create_spree_customer_returns.rb @@ -0,0 +1,9 @@ +class CreateSpreeCustomerReturns < ActiveRecord::Migration + def change + create_table :spree_customer_returns do |t| + t.string :number + t.integer :stock_location_id + t.timestamps + end + end +end diff --git a/core/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb b/core/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb new file mode 100644 index 00000000000..094276cc9df --- /dev/null +++ b/core/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb @@ -0,0 +1,6 @@ +class AddCustomerReturnIdToReturnItem < ActiveRecord::Migration + def change + add_column :spree_return_items, :customer_return_id, :integer + add_index :spree_return_items, :customer_return_id, name: 'index_return_items_on_customer_return_id' + end +end diff --git a/core/db/migrate/20140718195325_create_friendly_id_slugs.rb b/core/db/migrate/20140718195325_create_friendly_id_slugs.rb new file mode 100644 index 00000000000..770f626446b --- /dev/null +++ b/core/db/migrate/20140718195325_create_friendly_id_slugs.rb @@ -0,0 +1,15 @@ +class CreateFriendlyIdSlugs < ActiveRecord::Migration + def change + create_table :friendly_id_slugs do |t| + t.string :slug, :null => false + t.integer :sluggable_id, :null => false + t.string :sluggable_type, :limit => 50 + t.string :scope + t.datetime :created_at + end + add_index :friendly_id_slugs, :sluggable_id + add_index :friendly_id_slugs, [:slug, :sluggable_type] + add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], :unique => true + add_index :friendly_id_slugs, :sluggable_type + end +end diff --git a/core/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb b/core/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb new file mode 100644 index 00000000000..1373514492f --- /dev/null +++ b/core/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb @@ -0,0 +1,5 @@ +class RenameSpreeRefundReturnAuthorizationId < ActiveRecord::Migration + def change + rename_column :spree_refunds, :return_authorization_id, :customer_return_id + end +end diff --git a/core/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb b/core/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb new file mode 100644 index 00000000000..dc48900b86a --- /dev/null +++ b/core/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb @@ -0,0 +1,13 @@ +class IncreaseReturnItemPreTaxAmountPrecision < ActiveRecord::Migration + def up + change_column :spree_return_items, :pre_tax_amount, :decimal, precision: 12, scale: 4, default: 0.0, null: false + change_column :spree_return_items, :included_tax_total, :decimal, precision: 12, scale: 4, default: 0.0, null: false + change_column :spree_return_items, :additional_tax_total, :decimal, precision: 12, scale: 4, default: 0.0, null: false + end + + def down + change_column :spree_return_items, :pre_tax_amount, :decimal, precision: 10, scale: 2, default: 0.0, null: false + change_column :spree_return_items, :included_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false + change_column :spree_return_items, :additional_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false + end +end diff --git a/core/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb b/core/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb new file mode 100644 index 00000000000..c7df6b4da7a --- /dev/null +++ b/core/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb @@ -0,0 +1,15 @@ +class CopyProductSlugsToSlugHistory < ActiveRecord::Migration + def change + + # do what sql does best: copy all slugs into history table in a single query + # rather than load potentially millions of products into memory + Spree::Product.connection.execute <<-SQL +INSERT INTO #{FriendlyId::Slug.table_name} (slug, sluggable_id, sluggable_type, created_at) + SELECT slug, id, '#{Spree::Product.to_s}', #{ActiveRecord::Base.send(:sanitize_sql_array, ['?', Time.current])} + FROM #{Spree::Product.table_name} + WHERE slug IS NOT NULL + ORDER BY id +SQL + + end +end diff --git a/core/db/migrate/20140725131539_create_spree_reimbursements.rb b/core/db/migrate/20140725131539_create_spree_reimbursements.rb new file mode 100644 index 00000000000..e77d24e061b --- /dev/null +++ b/core/db/migrate/20140725131539_create_spree_reimbursements.rb @@ -0,0 +1,21 @@ +class CreateSpreeReimbursements < ActiveRecord::Migration + def change + create_table :spree_reimbursements do |t| + t.string :number + t.string :reimbursement_status + t.integer :customer_return_id + t.integer :order_id + t.decimal :total, precision: 10, scale: 2 + + t.timestamps + end + + add_index :spree_reimbursements, :customer_return_id + add_index :spree_reimbursements, :order_id + + remove_column :spree_refunds, :customer_return_id, :integer + add_column :spree_refunds, :reimbursement_id, :integer + + add_column :spree_return_items, :reimbursement_id, :integer + end +end diff --git a/core/db/migrate/20140728225422_add_promotionable_to_spree_products.rb b/core/db/migrate/20140728225422_add_promotionable_to_spree_products.rb new file mode 100644 index 00000000000..5c5606bf6ca --- /dev/null +++ b/core/db/migrate/20140728225422_add_promotionable_to_spree_products.rb @@ -0,0 +1,5 @@ +class AddPromotionableToSpreeProducts < ActiveRecord::Migration + def change + add_column :spree_products, :promotionable, :boolean, default: true + end +end diff --git a/core/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb b/core/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb new file mode 100644 index 00000000000..e0c0f9035dd --- /dev/null +++ b/core/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb @@ -0,0 +1,7 @@ +class AddExchangeInventoryUnitForeignKeys < ActiveRecord::Migration + def change + add_column :spree_return_items, :exchange_inventory_unit_id, :integer + + add_index :spree_return_items, :exchange_inventory_unit_id + end +end diff --git a/core/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb b/core/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb new file mode 100644 index 00000000000..31fcb47c2a9 --- /dev/null +++ b/core/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb @@ -0,0 +1,5 @@ +class AddAcceptanceStatusErrorsToReturnItem < ActiveRecord::Migration + def change + add_column :spree_return_items, :acceptance_status_errors, :text + end +end diff --git a/core/db/migrate/20140731150017_create_spree_reimbursement_types.rb b/core/db/migrate/20140731150017_create_spree_reimbursement_types.rb new file mode 100644 index 00000000000..f1cd2095c8f --- /dev/null +++ b/core/db/migrate/20140731150017_create_spree_reimbursement_types.rb @@ -0,0 +1,20 @@ +class CreateSpreeReimbursementTypes < ActiveRecord::Migration + def change + create_table :spree_reimbursement_types do |t| + t.string :name + t.boolean :active, default: true + t.boolean :mutable, default: true + + t.timestamps + end + + reversible do |direction| + direction.up do + Spree::ReimbursementType.create!(name: Spree::ReimbursementType::ORIGINAL) + end + end + + add_column :spree_return_items, :preferred_reimbursement_type_id, :integer + add_column :spree_return_items, :override_reimbursement_type_id, :integer + end +end diff --git a/core/db/migrate/20140804185157_add_default_to_shipment_cost.rb b/core/db/migrate/20140804185157_add_default_to_shipment_cost.rb new file mode 100644 index 00000000000..8106f43a832 --- /dev/null +++ b/core/db/migrate/20140804185157_add_default_to_shipment_cost.rb @@ -0,0 +1,10 @@ +class AddDefaultToShipmentCost < ActiveRecord::Migration + def up + change_column :spree_shipments, :cost, :decimal, precision: 10, scale: 2, default: 0.0 + Spree::Shipment.where(cost: nil).update_all(cost: 0) + end + + def down + change_column :spree_shipments, :cost, :decimal, precision: 10, scale: 2 + end +end diff --git a/core/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb b/core/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb new file mode 100644 index 00000000000..3815970f3d7 --- /dev/null +++ b/core/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb @@ -0,0 +1,5 @@ +class AddDefaultToSpreeCreditCards < ActiveRecord::Migration + def change + add_column :spree_credit_cards, :default, :boolean, null: false, default: false + end +end diff --git a/core/db/migrate/20140805171219_make_existing_credit_cards_default.rb b/core/db/migrate/20140805171219_make_existing_credit_cards_default.rb new file mode 100644 index 00000000000..5d2e3735bd1 --- /dev/null +++ b/core/db/migrate/20140805171219_make_existing_credit_cards_default.rb @@ -0,0 +1,10 @@ +class MakeExistingCreditCardsDefault < ActiveRecord::Migration + def up + # set the newest credit card for every user to be the default; SQL technique from + # http://stackoverflow.com/questions/121387/fetch-the-row-which-has-the-max-value-for-a-column + Spree::CreditCard.where.not(user_id: nil).joins("LEFT OUTER JOIN spree_credit_cards cc2 ON cc2.user_id = spree_credit_cards.user_id AND spree_credit_cards.created_at < cc2.created_at").where("cc2.user_id IS NULL").update_all(default: true) + end + def down + # do nothing + end +end diff --git a/core/db/migrate/20140806144901_add_type_to_reimbursement_type.rb b/core/db/migrate/20140806144901_add_type_to_reimbursement_type.rb new file mode 100644 index 00000000000..2e47e16901f --- /dev/null +++ b/core/db/migrate/20140806144901_add_type_to_reimbursement_type.rb @@ -0,0 +1,9 @@ +class AddTypeToReimbursementType < ActiveRecord::Migration + def change + add_column :spree_reimbursement_types, :type, :string + add_index :spree_reimbursement_types, :type + + Spree::ReimbursementType.reset_column_information + Spree::ReimbursementType.find_by(name: Spree::ReimbursementType::ORIGINAL).update_attributes!(type: 'Spree::ReimbursementType::OriginalPayment') + end +end diff --git a/core/db/migrate/20140808184039_create_spree_reimbursement_credits.rb b/core/db/migrate/20140808184039_create_spree_reimbursement_credits.rb new file mode 100644 index 00000000000..559a6b58c35 --- /dev/null +++ b/core/db/migrate/20140808184039_create_spree_reimbursement_credits.rb @@ -0,0 +1,10 @@ +class CreateSpreeReimbursementCredits < ActiveRecord::Migration + def change + create_table :spree_reimbursement_credits do |t| + t.decimal :amount, precision: 10, scale: 2, default: 0.0, null: false + t.integer :reimbursement_id + t.integer :creditable_id + t.string :creditable_type + end + end +end diff --git a/core/db/migrate/20140827170513_add_meta_title_to_spree_products.rb b/core/db/migrate/20140827170513_add_meta_title_to_spree_products.rb new file mode 100644 index 00000000000..4a0b513fbe2 --- /dev/null +++ b/core/db/migrate/20140827170513_add_meta_title_to_spree_products.rb @@ -0,0 +1,7 @@ +class AddMetaTitleToSpreeProducts < ActiveRecord::Migration + def change + change_table :spree_products do |t| + t.string :meta_title + end + end +end diff --git a/core/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb b/core/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb new file mode 100644 index 00000000000..2f6f7088786 --- /dev/null +++ b/core/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb @@ -0,0 +1,5 @@ +class AddCodeToSpreeTaxCategories < ActiveRecord::Migration + def change + add_column :spree_tax_categories, :tax_code, :string + end +end diff --git a/core/db/migrate/20140927193717_default_pre_tax_amount_should_be_zero.rb b/core/db/migrate/20140927193717_default_pre_tax_amount_should_be_zero.rb new file mode 100644 index 00000000000..5e0050af46c --- /dev/null +++ b/core/db/migrate/20140927193717_default_pre_tax_amount_should_be_zero.rb @@ -0,0 +1,6 @@ +class DefaultPreTaxAmountShouldBeZero < ActiveRecord::Migration + def change + change_column :spree_line_items, :pre_tax_amount, :decimal, precision: 8, scale: 2, default: 0 + change_column :spree_shipments, :pre_tax_amount, :decimal, precision: 8, scale: 2, default: 0 + end +end diff --git a/core/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb b/core/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb new file mode 100644 index 00000000000..9ea9f37c564 --- /dev/null +++ b/core/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb @@ -0,0 +1,5 @@ +class AddCodeToSpreeShippingMethods < ActiveRecord::Migration + def change + add_column :spree_shipping_methods, :code, :string + end +end diff --git a/core/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb b/core/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb new file mode 100644 index 00000000000..ad974a9f7d1 --- /dev/null +++ b/core/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb @@ -0,0 +1,6 @@ +class AddCancelAuditFieldsToSpreeOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :canceled_at, :datetime + add_column :spree_orders, :canceler_id, :integer + end +end diff --git a/core/db/migrate/20141009204607_add_store_id_to_orders.rb b/core/db/migrate/20141009204607_add_store_id_to_orders.rb new file mode 100644 index 00000000000..548c6e1b4df --- /dev/null +++ b/core/db/migrate/20141009204607_add_store_id_to_orders.rb @@ -0,0 +1,8 @@ +class AddStoreIdToOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :store_id, :integer + if Spree::Store.default.persisted? + Spree::Order.where(store_id: nil).update_all(store_id: Spree::Store.default.id) + end + end +end diff --git a/core/db/migrate/20141012083513_create_spree_taxons_prototypes.rb b/core/db/migrate/20141012083513_create_spree_taxons_prototypes.rb new file mode 100644 index 00000000000..46f988539ef --- /dev/null +++ b/core/db/migrate/20141012083513_create_spree_taxons_prototypes.rb @@ -0,0 +1,8 @@ +class CreateSpreeTaxonsPrototypes < ActiveRecord::Migration + def change + create_table :spree_taxons_prototypes do |t| + t.belongs_to :taxon, index: true + t.belongs_to :prototype, index: true + end + end +end diff --git a/core/db/migrate/20141021194502_add_state_lock_version_to_order.rb b/core/db/migrate/20141021194502_add_state_lock_version_to_order.rb new file mode 100644 index 00000000000..92582c50011 --- /dev/null +++ b/core/db/migrate/20141021194502_add_state_lock_version_to_order.rb @@ -0,0 +1,5 @@ +class AddStateLockVersionToOrder < ActiveRecord::Migration + def change + add_column :spree_orders, :state_lock_version, :integer, default: 0, null: false + end +end diff --git a/core/db/migrate/20141023005240_add_counter_cache_from_spree_variants_to_spree_stock_items.rb b/core/db/migrate/20141023005240_add_counter_cache_from_spree_variants_to_spree_stock_items.rb new file mode 100644 index 00000000000..fe8fece0f4d --- /dev/null +++ b/core/db/migrate/20141023005240_add_counter_cache_from_spree_variants_to_spree_stock_items.rb @@ -0,0 +1,13 @@ +class AddCounterCacheFromSpreeVariantsToSpreeStockItems < ActiveRecord::Migration + def up + add_column :spree_variants, :stock_items_count, :integer, default: 0, null: false + + Spree::Variant.find_each do |variant| + Spree::Variant.reset_counters(variant.id, :stock_items) + end + end + + def down + remove_column :spree_variants, :stock_items_count + end +end diff --git a/core/db/migrate/20141101231208_fix_adjustment_order_presence.rb b/core/db/migrate/20141101231208_fix_adjustment_order_presence.rb new file mode 100644 index 00000000000..fbca7355cd4 --- /dev/null +++ b/core/db/migrate/20141101231208_fix_adjustment_order_presence.rb @@ -0,0 +1,13 @@ +class FixAdjustmentOrderPresence < ActiveRecord::Migration + def change + say 'Fixing adjustments without direct order reference' + Spree::Adjustment.where(order: nil).find_each do |adjustment| + adjustable = adjustment.adjustable + if adjustable.is_a? Spree::Order + adjustment.update_attributes!(order_id: adjustable.id) + else + adjustment.update_attributes!(adjustable: adjustable.order) + end + end + end +end diff --git a/core/db/migrate/20141105213646_update_classifications_positions.rb b/core/db/migrate/20141105213646_update_classifications_positions.rb new file mode 100644 index 00000000000..c1d64794eb3 --- /dev/null +++ b/core/db/migrate/20141105213646_update_classifications_positions.rb @@ -0,0 +1,9 @@ +class UpdateClassificationsPositions < ActiveRecord::Migration + def up + Spree::Taxon.all.each do |taxon| + taxon.classifications.each_with_index do |c12n, i| + c12n.set_list_position(i + 1) + end + end + end +end diff --git a/core/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb b/core/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb new file mode 100644 index 00000000000..90ea9edb009 --- /dev/null +++ b/core/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb @@ -0,0 +1,5 @@ +class AddGuestTokenIndexToSpreeOrders < ActiveRecord::Migration + def change + add_index :spree_orders, :guest_token + end +end diff --git a/core/db/migrate/20150515211137_fix_adjustment_order_id.rb b/core/db/migrate/20150515211137_fix_adjustment_order_id.rb new file mode 100644 index 00000000000..4988887dde3 --- /dev/null +++ b/core/db/migrate/20150515211137_fix_adjustment_order_id.rb @@ -0,0 +1,70 @@ +class FixAdjustmentOrderId < ActiveRecord::Migration + def change + say 'Populate order_id from adjustable_id where appropriate' + execute(<<-SQL.squish) + UPDATE + spree_adjustments + SET + order_id = adjustable_id + WHERE + adjustable_type = 'Spree::Order' + ; + SQL + + # Submitter of change does not care about MySQL, as it is not officially supported. + # Still spree officials decided to provide a working code path for MySQL users, hence + # submitter made a AR code path he could validate on PostgreSQL. + # + # Whoever runs a big enough MySQL installation where the AR solution hurts: + # Will have to write a better MySQL specific equivalent. + if Spree::Order.connection.adapter_name.eql?('MySQL') + Spree::Adjustment.where(adjustable_type: 'Spree::LineItem').find_each do |adjustment| + adjustment.update_columns(order_id: Spree::LineItem.find(adjustment.adjustable_id).order_id) + end + else + execute(<<-SQL.squish) + UPDATE + spree_adjustments + SET + order_id = + (SELECT order_id FROM spree_line_items WHERE spree_line_items.id = spree_adjustments.adjustable_id) + WHERE + adjustable_type = 'Spree::LineItem' + SQL + end + + say 'Fix schema for spree_adjustments order_id column' + change_table :spree_adjustments do |t| + t.change :order_id, :integer, null: false + end + + # Improved schema for postgresql, uncomment if you like it: + # + # # Negated Logical implication. + # # + # # When adjustable_type is 'Spree::Order' (p) the adjustable_id must be order_id (q). + # # + # # When adjustable_type is NOT 'Spree::Order' the adjustable id allowed to be any value (including of order_id in + # # case foreign keys match). XOR does not work here. + # # + # # Postgresql does not have an operator for logical implication. So we need to build the following truth table + # # via AND with OR: + # # + # # p q | CHECK = !(p -> q) + # # ----------- + # # t t | t + # # t f | f + # # f t | t + # # f f | t + # # + # # According to de-morgans law the logical implication q -> p is equivalent to !p || q + # # + # execute(<<-SQL.squish) + # ALTER TABLE ONLY spree_adjustments + # ADD CONSTRAINT fk_spree_adjustments FOREIGN KEY (order_id) + # REFERENCES spree_orders(id) ON UPDATE RESTRICT ON DELETE RESTRICT, + # ADD CONSTRAINT check_spree_adjustments_order_id CHECK + # (adjustable_type <> 'Spree::Order' OR order_id = adjustable_id); + # SQL + end +end diff --git a/core/db/migrate/20150522181728_add_deleted_at_to_friendly_id_slugs.rb b/core/db/migrate/20150522181728_add_deleted_at_to_friendly_id_slugs.rb new file mode 100644 index 00000000000..14add83a485 --- /dev/null +++ b/core/db/migrate/20150522181728_add_deleted_at_to_friendly_id_slugs.rb @@ -0,0 +1,6 @@ +class AddDeletedAtToFriendlyIdSlugs < ActiveRecord::Migration + def change + add_column :friendly_id_slugs, :deleted_at, :datetime + add_index :friendly_id_slugs, :deleted_at + end +end diff --git a/core/db/migrate/20150707204155_enable_acts_as_paranoid_on_calculators.rb b/core/db/migrate/20150707204155_enable_acts_as_paranoid_on_calculators.rb new file mode 100644 index 00000000000..6e7a7db367c --- /dev/null +++ b/core/db/migrate/20150707204155_enable_acts_as_paranoid_on_calculators.rb @@ -0,0 +1,6 @@ +class EnableActsAsParanoidOnCalculators < ActiveRecord::Migration + def change + add_column :spree_calculators, :deleted_at, :datetime + add_index :spree_calculators, :deleted_at + end +end diff --git a/core/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt b/core/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt index eb8d5c6b2ec..03b6c095877 100644 --- a/core/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt +++ b/core/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt @@ -1,15 +1,20 @@ module Spree - module AuthenticationHelpers + module CurrentUserHelpers def self.included(receiver) - receiver.send :helper_method, :spree_login_path - receiver.send :helper_method, :spree_signup_path - receiver.send :helper_method, :spree_logout_path receiver.send :helper_method, :spree_current_user end def spree_current_user current_user end + end + + module AuthenticationHelpers + def self.included(receiver) + receiver.send :helper_method, :spree_login_path + receiver.send :helper_method, :spree_signup_path + receiver.send :helper_method, :spree_logout_path + end def spree_login_path main_app.login_path @@ -26,3 +31,6 @@ module Spree end ApplicationController.send :include, Spree::AuthenticationHelpers +ApplicationController.send :include, Spree::CurrentUserHelpers + +Spree::Api::BaseController.send :include, Spree::CurrentUserHelpers diff --git a/core/lib/generators/spree/dummy/dummy_generator.rb b/core/lib/generators/spree/dummy/dummy_generator.rb index 178821c2bec..58a3c6f00b6 100644 --- a/core/lib/generators/spree/dummy/dummy_generator.rb +++ b/core/lib/generators/spree/dummy/dummy_generator.rb @@ -1,4 +1,6 @@ require "rails/generators/rails/app/app_generator" +require 'active_support/core_ext/hash' +require 'spree/core/version' module Spree class DummyGenerator < Rails::Generators::Base @@ -22,7 +24,9 @@ def clean_up ] def generate_test_dummy - opts = (options || {}).slice(*PASSTHROUGH_OPTIONS) + # calling slice on a Thor::CoreExtensions::HashWithIndifferentAccess + # object has been known to return nil + opts = {}.merge(options).slice(*PASSTHROUGH_OPTIONS) opts[:database] = 'sqlite3' if opts[:database].blank? opts[:force] = true opts[:skip_bundle] = true @@ -41,8 +45,20 @@ def test_dummy_config template "rails/boot.rb", "#{dummy_path}/config/boot.rb", :force => true template "rails/application.rb", "#{dummy_path}/config/application.rb", :force => true template "rails/routes.rb", "#{dummy_path}/config/routes.rb", :force => true + template "rails/test.rb", "#{dummy_path}/config/environments/test.rb", :force => true template "rails/script/rails", "#{dummy_path}/spec/dummy/script/rails", :force => true template "initializers/custom_user.rb", "#{dummy_path}/config/initializers/custom_user.rb", :force => true + template "initializers/devise.rb", "#{dummy_path}/config/initializers/devise.rb", :force => true + end + + def test_dummy_inject_extension_requirements + if DummyGeneratorHelper.inject_extension_requirements + inside dummy_path do + inject_require_for('spree_frontend') + inject_require_for('spree_backend') + inject_require_for('spree_api') + end + end end def test_dummy_clean @@ -60,12 +76,24 @@ def test_dummy_clean remove_file "vendor" remove_file "spec" end + end attr :lib_name attr :database protected + + def inject_require_for(requirement) + inject_into_file 'config/application.rb', %Q[ +begin + require '#{requirement}' +rescue LoadError + # #{requirement} is not available. +end + ], :before => /require '#{@lib_name}'/, :verbose => true + end + def dummy_path ENV['DUMMY_PATH'] || 'spec/dummy' end @@ -95,13 +123,18 @@ def remove_directory_if_exists(path) end def gemfile_path - version_file = File.expand_path("../../Versionfile", Dir.pwd) - if File.exist?(version_file) - '../../../../Gemfile' - else + core_gems = ["spree/core", "spree/api", "spree/backend", "spree/frontend"] + + if core_gems.include?(lib_name) '../../../../../Gemfile' + else + '../../../../Gemfile' end end - end end + +module Spree::DummyGeneratorHelper + mattr_accessor :inject_extension_requirements + self.inject_extension_requirements = false +end diff --git a/core/lib/generators/spree/dummy/templates/initializers/devise.rb b/core/lib/generators/spree/dummy/templates/initializers/devise.rb new file mode 100644 index 00000000000..7dff9547df5 --- /dev/null +++ b/core/lib/generators/spree/dummy/templates/initializers/devise.rb @@ -0,0 +1,3 @@ +if Object.const_defined?("Devise") + Devise.secret_key = "<%= SecureRandom.hex(50) %>" +end \ No newline at end of file diff --git a/core/lib/generators/spree/dummy/templates/rails/database.yml b/core/lib/generators/spree/dummy/templates/rails/database.yml index edcb56b11a6..d24023adc51 100644 --- a/core/lib/generators/spree/dummy/templates/rails/database.yml +++ b/core/lib/generators/spree/dummy/templates/rails/database.yml @@ -1,54 +1,65 @@ +<% if agent_number = ENV['TC_AGENT_NUMBER'] +database_prefix = agent_number + '_' +end %> <% case ENV['DB'] when 'sqlite' %> development: adapter: sqlite3 - database: "db/spree_development.sqlite3" + database: db/spree_development.sqlite3 test: adapter: sqlite3 - database: "db/spree_test.sqlite3" + database: db/spree_test.sqlite3 + timeout: 10000 production: adapter: sqlite3 - database: "db/spree_production.sqlite3" + database: db/spree_production.sqlite3 <% when 'mysql' %> development: adapter: mysql2 - database: spree_development - username: + database: <%= database_prefix %><%= options[:lib_name] %>_spree_development encoding: utf8 test: adapter: mysql2 - database: spree_test - username: + database: <%= database_prefix %><%= options[:lib_name] %>_spree_test encoding: utf8 production: adapter: mysql2 - database: spree_production - username: + database: <%= database_prefix %><%= options[:lib_name] %>_spree_production encoding: utf8 <% when 'postgres' %> +<% db_host = ENV['DB_HOST'] -%> development: adapter: postgresql - database: spree_development + database: <%= database_prefix %><%= options[:lib_name] %>_spree_development username: postgres min_messages: warning +<% unless db_host.blank? %> + host: <%= db_host %> +<% end %> test: adapter: postgresql - database: spree_test + database: <%= database_prefix %><%= options[:lib_name] %>_spree_test username: postgres min_messages: warning +<% unless db_host.blank? %> + host: <%= db_host %> +<% end %> production: adapter: postgresql - database: spree_production + database: <%= database_prefix %><%= options[:lib_name] %>_spree_production username: postgres min_messages: warning +<% unless db_host.blank? %> + host: <%= db_host %> +<% end %> <% else %> development: adapter: sqlite3 - database: "db/spree_development.sqlite3" + database: db/spree_development.sqlite3 test: adapter: sqlite3 - database: "db/spree_test.sqlite3" + database: db/spree_test.sqlite3 production: adapter: sqlite3 - database: "db/spree_production.sqlite3" + database: db/spree_production.sqlite3 <% end %> diff --git a/core/lib/generators/spree/dummy/templates/rails/routes.rb b/core/lib/generators/spree/dummy/templates/rails/routes.rb index 6aa5d34d63d..1daf9a4121a 100644 --- a/core/lib/generators/spree/dummy/templates/rails/routes.rb +++ b/core/lib/generators/spree/dummy/templates/rails/routes.rb @@ -1,3 +1,2 @@ Rails.application.routes.draw do - <%= 'mount Spree::Core::Engine => "/"' if defined?(Spree::Core) %> end diff --git a/core/lib/generators/spree/dummy/templates/rails/test.rb b/core/lib/generators/spree/dummy/templates/rails/test.rb new file mode 100644 index 00000000000..9f175964881 --- /dev/null +++ b/core/lib/generators/spree/dummy/templates/rails/test.rb @@ -0,0 +1,34 @@ +Dummy::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Configure static asset server for tests with Cache-Control for performance + config.serve_static_assets = true + config.static_cache_control = "public, max-age=3600" + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + config.eager_load = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + ActionMailer::Base.default :from => "spree@example.com" + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr +end diff --git a/core/lib/generators/spree/install/install_generator.rb b/core/lib/generators/spree/install/install_generator.rb index 0727f77aabc..d7a07932c0b 100644 --- a/core/lib/generators/spree/install/install_generator.rb +++ b/core/lib/generators/spree/install/install_generator.rb @@ -13,6 +13,7 @@ class InstallGenerator < Rails::Generators::Base class_option :admin_email, :type => :string class_option :admin_password, :type => :string class_option :lib_name, :type => :string, :default => 'spree' + class_option :enforce_available_locales, :type => :boolean, :default => nil def self.source_paths paths = self.superclass.source_paths @@ -45,36 +46,36 @@ def config_spree_yml end end - def remove_unneeded_files - remove_file "public/index.html" - end - def additional_tweaks return unless File.exists? 'public/robots.txt' append_file "public/robots.txt", <<-ROBOTS User-agent: * -Disallow: /checkouts +Disallow: /checkout +Disallow: /cart Disallow: /orders -Disallow: /countries -Disallow: /line_items -Disallow: /password_resets -Disallow: /states -Disallow: /user_sessions -Disallow: /users +Disallow: /user +Disallow: /account +Disallow: /api +Disallow: /password ROBOTS end def setup_assets @lib_name = 'spree' %w{javascripts stylesheets images}.each do |path| - empty_directory "app/assets/#{path}/store" - empty_directory "app/assets/#{path}/admin" + empty_directory "vendor/assets/#{path}/spree/frontend" if defined? Spree::Frontend || Rails.env.test? + empty_directory "vendor/assets/#{path}/spree/backend" if defined? Spree::Backend || Rails.env.test? + end + + if defined? Spree::Frontend || Rails.env.test? + template "vendor/assets/javascripts/spree/frontend/all.js" + template "vendor/assets/stylesheets/spree/frontend/all.css" end - template "app/assets/javascripts/store/all.js" - template "app/assets/javascripts/admin/all.js" - template "app/assets/stylesheets/store/all.css" - template "app/assets/stylesheets/admin/all.css" + if defined? Spree::Backend || Rails.env.test? + template "vendor/assets/javascripts/spree/backend/all.js" + template "vendor/assets/stylesheets/spree/backend/all.css" + end end def create_overrides_directory @@ -97,7 +98,12 @@ def configure_application end APP - append_file "config/environment.rb", "\nActiveRecord::Base.include_root_in_json = true\n" + if !options[:enforce_available_locales].nil? + application <<-APP + # Prevent this deprecation message: https://github.com/svenfuchs/i18n/commit/3b6e56e + I18n.enforce_available_locales = #{options[:enforce_available_locales]} + APP + end end def include_seed_data @@ -162,7 +168,7 @@ def load_sample_data end def notify_about_routes - insert_into_file File.join('config', 'routes.rb'), :after => "Application.routes.draw do\n" do + insert_into_file File.join('config', 'routes.rb'), :after => "Rails.application.routes.draw do\n" do %Q{ # This line mounts Spree's routes at the root of your application. # This means, any requests to URLs such as /products, will go to Spree::ProductsController. @@ -189,6 +195,5 @@ def complete puts "Enjoy!" end end - end end diff --git a/core/lib/generators/spree/install/templates/app/assets/javascripts/admin/all.js b/core/lib/generators/spree/install/templates/app/assets/javascripts/admin/all.js deleted file mode 100644 index 590b2a0fba0..00000000000 --- a/core/lib/generators/spree/install/templates/app/assets/javascripts/admin/all.js +++ /dev/null @@ -1,15 +0,0 @@ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -//= require jquery -//= require jquery_ujs -<% if options[:lib_name] == 'spree' %> -//= require admin/spree_core -//= require admin/spree_promo -<% else %> -//= require admin/<%= options[:lib_name].gsub("/", "_") %> -<% end %> -//= require_tree . diff --git a/core/lib/generators/spree/install/templates/app/assets/javascripts/store/all.js b/core/lib/generators/spree/install/templates/app/assets/javascripts/store/all.js deleted file mode 100644 index 3512a2a08f0..00000000000 --- a/core/lib/generators/spree/install/templates/app/assets/javascripts/store/all.js +++ /dev/null @@ -1,15 +0,0 @@ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -//= require jquery -//= require jquery_ujs -<% if options[:lib_name] == 'spree' %> -//= require store/spree_core -//= require store/spree_promo -<% else %> -//= require store/<%= options[:lib_name].gsub("/", "_") %> -<% end %> -//= require_tree . diff --git a/core/lib/generators/spree/install/templates/app/assets/stylesheets/admin/all.css b/core/lib/generators/spree/install/templates/app/assets/stylesheets/admin/all.css deleted file mode 100644 index 86e4e5d5e10..00000000000 --- a/core/lib/generators/spree/install/templates/app/assets/stylesheets/admin/all.css +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This is a manifest file that'll automatically include all the stylesheets available in this directory - * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at - * the top of the compiled file, but it's generally better to create a new file per style scope. - * -<% if options[:lib_name] == 'spree' %> - *= require admin/spree_core - *= require admin/spree_promo -<% else %> - *= require admin/<%= options[:lib_name].gsub("/", "_") %> -<% end %> - *= require_self - *= require_tree . -*/ diff --git a/core/lib/generators/spree/install/templates/app/assets/stylesheets/store/all.css b/core/lib/generators/spree/install/templates/app/assets/stylesheets/store/all.css deleted file mode 100644 index a361ef2e865..00000000000 --- a/core/lib/generators/spree/install/templates/app/assets/stylesheets/store/all.css +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This is a manifest file that'll automatically include all the stylesheets available in this directory - * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at - * the top of the compiled file, but it's generally better to create a new file per style scope. - * -<% if options[:lib_name] == 'spree' %> - *= require store/spree_core - *= require store/spree_promo -<% else %> - *= require store/<%= options[:lib_name].gsub("/", "_") %> -<% end %> - *= require_self - *= require_tree . -*/ diff --git a/core/lib/generators/spree/install/templates/config/initializers/spree.rb b/core/lib/generators/spree/install/templates/config/initializers/spree.rb index f9a3c557ec4..9f06c10123c 100644 --- a/core/lib/generators/spree/install/templates/config/initializers/spree.rb +++ b/core/lib/generators/spree/install/templates/config/initializers/spree.rb @@ -7,8 +7,8 @@ # config.setting_name = 'new value' Spree.config do |config| # Example: - # Uncomment to override the default site name. - # config.site_name = "Spree Demo Site" + # Uncomment to stop tracking inventory levels in the application + # config.track_inventory_levels = false end Spree.user_class = <%= (options[:user_class].blank? ? "Spree::LegacyUser" : options[:user_class]).inspect %> diff --git a/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/backend/all.js b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/backend/all.js new file mode 100644 index 00000000000..d76d69041bd --- /dev/null +++ b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/backend/all.js @@ -0,0 +1,13 @@ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +//= require jquery +//= require jquery_ujs +//= require spree/backend +<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/backend' %> +//= require spree/backend/<%= options[:lib_name].gsub("/", "_") %> +<% end %> +//= require_tree . diff --git a/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/frontend/all.js b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/frontend/all.js new file mode 100644 index 00000000000..e493c1abe33 --- /dev/null +++ b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/frontend/all.js @@ -0,0 +1,13 @@ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +//= require jquery +//= require jquery_ujs +//= require spree/frontend +<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/frontend' %> +//= require spree/frontend/<%= options[:lib_name].gsub("/", "_") %> +<% end %> +//= require_tree . diff --git a/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/backend/all.css b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/backend/all.css new file mode 100644 index 00000000000..ef4f3000d6e --- /dev/null +++ b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/backend/all.css @@ -0,0 +1,12 @@ +/* + * This is a manifest file that'll automatically include all the stylesheets available in this directory + * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at + * the top of the compiled file, but it's generally better to create a new file per style scope. + * + *= require spree/backend +<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/backend' %> + *= require spree/backend/<%= options[:lib_name].gsub("/", "_") %> +<% end %> + *= require_self + *= require_tree . +*/ diff --git a/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/frontend/all.css b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/frontend/all.css new file mode 100644 index 00000000000..d4b7c30e5d1 --- /dev/null +++ b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/frontend/all.css @@ -0,0 +1,12 @@ +/* + * This is a manifest file that'll automatically include all the stylesheets available in this directory + * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at + * the top of the compiled file, but it's generally better to create a new file per style scope. + * + *= require spree/frontend +<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/frontend' %> + *= require spree/frontend/<%= options[:lib_name].gsub("/", "_") %> +<% end %> + *= require_self + *= require_tree . +*/ diff --git a/core/lib/spree/core.rb b/core/lib/spree/core.rb index d8330926dff..a8dfe47f5bb 100644 --- a/core/lib/spree/core.rb +++ b/core/lib/spree/core.rb @@ -1,46 +1,18 @@ -#++ -# Copyright (c) 2007-2012, Spree Commerce, Inc. and other contributors -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Spree Commerce, Inc. nor the names of its -# contributors may be used to endorse or promote products derived from this -# software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#-- require 'rails/all' -require 'rails/generators' -require 'state_machine' -require 'paperclip' -require 'kaminari' -require 'awesome_nested_set' -require 'acts_as_list' require 'active_merchant' -require 'ransack' -require 'jquery-rails' -require 'deface' +require 'acts_as_list' +require 'awesome_nested_set' require 'cancan' -require 'select2-rails' -require 'spree/money' -require 'rabl' +require 'friendly_id' +require 'font-awesome-rails' +require 'kaminari' +require 'mail' +require 'monetize' +require 'paperclip' +require 'paranoia' +require 'premailer/rails' +require 'ransack' +require 'state_machine' module Spree @@ -48,21 +20,18 @@ module Spree def self.user_class if @@user_class.is_a?(Class) - raise "Spree.user_class MUST be a String object, not a Class object." - elsif @@user_class.is_a?(String) - @@user_class.constantize + raise "Spree.user_class MUST be a String or Symbol object, not a Class object." + elsif @@user_class.is_a?(String) || @@user_class.is_a?(Symbol) + @@user_class.to_s.constantize end end - module Core - end - # Used to configure Spree. # # Example: # # Spree.config do |config| - # config.site_name = "An awesome Spree site" + # config.track_inventory_levels = false # end # # This method is defined within the core gem on purpose. @@ -70,41 +39,47 @@ module Core def self.config(&block) yield(Spree::Config) end -end - -require 'spree/core/ext/active_record' -require 'spree/core/delegate_belongs_to' + module Core + autoload :ProductFilters, "spree/core/product_filters" -require 'spree/core/responder' -require 'spree/core/ssl_requirement' -require 'spree/core/store_helpers' -require 'spree/core/calculated_adjustments' -require 'spree/core/mail_settings' -require 'spree/core/mail_interceptor' -require 'spree/core/middleware/redirect_legacy_product_url' -require 'spree/core/middleware/seo_assist' -require 'spree/core/permalinks' -require 'spree/core/token_resource' -require 'spree/core/s3_support' + class GatewayError < RuntimeError; end + class DestroyWithOrdersError < StandardError; end + end +end require 'spree/core/version' +require 'spree/core/environment_extension' +require 'spree/core/environment/calculators' +require 'spree/core/environment' +require 'spree/promo/environment' +require 'spree/migrations' require 'spree/core/engine' -require 'generators/spree/dummy/dummy_generator' -ActiveRecord::Base.class_eval do - include Spree::Core::CalculatedAdjustments - include CollectiveIdea::Acts::NestedSet -end +require 'spree/i18n' +require 'spree/localized_number' +require 'spree/money' +require 'spree/permitted_attributes' -if defined?(ActionView) - require 'awesome_nested_set/helper' - ActionView::Base.class_eval do - include CollectiveIdea::Acts::NestedSet::Helper - end -end +require 'spree/core/delegate_belongs_to' +require 'spree/core/importer' +require 'spree/core/permalinks' +require 'spree/core/product_duplicator' +require 'spree/core/controller_helpers/auth' +require 'spree/core/controller_helpers/common' +require 'spree/core/controller_helpers/order' +require 'spree/core/controller_helpers/respond_with' +require 'spree/core/controller_helpers/search' +require 'spree/core/controller_helpers/ssl' +require 'spree/core/controller_helpers/store' +require 'spree/core/controller_helpers/strong_parameters' -ActiveSupport.on_load(:action_view) do - include Spree::Core::StoreHelpers +# Hack waiting on https://github.com/pluginaweek/state_machine/pull/275 +module StateMachine + module Integrations + module ActiveModel + public :around_validation + end + end end diff --git a/core/lib/spree/core/calculated_adjustments.rb b/core/lib/spree/core/calculated_adjustments.rb deleted file mode 100644 index 5d8830cfa32..00000000000 --- a/core/lib/spree/core/calculated_adjustments.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Spree - module Core - module CalculatedAdjustments - module ClassMethods - def calculated_adjustments - has_one :calculator, :as => :calculable, :dependent => :destroy - accepts_nested_attributes_for :calculator - attr_accessible :calculator_type, :calculator_attributes - validates :calculator, :presence => true - - def self.calculators - Rails.application.config.spree.calculators.send(self.to_s.tableize.gsub('/', '_').sub('spree_', '')) - end - end - end - - def calculator_type - calculator.class.to_s if calculator - end - - def calculator_type=(calculator_type) - clazz = calculator_type.constantize if calculator_type - self.calculator = clazz.new if clazz and not self.calculator.is_a? clazz - end - - # Creates a new adjustment for the target object (which is any class that has_many :adjustments) and - # sets amount based on the calculator as applied to the calculable argument (Order, LineItems[], Shipment, etc.) - # By default the adjustment will not be considered mandatory - def create_adjustment(label, target, calculable, mandatory=false) - amount = compute_amount(calculable) - return if amount == 0 && !mandatory - target.adjustments.create({ :amount => amount, - :source => calculable, - :originator => self, - :label => label, - :mandatory => mandatory}, :without_protection => true) - end - - # Updates the amount of the adjustment using our Calculator and calling the +compute+ method with the +calculable+ - # referenced passed to the method. - def update_adjustment(adjustment, calculable) - adjustment.update_attribute_without_callbacks(:amount, compute_amount(calculable)) - end - - # Calculate the amount to be used when creating an adjustment - def compute_amount(calculable) - self.calculator.compute(calculable) - end - - def self.included(receiver) - receiver.extend ClassMethods - end - end - end -end diff --git a/core/lib/spree/core/controller_helpers/auth.rb b/core/lib/spree/core/controller_helpers/auth.rb index fae6d80d154..05c42cc4b9e 100644 --- a/core/lib/spree/core/controller_helpers/auth.rb +++ b/core/lib/spree/core/controller_helpers/auth.rb @@ -2,15 +2,14 @@ module Spree module Core module ControllerHelpers module Auth - def self.included(base) - base.class_eval do - include SslRequirement + extend ActiveSupport::Concern - helper_method :try_spree_current_user + included do + before_filter :set_guest_token + helper_method :try_spree_current_user - rescue_from CanCan::AccessDenied do |exception| - return unauthorized - end + rescue_from CanCan::AccessDenied do |exception| + redirect_unauthorized_access end end @@ -19,17 +18,14 @@ def current_ability @current_ability ||= Spree::Ability.new(try_spree_current_user) end - # Redirect as appropriate when an access request fails. The default action is to redirect to the login screen. - # Override this method in your controllers if you want to have special behavior in case the user is not authorized - # to access the requested action. For example, a popup window might simply close itself. - def unauthorized - if try_spree_current_user - flash[:error] = t(:authorization_failure) - redirect_to '/unauthorized' - else - store_location - url = respond_to?(:spree_login_path) ? spree_login_path : root_path - redirect_to url + def redirect_back_or_default(default) + redirect_to(session["spree_user_return_to"] || default) + session["spree_user_return_to"] = nil + end + + def set_guest_token + unless cookies.signed[:guest_token].present? + cookies.permanent.signed[:guest_token] = SecureRandom.urlsafe_base64(nil, false) end end @@ -45,20 +41,42 @@ def store_location disallowed_urls.map!{ |url| url[/\/\w+$/] } unless disallowed_urls.include?(request.fullpath) - session['user_return_to'] = request.fullpath.gsub('//', '/') + session['spree_user_return_to'] = request.fullpath.gsub('//', '/') end end # proxy method to *possible* spree_current_user method # Authentication extensions (such as spree_auth_devise) are meant to provide spree_current_user def try_spree_current_user - respond_to?(:spree_current_user) ? spree_current_user : nil + # This one will be defined by apps looking to hook into Spree + # As per authentication_helpers.rb + if respond_to?(:spree_current_user) + spree_current_user + # This one will be defined by Devise + elsif respond_to?(:current_spree_user) + current_spree_user + else + nil + end end - def redirect_back_or_default(default) - redirect_to(session["user_return_to"] || default) - session["user_return_to"] = nil + # Redirect as appropriate when an access request fails. The default action is to redirect to the login screen. + # Override this method in your controllers if you want to have special behavior in case the user is not authorized + # to access the requested action. For example, a popup window might simply close itself. + def redirect_unauthorized_access + if try_spree_current_user + flash[:error] = Spree.t(:authorization_failure) + redirect_to '/unauthorized' + else + store_location + if respond_to?(:spree_login_path) + redirect_to spree_login_path + else + redirect_to spree.respond_to?(:root_path) ? spree.root_path : root_path + end + end end + end end end diff --git a/core/lib/spree/core/controller_helpers/common.rb b/core/lib/spree/core/controller_helpers/common.rb index f3917c0d088..a2382e39a29 100644 --- a/core/lib/spree/core/controller_helpers/common.rb +++ b/core/lib/spree/core/controller_helpers/common.rb @@ -2,87 +2,71 @@ module Spree module Core module ControllerHelpers module Common - def self.included(base) - base.class_eval do - helper_method :title - helper_method :title= - helper_method :accurate_title - helper_method :current_order - helper_method :current_currency + extend ActiveSupport::Concern + included do + helper_method :title + helper_method :title= + helper_method :accurate_title - layout :get_layout + layout :get_layout - before_filter :set_user_language - end - end + before_filter :set_user_language - protected + protected - # Convenience method for firing instrumentation events with the default payload hash - def fire_event(name, extra_payload = {}) - ActiveSupport::Notifications.instrument(name, default_notification_payload.merge(extra_payload)) - end - - # Creates the hash that is sent as the payload for all notifications. Specific notifications will - # add additional keys as appropriate. Override this method if you need additional data when - # responding to a notification - def default_notification_payload - {:user => try_spree_current_user, :order => current_order} - end + # can be used in views as well as controllers. + # e.g. <% self.title = 'This is a custom title for this view' %> + attr_writer :title - # can be used in views as well as controllers. - # e.g. <% title = 'This is a custom title for this view' %> - attr_writer :title - - def title - title_string = @title.present? ? @title : accurate_title - if title_string.present? - if Spree::Config[:always_put_site_name_in_title] - [default_title, title_string].join(' - ') + def title + title_string = @title.present? ? @title : accurate_title + if title_string.present? + if Spree::Config[:always_put_site_name_in_title] + [title_string, default_title].join(' - ') + else + title_string + end else - title_string + default_title end - else - default_title end - end - def default_title - Spree::Config[:site_name] - end + def default_title + current_store.name + end - # this is a hook for subclasses to provide title - def accurate_title - Spree::Config[:default_seo_title] - end + # this is a hook for subclasses to provide title + def accurate_title + current_store.seo_title + end - def current_currency - Spree::Config[:currency] - end + def render_404(exception = nil) + respond_to do |type| + type.html { render :status => :not_found, :file => "#{::Rails.root}/public/404", :formats => [:html], :layout => nil} + type.all { render :status => :not_found, :nothing => true } + end + end - def render_404(exception = nil) - respond_to do |type| - type.html { render :status => :not_found, :file => "#{::Rails.root}/public/404", :formats => [:html], :layout => nil} - type.all { render :status => :not_found, :nothing => true } + private + + def set_user_language + locale = session[:locale] + locale ||= config_locale if respond_to?(:config_locale, true) + locale ||= Rails.application.config.i18n.default_locale + locale ||= I18n.default_locale unless I18n.available_locales.map(&:to_s).include?(locale) + I18n.locale = locale end - end - private - def set_user_language - locale = session[:locale] - locale ||= Rails.application.config.i18n.default_locale - locale ||= I18n.default_locale unless I18n.available_locales.include?(locale.try(:to_sym)) - I18n.locale = locale.to_sym - end + # Returns which layout to render. + # + # You can set the layout you want to render inside your Spree configuration with the +:layout+ option. + # + # Default layout is: +app/views/spree/layouts/spree_application+ + # + def get_layout + layout ||= Spree::Config[:layout] + end - # Returns which layout to render. - # - # You can set the layout you want to render inside your Spree configuration with the +:layout+ option. - # - # Default layout is: +app/views/spree/layouts/spree_application+ - # - def get_layout - layout ||= Spree::Config[:layout] end end end diff --git a/core/lib/spree/core/controller_helpers/order.rb b/core/lib/spree/core/controller_helpers/order.rb index a5b56315379..e4458418fa4 100644 --- a/core/lib/spree/core/controller_helpers/order.rb +++ b/core/lib/spree/core/controller_helpers/order.rb @@ -2,69 +2,104 @@ module Spree module Core module ControllerHelpers module Order - def self.included(base) - base.class_eval do - helper_method :current_order - before_filter :set_current_order - end - end + extend ActiveSupport::Concern - # This should be overridden by an auth-related extension which would then have the - # opportunity to associate the new order with the # current user before saving. - def before_save_new_order + included do + before_filter :set_current_order + + helper_method :current_currency + helper_method :current_order + helper_method :simple_current_order end - # This should be overridden by an auth-related extension which would then have the - # opporutnity to store tokens, etc. in the session # after saving. - def after_save_new_order + # Used in the link_to_cart helper. + def simple_current_order + + return @simple_current_order if @simple_current_order + + @simple_current_order = find_order_by_token_or_user + + if @simple_current_order + @simple_current_order.last_ip_address = ip_address + return @simple_current_order + else + @simple_current_order = Spree::Order.new + end end - # The current incomplete order from the session for use in cart and during checkout - def current_order(create_order_if_necessary = false) + # The current incomplete order from the guest_token for use in cart and during checkout + def current_order(options = {}) + options[:create_order_if_necessary] ||= false + return @current_order if @current_order - if session[:order_id] - current_order = Spree::Order.find_by_id_and_currency(session[:order_id], current_currency, :include => :adjustments) - @current_order = current_order unless current_order.try(:completed?) - end - if create_order_if_necessary and (@current_order.nil? or @current_order.completed?) - @current_order = Spree::Order.new(currency: current_currency) - before_save_new_order + + @current_order = find_order_by_token_or_user(options, true) + + if options[:create_order_if_necessary] && (@current_order.nil? || @current_order.completed?) + @current_order = Spree::Order.new(current_order_params) + @current_order.user ||= try_spree_current_user + # See issue #3346 for reasons why this line is here + @current_order.created_by ||= try_spree_current_user @current_order.save! - after_save_new_order end - session[:order_id] = @current_order ? @current_order.id : nil - @current_order + + if @current_order + @current_order.last_ip_address = ip_address + return @current_order + end end def associate_user @order ||= current_order if try_spree_current_user && @order - if @order.user.blank? || @order.email.blank? - @order.associate_user!(try_spree_current_user) - end + @order.associate_user!(try_spree_current_user) if @order.user.blank? || @order.email.blank? end + end - # This will trigger any "first order" promotions to be triggered - # Assuming of course that this session variable was set correctly in - # the authentication provider's registrations controller - if session[:spree_user_signup] - fire_event('spree.user.signup', :user => try_spree_current_user, :order => current_order(true)) + def set_current_order + if try_spree_current_user && current_order + try_spree_current_user.orders.incomplete.where('id != ?', current_order.id).each do |order| + current_order.merge!(order, try_spree_current_user) + end end + end - session[:guest_token] = nil - session[:spree_user_signup] = nil + def current_currency + Spree::Config[:currency] end - def set_current_order - if user = try_spree_current_user - last_incomplete_order = user.last_incomplete_spree_order - if session[:order_id].nil? && last_incomplete_order - session[:order_id] = last_incomplete_order.id - elsif current_order && last_incomplete_order && current_order != last_incomplete_order - current_order.merge!(last_incomplete_order) - end + def ip_address + request.remote_ip + end + + private + + def last_incomplete_order + @last_incomplete_order ||= try_spree_current_user.last_incomplete_spree_order + end + + def current_order_params + { currency: current_currency, guest_token: cookies.signed[:guest_token], store_id: current_store.id, user_id: try_spree_current_user.try(:id) } + end + + def find_order_by_token_or_user(options={}, with_adjustments = false) + options[:lock] ||= false + + # Find any incomplete orders for the guest_token + if with_adjustments + order = Spree::Order.incomplete.includes(:adjustments).lock(options[:lock]).find_by(current_order_params) + else + order = Spree::Order.incomplete.lock(options[:lock]).find_by(current_order_params) end + + # Find any incomplete orders for the current user + if order.nil? && try_spree_current_user + order = last_incomplete_order + end + + order end + end end end diff --git a/core/lib/spree/core/controller_helpers/respond_with.rb b/core/lib/spree/core/controller_helpers/respond_with.rb index e85777cb74f..a4e6a11ae21 100644 --- a/core/lib/spree/core/controller_helpers/respond_with.rb +++ b/core/lib/spree/core/controller_helpers/respond_with.rb @@ -1,3 +1,5 @@ +require 'spree/responder' + module ActionController class Base def respond_with(*resources, &block) @@ -17,7 +19,8 @@ def respond_with(*resources, &block) # The action name is needed for processing options.merge!(:action_name => action_name.to_sym) # If responder is not specified then pass in Spree::Responder - (options.delete(:responder) || Spree::Responder).call(self, resources, options) + responder = options.delete(:responder) || self.responder + responder.call(self, resources, options) end end end @@ -33,6 +36,8 @@ module RespondWith included do cattr_accessor :spree_responders self.spree_responders = {} + + self.responder = Spree::Responder end module ClassMethods diff --git a/core/lib/spree/core/controller_helpers/search.rb b/core/lib/spree/core/controller_helpers/search.rb new file mode 100644 index 00000000000..b1a2f8f6a7a --- /dev/null +++ b/core/lib/spree/core/controller_helpers/search.rb @@ -0,0 +1,14 @@ +module Spree + module Core + module ControllerHelpers + module Search + def build_searcher params + Spree::Config.searcher_class.new(params).tap do |searcher| + searcher.current_user = try_spree_current_user + searcher.current_currency = current_currency + end + end + end + end + end +end diff --git a/core/lib/spree/core/controller_helpers/ssl.rb b/core/lib/spree/core/controller_helpers/ssl.rb new file mode 100644 index 00000000000..852be6c197b --- /dev/null +++ b/core/lib/spree/core/controller_helpers/ssl.rb @@ -0,0 +1,60 @@ +module Spree + module Core + module ControllerHelpers + module SSL + extend ActiveSupport::Concern + + included do + before_filter :force_non_ssl_redirect, :if => Proc.new { Spree::Config[:redirect_https_to_http] } + class_attribute :ssl_allowed_actions + + def self.ssl_allowed(*actions) + self.ssl_allowed_actions ||= [] + self.ssl_allowed_actions.concat actions + end + + def self.ssl_required(*actions) + ssl_allowed *actions + if actions.empty? or Rails.application.config.force_ssl + force_ssl :if => :ssl_supported? + else + force_ssl :if => :ssl_supported?, :only => actions + end + end + + def ssl_supported? + return Spree::Config[:allow_ssl_in_production] if Rails.env.production? + return Spree::Config[:allow_ssl_in_staging] if Rails.env.staging? + return Spree::Config[:allow_ssl_in_development_and_test] if (Rails.env.development? or Rails.env.test?) + end + + private + def ssl_allowed? + (!ssl_allowed_actions.nil? && (ssl_allowed_actions.empty? || ssl_allowed_actions.include?(action_name.to_sym))) + end + + # Redirect the existing request to use the HTTP protocol. + # + # ==== Parameters + # * host - Redirect to a different host name + def force_non_ssl_redirect(host = nil) + if request.ssl? && !ssl_allowed? + if request.get? + redirect_options = { + :protocol => 'http://', + :host => host || request.host, + :path => request.fullpath, + } + flash.keep if respond_to?(:flash) + insecure_url = ActionDispatch::Http::URL.url_for(redirect_options) + redirect_to insecure_url, :status => :moved_permanently + else + render :text => Spree.t(:change_protocol, :scope => :ssl), :status => :upgrade_required + end + end + end + end + end + end + end +end diff --git a/core/lib/spree/core/controller_helpers/store.rb b/core/lib/spree/core/controller_helpers/store.rb new file mode 100644 index 00000000000..375bfee2a9e --- /dev/null +++ b/core/lib/spree/core/controller_helpers/store.rb @@ -0,0 +1,19 @@ +module Spree + module Core + module ControllerHelpers + module Store + extend ActiveSupport::Concern + + included do + + def current_store + @current_store ||= Spree::Store.current(request.env['SERVER_NAME']) + end + helper_method :current_store + + end + + end + end + end +end diff --git a/core/lib/spree/core/controller_helpers/strong_parameters.rb b/core/lib/spree/core/controller_helpers/strong_parameters.rb new file mode 100644 index 00000000000..78e75798556 --- /dev/null +++ b/core/lib/spree/core/controller_helpers/strong_parameters.rb @@ -0,0 +1,42 @@ +module Spree + module Core + module ControllerHelpers + module StrongParameters + def permitted_attributes + Spree::PermittedAttributes + end + + delegate *Spree::PermittedAttributes::ATTRIBUTES, + to: :permitted_attributes, + prefix: :permitted + + def permitted_payment_attributes + permitted_attributes.payment_attributes + [ + source_attributes: permitted_source_attributes + ] + end + + def permitted_checkout_attributes + permitted_attributes.checkout_attributes + [ + bill_address_attributes: permitted_address_attributes, + ship_address_attributes: permitted_address_attributes, + payments_attributes: permitted_payment_attributes, + shipments_attributes: permitted_shipment_attributes + ] + end + + def permitted_order_attributes + permitted_checkout_attributes + [ + line_items_attributes: permitted_line_item_attributes + ] + end + + def permitted_product_attributes + permitted_attributes.product_attributes + [ + product_properties_attributes: permitted_product_properties_attributes + ] + end + end + end + end +end diff --git a/core/lib/spree/core/custom_fixtures.rb b/core/lib/spree/core/custom_fixtures.rb deleted file mode 100644 index e80c804d02a..00000000000 --- a/core/lib/spree/core/custom_fixtures.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'active_record/fixtures' - -module Spree - module Core - class Fixtures < ActiveRecord::Fixtures - # Replace this method to prevent the table being emptied on each call. Needed - # when both core & auth have user fixtures, see below for code commented out. - # - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = [table_names].flatten.map { |n| n.to_s } - table_names.each { |n| - class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') - } - - # FIXME: Apparently JK uses this. - connection = block_given? ? yield : ActiveRecord::Base.connection - - files_to_read = table_names.reject { |table_name| - fixture_is_cached?(connection, table_name) - } - - unless files_to_read.empty? - connection.disable_referential_integrity do - fixtures_map = {} - - fixture_files = files_to_read.map do |path| - table_name = path.tr '/', '_' - - fixtures_map[path] = ActiveRecord::Fixtures.new( - connection, - table_name, - class_names[table_name.to_sym] || table_name.classify, - ::File.join(fixtures_directory, path)) - end - - all_loaded_fixtures.update(fixtures_map) - - connection.transaction(:requires_new => true) do - fixture_files.each do |ff| - conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection - table_rows = ff.table_rows - - # REMOVED BY SPREE - # table_rows.keys.each do |table| - # conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' - # end - - table_rows.each do |table_name,rows| - rows.each do |row| - conn.insert_fixture(row, table_name) - end - end - end - - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name.tr('/', '_')) - end - end - end - - cache_fixtures(connection, fixtures_map) - end - end - cached_fixtures(connection, table_names) - end - end - end -end diff --git a/core/lib/spree/core/delegate_belongs_to.rb b/core/lib/spree/core/delegate_belongs_to.rb index 759e336acc3..e168fc006b5 100644 --- a/core/lib/spree/core/delegate_belongs_to.rb +++ b/core/lib/spree/core/delegate_belongs_to.rb @@ -4,8 +4,8 @@ # # Todo - integrate with ActiveRecord::Dirty to make sure changes to delegate object are noticed # Should do -# class User < ActiveRecord::Base; delegate_belongs_to :contact, :firstname; end -# class Contact < ActiveRecord::Base; end +# class User < Spree::Base; delegate_belongs_to :contact, :firstname; end +# class Contact < Spree::Base; end # u = User.first # u.changed? # => false # u.firstname = 'Bobby' @@ -38,14 +38,11 @@ def delegate_belongs_to(association, *attrs) attrs.concat get_association_column_names(association) if attrs.delete :defaults attrs.each do |attr| class_def attr do |*args| - if args.empty? - send(:delegator_for, association).send(attr) - else - send(:delegator_for, association).send(attr, *args) - end + send(:delegator_for, association, attr, *args) end + class_def "#{attr}=" do |val| - send(:delegator_for, association).send("#{attr}=", val) + send(:delegator_for_setter, association, attr, val) end end end @@ -71,19 +68,28 @@ def initialize_association(type, association, opts={}) end private - def class_def(name, method=nil, &blk) class_eval { method.nil? ? define_method(name, &blk) : define_method(name, method) } end + end + def delegator_for(association, attr, *args) + return if self.class.column_names.include?(attr.to_s) + send("#{association}=", self.class.reflect_on_association(association).klass.new) if send(association).nil? + if args.empty? + send(association).send(attr) + else + send(association).send(attr, *args) + end end - def delegator_for(association) + def delegator_for_setter(association, attr, val) + return if self.class.column_names.include?(attr.to_s) send("#{association}=", self.class.reflect_on_association(association).klass.new) if send(association).nil? - send(association) + send(association).send("#{attr}=", val) end protected :delegator_for - + protected :delegator_for_setter end -ActiveRecord::Base.send :include, DelegateBelongsTo \ No newline at end of file +ActiveRecord::Base.send :include, DelegateBelongsTo diff --git a/core/lib/spree/core/engine.rb b/core/lib/spree/core/engine.rb index 32eb2b59fe5..3fbbaf18093 100644 --- a/core/lib/spree/core/engine.rb +++ b/core/lib/spree/core/engine.rb @@ -4,63 +4,34 @@ class Engine < ::Rails::Engine isolate_namespace Spree engine_name 'spree' - config.middleware.use "Spree::Core::Middleware::SeoAssist" - config.middleware.use "Spree::Core::Middleware::RedirectLegacyProductUrl" - - config.autoload_paths += %W(#{config.root}/lib) - - def self.activate - end - - config.to_prepare &method(:activate).to_proc - - Rabl.configure do |config| - config.include_json_root = false - config.include_child_root = false - end - - config.after_initialize do - ActiveSupport::Notifications.subscribe(/^spree\./) do |*args| - event_name, start_time, end_time, id, payload = args - Activator.active.event_name_starts_with(event_name).each do |activator| - payload[:event_name] = event_name - activator.activate(payload) - end - end + rake_tasks do + load File.join(root, "lib", "tasks", "exchanges.rake") end - # We need to reload the routes here due to how Spree sets them up. - # The different facets of Spree (auth, promo, etc.) append/prepend routes to Core - # *after* Core has been loaded. - # - # So we wait until after initialization is complete to do one final reload. - # This then makes the appended/prepended routes available to the application. - config.after_initialize do - Rails.application.routes_reloader.reload! - end - - initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new Spree::Config = app.config.spree.preferences #legacy access end - initializer "spree.load_preferences", :before => "spree.environment" do - ::ActiveRecord::Base.send :include, Spree::Preferences::Preferable - end - initializer "spree.register.calculators" do |app| app.config.spree.calculators.shipping_methods = [ - Spree::Calculator::FlatPercentItemTotal, - Spree::Calculator::FlatRate, - Spree::Calculator::FlexiRate, - Spree::Calculator::PerItem, - Spree::Calculator::PriceSack] + Spree::Calculator::Shipping::FlatPercentItemTotal, + Spree::Calculator::Shipping::FlatRate, + Spree::Calculator::Shipping::FlexiRate, + Spree::Calculator::Shipping::PerItem, + Spree::Calculator::Shipping::PriceSack] app.config.spree.calculators.tax_rates = [ Spree::Calculator::DefaultTax] end + initializer "spree.register.stock_splitters" do |app| + app.config.spree.stock_splitters = [ + Spree::Stock::Splitter::ShippingCategory, + Spree::Stock::Splitter::Backordered + ] + end + initializer "spree.register.payment_methods" do |app| app.config.spree.payment_methods = [ Spree::Gateway::Bogus, @@ -68,30 +39,77 @@ def self.activate Spree::PaymentMethod::Check ] end + # We need to define promotions rules here so extensions and existing apps + # can add their custom classes on their initializer files + initializer 'spree.promo.environment' do |app| + app.config.spree.add_class('promotions') + app.config.spree.promotions = Spree::Promo::Environment.new + app.config.spree.promotions.rules = [] + end + + initializer 'spree.promo.register.promotion.calculators' do |app| + app.config.spree.calculators.add_class('promotion_actions_create_adjustments') + app.config.spree.calculators.promotion_actions_create_adjustments = [ + Spree::Calculator::FlatPercentItemTotal, + Spree::Calculator::FlatRate, + Spree::Calculator::FlexiRate, + Spree::Calculator::TieredPercent, + Spree::Calculator::TieredFlatRate + ] + + app.config.spree.calculators.add_class('promotion_actions_create_item_adjustments') + app.config.spree.calculators.promotion_actions_create_item_adjustments = [ + Spree::Calculator::PercentOnLineItem, + Spree::Calculator::FlatRate, + Spree::Calculator::FlexiRate + ] + end + + # Promotion rules need to be evaluated on after initialize otherwise + # Spree.user_class would be nil and users might experience errors related + # to malformed model associations (Spree.user_class is only defined on + # the app initializer) + config.after_initialize do + Rails.application.config.spree.promotions.rules.concat [ + Spree::Promotion::Rules::ItemTotal, + Spree::Promotion::Rules::Product, + Spree::Promotion::Rules::User, + Spree::Promotion::Rules::FirstOrder, + Spree::Promotion::Rules::UserLoggedIn, + Spree::Promotion::Rules::OneUsePerUser, + Spree::Promotion::Rules::Taxon, + ] + end + + initializer 'spree.promo.register.promotions.actions' do |app| + app.config.spree.promotions.actions = [ + Promotion::Actions::CreateAdjustment, + Promotion::Actions::CreateItemAdjustments, + Promotion::Actions::CreateLineItems, + Promotion::Actions::FreeShipping] + end + # filter sensitive information during logging initializer "spree.params.filter" do |app| - app.config.filter_parameters += [:password, :password_confirmation, :number] + app.config.filter_parameters += [ + :password, + :password_confirmation, + :number, + :verification_value] end - # sets the manifests / assets to be precompiled, even when initialize_on_precompile is false - initializer "spree.assets.precompile", :group => :all do |app| - app.config.assets.precompile += %w[ - store/all.* - admin/all.* - admin/orders/edit_form.js - admin/address_states.js - jqPlot/excanvas.min.js - admin/images/new.js - jquery.jstree/themes/apple/* - ] + initializer "spree.core.checking_migrations" do |app| + Migrations.new(config, engine_name).check end - initializer "spree.mail.settings" do |app| - if Spree::MailMethod.table_exists? - Spree::Core::MailSettings.init - Mail.register_interceptor(Spree::Core::MailInterceptor) + config.to_prepare do + # Load application's model / class decorators + Dir.glob(File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')) do |c| + Rails.configuration.cache_classes ? require(c) : load(c) end end end end end + +require 'spree/core/routes' diff --git a/core/lib/spree/core/environment.rb b/core/lib/spree/core/environment.rb index 443b110c0a6..7ff2f0330b7 100644 --- a/core/lib/spree/core/environment.rb +++ b/core/lib/spree/core/environment.rb @@ -3,7 +3,8 @@ module Core class Environment include EnvironmentExtension - attr_accessor :calculators, :payment_methods, :preferences + attr_accessor :calculators, :payment_methods, :preferences, + :stock_splitters def initialize @calculators = Calculators.new diff --git a/core/lib/spree/core/ext/active_record.rb b/core/lib/spree/core/ext/active_record.rb deleted file mode 100644 index 233a6232ccd..00000000000 --- a/core/lib/spree/core/ext/active_record.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord::Persistence - - # Update attributes of a record in the database without callbacks, validations etc. - def update_attributes_without_callbacks(attributes) - self.assign_attributes(attributes, :without_protection => true) - self.class.update_all(attributes, { :id => id }) - end - - # Update a single attribute in the database - def update_attribute_without_callbacks(name, value) - send("#{name}=", value) - update_attributes_without_callbacks(name => value) - end - -end diff --git a/core/lib/spree/core/gateway_error.rb b/core/lib/spree/core/gateway_error.rb deleted file mode 100644 index 5a6ddda30b7..00000000000 --- a/core/lib/spree/core/gateway_error.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Spree - module Core - class GatewayError < RuntimeError; end - end -end diff --git a/core/lib/spree/core/importer.rb b/core/lib/spree/core/importer.rb new file mode 100644 index 00000000000..e71b9e0d5ba --- /dev/null +++ b/core/lib/spree/core/importer.rb @@ -0,0 +1,9 @@ +module Spree + module Core + module Importer + end + end +end + +require 'spree/core/importer/order' +require 'spree/core/importer/product' diff --git a/core/lib/spree/core/importer/order.rb b/core/lib/spree/core/importer/order.rb new file mode 100644 index 00000000000..8260c23fd58 --- /dev/null +++ b/core/lib/spree/core/importer/order.rb @@ -0,0 +1,236 @@ +module Spree + module Core + module Importer + class Order + + def self.import(user, params) + begin + ensure_country_id_from_params params[:ship_address_attributes] + ensure_state_id_from_params params[:ship_address_attributes] + ensure_country_id_from_params params[:bill_address_attributes] + ensure_state_id_from_params params[:bill_address_attributes] + + create_params = params.slice :currency + order = Spree::Order.create! create_params + order.associate_user!(user) + + shipments_attrs = params.delete(:shipments_attributes) + + create_shipments_from_params(shipments_attrs, order) + create_line_items_from_params(params.delete(:line_items_attributes),order) + create_shipments_from_params(params.delete(:shipments_attributes), order) + create_adjustments_from_params(params.delete(:adjustments_attributes), order) + create_payments_from_params(params.delete(:payments_attributes), order) + + if completed_at = params.delete(:completed_at) + order.completed_at = completed_at + order.state = 'complete' + end + + params.delete(:user_id) unless user.try(:has_spree_role?, "admin") && params.key?(:user_id) + + order.update_attributes!(params) + + order.create_proposed_shipments unless shipments_attrs.present? + + # Really ensure that the order totals & states are correct + order.updater.update + if shipments_attrs.present? + order.shipments.each_with_index do |shipment, index| + shipment.update_columns(cost: shipments_attrs[index][:cost].to_f) if shipments_attrs[index][:cost].present? + end + end + order.reload + rescue Exception => e + order.destroy if order && order.persisted? + raise e.message + end + end + + def self.create_shipments_from_params(shipments_hash, order) + return [] unless shipments_hash + + line_items = order.line_items + shipments_hash.each do |s| + begin + shipment = order.shipments.build + shipment.tracking = s[:tracking] + shipment.stock_location = Spree::StockLocation.find_by_admin_name(s[:stock_location]) || Spree::StockLocation.find_by_name!(s[:stock_location]) + + inventory_units = s[:inventory_units] || [] + inventory_units.each do |iu| + ensure_variant_id_from_params(iu) + + unit = shipment.inventory_units.build + unit.order = order + + # Spree expects a Inventory Unit to always reference a line + # item and variant otherwise users might get exceptions when + # trying to view these units. Note the Importer might not be + # able to find the line item if line_item.variant_id |= iu.variant_id + unit.variant_id = iu[:variant_id] + unit.line_item_id = line_items.select do |l| + l.variant_id.to_i == iu[:variant_id].to_i + end.first.try(:id) + end + + # Mark shipped if it should be. + if s[:shipped_at].present? + shipment.shipped_at = s[:shipped_at] + shipment.state = 'shipped' + shipment.inventory_units.each do |unit| + unit.state = 'shipped' + end + end + + shipment.save! + + shipping_method = Spree::ShippingMethod.find_by_name(s[:shipping_method]) || Spree::ShippingMethod.find_by_admin_name!(s[:shipping_method]) + rate = shipment.shipping_rates.create!(:shipping_method => shipping_method, + :cost => s[:cost]) + shipment.selected_shipping_rate_id = rate.id + shipment.update_amounts + + rescue Exception => e + raise "Order import shipments: #{e.message} #{s}" + end + end + end + + def self.create_line_items_from_params(line_items_hash, order) + return {} unless line_items_hash + line_items_hash.each_key do |k| + begin + extra_params = line_items_hash[k].except(:variant_id, :quantity, :sku) + line_item = ensure_variant_id_from_params(line_items_hash[k]) + line_item = order.contents.add(Spree::Variant.find(line_item[:variant_id]), line_item[:quantity]) + # Raise any errors with saving to prevent import succeeding with line items failing silently. + if extra_params.present? + line_item.update_attributes!(extra_params) + else + line_item.save! + end + rescue Exception => e + raise "Order import line items: #{e.message} #{line_item}" + end + end + end + + def self.create_adjustments_from_params(adjustments, order) + return [] unless adjustments + adjustments.each do |a| + begin + adjustment = order.adjustments.build( + order: order, + amount: a[:amount].to_f, + label: a[:label] + ) + adjustment.save! + adjustment.close! + rescue Exception => e + raise "Order import adjustments: #{e.message} #{a}" + end + end + end + + def self.create_payments_from_params(payments_hash, order) + return [] unless payments_hash + payments_hash.each do |p| + begin + payment = order.payments.build order: order + payment.amount = p[:amount].to_f + # Order API should be using state as that's the normal payment field. + # spree_wombat serializes payment state as status so imported orders should fall back to status field. + payment.state = p[:state] || p[:status] || 'completed' + payment.payment_method = Spree::PaymentMethod.find_by_name!(p[:payment_method]) + payment.source = create_source_payment_from_params(p[:source], payment) if p[:source] + payment.save! + rescue Exception => e + raise "Order import payments: #{e.message} #{p}" + end + end + end + + def self.create_source_payment_from_params(source_hash, payment) + begin + Spree::CreditCard.create( + month: source_hash[:month], + year: source_hash[:year], + cc_type: source_hash[:cc_type], + last_digits: source_hash[:last_digits], + name: source_hash[:name], + payment_method: payment.payment_method, + gateway_customer_profile_id: source_hash[:gateway_customer_profile_id], + gateway_payment_profile_id: source_hash[:gateway_payment_profile_id], + imported: true + ) + rescue Exception => e + raise "Order import source payments: #{e.message} #{source_hash}" + end + end + + def self.ensure_variant_id_from_params(hash) + begin + sku = hash.delete(:sku) + unless hash[:variant_id].present? + hash[:variant_id] = Spree::Variant.active.find_by_sku!(sku).id + end + hash + rescue ActiveRecord::RecordNotFound => e + raise "Ensure order import variant: Variant w/SKU #{sku} not found." + rescue Exception => e + raise "Ensure order import variant: #{e.message} #{hash}" + end + end + + def self.ensure_country_id_from_params(address) + return if address.nil? or address[:country_id].present? or address[:country].nil? + + begin + search = {} + if name = address[:country]['name'] + search[:name] = name + elsif iso_name = address[:country]['iso_name'] + search[:iso_name] = iso_name.upcase + elsif iso = address[:country]['iso'] + search[:iso] = iso.upcase + elsif iso3 = address[:country]['iso3'] + search[:iso3] = iso3.upcase + end + + address.delete(:country) + address[:country_id] = Spree::Country.where(search).first!.id + + rescue Exception => e + raise "Ensure order import address country: #{e.message} #{search}" + end + end + + def self.ensure_state_id_from_params(address) + return if address.nil? or address[:state_id].present? or address[:state].nil? + + begin + search = {} + if name = address[:state]['name'] + search[:name] = name + elsif abbr = address[:state]['abbr'] + search[:abbr] = abbr.upcase + end + + address.delete(:state) + search[:country_id] = address[:country_id] + + if state = Spree::State.where(search).first + address[:state_id] = state.id + else + address[:state_name] = search[:name] || search[:abbr] + end + rescue Exception => e + raise "Ensure order import address state: #{e.message} #{search}" + end + end + + end + end + end +end diff --git a/core/lib/spree/core/importer/product.rb b/core/lib/spree/core/importer/product.rb new file mode 100644 index 00000000000..b7875a0e9f0 --- /dev/null +++ b/core/lib/spree/core/importer/product.rb @@ -0,0 +1,62 @@ +module Spree + module Core + module Importer + class Product + attr_reader :product, :product_attrs, :variants_attrs, :options_attrs + + def initialize(product, product_params, options = {}) + @product = product || Spree::Product.new(product_params) + + @product_attrs = product_params + @variants_attrs = options[:variants_attrs] || [] + @options_attrs = options[:options_attrs] || [] + end + + def create + if product.save + variants_attrs.each do |variant_attribute| + # make sure the product is assigned before the options= + product.variants.create({ product: product }.merge(variant_attribute)) + end + + set_up_options + end + + product + end + + def update + if product.update_attributes(product_attrs) + variants_attrs.each do |variant_attribute| + # update the variant if the id is present in the payload + if variant_attribute['id'].present? + product.variants.find(variant_attribute['id'].to_i).update_attributes(variant_attribute) + else + # make sure the product is assigned before the options= + product.variants.create({ product: product }.merge(variant_attribute)) + end + end + + set_up_options + end + + product + end + + private + def set_up_options + options_attrs.each do |name| + option_type = Spree::OptionType.where(name: name).first_or_initialize do |option_type| + option_type.presentation = name + option_type.save! + end + + unless product.option_types.include?(option_type) + product.option_types << option_type + end + end + end + end + end + end +end diff --git a/core/lib/spree/core/mail_interceptor.rb b/core/lib/spree/core/mail_interceptor.rb deleted file mode 100644 index 3a45b7ef5d2..00000000000 --- a/core/lib/spree/core/mail_interceptor.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Allows us to intercept any outbound mail message and make last minute changes (such as specifying a "from" address or -# sending to a test email account.) -# -# See http://railscasts.com/episodes/206-action-mailer-in-rails-3 for more details. -module Spree - module Core - class MailInterceptor - - def self.delivering_email(message) - return unless mail_method = Spree::MailMethod.current - message.from ||= mail_method.preferred_mails_from - - if mail_method.preferred_intercept_email.present? - message.subject = "[#{message.to}] #{message.subject}" - message.to = mail_method.preferred_intercept_email - end - - if mail_method.preferred_mail_bcc.present? - message.bcc ||= mail_method.preferred_mail_bcc - end - end - - end - end -end diff --git a/core/lib/spree/core/mail_settings.rb b/core/lib/spree/core/mail_settings.rb deleted file mode 100644 index 6badb9e4770..00000000000 --- a/core/lib/spree/core/mail_settings.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Spree - module Core - module MailSettings - - # Override the Rails application mail settings based on preference. - # This makes it possible to configure the mail settings - # through an admin interface instead of requiring changes to the Rails envrionment file. - def self.init - ActionMailer::Base.default_url_options[:host] = Spree::Config[:site_url] - return unless mail_method = Spree::MailMethod.current - if mail_method.prefers_enable_mail_delivery? - mail_server_settings = { - :address => mail_method.preferred_mail_host, - :domain => mail_method.preferred_mail_domain, - :port => mail_method.preferred_mail_port, - :authentication => mail_method.preferred_mail_auth_type - } - - if mail_method.preferred_mail_auth_type != 'none' - mail_server_settings[:user_name] = mail_method.preferred_smtp_username - mail_server_settings[:password] = mail_method.preferred_smtp_password - end - - tls = mail_method.preferred_secure_connection_type == 'TLS' - mail_server_settings[:enable_starttls_auto] = tls - - ActionMailer::Base.smtp_settings = mail_server_settings - ActionMailer::Base.perform_deliveries = true - else - #logger.warn "NOTICE: Mail not enabled" - ActionMailer::Base.perform_deliveries = false - end - end - - end - end -end diff --git a/core/lib/spree/core/middleware/redirect_legacy_product_url.rb b/core/lib/spree/core/middleware/redirect_legacy_product_url.rb deleted file mode 100644 index 9e888cf318b..00000000000 --- a/core/lib/spree/core/middleware/redirect_legacy_product_url.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Spree - module Core - module Middleware - class RedirectLegacyProductUrl - def initialize(app) - @app = app - end - - def call(env) - if env["PATH_INFO"] =~ %r{/t/.+/p/(.+)} - return [301, {'Location'=> "/products/#{$1}" }, []] - end - @app.call(env) - end - end - end - end -end diff --git a/core/lib/spree/core/permalinks.rb b/core/lib/spree/core/permalinks.rb index 094223cfcce..e875a94f978 100644 --- a/core/lib/spree/core/permalinks.rb +++ b/core/lib/spree/core/permalinks.rb @@ -14,11 +14,7 @@ def make_permalink(options={}) options[:field] ||= :permalink self.permalink_options = options - validates permalink_options[:field], :uniqueness => true - - if self.table_exists? && self.column_names.include?(permalink_options[:field].to_s) - before_validation(:on => :create) { save_permalink } - end + before_validation(:on => :create) { save_permalink } end def find_by_param(value, *args) @@ -33,27 +29,43 @@ def permalink_field permalink_options[:field] end + def permalink_prefix + permalink_options[:prefix] || "" + end + + def permalink_length + permalink_options[:length] || 9 + end + def permalink_order order = permalink_options[:order] "#{order} ASC," if order end end + def generate_permalink + "#{self.class.permalink_prefix}#{Array.new(self.class.permalink_length){rand(9)}.join}" + end + def save_permalink(permalink_value=self.to_param) - field = self.class.permalink_field - # Do other links exist with this permalink? - other = self.class.where("#{field} LIKE ?", "#{permalink_value}%") - if other.any? - # Find the existing permalink with the highest number, and increment that number. - # (If none of the existing permalinks have a number, this will evaluate to 1.) - number = other.map { |o| o.send(field)[/-(\d+)$/, 1].to_i }.max + 1 - permalink_value += "-#{number.to_s}" - end - write_attribute(field, permalink_value) + self.with_lock do + permalink_value ||= generate_permalink + + field = self.class.permalink_field + # Do other links exist with this permalink? + other = self.class.where("#{self.class.table_name}.#{field} LIKE ?", "#{permalink_value}%") + if other.any? + # Find the existing permalink with the highest number, and increment that number. + # (If none of the existing permalinks have a number, this will evaluate to 1.) + number = other.map { |o| o.send(field)[/-(\d+)$/, 1].to_i }.max + 1 + permalink_value += "-#{number.to_s}" + end + write_attribute(field, permalink_value) + end end end end end ActiveRecord::Base.send :include, Spree::Core::Permalinks -ActiveRecord::Relation.send :include, Spree::Core::Permalinks +ActiveRecord::Relation.send :include, Spree::Core::Permalinks \ No newline at end of file diff --git a/core/lib/spree/core/product_duplicator.rb b/core/lib/spree/core/product_duplicator.rb new file mode 100644 index 00000000000..8615683cab2 --- /dev/null +++ b/core/lib/spree/core/product_duplicator.rb @@ -0,0 +1,74 @@ +module Spree + class ProductDuplicator + attr_accessor :product + + @@clone_images_default = true + mattr_accessor :clone_images_default + + def initialize(product, include_images = @@clone_images_default) + @product = product + @include_images = include_images + end + + def duplicate + new_product = duplicate_product + + # don't dup the actual variants, just the characterising types + new_product.option_types = product.option_types if product.has_variants? + + # allow site to do some customization + new_product.send(:duplicate_extra, product) if new_product.respond_to?(:duplicate_extra) + new_product.save! + new_product + end + + protected + + def duplicate_product + product.dup.tap do |new_product| + new_product.name = "COPY OF #{product.name}" + new_product.taxons = product.taxons + new_product.created_at = nil + new_product.deleted_at = nil + new_product.updated_at = nil + new_product.product_properties = reset_properties + new_product.master = duplicate_master + new_product.variants = product.variants.map { |variant| duplicate_variant variant } + end + end + + def duplicate_master + master = product.master + master.dup.tap do |new_master| + new_master.sku = "COPY OF #{master.sku}" + new_master.deleted_at = nil + new_master.images = master.images.map { |image| duplicate_image image } if @include_images + new_master.price = master.price + new_master.currency = master.currency + end + end + + def duplicate_variant(variant) + new_variant = variant.dup + new_variant.sku = "COPY OF #{new_variant.sku}" + new_variant.deleted_at = nil + new_variant.option_values = variant.option_values.map { |option_value| option_value} + new_variant + end + + def duplicate_image(image) + new_image = image.dup + new_image.assign_attributes(:attachment => image.attachment.clone) + new_image + end + + def reset_properties + product.product_properties.map do |prop| + prop.dup.tap do |new_prop| + new_prop.created_at = nil + new_prop.updated_at = nil + end + end + end + end +end diff --git a/core/lib/spree/core/product_filters.rb b/core/lib/spree/core/product_filters.rb new file mode 100644 index 00000000000..81e11ccaa9b --- /dev/null +++ b/core/lib/spree/core/product_filters.rb @@ -0,0 +1,194 @@ +module Spree + module Core + # THIS FILE SHOULD BE OVER-RIDDEN IN YOUR SITE EXTENSION! + # the exact code probably won't be useful, though you're welcome to modify and reuse + # the current contents are mainly for testing and documentation + + # To override this file... + # 1) Make a copy of it in your sites local /lib/spree folder + # 2) Add it to the config load path, or require it in an initializer, e.g... + # + # # config/initializers/spree.rb + # require 'spree/product_filters' + # + + # set up some basic filters for use with products + # + # Each filter has two parts + # * a parametrized named scope which expects a list of labels + # * an object which describes/defines the filter + # + # The filter description has three components + # * a name, for displaying on pages + # * a named scope which will 'execute' the filter + # * a mapping of presentation labels to the relevant condition (in the context of the named scope) + # * an optional list of labels and values (for use with object selection - see taxons examples below) + # + # The named scopes here have a suffix '_any', following Ransack's convention for a + # scope which returns results which match any of the inputs. This is purely a convention, + # but might be a useful reminder. + # + # When creating a form, the name of the checkbox group for a filter F should be + # the name of F's scope with [] appended, eg "price_range_any[]", and for + # each label you should have a checkbox with the label as its value. On submission, + # Rails will send the action a hash containing (among other things) an array named + # after the scope whose values are the active labels. + # + # Ransack will then convert this array to a call to the named scope with the array + # contents, and the named scope will build a query with the disjunction of the conditions + # relating to the labels, all relative to the scope's context. + # + # The details of how/when filters are used is a detail for specific models (eg products + # or taxons), eg see the taxon model/controller. + + # See specific filters below for concrete examples. + module ProductFilters + # Example: filtering by price + # The named scope just maps incoming labels onto their conditions, and builds the conjunction + # 'price' is in the base scope's context (ie, "select foo from products where ...") so + # we can access the field right away + # The filter identifies which scope to use, then sets the conditions for each price range + # + # If user checks off three different price ranges then the argument passed to + # below scope would be something like ["$10 - $15", "$15 - $18", "$18 - $20"] + # + Spree::Product.add_search_scope :price_range_any do |*opts| + conds = opts.map {|o| Spree::Core::ProductFilters.price_filter[:conds][o]}.reject { |c| c.nil? } + scope = conds.shift + conds.each do |new_scope| + scope = scope.or(new_scope) + end + Spree::Product.joins(master: :default_price).where(scope) + end + + def ProductFilters.format_price(amount) + Spree::Money.new(amount) + end + + def ProductFilters.price_filter + v = Spree::Price.arel_table + conds = [ [ Spree.t(:under_price, price: format_price(10)) , v[:amount].lteq(10)], + [ "#{format_price(10)} - #{format_price(15)}" , v[:amount].in(10..15)], + [ "#{format_price(15)} - #{format_price(18)}" , v[:amount].in(15..18)], + [ "#{format_price(18)} - #{format_price(20)}" , v[:amount].in(18..20)], + [ Spree.t(:or_over_price, price: format_price(20)) , v[:amount].gteq(20)]] + { + name: Spree.t(:price_range), + scope: :price_range_any, + conds: Hash[*conds.flatten], + labels: conds.map { |k,v| [k, k] } + } + end + + + # Example: filtering by possible brands + # + # First, we define the scope. Two interesting points here: (a) we run our conditions + # in the scope where the info for the 'brand' property has been loaded; and (b) + # because we may want to filter by other properties too, we give this part of the + # query a unique name (which must be used in the associated conditions too). + # + # Secondly, the filter. Instead of a static list of values, we pull out all existing + # brands from the db, and then build conditions which test for string equality on + # the (uniquely named) field "p_brand.value". There's also a test for brand info + # being blank: note that this relies on with_property doing a left outer join + # rather than an inner join. + Spree::Product.add_search_scope :brand_any do |*opts| + conds = opts.map {|o| ProductFilters.brand_filter[:conds][o]}.reject { |c| c.nil? } + scope = conds.shift + conds.each do |new_scope| + scope = scope.or(new_scope) + end + Spree::Product.with_property('brand').where(scope) + end + + def ProductFilters.brand_filter + brand_property = Spree::Property.find_by(name: 'brand') + brands = brand_property ? Spree::ProductProperty.where(property_id: brand_property.id).pluck(:value).uniq.map(&:to_s) : [] + pp = Spree::ProductProperty.arel_table + conds = Hash[*brands.map { |b| [b, pp[:value].eq(b)] }.flatten] + { + name: 'Brands', + scope: :brand_any, + conds: conds, + labels: (brands.sort).map { |k| [k, k] } + } + end + + # Example: a parameterized filter + # The filter above may show brands which aren't applicable to the current taxon, + # so this one only shows the brands that are relevant to a particular taxon and + # its descendants. + # + # We don't have to give a new scope since the conditions here are a subset of the + # more general filter, so decoding will still work - as long as the filters on a + # page all have unique names (ie, you can't use the two brand filters together + # if they use the same scope). To be safe, the code uses a copy of the scope. + # + # HOWEVER: what happens if we want a more precise scope? we can't pass + # parametrized scope names to Ransack, only atomic names, so couldn't ask + # for taxon T's customized filter to be used. BUT: we can arrange for the form + # to pass back a hash instead of an array, where the key acts as the (taxon) + # parameter and value is its label array, and then get a modified named scope + # to get its conditions from a particular filter. + # + # The brand-finding code can be simplified if a few more named scopes were added to + # the product properties model. + Spree::Product.add_search_scope :selective_brand_any do |*opts| + Spree::Product.brand_any(*opts) + end + + def ProductFilters.selective_brand_filter(taxon = nil) + taxon ||= Spree::Taxonomy.first.root + brand_property = Spree::Property.find_by(name: 'brand') + scope = Spree::ProductProperty.where(property: brand_property). + joins(product: :taxons). + where("#{Spree::Taxon.table_name}.id" => [taxon] + taxon.descendants) + brands = scope.pluck(:value).uniq + { + name: 'Applicable Brands', + scope: :selective_brand_any, + labels: brands.sort.map { |k| [k, k] } + } + end + + # Provide filtering on the immediate children of a taxon + # + # This doesn't fit the pattern of the examples above, so there's a few changes. + # Firstly, it uses an existing scope which was not built for filtering - and so + # has no need of a conditions mapping, and secondly, it has a mapping of name + # to the argument type expected by the other scope. + # + # This technique is useful for filtering on objects (by passing ids) or with a + # scope that can be used directly (eg. testing only ever on a single property). + # + # This scope selects products in any of the active taxons or their children. + # + def ProductFilters.taxons_below(taxon) + return Spree::Core::ProductFilters.all_taxons if taxon.nil? + { + name: 'Taxons under ' + taxon.name, + scope: :taxons_id_in_tree_any, + labels: taxon.children.sort_by(&:position).map { |t| [t.name, t.id] }, + conds: nil + } + end + + # Filtering by the list of all taxons + # + # Similar idea as above, but we don't want the descendants' products, hence + # it uses one of the auto-generated scopes from Ransack. + # + # idea: expand the format to allow nesting of labels? + def ProductFilters.all_taxons + taxons = Spree::Taxonomy.all.map { |t| [t.root] + t.root.descendants }.flatten + { + name: 'All taxons', + scope: :taxons_id_equals_any, + labels: taxons.sort_by(&:name).map { |t| [t.name, t.id] }, + conds: nil # not needed + } + end + end + end +end diff --git a/core/lib/spree/core/routes.rb b/core/lib/spree/core/routes.rb new file mode 100644 index 00000000000..a1f568090bb --- /dev/null +++ b/core/lib/spree/core/routes.rb @@ -0,0 +1,46 @@ +module Spree + module Core + class Engine < ::Rails::Engine + def self.add_routes(&block) + @spree_routes ||= [] + + # Anything that causes the application's routes to be reloaded, + # will cause this method to be called more than once + # i.e. https://github.com/plataformatec/devise/blob/31971e69e6a1bcf6c7f01eaaa44f227c4af5d4d2/lib/devise/rails.rb#L14 + # In the case of Devise, this *only* happens in the production env + # This coupled with Rails 4's insistence that routes are not drawn twice, + # poses quite a serious problem. + # + # This is mainly why this whole file exists in the first place. + # + # Thus we need to make sure that the routes aren't drawn twice. + unless @spree_routes.include?(block) + @spree_routes << block + end + end + + def self.append_routes(&block) + @append_routes ||= [] + # See comment in add_routes. + unless @append_routes.include?(block) + @append_routes << block + end + end + + def self.draw_routes(&block) + @spree_routes ||= [] + @append_routes ||= [] + eval_block(block) if block_given? + @spree_routes.each { |r| eval_block(&r) } + @append_routes.each { |r| eval_block(&r) } + # # Clear out routes so that they aren't drawn twice. + @spree_routes = [] + @append_routes = [] + end + + def eval_block(&block) + Spree::Core::Engine.routes.eval_block(block) + end + end + end +end diff --git a/core/lib/spree/core/s3_support.rb b/core/lib/spree/core/s3_support.rb deleted file mode 100644 index 60f6f93fd7b..00000000000 --- a/core/lib/spree/core/s3_support.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Spree - module Core - # This module exists to reduce duplication in S3 settings between - # the Image and Taxon models in Spree - module S3Support - extend ActiveSupport::Concern - - included do - def self.supports_s3(field) - # Load user defined paperclip settings - config = Spree::Config - if config[:use_s3] - s3_creds = { :access_key_id => config[:s3_access_key], :secret_access_key => config[:s3_secret], :bucket => config[:s3_bucket] } - self.attachment_definitions[field][:storage] = :s3 - self.attachment_definitions[field][:s3_credentials] = s3_creds - self.attachment_definitions[field][:s3_headers] = ActiveSupport::JSON.decode(config[:s3_headers]) - self.attachment_definitions[field][:bucket] = config[:s3_bucket] - self.attachment_definitions[field][:s3_protocol] = config[:s3_protocol] unless config[:s3_protocol].blank? - self.attachment_definitions[field][:s3_host_alias] = config[:s3_host_alias] unless config[:s3_host_alias].blank? - end - end - end - end - end -end diff --git a/core/lib/spree/core/scopes.rb b/core/lib/spree/core/scopes.rb deleted file mode 100644 index 32e9799edb2..00000000000 --- a/core/lib/spree/core/scopes.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Spree - module Core - # This module contains all custom scopes created for selecting products. - # - # All usable scopes *should* be included in SCOPES constant, it represents - # all scopes that are selectable from user interface, extensions can extend - # and modify it, but should provide corresponding translations. - # - # Format of constant is following: - # - # { - # :namespace/grouping => { - # :name_of_the_scope => [:list, :of, :arguments] - # } - # } - # - # This values are used in translation file, to describe them in the interface. - # So for each scope you define here you have to provide following entry in translation file - # product_scopes: - # name_of_the_group: - # name: Translated name of the group - # description: Longer description of what this scope group does, inluding - # any possible help user may need - # scopes: - # name_of_the_scope: - # name: Short name of the scope - # description: What does this scope does exactly - # arguments: - # arg1: Description of argument - # arg2: Description of second Argument - # - module Scopes - module_function - - def generate_translation(all_scopes) - result = {"groups" => {}, "scopes" => {}} - all_scopes.dup.each_pair do |group_name, scopes| - result["groups"][group_name.to_s] = { - 'name' => group_name.to_s.humanize, - 'description' => "Scopes for selecting products based on #{group_name.to_s}", - } - - scopes.each_pair do |scope_name, targs| - hashed_args = {} - targs.each{|v| hashed_args[v.to_s] = v.to_s.humanize} - - result['scopes'][scope_name.to_s] = { - 'name' => scope_name.to_s.humanize, - 'description' => "", - 'args' => hashed_args.dup - } - end - end - result - end - - def generate_translations - require 'ya2yaml' - { - 'product_scopes' => generate_translation(Spree::ProductScope.all_scopes) - }.ya2yaml - end - end - end -end diff --git a/core/lib/spree/core/search/base.rb b/core/lib/spree/core/search/base.rb index 1781ad02794..f90dc352b8c 100644 --- a/core/lib/spree/core/search/base.rb +++ b/core/lib/spree/core/search/base.rb @@ -13,10 +13,13 @@ def initialize(params) end def retrieve_products - @products_scope = get_base_scope + @products = get_base_scope curr_page = page || 1 - @products = @products_scope.includes([:master => :prices]).where("spree_prices.amount IS NOT NULL").where("spree_prices.currency" => current_currency).page(curr_page).per(per_page) + unless Spree::Config.show_products_without_price + @products = @products.where("spree_prices.amount IS NOT NULL").where("spree_prices.currency" => current_currency) + end + @products = @products.page(curr_page).per(per_page) end def method_missing(name) @@ -32,18 +35,39 @@ def get_base_scope base_scope = Spree::Product.active base_scope = base_scope.in_taxon(taxon) unless taxon.blank? base_scope = get_products_conditions_for(base_scope, keywords) - base_scope = base_scope.on_hand unless Spree::Config[:show_zero_stock_products] base_scope = add_search_scopes(base_scope) + base_scope = add_eagerload_scopes(base_scope) base_scope end + def add_eagerload_scopes scope + # TL;DR Switch from `preload` to `includes` as soon as Rails starts honoring + # `order` clauses on `has_many` associations when a `where` constraint + # affecting a joined table is present (see + # https://github.com/rails/rails/issues/6769). + # + # Ideally this would use `includes` instead of `preload` calls, leaving it + # up to Rails whether associated objects should be fetched in one big join + # or multiple independent queries. However as of Rails 4.1.8 any `order` + # defined on `has_many` associations are ignored when Rails builds a join + # query. + # + # Would we use `includes` in this particular case, Rails would do + # separate queries most of the time but opt for a join as soon as any + # `where` constraints affecting joined tables are added to the search; + # which is the case as soon as a taxon is added to the base scope. + scope = scope.preload(master: :prices) + scope = scope.preload(master: :images) if include_images + scope + end + def add_search_scopes(base_scope) search.each do |name, scope_attribute| scope_name = name.to_sym if base_scope.respond_to?(:search_scopes) && base_scope.search_scopes.include?(scope_name.to_sym) base_scope = base_scope.send(scope_name, *scope_attribute) else - base_scope = base_scope.merge(Spree::Product.search({scope_name => scope_attribute}).result) + base_scope = base_scope.merge(Spree::Product.ransack({scope_name => scope_attribute}).result) end end if search base_scope @@ -61,6 +85,7 @@ def prepare(params) @properties[:taxon] = params[:taxon].blank? ? nil : Spree::Taxon.find(params[:taxon]) @properties[:keywords] = params[:keywords] @properties[:search] = params[:search] + @properties[:include_images] = params[:include_images] per_page = params[:per_page].to_i @properties[:per_page] = per_page > 0 ? per_page : Spree::Config[:products_per_page] diff --git a/core/lib/spree/core/ssl_requirement.rb b/core/lib/spree/core/ssl_requirement.rb deleted file mode 100644 index eecc0bc9faf..00000000000 --- a/core/lib/spree/core/ssl_requirement.rb +++ /dev/null @@ -1,113 +0,0 @@ -# ++ -# Copyright (c) 2007-2012, Spree Commerce, Inc. and other contributors -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Spree Commerce, Inc. nor the names of its -# contributors may be used to endorse or promote products derived from this -# software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -- - -# ++ -# Copyright (c) 2005 David Heinemeier Hansson -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -- - -# Modified version of the ssl_requirement plugin by DHH -module SslRequirement - extend ActiveSupport::Concern - - included do - before_filter(:ensure_proper_protocol) - end - - module ClassMethods - # Specifies that the named actions requires an SSL connection to be performed (which is enforced by ensure_proper_protocol). - def ssl_required(*actions) - class_attribute(:ssl_required_actions) - self.ssl_required_actions = actions - end - - def ssl_allowed(*actions) - class_attribute(:ssl_allowed_actions) - self.ssl_allowed_actions = actions - end - end - - protected - # Returns true if the current action is supposed to run as SSL - def ssl_required? - if self.class.respond_to?(:ssl_required_actions) - actions = self.class.ssl_required_actions - actions.empty? || actions.include?(action_name.to_sym) - else - return false - end - end - - def ssl_allowed? - if self.class.respond_to?(:ssl_allowed_actions) - actions = self.class.ssl_allowed_actions - actions.empty? || actions.include?(action_name.to_sym) - else - return false - end - end - - private - - def ssl_supported? - return Spree::Config[:allow_ssl_in_production] if Rails.env.production? - return Spree::Config[:allow_ssl_in_staging] if Rails.env.staging? - return Spree::Config[:allow_ssl_in_development_and_test] if (Rails.env.development? or Rails.env.test?) - end - - def ensure_proper_protocol - return true if ssl_allowed? - if ssl_required? && !request.ssl? && ssl_supported? - redirect_to "https://" + request.host + request.fullpath - flash.keep - elsif request.ssl? && !ssl_required? - redirect_to "http://" + request.host + request.fullpath - flash.keep - end - - end -end diff --git a/core/lib/spree/core/store_helpers.rb b/core/lib/spree/core/store_helpers.rb deleted file mode 100644 index 20168bc22fb..00000000000 --- a/core/lib/spree/core/store_helpers.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Methods added to this helper will be available to all templates in the application. -module Spree - module Core - module StoreHelpers - - # helper to determine if its appropriate to show the store menu - def store_menu? - %w{thank_you}.exclude? params[:action] - end - - end - end -end diff --git a/core/lib/spree/core/testing_support/authorization_helpers.rb b/core/lib/spree/core/testing_support/authorization_helpers.rb deleted file mode 100644 index cb40b9c3bc0..00000000000 --- a/core/lib/spree/core/testing_support/authorization_helpers.rb +++ /dev/null @@ -1,30 +0,0 @@ -module AuthorizationHelpers - module Controller - def stub_authorization! - before do - controller.should_receive(:authorize!).twice.and_return(true) - end - end - end - - module Request - class SuperAbility - include CanCan::Ability - - def initialize(user) - # allow anyone to perform index on Order - can :manage, :all - end - end - - def stub_authorization! - before(:all) { Spree::Ability.register_ability(AuthorizationHelpers::Request::SuperAbility) } - after(:all) { Spree::Ability.remove_ability(AuthorizationHelpers::Request::SuperAbility) } - end - end -end - -RSpec.configure do |config| - config.extend AuthorizationHelpers::Controller, :type => :controller - config.extend AuthorizationHelpers::Request, :type => :request -end \ No newline at end of file diff --git a/core/lib/spree/core/testing_support/common_rake.rb b/core/lib/spree/core/testing_support/common_rake.rb deleted file mode 100644 index 54400382eb9..00000000000 --- a/core/lib/spree/core/testing_support/common_rake.rb +++ /dev/null @@ -1,25 +0,0 @@ -unless defined?(Spree::InstallGenerator) - require 'generators/spree/install/install_generator' -end - -desc "Generates a dummy app for testing" -namespace :common do - task :test_app, :user_class do |t, args| - args.with_defaults(:user_class => "Spree::LegacyUser") - require "#{ENV['LIB_NAME']}" - - Spree::DummyGenerator.start ["--lib_name=#{ENV['LIB_NAME']}", "--database=#{ENV['DB_NAME']}", "--quiet"] - Spree::InstallGenerator.start ["--lib_name=#{ENV['LIB_NAME']}", "--auto-accept", "--migrate=false", "--seed=false", "--sample=false", "--quiet", "--user_class=#{args[:user_class]}"] - - puts "Setting up dummy database..." - cmd = "bundle exec rake db:drop db:create db:migrate db:test:prepare" - - if RUBY_PLATFORM =~ /mswin/ #windows - cmd += " >nul" - else - cmd += " >/dev/null" - end - - system(cmd) - end -end diff --git a/core/lib/spree/core/testing_support/controller_requests.rb b/core/lib/spree/core/testing_support/controller_requests.rb deleted file mode 100644 index fdc298d35ba..00000000000 --- a/core/lib/spree/core/testing_support/controller_requests.rb +++ /dev/null @@ -1,67 +0,0 @@ -# Use this module to easily test Spree actions within Spree components -# or inside your application to test routes for the mounted Spree engine. -# -# Inside your spec_helper.rb, include this module inside the RSpec.configure -# block by doing this: -# -# require 'spree/core/testing_support/controller_requests' -# RSpec.configure do |c| -# c.include Spree::Core::TestingSupport::ControllerRequests, :type => :controller -# end -# -# Then, in your controller tests, you can access spree routes like this: -# -# require 'spec_helper' -# -# describe Spree::ProductsController do -# it "can see all the products" do -# spree_get :index -# end -# end -# -# Use spree_get, spree_post, spree_put or spree_delete to make requests -# to the Spree engine, and use regular get, post, put or delete to make -# requests to your application. -# -module Spree - module Core - module TestingSupport - module ControllerRequests - def spree_get(action, parameters = nil, session = nil, flash = nil) - process_spree_action(action, parameters, session, flash, "GET") - end - - # Executes a request simulating POST HTTP method and set/volley the response - def spree_post(action, parameters = nil, session = nil, flash = nil) - process_spree_action(action, parameters, session, flash, "POST") - end - - # Executes a request simulating PUT HTTP method and set/volley the response - def spree_put(action, parameters = nil, session = nil, flash = nil) - process_spree_action(action, parameters, session, flash, "PUT") - end - - # Executes a request simulating DELETE HTTP method and set/volley the response - def spree_delete(action, parameters = nil, session = nil, flash = nil) - process_spree_action(action, parameters, session, flash, "DELETE") - end - - def spree_xhr_get(action, parameters = nil, session = nil, flash = nil) - parameters ||= {} - parameters.reverse_merge!(:format => :json) - parameters.merge!(:use_route => :spree) - xml_http_request(:get, action, parameters, session, flash) - end - - private - - def process_spree_action(action, parameters = nil, session = nil, flash = nil, method = "GET") - parameters ||= {} - process(action, parameters.merge!(:use_route => :spree), session, flash, method) - end - end - end - end -end - - diff --git a/core/lib/spree/core/testing_support/factories.rb b/core/lib/spree/core/testing_support/factories.rb deleted file mode 100644 index 43b2368c2e2..00000000000 --- a/core/lib/spree/core/testing_support/factories.rb +++ /dev/null @@ -1,11 +0,0 @@ -Spree::Zone.class_eval do - def self.global - find_by_name("GlobalZone") || create(:global_zone) - end -end - -require 'factory_girl' - -Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f| - require File.expand_path(f) -end diff --git a/core/lib/spree/core/testing_support/factories/activator_factory.rb b/core/lib/spree/core/testing_support/factories/activator_factory.rb deleted file mode 100644 index 934474484e1..00000000000 --- a/core/lib/spree/core/testing_support/factories/activator_factory.rb +++ /dev/null @@ -1,8 +0,0 @@ -FactoryGirl.define do - factory :activator, :class => Spree::Activator do - name 'Activator name' - event_name 'spree.order.contents_changed' - starts_at 2.weeks.ago - expires_at 2.weeks.from_now - end -end diff --git a/core/lib/spree/core/testing_support/factories/address_factory.rb b/core/lib/spree/core/testing_support/factories/address_factory.rb deleted file mode 100644 index 66797492ab5..00000000000 --- a/core/lib/spree/core/testing_support/factories/address_factory.rb +++ /dev/null @@ -1,22 +0,0 @@ -FactoryGirl.define do - factory :address, :class => Spree::Address do - firstname 'John' - lastname 'Doe' - company 'Company' - address1 '10 Lovely Street' - address2 'Northwest' - city 'Herndon' - zipcode '20170' - phone '123-456-7890' - alternative_phone '123-456-7899' - - state { |address| address.association(:state) } - country do |address| - if address.state - address.state.country - else - address.association(:country) - end - end - end -end diff --git a/core/lib/spree/core/testing_support/factories/adjustment_factory.rb b/core/lib/spree/core/testing_support/factories/adjustment_factory.rb deleted file mode 100644 index 433048dee14..00000000000 --- a/core/lib/spree/core/testing_support/factories/adjustment_factory.rb +++ /dev/null @@ -1,16 +0,0 @@ -FactoryGirl.define do - factory :adjustment, :class => Spree::Adjustment do - adjustable { FactoryGirl.create(:order) } - amount '100.0' - label 'Shipping' - source { FactoryGirl.create(:shipment) } - eligible true - end - factory :line_item_adjustment, :class => Spree::Adjustment do - adjustable { FactoryGirl.create(:line_item) } - amount '10.0' - label 'VAT 5%' - source { FactoryGirl.create(:tax_rate) } - eligible true - end -end diff --git a/core/lib/spree/core/testing_support/factories/calculator_factory.rb b/core/lib/spree/core/testing_support/factories/calculator_factory.rb deleted file mode 100644 index 9b49a993734..00000000000 --- a/core/lib/spree/core/testing_support/factories/calculator_factory.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryGirl.define do - factory :calculator, :class => Spree::Calculator::FlatRate do - after_create { |c| c.set_preference(:amount, 10.0) } - end - - factory :no_amount_calculator, :class => Spree::Calculator::FlatRate do - after_create { |c| c.set_preference(:amount, 0) } - end -end diff --git a/core/lib/spree/core/testing_support/factories/configuration_factory.rb b/core/lib/spree/core/testing_support/factories/configuration_factory.rb deleted file mode 100644 index d0d25fa05ad..00000000000 --- a/core/lib/spree/core/testing_support/factories/configuration_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryGirl.define do - factory :configuration, :class => Spree::Configuration do - name 'Default Configuration' - type 'app_configuration' - end -end diff --git a/core/lib/spree/core/testing_support/factories/country_factory.rb b/core/lib/spree/core/testing_support/factories/country_factory.rb deleted file mode 100644 index 54a912de2f0..00000000000 --- a/core/lib/spree/core/testing_support/factories/country_factory.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryGirl.define do - factory :country, :class => Spree::Country do - iso_name 'UNITED STATES' - name 'United States of Foo' - iso 'US' - iso3 'USA' - numcode 840 - end -end diff --git a/core/lib/spree/core/testing_support/factories/credit_card_factory.rb b/core/lib/spree/core/testing_support/factories/credit_card_factory.rb deleted file mode 100644 index 7af2667f24f..00000000000 --- a/core/lib/spree/core/testing_support/factories/credit_card_factory.rb +++ /dev/null @@ -1,13 +0,0 @@ -# allows credit card info to be saved to the database which is needed for factories to work properly -class TestCard < Spree::CreditCard - def remove_readonly_attributes(attributes) attributes; end -end - -FactoryGirl.define do - factory :credit_card, :class => TestCard do - verification_value 123 - month 12 - year 2013 - number '4111111111111111' - end -end diff --git a/core/lib/spree/core/testing_support/factories/inventory_unit_factory.rb b/core/lib/spree/core/testing_support/factories/inventory_unit_factory.rb deleted file mode 100644 index f04b701b71e..00000000000 --- a/core/lib/spree/core/testing_support/factories/inventory_unit_factory.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryGirl.define do - factory :inventory_unit, :class => Spree::InventoryUnit do - variant { FactoryGirl.create(:variant) } - order { FactoryGirl.create(:order) } - state 'sold' - shipment { FactoryGirl.create(:shipment, :state => 'pending') } - #return_authorization { FactoryGirl.create(:return_authorization) } - end -end diff --git a/core/lib/spree/core/testing_support/factories/line_item_factory.rb b/core/lib/spree/core/testing_support/factories/line_item_factory.rb deleted file mode 100644 index 7d81a35692f..00000000000 --- a/core/lib/spree/core/testing_support/factories/line_item_factory.rb +++ /dev/null @@ -1,10 +0,0 @@ -FactoryGirl.define do - factory :line_item, :class => Spree::LineItem do - quantity 1 - price { BigDecimal.new('10.00') } - - # associations: - association(:order, :factory => :order) - association(:variant, :factory => :variant) - end -end diff --git a/core/lib/spree/core/testing_support/factories/mail_method_factory.rb b/core/lib/spree/core/testing_support/factories/mail_method_factory.rb deleted file mode 100644 index c7c2c12e19e..00000000000 --- a/core/lib/spree/core/testing_support/factories/mail_method_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryGirl.define do - factory :mail_method, :class => Spree::MailMethod do - environment { Rails.env } - active true - end -end diff --git a/core/lib/spree/core/testing_support/factories/options_factory.rb b/core/lib/spree/core/testing_support/factories/options_factory.rb deleted file mode 100644 index e32383a7afe..00000000000 --- a/core/lib/spree/core/testing_support/factories/options_factory.rb +++ /dev/null @@ -1,12 +0,0 @@ -FactoryGirl.define do - factory :option_value, :class => Spree::OptionValue do - name 'Size' - presentation 'S' - option_type - end - - factory :option_type, :class => Spree::OptionType do - name 'foo-size' - presentation 'Size' - end -end diff --git a/core/lib/spree/core/testing_support/factories/order_factory.rb b/core/lib/spree/core/testing_support/factories/order_factory.rb deleted file mode 100644 index b1b24bf66fc..00000000000 --- a/core/lib/spree/core/testing_support/factories/order_factory.rb +++ /dev/null @@ -1,32 +0,0 @@ -FactoryGirl.define do - factory :order, :class => Spree::Order do - # associations: - association(:user, :factory => :user) - association(:bill_address, :factory => :address) - completed_at nil - bill_address_id nil - ship_address_id nil - email 'foo@example.com' - end - - factory :order_with_totals, :parent => :order do - after_create { |order| FactoryGirl.create(:line_item, :order => order) } - end - - factory :order_with_inventory_unit_shipped, :parent => :order do - after_create do |order| - FactoryGirl.create(:line_item, :order => order) - FactoryGirl.create(:inventory_unit, :order => order, :state => 'shipped') - end - end - - factory :completed_order_with_totals, :parent => :order_with_totals do - bill_address { FactoryGirl.create(:address) } - ship_address { FactoryGirl.create(:address) } - after_create do |order| - FactoryGirl.create(:inventory_unit, :order => order, :state => 'shipped') - end - state 'complete' - completed_at Time.now - end -end diff --git a/core/lib/spree/core/testing_support/factories/payment_factory.rb b/core/lib/spree/core/testing_support/factories/payment_factory.rb deleted file mode 100644 index 02ee59fef5f..00000000000 --- a/core/lib/spree/core/testing_support/factories/payment_factory.rb +++ /dev/null @@ -1,34 +0,0 @@ -FactoryGirl.define do - factory :payment, :class => Spree::Payment do - amount 45.75 - payment_method { FactoryGirl.create(:bogus_payment_method) } - source { FactoryGirl.build(:credit_card) } - order { FactoryGirl.create(:order) } - state 'pending' - response_code '12345' - - # limit the payment amount to order's remaining balance, to avoid over-pay exceptions - after_create do |pmt| - #pmt.update_attribute(:amount, [pmt.amount, pmt.order.outstanding_balance].min) - end - end - - # factory :creditcard_txn do - # payment - # amount 45.75 - # response_code 12345 - # txn_type CreditcardTxn::TxnType::AUTHORIZE - # - # # match the payment amount to the payment's value - # after_create do |txn| - # # txn.update_attribute(:amount, [txn.amount, txn.payment.payment].min) - # txn.update_attribute(:amount, txn.payment.amount) - # end - # end - - factory :check_payment, :class => Spree::Payment do - amount 45.75 - payment_method { FactoryGirl.create(:payment_method) } - order { FactoryGirl.create(:order) } - end -end diff --git a/core/lib/spree/core/testing_support/factories/payment_method_factory.rb b/core/lib/spree/core/testing_support/factories/payment_method_factory.rb deleted file mode 100644 index 8d9c839f68f..00000000000 --- a/core/lib/spree/core/testing_support/factories/payment_method_factory.rb +++ /dev/null @@ -1,18 +0,0 @@ -FactoryGirl.define do - factory :payment_method, :class => Spree::PaymentMethod::Check do - name 'Check' - environment 'test' - end - - factory :bogus_payment_method, :class => Spree::Gateway::Bogus do - name 'Credit Card' - environment 'test' - end - - # authorize.net was moved to spree_gateway. Leaving this factory - # in place with bogus in case anyone is using it - factory :authorize_net_payment_method, :class => Spree::Gateway::BogusSimple do - name 'Credit Card' - environment 'test' - end -end diff --git a/core/lib/spree/core/testing_support/factories/price_factory.rb b/core/lib/spree/core/testing_support/factories/price_factory.rb deleted file mode 100644 index d59f18bf418..00000000000 --- a/core/lib/spree/core/testing_support/factories/price_factory.rb +++ /dev/null @@ -1,8 +0,0 @@ -FactoryGirl.define do - factory :price, :class => Spree::Price do - variant :variant - amount 19.99 - currency 'USD' - end -end - diff --git a/core/lib/spree/core/testing_support/factories/product_factory.rb b/core/lib/spree/core/testing_support/factories/product_factory.rb deleted file mode 100644 index 8f13ecbe3b3..00000000000 --- a/core/lib/spree/core/testing_support/factories/product_factory.rb +++ /dev/null @@ -1,36 +0,0 @@ -FactoryGirl.define do - factory :simple_product, :class => Spree::Product do - sequence(:name) { |n| "Product ##{n} - #{Kernel.rand(9999)}" } - description { Faker::Lorem.paragraphs(1 + Kernel.rand(5)).join("\n") } - price 19.99 - cost_price 17.00 - sku 'ABC' - available_on 1.year.ago - deleted_at nil - end - - factory :product, :parent => :simple_product do - tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) } - shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) } - end - - factory :product_with_option_types, :parent => :product do - after_create { |product| FactoryGirl.create(:product_option_type, :product => product) } - end - - factory :custom_product, :class => Spree::Product do - name "Custom Product" - price "17.99" - description { Faker::Lorem.paragraphs(1 + Kernel.rand(5)).join("\n") } - - # associations: - tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) } - shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) } - - sku 'ABC' - available_on 1.year.ago - deleted_at nil - - association :taxons - end -end diff --git a/core/lib/spree/core/testing_support/factories/product_option_type_factory.rb b/core/lib/spree/core/testing_support/factories/product_option_type_factory.rb deleted file mode 100644 index 0321eb49cb0..00000000000 --- a/core/lib/spree/core/testing_support/factories/product_option_type_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryGirl.define do - factory :product_option_type, :class => Spree::ProductOptionType do - product { FactoryGirl.create(:product) } - option_type { FactoryGirl.create(:option_type) } - end -end diff --git a/core/lib/spree/core/testing_support/factories/product_property_factory.rb b/core/lib/spree/core/testing_support/factories/product_property_factory.rb deleted file mode 100644 index 385d6688e0f..00000000000 --- a/core/lib/spree/core/testing_support/factories/product_property_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryGirl.define do - factory :product_property, :class => Spree::ProductProperty do - product { FactoryGirl.create(:product) } - property { FactoryGirl.create(:property) } - end -end diff --git a/core/lib/spree/core/testing_support/factories/property_factory.rb b/core/lib/spree/core/testing_support/factories/property_factory.rb deleted file mode 100644 index bf3259837cc..00000000000 --- a/core/lib/spree/core/testing_support/factories/property_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryGirl.define do - factory :property, :class => Spree::Property do - name 'baseball_cap_color' - presentation 'cap color' - end -end diff --git a/core/lib/spree/core/testing_support/factories/prototype_factory.rb b/core/lib/spree/core/testing_support/factories/prototype_factory.rb deleted file mode 100644 index 69e882f2301..00000000000 --- a/core/lib/spree/core/testing_support/factories/prototype_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryGirl.define do - factory :prototype, :class => Spree::Prototype do - name 'Baseball Cap' - properties { [FactoryGirl.create(:property)] } - end -end diff --git a/core/lib/spree/core/testing_support/factories/return_authorization_factory.rb b/core/lib/spree/core/testing_support/factories/return_authorization_factory.rb deleted file mode 100644 index 4446d7fbe3f..00000000000 --- a/core/lib/spree/core/testing_support/factories/return_authorization_factory.rb +++ /dev/null @@ -1,10 +0,0 @@ -FactoryGirl.define do - factory :return_authorization, :class => Spree::ReturnAuthorization do - number '100' - amount 100.00 - #order { FactoryGirl.create(:order) } - order { FactoryGirl.create(:order_with_inventory_unit_shipped) } - reason 'no particular reason' - state 'received' - end -end diff --git a/core/lib/spree/core/testing_support/factories/role_factory.rb b/core/lib/spree/core/testing_support/factories/role_factory.rb deleted file mode 100644 index 56fcd29dcc6..00000000000 --- a/core/lib/spree/core/testing_support/factories/role_factory.rb +++ /dev/null @@ -1,11 +0,0 @@ -FactoryGirl.define do - sequence(:role_sequence) { |n| "Role ##{n}" } - - factory :role, :class => Spree::Role do - name { FactoryGirl.generate :role_sequence } - end - - factory :admin_role, :parent => :role do - name 'admin' - end -end diff --git a/core/lib/spree/core/testing_support/factories/shipment_factory.rb b/core/lib/spree/core/testing_support/factories/shipment_factory.rb deleted file mode 100644 index a3374b9bd7e..00000000000 --- a/core/lib/spree/core/testing_support/factories/shipment_factory.rb +++ /dev/null @@ -1,11 +0,0 @@ -FactoryGirl.define do - factory :shipment, :class => Spree::Shipment do - order { FactoryGirl.create(:order) } - shipping_method { FactoryGirl.create(:shipping_method) } - tracking 'U10000' - number '100' - cost 100.00 - address { FactoryGirl.create(:address) } - state 'pending' - end -end diff --git a/core/lib/spree/core/testing_support/factories/shipping_category_factory.rb b/core/lib/spree/core/testing_support/factories/shipping_category_factory.rb deleted file mode 100644 index 78b3f35d4b8..00000000000 --- a/core/lib/spree/core/testing_support/factories/shipping_category_factory.rb +++ /dev/null @@ -1,7 +0,0 @@ -FactoryGirl.define do - sequence(:shipping_category_sequence) { |n| "ShippingCategory ##{n}" } - - factory :shipping_category, :class => Spree::ShippingCategory do - name { FactoryGirl.generate :shipping_category_sequence } - end -end diff --git a/core/lib/spree/core/testing_support/factories/shipping_method_factory.rb b/core/lib/spree/core/testing_support/factories/shipping_method_factory.rb deleted file mode 100644 index 0ac3d5922a4..00000000000 --- a/core/lib/spree/core/testing_support/factories/shipping_method_factory.rb +++ /dev/null @@ -1,23 +0,0 @@ -FactoryGirl.define do - factory :shipping_method, :class => Spree::ShippingMethod do - zone { |a| Spree::Zone.find_by_name('GlobalZone') || a.association(:global_zone) } - name 'UPS Ground' - calculator { FactoryGirl.build(:calculator) } - end - - factory :free_shipping_method, :class => Spree::ShippingMethod do - zone { |a| Spree::Zone.find_by_name('GlobalZone') || a.association(:global_zone) } - name 'UPS Ground' - calculator { FactoryGirl.build(:no_amount_calculator) } - end - - factory :shipping_method_with_category, :class => Spree::ShippingMethod do - zone { |a| Spree::Zone.find_by_name('GlobalZone') || a.association(:global_zone) } - name 'UPS Ground' - match_none nil - match_one nil - match_all nil - association(:shipping_category, :factory => :shipping_category) - calculator { FactoryGirl.build(:calculator) } - end -end diff --git a/core/lib/spree/core/testing_support/factories/tax_category_factory.rb b/core/lib/spree/core/testing_support/factories/tax_category_factory.rb deleted file mode 100644 index 446a38f1657..00000000000 --- a/core/lib/spree/core/testing_support/factories/tax_category_factory.rb +++ /dev/null @@ -1,16 +0,0 @@ -FactoryGirl.define do - factory :tax_category, :class => Spree::TaxCategory do - name { "TaxCategory - #{rand(999999)}" } - description { Faker::Lorem.sentence } - end - - factory :tax_category_with_rates, :parent => :tax_category do - after_create do |tax_category| - tax_category.tax_rates.create!({ - :amount => 0.05, - :calculator => Spree::Calculator::DefaultTax.new, - :zone => Spree::Zone.find_by_name('GlobalZone') || FactoryGirl.create(:global_zone) - }, :without_protection => true) - end - end -end diff --git a/core/lib/spree/core/testing_support/factories/tax_rate_factory.rb b/core/lib/spree/core/testing_support/factories/tax_rate_factory.rb deleted file mode 100644 index c24821a0fad..00000000000 --- a/core/lib/spree/core/testing_support/factories/tax_rate_factory.rb +++ /dev/null @@ -1,7 +0,0 @@ -FactoryGirl.define do - factory :tax_rate, :class => Spree::TaxRate do - zone { FactoryGirl.create(:zone) } - amount 100.00 - tax_category { FactoryGirl.create(:tax_category) } - end -end diff --git a/core/lib/spree/core/testing_support/factories/taxon_factory.rb b/core/lib/spree/core/testing_support/factories/taxon_factory.rb deleted file mode 100644 index 2e09cd3dbed..00000000000 --- a/core/lib/spree/core/testing_support/factories/taxon_factory.rb +++ /dev/null @@ -1,7 +0,0 @@ -FactoryGirl.define do - factory :taxon, :class => Spree::Taxon do - name 'Ruby on Rails' - taxonomy { FactoryGirl.create(:taxonomy) } - parent_id nil - end -end diff --git a/core/lib/spree/core/testing_support/factories/taxonomy_factory.rb b/core/lib/spree/core/testing_support/factories/taxonomy_factory.rb deleted file mode 100644 index c71873a9aca..00000000000 --- a/core/lib/spree/core/testing_support/factories/taxonomy_factory.rb +++ /dev/null @@ -1,5 +0,0 @@ -FactoryGirl.define do - factory :taxonomy, :class => Spree::Taxonomy do - name 'Brand' - end -end diff --git a/core/lib/spree/core/testing_support/factories/tracker_factory.rb b/core/lib/spree/core/testing_support/factories/tracker_factory.rb deleted file mode 100644 index e0d25cce4fc..00000000000 --- a/core/lib/spree/core/testing_support/factories/tracker_factory.rb +++ /dev/null @@ -1,7 +0,0 @@ -FactoryGirl.define do - factory :tracker, :class => Spree::Tracker do - environment { Rails.env } - analytics_id 'A100' - active true - end -end diff --git a/core/lib/spree/core/testing_support/factories/user_factory.rb b/core/lib/spree/core/testing_support/factories/user_factory.rb deleted file mode 100644 index ede76b713df..00000000000 --- a/core/lib/spree/core/testing_support/factories/user_factory.rb +++ /dev/null @@ -1,17 +0,0 @@ -FactoryGirl.define do - sequence :user_authentication_token do |n| - "xxxx#{Time.now.to_i}#{rand(1000)}#{n}xxxxxxxxxxxxx" - end - - factory :user, :class => Spree.user_class do - email { Faker::Internet.email } - login { email } - password 'secret' - password_confirmation 'secret' - authentication_token { FactoryGirl.generate(:user_authentication_token) } if Spree.user_class.attribute_method? :authentication_token - end - - factory :admin_user, :parent => :user do - spree_roles { [Spree::Role.find_by_name('admin') || FactoryGirl.create(:role, :name => 'admin')] } - end -end diff --git a/core/lib/spree/core/testing_support/factories/variant_factory.rb b/core/lib/spree/core/testing_support/factories/variant_factory.rb deleted file mode 100644 index a025a928b0e..00000000000 --- a/core/lib/spree/core/testing_support/factories/variant_factory.rb +++ /dev/null @@ -1,16 +0,0 @@ -FactoryGirl.define do - factory :variant, :class => Spree::Variant do - price 19.99 - cost_price 17.00 - sku { Faker::Lorem.sentence } - weight { BigDecimal.new("#{rand(200)}.#{rand(99)}") } - height { BigDecimal.new("#{rand(200)}.#{rand(99)}") } - width { BigDecimal.new("#{rand(200)}.#{rand(99)}") } - depth { BigDecimal.new("#{rand(200)}.#{rand(99)}") } - on_hand 5 - - # associations: - product { |p| p.association(:product) } - option_values { [FactoryGirl.create(:option_value)] } - end -end diff --git a/core/lib/spree/core/testing_support/factories/zone_factory.rb b/core/lib/spree/core/testing_support/factories/zone_factory.rb deleted file mode 100644 index 8df83174545..00000000000 --- a/core/lib/spree/core/testing_support/factories/zone_factory.rb +++ /dev/null @@ -1,17 +0,0 @@ -FactoryGirl.define do - factory :global_zone, :class => Spree::Zone do - name 'GlobalZone' - description { Faker::Lorem.sentence } - zone_members do |proxy| - zone = proxy.instance_eval { @instance } - Spree::Country.all.map do |c| - zone_member = Spree::ZoneMember.create(:zoneable => c, :zone => zone) - end - end - end - - factory :zone, :class => Spree::Zone do - name { Faker::Lorem.sentence } - description { Faker::Lorem.sentence } - end -end diff --git a/core/lib/spree/core/testing_support/fixtures.rb b/core/lib/spree/core/testing_support/fixtures.rb deleted file mode 100644 index bc2e7ea217a..00000000000 --- a/core/lib/spree/core/testing_support/fixtures.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'active_record/fixtures' - -fixtures_dir = File.expand_path('../../../../../db/default', __FILE__) -ActiveRecord::Fixtures.create_fixtures(fixtures_dir, ['spree/countries', 'spree/zones', 'spree/zone_members', 'spree/states', 'spree/roles']) \ No newline at end of file diff --git a/core/lib/spree/core/testing_support/flash.rb b/core/lib/spree/core/testing_support/flash.rb deleted file mode 100644 index 5ab2ae05251..00000000000 --- a/core/lib/spree/core/testing_support/flash.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Spree - module Core - module TestingSupport - module Flash - def assert_flash_success(flash) - flash = convert_flash(flash) - - within("[class='flash success']") do - page.should have_content(flash) - end - end - - def assert_successful_update_message(resource) - flash = I18n.t(:successfully_updated, :resource => I18n.t(resource)) - assert_flash_success(flash) - end - - private - - def convert_flash(flash) - if flash.is_a?(Symbol) - flash = I18n.t(flash) - end - flash - end - end - end - end -end diff --git a/core/lib/spree/core/testing_support/preferences.rb b/core/lib/spree/core/testing_support/preferences.rb deleted file mode 100644 index f2c2c94bc06..00000000000 --- a/core/lib/spree/core/testing_support/preferences.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Spree - module Core - module TestingSupport - module Preferences - # Resets all preferences to default values, you can - # pass a block to override the defaults with a block - # - # reset_spree_preferences do |config| - # config.site_name = "my fancy pants store" - # end - # - def reset_spree_preferences - Spree::Preferences::Store.instance.persistence = false - config = Rails.application.config.spree.preferences - config.reset - yield(config) if block_given? - end - - def assert_preference_unset(preference) - find("#preferences_#{preference}")['checked'].should be_false - Spree::Config[preference].should be_false - end - end - end - end -end diff --git a/core/lib/spree/core/token_resource.rb b/core/lib/spree/core/token_resource.rb deleted file mode 100644 index 48697f29b2a..00000000000 --- a/core/lib/spree/core/token_resource.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Spree - module Core - module TokenResource - module ClassMethods - def token_resource - has_one :tokenized_permission, :as => :permissable - delegate :token, :to => :tokenized_permission, :allow_nil => true - after_create :create_token - end - end - - def create_token - permission = build_tokenized_permission - permission.token = token = ::SecureRandom::hex(8) - permission.save! - token - end - - def self.included(receiver) - receiver.extend ClassMethods - end - end - end -end - -ActiveRecord::Base.class_eval { include Spree::Core::TokenResource } - diff --git a/core/lib/spree/core/user_banners.rb b/core/lib/spree/core/user_banners.rb deleted file mode 100644 index d7d7ec16276..00000000000 --- a/core/lib/spree/core/user_banners.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Utility methods for dealing with user banners and saving -# an array of dismissed banners per user -# use symbols as banner id -module Spree - module Core - module UserBanners - def self.included(base) - base.preference :dismissed_banners, :string, :default => '' - end - - def dismissed_banner_ids - dismissed = self.preferred_dismissed_banners - dismissed.split(',').map(&:to_sym) - end - - def dismiss_banner(banner_id) - self.preferred_dismissed_banners = dismissed_banner_ids.push(banner_id.to_sym).uniq.join(',') - end - - def dismissed_banner?(banner_id) - dismissed_banner_ids.include? banner_id.to_sym - end - end - end -end \ No newline at end of file diff --git a/core/lib/spree/core/validators/email.rb b/core/lib/spree/core/validators/email.rb index 793f0db77c4..997d5c1dc4e 100644 --- a/core/lib/spree/core/validators/email.rb +++ b/core/lib/spree/core/validators/email.rb @@ -1,23 +1,7 @@ -# Borrowed from http://my.rails-royce.org/2010/07/21/email-validation-in-ruby-on-rails-without-regexp/ -# Mentioned in tweet here: https://twitter.com/_sohara/status/177120126083141633 -require 'mail' class EmailValidator < ActiveModel::EachValidator def validate_each(record,attribute,value) - begin - m = Mail::Address.new(value) - # We must check that value contains a domain and that value is an email address - r = m.domain && m.address == value - t = m.__send__(:tree) - # We need to dig into treetop - # A valid domain must have dot_atom_text elements size > 1 - # user@localhost is excluded - # treetop must respond to domain - # We exclude valid email values like - # Hence we use m.__send__(tree).domain - r &&= (t.domain.dot_atom_text.elements.size > 1) - rescue Exception => e - r = false + unless value =~ /\A([^@\.]|[^@\.]([^@\s]*)[^@\.])@([^@\s]+\.)+[^@\s]+\z/ + record.errors.add(attribute, :invalid, {:value => value}.merge!(options)) end - record.errors[attribute] << (options[:message] || "is invalid") unless r end end diff --git a/core/lib/spree/core/version.rb b/core/lib/spree/core/version.rb index f8568b08528..b6c759db741 100644 --- a/core/lib/spree/core/version.rb +++ b/core/lib/spree/core/version.rb @@ -1,5 +1,5 @@ module Spree def self.version - "2.0.0.beta" + '2.4.11.beta' end end diff --git a/core/lib/spree/i18n.rb b/core/lib/spree/i18n.rb new file mode 100644 index 00000000000..fd289d8122e --- /dev/null +++ b/core/lib/spree/i18n.rb @@ -0,0 +1,37 @@ +require 'i18n' +require 'active_support/core_ext/array/extract_options' +require 'spree/i18n/base' + +module Spree + extend ActionView::Helpers::TranslationHelper + extend ActionView::Helpers::TagHelper + + class << self + # Add spree namespace and delegate to Rails TranslationHelper for some nice + # extra functionality. e.g return reasonable strings for missing translations + def translate(*args) + @virtual_path = virtual_path + + options = args.extract_options! + options[:scope] = [*options[:scope]].unshift(:spree) + args << options + super(*args) + end + + alias_method :t, :translate + + def context + Spree::ViewContext.context + end + + def virtual_path + if context + path = context.instance_variable_get("@virtual_path") + + if path + path.gsub(/spree/, '') + end + end + end + end +end diff --git a/core/lib/spree/i18n/base.rb b/core/lib/spree/i18n/base.rb new file mode 100644 index 00000000000..765c8ad169b --- /dev/null +++ b/core/lib/spree/i18n/base.rb @@ -0,0 +1,17 @@ +module Spree + module ViewContext + def self.context=(context) + @context = context + end + + def self.context + @context + end + + def view_context + super.tap do |context| + Spree::ViewContext.context = context + end + end + end +end diff --git a/core/lib/spree/i18n/initializer.rb b/core/lib/spree/i18n/initializer.rb new file mode 100644 index 00000000000..79f5917cb2b --- /dev/null +++ b/core/lib/spree/i18n/initializer.rb @@ -0,0 +1 @@ +Spree::BaseController.send(:include, Spree::ViewContext) diff --git a/core/lib/spree/localized_number.rb b/core/lib/spree/localized_number.rb new file mode 100644 index 00000000000..0fb62a19be5 --- /dev/null +++ b/core/lib/spree/localized_number.rb @@ -0,0 +1,20 @@ +module Spree + class LocalizedNumber + + # Strips all non-price-like characters from the number, taking into account locale settings. + def self.parse(number) + return number unless number.is_a?(String) + + separator, delimiter = I18n.t([:'number.currency.format.separator', :'number.currency.format.delimiter']) + non_number_characters = /[^0-9\-#{separator}]/ + + # strip everything else first + number.gsub!(non_number_characters, '') + # then replace the locale-specific decimal separator with the standard separator if necessary + number.gsub!(separator, '.') unless separator == '.' + + number.to_d + end + + end +end diff --git a/core/lib/spree/migrations.rb b/core/lib/spree/migrations.rb new file mode 100644 index 00000000000..1f19d2dadc9 --- /dev/null +++ b/core/lib/spree/migrations.rb @@ -0,0 +1,68 @@ +module Spree + class Migrations + attr_reader :config, :engine_name + + # Takes the engine config block and engine name + def initialize(config, engine_name) + @config, @engine_name = config, engine_name + end + + # Puts warning when any engine migration is not present on the Rails app + # db/migrate dir + # + # First split: + # + # ["20131128203548", "update_name_fields_on_spree_credit_cards.spree.rb"] + # + # Second split should give the engine_name of the migration + # + # ["update_name_fields_on_spree_credit_cards", "spree.rb"] + # + # Shouldn't run on test mode because migrations inside engine don't have + # engine name on the file name + def check + if File.exists?("config/spree.yml") && File.directory?("db/migrate") + engine_in_app = app_migrations.map do |file_name| + name, engine = file_name.split(".", 2) + next unless match_engine?(engine) + name + end.compact! || [] + + missing_migrations = engine_migrations.sort - engine_in_app.sort + unless missing_migrations.empty? + puts "[#{engine_name.capitalize} WARNING] Missing migrations." + missing_migrations.each do |migration| + puts "[#{engine_name.capitalize} WARNING] #{migration} from #{engine_name} is missing." + end + puts "[#{engine_name.capitalize} WARNING] Run `bundle exec rake railties:install:migrations` to get them.\n\n" + true + end + end + end + + private + def engine_migrations + Dir.entries("#{config.root}/db/migrate").map do |file_name| + name = file_name.split("_", 2).last.split(".", 2).first + name.empty? ? next : name + end.compact! || [] + end + + def app_migrations + Dir.entries("db/migrate").map do |file_name| + next if [".", ".."].include? file_name + name = file_name.split("_", 2).last + name.empty? ? next : name + end.compact! || [] + end + + def match_engine?(engine) + if engine_name == "spree" + # Avoid stores upgrading from 1.3 getting wrong warnings + ["spree.rb", "spree_promo.rb"].include? engine + else + engine == "#{engine_name}.rb" + end + end + end +end diff --git a/core/lib/spree/money.rb b/core/lib/spree/money.rb index 7cf950e1e7c..a6a4badbab1 100644 --- a/core/lib/spree/money.rb +++ b/core/lib/spree/money.rb @@ -1,25 +1,45 @@ +# encoding: utf-8 + require 'money' module Spree class Money attr_reader :money + delegate :cents, to: :money + def initialize(amount, options={}) - @money = ::Money.parse([amount, (options[:currency] || Spree::Config[:currency])].join) + @money = Monetize.parse([amount, (options[:currency] || Spree::Config[:currency])].join) @options = {} - @options[:with_currency] = true if Spree::Config[:display_currency] + @options[:with_currency] = Spree::Config[:display_currency] @options[:symbol_position] = Spree::Config[:currency_symbol_position].to_sym - @options[:no_cents] = true if Spree::Config[:hide_cents] + @options[:no_cents] = Spree::Config[:hide_cents] + @options[:decimal_mark] = Spree::Config[:currency_decimal_mark] + @options[:thousands_separator] = Spree::Config[:currency_thousands_separator] + @options[:sign_before_symbol] = Spree::Config[:currency_sign_before_symbol] @options.merge!(options) # Must be a symbol because the Money gem doesn't do the conversion @options[:symbol_position] = @options[:symbol_position].to_sym - end def to_s @money.format(@options) end + def to_html(options = { html: true }) + output = @money.format(@options.merge(options)) + if options[:html] + # 1) prevent blank, breaking spaces + # 2) prevent escaping of HTML character entities + output = output.sub(" ", " ").html_safe + end + output + end + + def as_json(*) + to_s + end + def ==(obj) @money == obj.money end diff --git a/core/lib/spree/permitted_attributes.rb b/core/lib/spree/permitted_attributes.rb new file mode 100644 index 00000000000..825aebc63ea --- /dev/null +++ b/core/lib/spree/permitted_attributes.rb @@ -0,0 +1,112 @@ +module Spree + module PermittedAttributes + ATTRIBUTES = [ + :address_attributes, + :checkout_attributes, + :customer_return_attributes, + :image_attributes, + :inventory_unit_attributes, + :line_item_attributes, + :option_type_attributes, + :option_value_attributes, + :payment_attributes, + :product_attributes, + :product_properties_attributes, + :property_attributes, + :return_authorization_attributes, + :shipment_attributes, + :source_attributes, + :stock_item_attributes, + :stock_location_attributes, + :stock_movement_attributes, + :store_attributes, + :taxon_attributes, + :taxonomy_attributes, + :user_attributes, + :variant_attributes + ] + + mattr_reader *ATTRIBUTES + + @@address_attributes = [ + :id, :firstname, :lastname, :first_name, :last_name, + :address1, :address2, :city, :country_id, :state_id, + :zipcode, :phone, :state_name, :alternative_phone, :company, + country: [:iso, :name, :iso3, :iso_name], + state: [:name, :abbr] + ] + + @@checkout_attributes = [ + :coupon_code, :email, :shipping_method_id, :special_instructions, :use_billing + ] + + @@customer_return_attributes = [:stock_location_id, return_items_attributes: [:id, :inventory_unit_id, :return_authorization_id, :returned, :pre_tax_amount, :acceptance_status, :exchange_variant_id]] + + @@image_attributes = [:alt, :attachment, :position, :viewable_type, :viewable_id] + + @@inventory_unit_attributes = [:shipment, :variant_id] + + @@line_item_attributes = [:id, :variant_id, :quantity] + + @@option_type_attributes = [:name, :presentation, :option_values_attributes] + + @@option_value_attributes = [:name, :presentation] + + @@payment_attributes = [:amount, :payment_method_id, :payment_method] + + @@product_properties_attributes = [:property_name, :value, :position] + + @@product_attributes = [ + :name, :description, :available_on, :permalink, :meta_description, + :meta_keywords, :price, :sku, :deleted_at, :prototype_id, + :option_values_hash, :weight, :height, :width, :depth, + :shipping_category_id, :tax_category_id, + :taxon_ids, :cost_currency, :cost_price, + option_type_ids: [] + ] + + @@property_attributes = [:name, :presentation] + + @@return_authorization_attributes = [:amount, :memo, :stock_location_id, :inventory_units_attributes, :return_authorization_reason_id] + + @@shipment_attributes = [ + :order, :special_instructions, :stock_location_id, :id, + :tracking, :address, :inventory_units, :selected_shipping_rate_id] + + # month / year may be provided by some sources, or others may elect to use one field + @@source_attributes = [ + :number, :month, :year, :expiry, :verification_value, + :first_name, :last_name, :cc_type, :gateway_customer_profile_id, + :gateway_payment_profile_id, :last_digits, :name, :encrypted_data] + + @@stock_item_attributes = [:variant, :stock_location, :backorderable, :variant_id] + + @@stock_location_attributes = [ + :name, :active, :address1, :address2, :city, :zipcode, + :backorderable_default, :state_name, :state_id, :country_id, :phone, + :propagate_all_variants] + + @@stock_movement_attributes = [ + :quantity, :stock_item, :stock_item_id, :originator, :action] + + @@store_attributes = [:name, :url, :seo_title, :meta_keywords, + :meta_description, :default_currency, :mail_from_address] + + @@taxonomy_attributes = [:name] + + @@taxon_attributes = [ + :name, :parent_id, :position, :icon, :description, :permalink, :taxonomy_id, + :meta_description, :meta_keywords, :meta_title, :child_index] + + # TODO Should probably use something like Spree.user_class.attributes + @@user_attributes = [:email, :password, :password_confirmation] + + @@variant_attributes = [ + :name, :presentation, :cost_price, :lock_version, + :position, :track_inventory, + :product_id, :product, :option_values_attributes, :price, + :weight, :height, :width, :depth, :sku, :cost_currency, + options: [:name, :value], option_value_ids: [] + ] + end +end diff --git a/core/lib/spree/product_filters.rb b/core/lib/spree/product_filters.rb deleted file mode 100644 index cf106d489bf..00000000000 --- a/core/lib/spree/product_filters.rb +++ /dev/null @@ -1,197 +0,0 @@ -module Spree - # THIS FILE SHOULD BE OVER-RIDDEN IN YOUR SITE EXTENSION! - # the exact code probably won't be useful, though you're welcome to modify and reuse - # the current contents are mainly for testing and documentation - - # To override this file... - # 1) Make a copy of it in your sites local /lib/spree folder - # 2) Add it to the config load path, or require it in an initializer, e.g... - # - # # config/initializers/spree.rb - # require 'spree/product_filters' - # - - # set up some basic filters for use with products - # - # Each filter has two parts - # * a parametrized named scope which expects a list of labels - # * an object which describes/defines the filter - # - # The filter description has three components - # * a name, for displaying on pages - # * a named scope which will 'execute' the filter - # * a mapping of presentation labels to the relevant condition (in the context of the named scope) - # * an optional list of labels and values (for use with object selection - see taxons examples below) - # - # The named scopes here have a suffix '_any', following SearchLogic's convention for a - # scope which returns results which match any of the inputs. This is purely a convention, - # but might be a useful reminder. - # - # When creating a form, the name of the checkbox group for a filter F should be - # the name of F's scope with [] appended, eg "price_range_any[]", and for - # each label you should have a checkbox with the label as its value. On submission, - # Rails will send the action a hash containing (among other things) an array named - # after the scope whose values are the active labels. - # - # SearchLogic will then convert this array to a call to the named scope with the array - # contents, and the named scope will build a query with the disjunction of the conditions - # relating to the labels, all relative to the scope's context. - # - # The details of how/when filters are used is a detail for specific models (eg products - # or taxons), eg see the taxon model/controller. - - # See specific filters below for concrete examples. - - # This module is included by Taxon. In development mode that inclusion does not - # happen until Taxon class is loaded. Ensure that Taxon class is loaded before - # you try something like Product.price_range_any - module ProductFilters - # Example: filtering by price - # The named scope just maps incoming labels onto their conditions, and builds the conjunction - # 'price' is in the base scope's context (ie, "select foo from products where ...") so - # we can access the field right away - # The filter identifies which scope to use, then sets the conditions for each price range - # - # If user checks off three different price ranges then the argument passed to - # below scope would be something like ["$10 - $15", "$15 - $18", "$18 - $20"] - # - Spree::Product.add_search_scope :price_range_any do |*opts| - conds = opts.map {|o| Spree::ProductFilters.price_filter[:conds][o]}.reject {|c| c.nil?} - scope = conds.shift - conds.each do |new_scope| - scope = scope.or(new_scope) - end - Spree::Product.joins(:master => :default_price).where(scope) - end - - def ProductFilters.format_price(amount) - Spree::Money.new(amount) - end - - def ProductFilters.price_filter - v = Spree::Price.arel_table - conds = [ [ I18n.t(:under_price, :price => format_price(10)) , v[:amount].lteq(10)], - [ "#{format_price(10)} - #{format_price(15)}" , v[:amount].in(10..15)], - [ "#{format_price(15)} - #{format_price(18)}" , v[:amount].in(15..18)], - [ "#{format_price(18)} - #{format_price(20)}" , v[:amount].in(18..20)], - [ I18n.t(:or_over_price, :price => format_price(20)) , v[:amount].gteq(20)]] - { :name => I18n.t(:price_range), - :scope => :price_range_any, - :conds => Hash[*conds.flatten], - :labels => conds.map {|k,v| [k,k]} - } - end - - - # Example: filtering by possible brands - # - # First, we define the scope. Two interesting points here: (a) we run our conditions - # in the scope where the info for the 'brand' property has been loaded; and (b) - # because we may want to filter by other properties too, we give this part of the - # query a unique name (which must be used in the associated conditions too). - # - # Secondly, the filter. Instead of a static list of values, we pull out all existing - # brands from the db, and then build conditions which test for string equality on - # the (uniquely named) field "p_brand.value". There's also a test for brand info - # being blank: note that this relies on with_property doing a left outer join - # rather than an inner join. - if Spree::Property.table_exists? - Spree::Product.add_search_scope :brand_any do |*opts| - conds = opts.map {|o| ProductFilters.brand_filter[:conds][o]}.reject {|c| c.nil?} - scope = conds.shift - conds.each do |new_scope| - scope = scope.or(new_scope) - end - Spree::Product.with_property("brand").where(scope) - end - - def ProductFilters.brand_filter - brand_property = Spree::Property.find_by_name("brand") - brands = Spree::ProductProperty.where(:property_id => brand_property).pluck(:value).uniq - pp = Spree::ProductProperty.arel_table - conds = Hash[*brands.map { |b| [b, pp[:value].eq(b)] }.flatten] - { :name => "Brands", - :scope => :brand_any, - :conds => conds, - :labels => (brands.sort).map { |k| [k, k] } - } - end - end - - # Example: a parameterized filter - # The filter above may show brands which aren't applicable to the current taxon, - # so this one only shows the brands that are relevant to a particular taxon and - # its descendants. - # - # We don't have to give a new scope since the conditions here are a subset of the - # more general filter, so decoding will still work - as long as the filters on a - # page all have unique names (ie, you can't use the two brand filters together - # if they use the same scope). To be safe, the code uses a copy of the scope. - # - # HOWEVER: what happens if we want a more precise scope? we can't pass - # parametrized scope names to SearchLogic, only atomic names, so couldn't ask - # for taxon T's customized filter to be used. BUT: we can arrange for the form - # to pass back a hash instead of an array, where the key acts as the (taxon) - # parameter and value is its label array, and then get a modified named scope - # to get its conditions from a particular filter. - # - # The brand-finding code can be simplified if a few more named scopes were added to - # the product properties model. - if Spree::Property.table_exists? - Spree::Product.add_search_scope :selective_brand_any do |*opts| - Spree::Product.brand_any(*opts) - end - - def ProductFilters.selective_brand_filter(taxon = nil) - taxon ||= Spree::Taxonomy.first.root - brand_property = Spree::Property.find_by_name("brand") - scope = Spree::ProductProperty.where(:property_id => brand_property). - joins(:product => :taxons). - where("#{Spree::Taxon.table_name}.id" => [taxon] + taxon.descendants). - scoped - brands = scope.pluck(:value).uniq - { - :name => "Applicable Brands", - :scope => :selective_brand_any, - :labels => brands.sort.map { |k| [k,k] } - } - end - end - - # Provide filtering on the immediate children of a taxon - # - # This doesn't fit the pattern of the examples above, so there's a few changes. - # Firstly, it uses an existing scope which was not built for filtering - and so - # has no need of a conditions mapping, and secondly, it has a mapping of name - # to the argument type expected by the other scope. - # - # This technique is useful for filtering on objects (by passing ids) or with a - # scope that can be used directly (eg. testing only ever on a single property). - # - # This scope selects products in any of the active taxons or their children. - # - def ProductFilters.taxons_below(taxon) - return Spree::ProductFilters.all_taxons if taxon.nil? - { :name => "Taxons under " + taxon.name, - :scope => :taxons_id_in_tree_any, - :labels => taxon.children.sort_by(&:position).map {|t| [t.name, t.id]}, - :conds => nil - } - end - - # Filtering by the list of all taxons - # - # Similar idea as above, but we don't want the descendants' products, hence - # it uses one of the auto-generated scopes from SearchLogic. - # - # idea: expand the format to allow nesting of labels? - def ProductFilters.all_taxons - taxons = Spree::Taxonomy.all.map {|t| [t.root] + t.root.descendants }.flatten - { :name => "All taxons", - :scope => :taxons_id_equals_any, - :labels => taxons.sort_by(&:name).map {|t| [t.name, t.id]}, - :conds => nil # not needed - } - end - end -end diff --git a/promo/lib/spree/promo/environment.rb b/core/lib/spree/promo/environment.rb similarity index 100% rename from promo/lib/spree/promo/environment.rb rename to core/lib/spree/promo/environment.rb diff --git a/core/lib/spree/core/responder.rb b/core/lib/spree/responder.rb similarity index 100% rename from core/lib/spree/core/responder.rb rename to core/lib/spree/responder.rb diff --git a/core/lib/spree/scopes/dynamic.rb b/core/lib/spree/scopes/dynamic.rb deleted file mode 100644 index a0067c1f8be..00000000000 --- a/core/lib/spree/scopes/dynamic.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Spree - module Scopes - # This module is extended by ProductScope - module Dynamic - module_function - - # Sample dynamic scope generating from set of products - # generates 0 or (2..scope_limit) scopes for prices, based - # on number of products (uses Math.log, to guess number of scopes) - def price_scopes_for(products, scope_limit=5) - scopes = [] - - # Price based scopes - all_prices = products.map(&:price).sort - - ranges = [Math.log(products.length).floor, scope_limit].max - - if ranges >= 2 - l = all_prices.length / ranges - scopes << ProductScope.new({:name => "master_price_lte", :arguments => [all_prices[l]] }) - - (ranges - 2).times do |x| - scopes << ProductScope.new({:name => "price_between", - :arguments => [ all_prices[l*(x+1)+1], all_prices[l*(x+2)] ] }) - end - scopes << ProductScope.new({:name => "master_price_gte", :arguments => [all_prices[l*(ranges-1)+1]] }) - end - - scopes - end - end - end -end diff --git a/core/lib/spree/testing_support/ability_helpers.rb b/core/lib/spree/testing_support/ability_helpers.rb new file mode 100644 index 00000000000..89c7a122b05 --- /dev/null +++ b/core/lib/spree/testing_support/ability_helpers.rb @@ -0,0 +1,105 @@ +shared_examples_for 'access granted' do + it 'should allow read' do + expect(ability).to be_able_to(:read, resource, token) if token + expect(ability).to be_able_to(:read, resource) unless token + end + + it 'should allow create' do + expect(ability).to be_able_to(:create, resource, token) if token + expect(ability).to be_able_to(:create, resource) unless token + end + + it 'should allow update' do + expect(ability).to be_able_to(:update, resource, token) if token + expect(ability).to be_able_to(:update, resource) unless token + end +end + +shared_examples_for 'access denied' do + it 'should not allow read' do + expect(ability).to_not be_able_to(:read, resource) + end + + it 'should not allow create' do + expect(ability).to_not be_able_to(:create, resource) + end + + it 'should not allow update' do + expect(ability).to_not be_able_to(:update, resource) + end +end + +shared_examples_for 'admin granted' do + it 'should allow admin' do + expect(ability).to be_able_to(:admin, resource, token) if token + expect(ability).to be_able_to(:admin, resource) unless token + end +end + +shared_examples_for 'admin denied' do + it 'should not allow admin' do + expect(ability).to_not be_able_to(:admin, resource) + end +end + +shared_examples_for 'index allowed' do + it 'should allow index' do + expect(ability).to be_able_to(:index, resource) + end +end + +shared_examples_for 'no index allowed' do + it 'should not allow index' do + expect(ability).to_not be_able_to(:index, resource) + end +end + +shared_examples_for 'create only' do + it 'should allow create' do + expect(ability).to be_able_to(:create, resource) + end + + it 'should not allow read' do + expect(ability).to_not be_able_to(:read, resource) + end + + it 'should not allow update' do + expect(ability).to_not be_able_to(:update, resource) + end + + it 'should not allow index' do + expect(ability).to_not be_able_to(:index, resource) + end +end + +shared_examples_for 'read only' do + it 'should not allow create' do + expect(ability).to_not be_able_to(:create, resource) + end + + it 'should not allow update' do + expect(ability).to_not be_able_to(:update, resource) + end + + it 'should allow index' do + expect(ability).to be_able_to(:index, resource) + end +end + +shared_examples_for 'update only' do + it 'should not allow create' do + expect(ability).to_not be_able_to(:create, resource) + end + + it 'should not allow read' do + expect(ability).to_not be_able_to(:read, resource) + end + + it 'should allow update' do + expect(ability).to be_able_to(:update, resource) + end + + it 'should not allow index' do + expect(ability).to_not be_able_to(:index, resource) + end +end diff --git a/core/lib/spree/testing_support/authorization_helpers.rb b/core/lib/spree/testing_support/authorization_helpers.rb new file mode 100644 index 00000000000..766d94362b4 --- /dev/null +++ b/core/lib/spree/testing_support/authorization_helpers.rb @@ -0,0 +1,63 @@ +module Spree + module TestingSupport + module AuthorizationHelpers + module CustomAbility + def build_ability(&block) + block ||= proc{ |u| can :manage, :all } + Class.new do + include CanCan::Ability + define_method(:initialize, block) + end + end + end + + module Controller + include CustomAbility + + def stub_authorization!(&block) + ability_class = build_ability(&block) + before do + allow(controller).to receive(:current_ability).and_return(ability_class.new(nil)) + end + end + end + + module Request + include CustomAbility + + def stub_authorization! + ability = build_ability + + after(:all) do + Spree::Ability.remove_ability(ability) + end + + before(:all) do + Spree::Ability.register_ability(ability) + end + + before do + allow(Spree.user_class).to receive(:find_by). + with(hash_including(:spree_api_key)). + and_return(Spree.user_class.new) + end + end + + def custom_authorization!(&block) + ability = build_ability(&block) + after(:all) do + Spree::Ability.remove_ability(ability) + end + before(:all) do + Spree::Ability.register_ability(ability) + end + end + end + end + end +end + +RSpec.configure do |config| + config.extend Spree::TestingSupport::AuthorizationHelpers::Controller, type: :controller + config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :feature +end diff --git a/core/lib/spree/testing_support/bar_ability.rb b/core/lib/spree/testing_support/bar_ability.rb new file mode 100644 index 00000000000..ad23f1e4360 --- /dev/null +++ b/core/lib/spree/testing_support/bar_ability.rb @@ -0,0 +1,14 @@ +# Fake ability for testing administration +class BarAbility + include CanCan::Ability + + def initialize(user) + user ||= Spree::User.new + if user.has_spree_role? 'bar' + # allow dispatch to :admin, :index, and :show on Spree::Order + can [:admin, :index, :show], Spree::Order + # allow dispatch to :index, :show, :create and :update shipments on the admin + can [:admin, :manage], Spree::Shipment + end + end +end diff --git a/core/lib/spree/testing_support/caching.rb b/core/lib/spree/testing_support/caching.rb new file mode 100644 index 00000000000..8405e9f4eff --- /dev/null +++ b/core/lib/spree/testing_support/caching.rb @@ -0,0 +1,47 @@ +module Spree + module TestingSupport + module Caching + def cache_writes + @cache_write_events + end + + def assert_written_to_cache(key) + unless @cache_write_events.detect { |event| event[:key].starts_with?(key) } + fail %Q{Expected to find #{key} in the cache, but didn't. + + Cache writes: + #{@cache_write_events.join("\n")} + } + end + end + + def clear_cache_events + @cache_read_events = [] + @cache_write_events = [] + end + end + end +end + +RSpec.configure do |config| + config.include Spree::TestingSupport::Caching, :caching => true + + config.before(:each, :caching => true) do + ActionController::Base.perform_caching = true + + ActiveSupport::Notifications.subscribe("read_fragment.action_controller") do |event, start_time, finish_time, _, details| + @cache_read_events ||= [] + @cache_read_events << details + end + + ActiveSupport::Notifications.subscribe("write_fragment.action_controller") do |event, start_time, finish_time, _, details| + @cache_write_events ||= [] + @cache_write_events << details + end + end + + config.after(:each, :caching => true) do + ActionController::Base.perform_caching = false + Rails.cache.clear + end +end \ No newline at end of file diff --git a/core/lib/spree/testing_support/capybara_ext.rb b/core/lib/spree/testing_support/capybara_ext.rb new file mode 100644 index 00000000000..39e20243015 --- /dev/null +++ b/core/lib/spree/testing_support/capybara_ext.rb @@ -0,0 +1,163 @@ +module CapybaraExt + def page! + save_and_open_page + end + + def click_icon(type) + find(".fa-#{type}").click + end + + def eventually_fill_in(field, options={}) + expect(page).to have_css('#' + field) + fill_in field, options + end + + def within_row(num, &block) + if RSpec.current_example.metadata[:js] + within("table.index tbody tr:nth-child(#{num})", &block) + else + within(:xpath, all("table.index tbody tr")[num-1].path, &block) + end + end + + def column_text(num) + if RSpec.current_example.metadata[:js] + find("td:nth-child(#{num})").text + else + all("td")[num-1].text + end + end + + def set_select2_field(field, value) + page.execute_script %Q{$('#{field}').select2('val', '#{value}')} + end + + def select2_search(value, options) + label = find_label_by_text(options[:from]) + within label.first(:xpath,".//..") do + options[:from] = "##{find(".select2-container")["id"]}" + end + targetted_select2_search(value, options) + end + + def targetted_select2_search(value, options) + page.execute_script %Q{$('#{options[:from]}').select2('open')} + page.execute_script "$('#{options[:dropdown_css]} input.select2-input').val('#{value}').trigger('keyup-change');" + select_select2_result(value) + end + + def select2(value, options) + label = find_label_by_text(options[:from]) + + within label.first(:xpath,".//..") do + options[:from] = "##{find(".select2-container")["id"]}" + end + targetted_select2(value, options) + end + + def select2_no_label value, options={} + raise "Must pass a hash containing 'from'" if not options.is_a?(Hash) or not options.has_key?(:from) + + placeholder = options[:from] + minlength = options[:minlength] || 4 + + click_link placeholder + + select_select2_result(value) + end + + def targetted_select2(value, options) + # find select2 element and click it + find(options[:from]).find('a').click + select_select2_result(value) + end + + def select_select2_result(value) + # results are in a div appended to the end of the document + within(:xpath, '//body') do + page.find("div.select2-result-label", text: %r{#{Regexp.escape(value)}}i).click + end + end + + def find_label_by_text(text) + label = find_label(text) + counter = 0 + + # Because JavaScript testing is prone to errors... + while label.nil? && counter < 10 + sleep(1) + counter += 1 + label = find_label(text) + end + + if label.nil? + raise "Could not find label by text #{text}" + end + + label + end + + def find_label(text) + first(:xpath, "//label[text()[contains(.,'#{text}')]]") + end + + def wait_for_ajax + counter = 0 + while page.evaluate_script("typeof($) === 'undefined' || $.active > 0") + counter += 1 + sleep(0.1) + raise "AJAX request took longer than 5 seconds." if counter >= 50 + end + end + + def accept_alert + page.evaluate_script('window.confirm = function() { return true; }') + yield + end + + def dismiss_alert + page.evaluate_script('window.confirm = function() { return false; }') + yield + # Restore existing default + page.evaluate_script('window.confirm = function() { return true; }') + end +end + +Capybara.configure do |config| + config.match = :prefer_exact + config.ignore_hidden_elements = true +end + +RSpec::Matchers.define :have_meta do |name, expected| + match do |actual| + has_css?("meta[name='#{name}'][content='#{expected}']", visible: false) + end + + failure_message do |actual| + actual = first("meta[name='#{name}']") + if actual + "expected that meta #{name} would have content='#{expected}' but was '#{actual[:content]}'" + else + "expected that meta #{name} would exist with content='#{expected}'" + end + end +end + +RSpec::Matchers.define :have_title do |expected| + match do |actual| + has_css?("title", text: expected, visible: false) + end + + failure_message do |actual| + actual = first("title") + if actual + "expected that title would have been '#{expected}' but was '#{actual.text}'" + else + "expected that title would exist with '#{expected}'" + end + end +end + +RSpec.configure do |c| + c.include CapybaraExt +end diff --git a/core/lib/spree/testing_support/common_rake.rb b/core/lib/spree/testing_support/common_rake.rb new file mode 100644 index 00000000000..745182e1a11 --- /dev/null +++ b/core/lib/spree/testing_support/common_rake.rb @@ -0,0 +1,50 @@ +unless defined?(Spree::InstallGenerator) + require 'generators/spree/install/install_generator' +end + +require 'generators/spree/dummy/dummy_generator' + +desc "Generates a dummy app for testing" +namespace :common do + task :test_app, :user_class do |t, args| + args.with_defaults(:user_class => "Spree::LegacyUser") + require "#{ENV['LIB_NAME']}" + + ENV["RAILS_ENV"] = 'test' + + Spree::DummyGenerator.start ["--lib_name=#{ENV['LIB_NAME']}", "--quiet"] + Spree::InstallGenerator.start ["--lib_name=#{ENV['LIB_NAME']}", "--auto-accept", "--migrate=false", "--seed=false", "--sample=false", "--quiet", "--user_class=#{args[:user_class]}"] + + puts "Setting up dummy database..." + cmd = "bundle exec rake db:drop db:create db:migrate" + + if RUBY_PLATFORM =~ /mswin/ #windows + cmd += " >nul" + else + cmd += " >/dev/null" + end + + system(cmd) + + begin + require "generators/#{ENV['LIB_NAME']}/install/install_generator" + puts 'Running extension installation generator...' + "#{ENV['LIB_NAME'].camelize}::Generators::InstallGenerator".constantize.start(["--auto-run-migrations"]) + rescue LoadError + puts 'Skipping installation no generator to run...' + end + end + + task :seed do |t, args| + puts "Seeding ..." + cmd = "bundle exec rake db:seed RAILS_ENV=test" + + if RUBY_PLATFORM =~ /mswin/ #windows + cmd += " >nul" + else + cmd += " >/dev/null" + end + + system(cmd) + end +end diff --git a/core/lib/spree/testing_support/controller_requests.rb b/core/lib/spree/testing_support/controller_requests.rb new file mode 100644 index 00000000000..cdb331d391c --- /dev/null +++ b/core/lib/spree/testing_support/controller_requests.rb @@ -0,0 +1,79 @@ +# Use this module to easily test Spree actions within Spree components +# or inside your application to test routes for the mounted Spree engine. +# +# Inside your spec_helper.rb, include this module inside the RSpec.configure +# block by doing this: +# +# require 'spree/testing_support/controller_requests' +# RSpec.configure do |c| +# c.include Spree::TestingSupport::ControllerRequests, :type => :controller +# end +# +# Then, in your controller tests, you can access spree routes like this: +# +# require 'spec_helper' +# +# describe Spree::ProductsController do +# it "can see all the products" do +# spree_get :index +# end +# end +# +# Use spree_get, spree_post, spree_put or spree_delete to make requests +# to the Spree engine, and use regular get, post, put or delete to make +# requests to your application. +# +module Spree + module TestingSupport + module ControllerRequests + def spree_get(action, parameters = nil, session = nil, flash = nil) + process_spree_action(action, parameters, session, flash, "GET") + end + + # Executes a request simulating POST HTTP method and set/volley the response + def spree_post(action, parameters = nil, session = nil, flash = nil) + process_spree_action(action, parameters, session, flash, "POST") + end + + # Executes a request simulating PUT HTTP method and set/volley the response + def spree_put(action, parameters = nil, session = nil, flash = nil) + process_spree_action(action, parameters, session, flash, "PUT") + end + + # Executes a request simulating DELETE HTTP method and set/volley the response + def spree_delete(action, parameters = nil, session = nil, flash = nil) + process_spree_action(action, parameters, session, flash, "DELETE") + end + + def spree_xhr_get(action, parameters = nil, session = nil, flash = nil) + process_spree_xhr_action(action, parameters, session, flash, :get) + end + + def spree_xhr_post(action, parameters = nil, session = nil, flash = nil) + process_spree_xhr_action(action, parameters, session, flash, :post) + end + + def spree_xhr_put(action, parameters = nil, session = nil, flash = nil) + process_spree_xhr_action(action, parameters, session, flash, :put) + end + + def spree_xhr_delete(action, parameters = nil, session = nil, flash = nil) + process_spree_xhr_action(action, parameters, session, flash, :delete) + end + + private + + def process_spree_action(action, parameters = nil, session = nil, flash = nil, method = "GET") + parameters ||= {} + process(action, method, parameters.merge!(:use_route => :spree), session, flash) + end + + def process_spree_xhr_action(action, parameters = nil, session = nil, flash = nil, method = :get) + parameters ||= {} + parameters.reverse_merge!(:format => :json) + parameters.merge!(:use_route => :spree) + xml_http_request(method, action, parameters, session, flash) + end + end + end +end diff --git a/core/lib/spree/testing_support/extension_rake.rb b/core/lib/spree/testing_support/extension_rake.rb new file mode 100644 index 00000000000..10959ffce5d --- /dev/null +++ b/core/lib/spree/testing_support/extension_rake.rb @@ -0,0 +1,10 @@ +require 'spree/testing_support/common_rake' + +desc "Generates a dummy app for testing an extension" +namespace :extension do + task :test_app, [:user_class] do |t, args| + Spree::DummyGeneratorHelper.inject_extension_requirements = true + Rake::Task['common:test_app'].invoke + end +end + diff --git a/core/lib/spree/testing_support/factories.rb b/core/lib/spree/testing_support/factories.rb new file mode 100644 index 00000000000..15208c88d36 --- /dev/null +++ b/core/lib/spree/testing_support/factories.rb @@ -0,0 +1,19 @@ +require 'factory_girl' + +Spree::Zone.class_eval do + def self.global + find_by(name: 'GlobalZone') || FactoryGirl.create(:global_zone) + end +end + +Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f| + require File.expand_path(f) +end + +FactoryGirl.define do + sequence(:random_string) { Faker::Lorem.sentence } + sequence(:random_description) { Faker::Lorem.paragraphs(1 + Kernel.rand(5)).join("\n") } + sequence(:random_email) { Faker::Internet.email } + + sequence(:sku) { |n| "SKU-#{n}" } +end diff --git a/core/lib/spree/testing_support/factories/address_factory.rb b/core/lib/spree/testing_support/factories/address_factory.rb new file mode 100644 index 00000000000..be339bd4c4c --- /dev/null +++ b/core/lib/spree/testing_support/factories/address_factory.rb @@ -0,0 +1,22 @@ +FactoryGirl.define do + factory :address, aliases: [:bill_address, :ship_address], class: Spree::Address do + firstname 'John' + lastname 'Doe' + company 'Company' + address1 '10 Lovely Street' + address2 'Northwest' + city 'Herndon' + zipcode '35005' + phone '555-555-0199' + alternative_phone '555-555-0199' + + state { |address| address.association(:state) } + country do |address| + if address.state + address.state.country + else + address.association(:country) + end + end + end +end diff --git a/core/lib/spree/testing_support/factories/adjustment_factory.rb b/core/lib/spree/testing_support/factories/adjustment_factory.rb new file mode 100644 index 00000000000..596c977e7a1 --- /dev/null +++ b/core/lib/spree/testing_support/factories/adjustment_factory.rb @@ -0,0 +1,26 @@ +FactoryGirl.define do + factory :adjustment, class: Spree::Adjustment do + association(:adjustable, factory: :order) + amount 100.0 + label 'Shipping' + association(:source, factory: :tax_rate) + eligible true + end + + factory :tax_adjustment, class: Spree::Adjustment do + association(:adjustable, factory: :line_item) + amount 10.0 + label 'VAT 5%' + association(:source, factory: :tax_rate) + eligible true + + after(:create) do |adjustment| + # Set correct tax category, so that adjustment amount is not 0 + if adjustment.adjustable.is_a?(Spree::LineItem) + adjustment.source.tax_category = adjustment.adjustable.tax_category + adjustment.source.save + adjustment.update! + end + end + end +end diff --git a/core/lib/spree/testing_support/factories/calculator_factory.rb b/core/lib/spree/testing_support/factories/calculator_factory.rb new file mode 100644 index 00000000000..66d65203ae6 --- /dev/null +++ b/core/lib/spree/testing_support/factories/calculator_factory.rb @@ -0,0 +1,20 @@ +FactoryGirl.define do + factory :calculator, class: Spree::Calculator::FlatRate do + after(:create) { |c| c.set_preference(:amount, 10.0) } + end + + factory :no_amount_calculator, class: Spree::Calculator::FlatRate do + after(:create) { |c| c.set_preference(:amount, 0) } + end + + factory :default_tax_calculator, class: Spree::Calculator::DefaultTax do + end + + factory :shipping_calculator, class: Spree::Calculator::Shipping::FlatRate do + after(:create) { |c| c.set_preference(:amount, 10.0) } + end + + factory :shipping_no_amount_calculator, class: Spree::Calculator::Shipping::FlatRate do + after(:create) { |c| c.set_preference(:amount, 0) } + end +end diff --git a/core/lib/spree/testing_support/factories/configuration_factory.rb b/core/lib/spree/testing_support/factories/configuration_factory.rb new file mode 100644 index 00000000000..ca71ce69829 --- /dev/null +++ b/core/lib/spree/testing_support/factories/configuration_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :configuration, class: Spree::Configuration do + name 'Default Configuration' + type 'app_configuration' + end +end diff --git a/core/lib/spree/testing_support/factories/country_factory.rb b/core/lib/spree/testing_support/factories/country_factory.rb new file mode 100644 index 00000000000..04953808329 --- /dev/null +++ b/core/lib/spree/testing_support/factories/country_factory.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :country, class: Spree::Country do + iso_name 'UNITED STATES' + name 'United States of America' + iso 'US' + iso3 'USA' + numcode 840 + end +end diff --git a/core/lib/spree/testing_support/factories/credit_card_factory.rb b/core/lib/spree/testing_support/factories/credit_card_factory.rb new file mode 100644 index 00000000000..f91335f7879 --- /dev/null +++ b/core/lib/spree/testing_support/factories/credit_card_factory.rb @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :credit_card, class: Spree::CreditCard do + verification_value 123 + month 12 + year { 1.year.from_now.year } + number '4111111111111111' + name 'Spree Commerce' + association(:payment_method, factory: :credit_card_payment_method) + end +end diff --git a/core/lib/spree/testing_support/factories/customer_return_factory.rb b/core/lib/spree/testing_support/factories/customer_return_factory.rb new file mode 100644 index 00000000000..3f1042e53d3 --- /dev/null +++ b/core/lib/spree/testing_support/factories/customer_return_factory.rb @@ -0,0 +1,31 @@ +FactoryGirl.define do + + factory :customer_return, class: Spree::CustomerReturn do + association(:stock_location, factory: :stock_location) + + transient do + line_items_count 1 + return_items_count { line_items_count } + end + + before(:create) do |customer_return, evaluator| + shipped_order = create(:shipped_order, line_items_count: evaluator.line_items_count) + + shipped_order.inventory_units.take(evaluator.return_items_count).each do |inventory_unit| + customer_return.return_items << build(:return_item, inventory_unit: inventory_unit) + end + end + + factory :customer_return_with_accepted_items do + after(:create) do |customer_return| + customer_return.return_items.each(&:accept!) + end + end + end + + # for the case when you want to supply existing return items instead of generating some + factory :customer_return_without_return_items, class: Spree::CustomerReturn do + association(:stock_location, factory: :stock_location) + end + +end diff --git a/core/lib/spree/testing_support/factories/inventory_unit_factory.rb b/core/lib/spree/testing_support/factories/inventory_unit_factory.rb new file mode 100644 index 00000000000..6486c6c7a99 --- /dev/null +++ b/core/lib/spree/testing_support/factories/inventory_unit_factory.rb @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :inventory_unit, class: Spree::InventoryUnit do + variant + order + line_item + state 'on_hand' + association(:shipment, factory: :shipment, state: 'pending') + # return_authorization + end +end diff --git a/core/lib/spree/testing_support/factories/line_item_factory.rb b/core/lib/spree/testing_support/factories/line_item_factory.rb new file mode 100644 index 00000000000..f9c85d53f38 --- /dev/null +++ b/core/lib/spree/testing_support/factories/line_item_factory.rb @@ -0,0 +1,12 @@ +FactoryGirl.define do + factory :line_item, class: Spree::LineItem do + quantity 1 + price { BigDecimal.new('10.00') } + pre_tax_amount { price } + order + transient do + association :product + end + variant{ product.master } + end +end diff --git a/core/lib/spree/testing_support/factories/options_factory.rb b/core/lib/spree/testing_support/factories/options_factory.rb new file mode 100644 index 00000000000..0e4ba0e1a29 --- /dev/null +++ b/core/lib/spree/testing_support/factories/options_factory.rb @@ -0,0 +1,13 @@ +FactoryGirl.define do + factory :option_value, class: Spree::OptionValue do + sequence(:name) { |n| "Size-#{n}" } + + presentation 'S' + option_type + end + + factory :option_type, class: Spree::OptionType do + sequence(:name) { |n| "foo-size-#{n}" } + presentation 'Size' + end +end diff --git a/core/lib/spree/testing_support/factories/order_factory.rb b/core/lib/spree/testing_support/factories/order_factory.rb new file mode 100644 index 00000000000..c6cdf49c790 --- /dev/null +++ b/core/lib/spree/testing_support/factories/order_factory.rb @@ -0,0 +1,90 @@ +FactoryGirl.define do + factory :order, class: Spree::Order do + user + bill_address + completed_at nil + email { user.email } + store + + transient do + line_items_price BigDecimal.new(10) + end + + factory :order_with_totals do + after(:create) do |order, evaluator| + create(:line_item, order: order, price: evaluator.line_items_price) + order.line_items.reload # to ensure order.line_items is accessible after + end + end + + factory :order_with_line_item_quantity do + transient do + line_items_quantity 1 + end + + after(:create) do |order, evaluator| + create(:line_item, order: order, price: evaluator.line_items_price, quantity: evaluator.line_items_quantity) + order.line_items.reload # to ensure order.line_items is accessible after + end + end + + factory :order_with_line_items do + bill_address + ship_address + + transient do + line_items_count 1 + shipment_cost 100 + end + + after(:create) do |order, evaluator| + create_list(:line_item, evaluator.line_items_count, order: order, price: evaluator.line_items_price) + order.line_items.reload + + create(:shipment, order: order, cost: evaluator.shipment_cost) + order.shipments.reload + + order.update! + end + + factory :completed_order_with_totals do + state 'complete' + + after(:create) do |order| + order.refresh_shipment_rates + order.update_column(:completed_at, Time.now) + end + + factory :completed_order_with_pending_payment do + after(:create) do |order| + create(:payment, amount: order.total, order: order) + end + end + + factory :order_ready_to_ship do + payment_state 'paid' + shipment_state 'ready' + + after(:create) do |order| + create(:payment, amount: order.total, order: order, state: 'completed') + order.shipments.each do |shipment| + shipment.inventory_units.update_all state: 'on_hand' + shipment.update_column('state', 'ready') + end + order.reload + end + + factory :shipped_order do + after(:create) do |order| + order.shipments.each do |shipment| + shipment.inventory_units.update_all state: 'shipped' + shipment.update_column('state', 'shipped') + end + order.reload + end + end + end + end + end + end +end diff --git a/core/lib/spree/testing_support/factories/payment_factory.rb b/core/lib/spree/testing_support/factories/payment_factory.rb new file mode 100644 index 00000000000..dfdc433520c --- /dev/null +++ b/core/lib/spree/testing_support/factories/payment_factory.rb @@ -0,0 +1,23 @@ +FactoryGirl.define do + factory :payment, class: Spree::Payment do + amount 45.75 + association(:payment_method, factory: :credit_card_payment_method) + association(:source, factory: :credit_card) + order + state 'checkout' + response_code '12345' + + factory :payment_with_refund do + state 'completed' + after :create do |payment| + create(:refund, amount: 5, payment: payment) + end + end + end + + factory :check_payment, class: Spree::Payment do + amount 45.75 + association(:payment_method, factory: :check_payment_method) + order + end +end diff --git a/core/lib/spree/testing_support/factories/payment_method_factory.rb b/core/lib/spree/testing_support/factories/payment_method_factory.rb new file mode 100644 index 00000000000..3417eb9acbe --- /dev/null +++ b/core/lib/spree/testing_support/factories/payment_method_factory.rb @@ -0,0 +1,18 @@ +FactoryGirl.define do + factory :check_payment_method, class: Spree::PaymentMethod::Check do + name 'Check' + environment 'test' + end + + factory :credit_card_payment_method, class: Spree::Gateway::Bogus do + name 'Credit Card' + environment 'test' + end + + # authorize.net was moved to spree_gateway. + # Leaving this factory in place with bogus in case anyone is using it. + factory :simple_credit_card_payment_method, class: Spree::Gateway::BogusSimple do + name 'Credit Card' + environment 'test' + end +end diff --git a/core/lib/spree/testing_support/factories/price_factory.rb b/core/lib/spree/testing_support/factories/price_factory.rb new file mode 100644 index 00000000000..1612eb11b15 --- /dev/null +++ b/core/lib/spree/testing_support/factories/price_factory.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :price, class: Spree::Price do + variant + amount 19.99 + currency 'USD' + end +end diff --git a/core/lib/spree/testing_support/factories/product_factory.rb b/core/lib/spree/testing_support/factories/product_factory.rb new file mode 100644 index 00000000000..d679fb9fff6 --- /dev/null +++ b/core/lib/spree/testing_support/factories/product_factory.rb @@ -0,0 +1,36 @@ +FactoryGirl.define do + factory :base_product, class: Spree::Product do + sequence(:name) { |n| "Product ##{n} - #{Kernel.rand(9999)}" } + description { generate(:random_description) } + price 19.99 + cost_price 17.00 + sku { generate(:sku) } + available_on { 1.year.ago } + deleted_at nil + shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) } + + # ensure stock item will be created for this products master + before(:create) { create(:stock_location) if Spree::StockLocation.count == 0 } + + factory :custom_product do + name 'Custom Product' + price 17.99 + + tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) } + end + + factory :product do + tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) } + + factory :product_in_stock do + after :create do |product| + product.master.stock_items.first.adjust_count_on_hand(10) + end + end + + factory :product_with_option_types do + after(:create) { |product| create(:product_option_type, product: product) } + end + end + end +end diff --git a/core/lib/spree/testing_support/factories/product_option_type_factory.rb b/core/lib/spree/testing_support/factories/product_option_type_factory.rb new file mode 100644 index 00000000000..ddb8a0bb1e4 --- /dev/null +++ b/core/lib/spree/testing_support/factories/product_option_type_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :product_option_type, class: Spree::ProductOptionType do + product + option_type + end +end diff --git a/core/lib/spree/testing_support/factories/product_property_factory.rb b/core/lib/spree/testing_support/factories/product_property_factory.rb new file mode 100644 index 00000000000..68f4f439c3e --- /dev/null +++ b/core/lib/spree/testing_support/factories/product_property_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :product_property, class: Spree::ProductProperty do + product + property + end +end diff --git a/core/lib/spree/testing_support/factories/promotion_category_factory.rb b/core/lib/spree/testing_support/factories/promotion_category_factory.rb new file mode 100644 index 00000000000..ebde03a97ad --- /dev/null +++ b/core/lib/spree/testing_support/factories/promotion_category_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :promotion_category, class: Spree::PromotionCategory do + name 'Promotion Category' + end +end + diff --git a/core/lib/spree/testing_support/factories/promotion_factory.rb b/core/lib/spree/testing_support/factories/promotion_factory.rb new file mode 100644 index 00000000000..e97a75bf889 --- /dev/null +++ b/core/lib/spree/testing_support/factories/promotion_factory.rb @@ -0,0 +1,52 @@ +FactoryGirl.define do + factory :promotion, class: Spree::Promotion do + name 'Promo' + + trait :with_line_item_adjustment do + transient do + adjustment_rate 10 + end + + after(:create) do |promotion, evaluator| + calculator = Spree::Calculator::FlatRate.new + calculator.preferred_amount = evaluator.adjustment_rate + Spree::Promotion::Actions::CreateItemAdjustments.create!(calculator: calculator, promotion: promotion) + end + end + factory :promotion_with_item_adjustment, traits: [:with_line_item_adjustment] + + trait :with_order_adjustment do + transient do + weighted_order_adjustment_amount 10 + end + + after(:create) do |promotion, evaluator| + calculator = Spree::Calculator::FlatRate.new + calculator.preferred_amount = evaluator.weighted_order_adjustment_amount + action = Spree::Promotion::Actions::CreateAdjustment.create!(calculator: calculator) + promotion.actions << action + promotion.save! + end + end + factory :promotion_with_order_adjustment, traits: [:with_order_adjustment] + + trait :with_item_total_rule do + transient do + item_total_threshold_amount 10 + end + + after(:create) do |promotion, evaluator| + rule = Spree::Promotion::Rules::ItemTotal.create!( + preferred_operator_min: 'gte', + preferred_operator_max: 'lte', + preferred_amount_min: evaluator.item_total_threshold_amount, + preferred_amount_max: evaluator.item_total_threshold_amount + 100 + ) + promotion.rules << rule + promotion.save! + end + end + factory :promotion_with_item_total_rule, traits: [:with_item_total_rule] + + end +end diff --git a/core/lib/spree/testing_support/factories/property_factory.rb b/core/lib/spree/testing_support/factories/property_factory.rb new file mode 100644 index 00000000000..793f3d5f67c --- /dev/null +++ b/core/lib/spree/testing_support/factories/property_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :property, class: Spree::Property do + name 'baseball_cap_color' + presentation 'cap color' + end +end diff --git a/core/lib/spree/testing_support/factories/prototype_factory.rb b/core/lib/spree/testing_support/factories/prototype_factory.rb new file mode 100644 index 00000000000..897a86a4f56 --- /dev/null +++ b/core/lib/spree/testing_support/factories/prototype_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :prototype, class: Spree::Prototype do + name 'Baseball Cap' + properties { [create(:property)] } + end +end diff --git a/core/lib/spree/testing_support/factories/refund_factory.rb b/core/lib/spree/testing_support/factories/refund_factory.rb new file mode 100644 index 00000000000..1a79c4a02c1 --- /dev/null +++ b/core/lib/spree/testing_support/factories/refund_factory.rb @@ -0,0 +1,14 @@ +FactoryGirl.define do + sequence(:refund_transaction_id) { |n| "fake-refund-transaction-#{n}"} + + factory :refund, class: Spree::Refund do + amount 100.00 + transaction_id { generate(:refund_transaction_id) } + association(:payment, state: 'completed') + association(:reason, factory: :refund_reason) + end + + factory :refund_reason, class: Spree::RefundReason do + sequence(:name) { |n| "Refund for return ##{n}" } + end +end diff --git a/core/lib/spree/testing_support/factories/reimbursement_factory.rb b/core/lib/spree/testing_support/factories/reimbursement_factory.rb new file mode 100644 index 00000000000..aed755effc9 --- /dev/null +++ b/core/lib/spree/testing_support/factories/reimbursement_factory.rb @@ -0,0 +1,16 @@ +FactoryGirl.define do + factory :reimbursement, class: Spree::Reimbursement do + transient do + return_items_count 1 + end + + customer_return { create(:customer_return_with_accepted_items, line_items_count: return_items_count) } + + before(:create) do |reimbursement, evaluator| + reimbursement.order ||= reimbursement.customer_return.order + if reimbursement.return_items.empty? + reimbursement.return_items = reimbursement.customer_return.return_items + end + end + end +end diff --git a/core/lib/spree/testing_support/factories/reimbursement_type_factory.rb b/core/lib/spree/testing_support/factories/reimbursement_type_factory.rb new file mode 100644 index 00000000000..74495bbfb59 --- /dev/null +++ b/core/lib/spree/testing_support/factories/reimbursement_type_factory.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :reimbursement_type, class: Spree::ReimbursementType do + sequence(:name) { |n| "Reimbursement Type #{n}" } + active true + mutable true + end +end diff --git a/core/lib/spree/testing_support/factories/return_authorization_factory.rb b/core/lib/spree/testing_support/factories/return_authorization_factory.rb new file mode 100644 index 00000000000..c0f14f2eb75 --- /dev/null +++ b/core/lib/spree/testing_support/factories/return_authorization_factory.rb @@ -0,0 +1,18 @@ +FactoryGirl.define do + factory :return_authorization, class: Spree::ReturnAuthorization do + association(:order, factory: :shipped_order) + association(:stock_location, factory: :stock_location) + association(:reason, factory: :return_authorization_reason) + memo 'Items were broken' + end + + factory :new_return_authorization, class: Spree::ReturnAuthorization do + association(:order, factory: :shipped_order) + association(:stock_location, factory: :stock_location) + association(:reason, factory: :return_authorization_reason) + end + + factory :return_authorization_reason, class: Spree::ReturnAuthorizationReason do + sequence(:name) { |n| "Defect ##{n}" } + end +end diff --git a/core/lib/spree/testing_support/factories/return_item_factory.rb b/core/lib/spree/testing_support/factories/return_item_factory.rb new file mode 100644 index 00000000000..271b6826218 --- /dev/null +++ b/core/lib/spree/testing_support/factories/return_item_factory.rb @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :return_item, class: Spree::ReturnItem do + association(:inventory_unit, factory: :inventory_unit, state: :shipped) + association(:return_authorization, factory: :return_authorization) + + factory :exchange_return_item do + association(:exchange_variant, factory: :variant) + end + end +end diff --git a/core/lib/spree/testing_support/factories/role_factory.rb b/core/lib/spree/testing_support/factories/role_factory.rb new file mode 100644 index 00000000000..4974b645a00 --- /dev/null +++ b/core/lib/spree/testing_support/factories/role_factory.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :role, class: Spree::Role do + sequence(:name) { |n| "Role ##{n}" } + + factory :admin_role do + name 'admin' + end + end +end diff --git a/core/lib/spree/testing_support/factories/shipment_factory.rb b/core/lib/spree/testing_support/factories/shipment_factory.rb new file mode 100644 index 00000000000..06c1c96e888 --- /dev/null +++ b/core/lib/spree/testing_support/factories/shipment_factory.rb @@ -0,0 +1,23 @@ +FactoryGirl.define do + factory :shipment, class: Spree::Shipment do + tracking 'U10000' + cost 100.00 + state 'pending' + order + stock_location + + after(:create) do |shipment, evalulator| + shipment.add_shipping_method(create(:shipping_method), true) + + shipment.order.line_items.each do |line_item| + line_item.quantity.times do + shipment.inventory_units.create( + order_id: shipment.order_id, + variant_id: line_item.variant_id, + line_item_id: line_item.id + ) + end + end + end + end +end diff --git a/core/lib/spree/testing_support/factories/shipping_category_factory.rb b/core/lib/spree/testing_support/factories/shipping_category_factory.rb new file mode 100644 index 00000000000..c3ea75c9f03 --- /dev/null +++ b/core/lib/spree/testing_support/factories/shipping_category_factory.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :shipping_category, class: Spree::ShippingCategory do + sequence(:name) { |n| "ShippingCategory ##{n}" } + end +end diff --git a/core/lib/spree/testing_support/factories/shipping_method_factory.rb b/core/lib/spree/testing_support/factories/shipping_method_factory.rb new file mode 100644 index 00000000000..acab43731f5 --- /dev/null +++ b/core/lib/spree/testing_support/factories/shipping_method_factory.rb @@ -0,0 +1,21 @@ +FactoryGirl.define do + factory :base_shipping_method, class: Spree::ShippingMethod do + zones { |a| [Spree::Zone.global] } + name 'UPS Ground' + code 'UPS_GROUND' + + before(:create) do |shipping_method, evaluator| + if shipping_method.shipping_categories.empty? + shipping_method.shipping_categories << (Spree::ShippingCategory.first || create(:shipping_category)) + end + end + + factory :shipping_method, class: Spree::ShippingMethod do + association(:calculator, factory: :shipping_calculator, strategy: :build) + end + + factory :free_shipping_method, class: Spree::ShippingMethod do + association(:calculator, factory: :shipping_no_amount_calculator, strategy: :build) + end + end +end diff --git a/core/lib/spree/core/testing_support/factories/state_factory.rb b/core/lib/spree/testing_support/factories/state_factory.rb similarity index 83% rename from core/lib/spree/core/testing_support/factories/state_factory.rb rename to core/lib/spree/testing_support/factories/state_factory.rb index 23d7107cbe1..c5ca779c507 100644 --- a/core/lib/spree/core/testing_support/factories/state_factory.rb +++ b/core/lib/spree/testing_support/factories/state_factory.rb @@ -1,5 +1,5 @@ FactoryGirl.define do - factory :state, :class => Spree::State do + factory :state, class: Spree::State do name 'Alabama' abbr 'AL' country do |country| diff --git a/core/lib/spree/testing_support/factories/stock_factory.rb b/core/lib/spree/testing_support/factories/stock_factory.rb new file mode 100644 index 00000000000..adabf9cee14 --- /dev/null +++ b/core/lib/spree/testing_support/factories/stock_factory.rb @@ -0,0 +1,31 @@ +FactoryGirl.define do + # must use build() + factory :stock_packer, class: Spree::Stock::Packer do + transient do + stock_location { build(:stock_location) } + contents [] + end + + initialize_with { new(stock_location, contents) } + end + + factory :stock_package, class: Spree::Stock::Package do + transient do + stock_location { build(:stock_location) } + contents { [] } + variants_contents { {} } + end + + initialize_with { new(stock_location, contents) } + + after(:build) do |package, evaluator| + evaluator.variants_contents.each do |variant, count| + package.add_multiple build_list(:inventory_unit, count, variant: variant) + end + end + + factory :stock_package_fulfilled do + transient { variants_contents { { build(:variant) => 2 } } } + end + end +end diff --git a/core/lib/spree/testing_support/factories/stock_item_factory.rb b/core/lib/spree/testing_support/factories/stock_item_factory.rb new file mode 100644 index 00000000000..1a24818de1c --- /dev/null +++ b/core/lib/spree/testing_support/factories/stock_item_factory.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :stock_item, class: Spree::StockItem do + backorderable true + stock_location + variant + + after(:create) { |object| object.adjust_count_on_hand(10) } + end +end diff --git a/core/lib/spree/testing_support/factories/stock_location_factory.rb b/core/lib/spree/testing_support/factories/stock_location_factory.rb new file mode 100644 index 00000000000..61251381f49 --- /dev/null +++ b/core/lib/spree/testing_support/factories/stock_location_factory.rb @@ -0,0 +1,28 @@ +FactoryGirl.define do + factory :stock_location, class: Spree::StockLocation do + name 'NY Warehouse' + address1 '1600 Pennsylvania Ave NW' + city 'Washington' + zipcode '20500' + phone '(202) 456-1111' + active true + backorderable_default true + + country { |stock_location| Spree::Country.first || stock_location.association(:country) } + state do |stock_location| + stock_location.country.states.first || stock_location.association(:state, :country => stock_location.country) + end + + factory :stock_location_with_items do + after(:create) do |stock_location, evaluator| + # variant will add itself to all stock_locations in an after_create + # creating a product will automatically create a master variant + product_1 = create(:product) + product_2 = create(:product) + + stock_location.stock_items.where(:variant_id => product_1.master.id).first.adjust_count_on_hand(10) + stock_location.stock_items.where(:variant_id => product_2.master.id).first.adjust_count_on_hand(20) + end + end + end +end diff --git a/core/lib/spree/testing_support/factories/stock_movement_factory.rb b/core/lib/spree/testing_support/factories/stock_movement_factory.rb new file mode 100644 index 00000000000..04b5b0cc3c5 --- /dev/null +++ b/core/lib/spree/testing_support/factories/stock_movement_factory.rb @@ -0,0 +1,11 @@ +FactoryGirl.define do + factory :stock_movement, class: Spree::StockMovement do + quantity 1 + action 'sold' + stock_item + end + + trait :received do + action 'received' + end +end diff --git a/core/lib/spree/testing_support/factories/store_factory.rb b/core/lib/spree/testing_support/factories/store_factory.rb new file mode 100644 index 00000000000..092490de34d --- /dev/null +++ b/core/lib/spree/testing_support/factories/store_factory.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :store, class: Spree::Store do + sequence(:code) { |i| "spree_#{i}" } + name 'Spree Test Store' + url 'www.example.com' + mail_from_address 'spree@example.org' + end +end diff --git a/core/lib/spree/testing_support/factories/tax_category_factory.rb b/core/lib/spree/testing_support/factories/tax_category_factory.rb new file mode 100644 index 00000000000..57e3652689b --- /dev/null +++ b/core/lib/spree/testing_support/factories/tax_category_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :tax_category, class: Spree::TaxCategory do + name { "TaxCategory - #{rand(999999)}" } + description { generate(:random_string) } + end +end diff --git a/core/lib/spree/testing_support/factories/tax_rate_factory.rb b/core/lib/spree/testing_support/factories/tax_rate_factory.rb new file mode 100644 index 00000000000..60cb9db804c --- /dev/null +++ b/core/lib/spree/testing_support/factories/tax_rate_factory.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :tax_rate, class: Spree::TaxRate do + zone + amount 0.1 + tax_category + association(:calculator, factory: :default_tax_calculator) + end +end diff --git a/core/lib/spree/testing_support/factories/taxon_factory.rb b/core/lib/spree/testing_support/factories/taxon_factory.rb new file mode 100644 index 00000000000..2353d1927c2 --- /dev/null +++ b/core/lib/spree/testing_support/factories/taxon_factory.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :taxon, class: Spree::Taxon do + name 'Ruby on Rails' + taxonomy + parent_id nil + end +end diff --git a/core/lib/spree/testing_support/factories/taxonomy_factory.rb b/core/lib/spree/testing_support/factories/taxonomy_factory.rb new file mode 100644 index 00000000000..c4e8681d4af --- /dev/null +++ b/core/lib/spree/testing_support/factories/taxonomy_factory.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :taxonomy, class: Spree::Taxonomy do + name 'Brand' + end +end diff --git a/core/lib/spree/testing_support/factories/tracker_factory.rb b/core/lib/spree/testing_support/factories/tracker_factory.rb new file mode 100644 index 00000000000..9f0bc890b04 --- /dev/null +++ b/core/lib/spree/testing_support/factories/tracker_factory.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :tracker, class: Spree::Tracker do + environment { Rails.env } + analytics_id 'A100' + active true + end +end diff --git a/core/lib/spree/testing_support/factories/user_factory.rb b/core/lib/spree/testing_support/factories/user_factory.rb new file mode 100644 index 00000000000..b9544caa8ff --- /dev/null +++ b/core/lib/spree/testing_support/factories/user_factory.rb @@ -0,0 +1,22 @@ +FactoryGirl.define do + sequence :user_authentication_token do |n| + "xxxx#{Time.now.to_i}#{rand(1000)}#{n}xxxxxxxxxxxxx" + end + + factory :user, class: Spree.user_class do + email { generate(:random_email) } + login { email } + password 'secret' + password_confirmation { password } + authentication_token { generate(:user_authentication_token) } if Spree.user_class.attribute_method? :authentication_token + + factory :admin_user do + spree_roles { [Spree::Role.find_by(name: 'admin') || create(:role, name: 'admin')] } + end + + factory :user_with_addreses do + ship_address + bill_address + end + end +end diff --git a/core/lib/spree/testing_support/factories/variant_factory.rb b/core/lib/spree/testing_support/factories/variant_factory.rb new file mode 100644 index 00000000000..93d43499c84 --- /dev/null +++ b/core/lib/spree/testing_support/factories/variant_factory.rb @@ -0,0 +1,39 @@ +FactoryGirl.define do + sequence(:random_float) { BigDecimal.new("#{rand(200)}.#{rand(99)}") } + + factory :base_variant, class: Spree::Variant do + price 19.99 + cost_price 17.00 + sku { generate(:sku) } + weight { generate(:random_float) } + height { generate(:random_float) } + width { generate(:random_float) } + depth { generate(:random_float) } + is_master 0 + track_inventory true + + product { |p| p.association(:base_product) } + option_values { [create(:option_value)] } + + # ensure stock item will be created for this variant + before(:create) { create(:stock_location) if Spree::StockLocation.count == 0 } + + factory :variant do + # on_hand 5 + product { |p| p.association(:product) } + end + + factory :master_variant do + is_master 1 + end + + factory :on_demand_variant do + track_inventory false + + factory :on_demand_master_variant do + is_master 1 + end + end + + end +end diff --git a/core/lib/spree/testing_support/factories/zone_factory.rb b/core/lib/spree/testing_support/factories/zone_factory.rb new file mode 100644 index 00000000000..db3c3e583dd --- /dev/null +++ b/core/lib/spree/testing_support/factories/zone_factory.rb @@ -0,0 +1,17 @@ +FactoryGirl.define do + factory :global_zone, class: Spree::Zone do + name 'GlobalZone' + description { generate(:random_string) } + zone_members do |proxy| + zone = proxy.instance_eval { @instance } + Spree::Country.all.map do |c| + zone_member = Spree::ZoneMember.create(zoneable: c, zone: zone) + end + end + end + + factory :zone, class: Spree::Zone do + name { generate(:random_string) } + description { generate(:random_string) } + end +end diff --git a/core/lib/spree/testing_support/flash.rb b/core/lib/spree/testing_support/flash.rb new file mode 100644 index 00000000000..703bdb4f6f0 --- /dev/null +++ b/core/lib/spree/testing_support/flash.rb @@ -0,0 +1,27 @@ +module Spree + module TestingSupport + module Flash + def assert_flash_success(flash) + flash = convert_flash(flash) + + within("[class='flash success']") do + expect(page).to have_content(flash) + end + end + + def assert_successful_update_message(resource) + flash = Spree.t(:successfully_updated, resource: Spree.t(resource)) + assert_flash_success(flash) + end + + private + + def convert_flash(flash) + if flash.is_a?(Symbol) + flash = Spree.t(flash) + end + flash + end + end + end +end diff --git a/core/lib/spree/testing_support/i18n.rb b/core/lib/spree/testing_support/i18n.rb new file mode 100644 index 00000000000..786e2f1cf91 --- /dev/null +++ b/core/lib/spree/testing_support/i18n.rb @@ -0,0 +1,97 @@ +# This file exists solely to test whether or not there are missing translations +# within the code that Spree's test suite covers. +# +# If there is a translation referenced which has no corresponding key within the +# .yml file, then there will be a message output at the end of the suite showing +# that. +# +# If there is a translation within the locale file which *isn't* used in the +# test, this will also be shown at the end of the suite run. +module Spree + class << self + attr_accessor :used_translations, :missing_translation_messages, + :unused_translations, :unused_translation_messages + alias_method :normal_t, :t + end + + def self.t(*args) + original_args = args.dup + options = args.extract_options! + self.used_translations ||= [] + [*args.first].each do |translation_key| + key = ([*options[:scope]] << translation_key).join('.') + self.used_translations << key + end + normal_t(*original_args) + end + + def self.check_missing_translations + self.missing_translation_messages = [] + self.used_translations ||= [] + used_translations.map { |a| a.split('.') }.each do |translation_keys| + root = translations + processed_keys = [] + translation_keys.each do |key| + begin + root = root.fetch(key.to_sym) + processed_keys << key.to_sym + rescue KeyError + error = "#{(processed_keys << key).join('.')} (#{I18n.locale})" + unless Spree.missing_translation_messages.include?(error) + Spree.missing_translation_messages << error + end + end + end + end + end + + def self.check_unused_translations + self.used_translations ||= [] + self.unused_translation_messages = [] + self.unused_translations = [] + self.load_translations(translations) + translation_diff = unused_translations - used_translations + translation_diff.each do |translation| + Spree.unused_translation_messages << "#{translation} (#{I18n.locale})" + end + end + + private + + def self.load_translations(hash, root=[]) + hash.each do |k,v| + if v.is_a?(Hash) + load_translations(v, root.dup << k) + else + key = (root + [k]).join('.') + self.unused_translations << key + end + end + end + + def self.translations + @translations ||= I18n.backend.send(:translations)[I18n.locale][:spree] + end +end + +RSpec.configure do |config| + # Need to check here again because this is used in i18n_spec too. + if ENV['CHECK_TRANSLATIONS'] + config.after :suite do + Spree.check_missing_translations + if Spree.missing_translation_messages.any? + puts "\nThere are missing translations within Spree:" + puts Spree.missing_translation_messages.sort + exit(1) + end + + Spree.check_unused_translations + if false && Spree.unused_translation_messages.any? + puts "\nThere are unused translations within Spree:" + puts Spree.unused_translation_messages.sort + exit(1) + end + end + end +end + diff --git a/core/lib/spree/testing_support/order_walkthrough.rb b/core/lib/spree/testing_support/order_walkthrough.rb new file mode 100644 index 00000000000..5982915e3ba --- /dev/null +++ b/core/lib/spree/testing_support/order_walkthrough.rb @@ -0,0 +1,67 @@ +class OrderWalkthrough + def self.up_to(state) + # A payment method must exist for an order to proceed through the Address state + unless Spree::PaymentMethod.exists? + FactoryGirl.create(:check_payment_method) + end + + # Need to create a valid zone too... + zone = FactoryGirl.create(:zone) + country = FactoryGirl.create(:country) + zone.members << Spree::ZoneMember.create(:zoneable => country) + country.states << FactoryGirl.create(:state, :country => country) + + # A shipping method must exist for rates to be displayed on checkout page + unless Spree::ShippingMethod.exists? + FactoryGirl.create(:shipping_method).tap do |sm| + sm.calculator.preferred_amount = 10 + sm.calculator.preferred_currency = Spree::Config[:currency] + sm.calculator.save + end + end + + order = Spree::Order.create!(email: "spree@example.com") + add_line_item!(order) + order.next! + + end_state_position = states.index(state.to_sym) + states[0..end_state_position].each do |state| + send(state, order) + end + + order + end + + private + + def self.add_line_item!(order) + FactoryGirl.create(:line_item, order: order) + order.reload + end + + def self.address(order) + order.bill_address = FactoryGirl.create(:address, :country_id => Spree::Zone.global.members.first.zoneable.id) + order.ship_address = FactoryGirl.create(:address, :country_id => Spree::Zone.global.members.first.zoneable.id) + order.next! + end + + def self.delivery(order) + order.next! + end + + def self.payment(order) + order.payments.create!(payment_method: Spree::PaymentMethod.first, amount: order.total) + # TODO: maybe look at some way of making this payment_state change automatic + order.payment_state = 'paid' + order.next! + end + + def self.complete(order) + #noop? + end + + def self.states + [:address, :delivery, :payment, :complete] + end + +end diff --git a/core/lib/spree/testing_support/preferences.rb b/core/lib/spree/testing_support/preferences.rb new file mode 100644 index 00000000000..7c6c0fbf75c --- /dev/null +++ b/core/lib/spree/testing_support/preferences.rb @@ -0,0 +1,31 @@ +module Spree + module TestingSupport + module Preferences + # Resets all preferences to default values, you can + # pass a block to override the defaults with a block + # + # reset_spree_preferences do |config| + # config.track_inventory_levels = false + # end + # + def reset_spree_preferences(&config_block) + Spree::Preferences::Store.instance.persistence = false + Spree::Preferences::Store.instance.clear_cache + + config = Rails.application.config.spree.preferences + configure_spree_preferences &config_block if block_given? + end + + def configure_spree_preferences + config = Rails.application.config.spree.preferences + yield(config) if block_given? + end + + def assert_preference_unset(preference) + find("#preferences_#{preference}")['checked'].should be false + Spree::Config[preference].should be false + end + end + end +end + diff --git a/core/lib/spree/core/url_helpers.rb b/core/lib/spree/testing_support/url_helpers.rb similarity index 84% rename from core/lib/spree/core/url_helpers.rb rename to core/lib/spree/testing_support/url_helpers.rb index 16f74992907..d238624cc68 100644 --- a/core/lib/spree/core/url_helpers.rb +++ b/core/lib/spree/testing_support/url_helpers.rb @@ -1,5 +1,5 @@ module Spree - module Core + module TestingSupport module UrlHelpers def spree Spree::Core::Engine.routes.url_helpers diff --git a/core/lib/tasks/core.rake b/core/lib/tasks/core.rake index fdc031d2c63..7ce6b5aa2f4 100644 --- a/core/lib/tasks/core.rake +++ b/core/lib/tasks/core.rake @@ -1,43 +1,31 @@ require 'active_record' -require 'spree/core/custom_fixtures' namespace :db do desc %q{Loads a specified fixture file: -For .yml/.csv use rake db:load_file[spree/filename.yml,/absolute/path/to/parent/] -For .rb use rake db:load_file[/absolute/path/to/sample/filename.rb]} +use rake db:load_file[/absolute/path/to/sample/filename.rb]} task :load_file , [:file, :dir] => :environment do |t, args| file = Pathname.new(args.file) - if %w{.csv .yml}.include? file.extname - puts "loading fixture #{Pathname.new(args.dir).join(file)}" - Spree::Core::Fixtures.create_fixtures(args.dir, file.to_s.sub(file.extname, "")) - elsif file.exist? - puts "loading ruby #{file}" - require file - end + puts "loading ruby #{file}" + require file end desc "Loads fixtures from the the dir you specify using rake db:load_dir[loadfrom]" - task :load_dir , [:dir] => :environment do |t , args| + task :load_dir , [:dir] => :environment do |t, args| dir = args.dir dir = File.join(Rails.root, "db", dir) if Pathname.new(dir).relative? - fixtures = ActiveSupport::OrderedHash.new - ruby_files = ActiveSupport::OrderedHash.new - Dir.glob(File.join(dir , '**/*.{yml,csv,rb}')).each do |fixture_file| + ruby_files = {} + Dir.glob(File.join(dir , '**/*.{rb}')).each do |fixture_file| ext = File.extname fixture_file - if ext == ".rb" - ruby_files[File.basename(fixture_file, '.*')] = fixture_file - else - fixtures[fixture_file.sub(dir, "")[1..-1]] = fixture_file - end - end - fixtures.sort.each do |relative_path , fixture_file| - # an invoke will only execute the task once - Rake::Task["db:load_file"].execute( Rake::TaskArguments.new([:file, :dir], [relative_path, dir]) ) + ruby_files[File.basename(fixture_file, '.*')] = fixture_file end ruby_files.sort.each do |fixture , ruby_file| + # If file is exists within application it takes precendence. + if File.exists?(File.join(Rails.root, "db/default/spree", "#{fixture}.rb")) + ruby_file = File.expand_path(File.join(Rails.root, "db/default/spree", "#{fixture}.rb")) + end # an invoke will only execute the task once Rake::Task["db:load_file"].execute( Rake::TaskArguments.new([:file], [ruby_file]) ) end @@ -102,8 +90,8 @@ For .rb use rake db:load_file[/absolute/path/to/sample/filename.rb]} end if load_sample - #prevent errors for missing attributes (since rails 3.1 upgrade) - + # Reload models' attributes in case they were loaded in old migrations with wrong attributes + ActiveRecord::Base.descendants.each(&:reset_column_information) Rake::Task["spree_sample:load"].invoke end diff --git a/core/lib/tasks/email.rake b/core/lib/tasks/email.rake new file mode 100644 index 00000000000..5b1db4a970e --- /dev/null +++ b/core/lib/tasks/email.rake @@ -0,0 +1,7 @@ +namespace :email do + desc 'Sends test email to specified address - Example: EMAIL=spree@example.com bundle exec rake test:email' + task :test => :environment do + raise ArgumentError, "Must pass EMAIL environment variable. Example: EMAIL=spree@example.com bundle exec rake test:email" unless ENV['EMAIL'].present? + Spree::TestMailer.test_email(ENV['EMAIL']).deliver! + end +end diff --git a/core/lib/tasks/exchanges.rake b/core/lib/tasks/exchanges.rake new file mode 100644 index 00000000000..a5561041282 --- /dev/null +++ b/core/lib/tasks/exchanges.rake @@ -0,0 +1,70 @@ +namespace :exchanges do + desc %q{Takes unreturned exchanged items and creates a new order to charge + the customer for not returning them} + task charge_unreturned_items: :environment do + + unreturned_return_items = Spree::ReturnItem.awaiting_return.exchange_processed.joins(:exchange_inventory_unit).where([ + "spree_inventory_units.created_at < :days_ago AND spree_inventory_units.state = :iu_state", + days_ago: Spree::Config[:expedited_exchanges_days_window].days.ago, iu_state: "shipped" + ]).to_a + + # Determine that a return item has already been deemed unreturned and therefore charged + # by the fact that its exchange inventory unit has popped off to a different order + unreturned_return_items.select! { |ri| ri.inventory_unit.order_id == ri.exchange_inventory_unit.order_id } + + failed_orders = [] + + unreturned_return_items.group_by(&:exchange_shipment).each do |shipment, return_items| + begin + inventory_units = return_items.map(&:exchange_inventory_unit) + + original_order = shipment.order + order_attributes = { + bill_address: original_order.bill_address, + ship_address: original_order.ship_address, + email: original_order.email + } + order_attributes[:store_id] = original_order.store_id + order = Spree::Order.create!(order_attributes) + + order.associate_user!(original_order.user) if original_order.user + + return_items.group_by(&:exchange_variant).map do |variant, variant_return_items| + variant_inventory_units = variant_return_items.map(&:exchange_inventory_unit) + line_item = Spree::LineItem.create!(variant: variant, quantity: variant_return_items.count, order: order) + variant_inventory_units.each { |i| i.update_attributes!(line_item_id: line_item.id, order_id: order.id) } + end + + order.reload.update! + while order.state != order.checkout_steps[-2] && order.next; end + + unless order.payments.present? + card_to_reuse = original_order.valid_credit_cards.first + card_to_reuse = original_order.user.credit_cards.default.first if !card_to_reuse && original_order.user + Spree::Payment.create!(order: order, + payment_method_id: card_to_reuse.try(:payment_method_id), + source: card_to_reuse, + amount: order.total) + end + + # the order builds a shipment on its own on transition to delivery, but we want + # the original exchange shipment, not the built one + order.shipments.destroy_all + shipment.update_attributes!(order_id: order.id) + order.update_attributes!(state: "confirm") + + order.reload.next! + order.update! + order.finalize! + + failed_orders << order unless order.completed? && order.valid? + rescue + failed_orders << order + end + end + failure_message = failed_orders.map { |o| "#{o.number} - #{o.errors.full_messages}" }.join(", ") + raise UnableToChargeForUnreturnedItems.new(failure_message) if failed_orders.present? + end +end + +class UnableToChargeForUnreturnedItems < StandardError; end diff --git a/core/spec/controllers/controller_helpers_spec.rb b/core/spec/controllers/controller_helpers_spec.rb deleted file mode 100644 index cfea32146d7..00000000000 --- a/core/spec/controllers/controller_helpers_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' - -# In this file, we want to test that the controller helpers function correctly -# So we need to use one of the controllers inside Spree. -# ProductsController is good. -describe Spree::ProductsController do - - before do - I18n.stub(:available_locales => [:en, :de]) - Rails.application.config.i18n.default_locale = :de - end - - after do - Rails.application.config.i18n.default_locale = :en - I18n.locale = :en - end - - # Regression test for #1184 - it "sets the default locale based off config.i18n.default_locale" do - I18n.locale.should == :en - spree_get :index - I18n.locale.should == :de - end -end diff --git a/core/spec/controllers/spree/admin/base_controller_spec.rb b/core/spec/controllers/spree/admin/base_controller_spec.rb deleted file mode 100644 index 5c1757b1f48..00000000000 --- a/core/spec/controllers/spree/admin/base_controller_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# Spree's rpsec controller tests get the Spree::ControllerHacks -# we don't need those for the anonymous controller here, so -# we call process directly instead of get -require 'spec_helper' - -describe Spree::Admin::BaseController do - - controller(Spree::Admin::BaseController) do - def index - render :text => 'test' - end - end - - describe "check alerts" do - it "checks alerts with before_filter" do - controller.should_receive :check_alerts - process :index - end - - it "saves alerts into session" do - controller.stub(:should_check_alerts? => true) - Spree::Alert.should_receive(:current).and_return([Spree::Alert.new(:message => "test alert", :severity => 'release')]) - process :index - session[:alerts].first.message.should eq "test alert" - end - - describe "should_check_alerts?" do - before do - Rails.env.stub(:production? => true) - Spree::Config[:check_for_spree_alerts] = true - Spree::Config[:last_check_for_spree_alerts] = nil - end - - it "only checks alerts if production and preference is true" do - controller.send(:should_check_alerts?).should be_true - end - - it "only checks for production" do - Rails.env.stub(:production? => false) - controller.send(:should_check_alerts?).should be_false - end - - it "only checks if preference is true" do - Spree::Config[:check_for_spree_alerts] = false - controller.send(:should_check_alerts?).should be_false - end - end - end -end diff --git a/core/spec/controllers/spree/admin/image_settings_controller_spec.rb b/core/spec/controllers/spree/admin/image_settings_controller_spec.rb deleted file mode 100644 index ab8d6a77dfe..00000000000 --- a/core/spec/controllers/spree/admin/image_settings_controller_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::ImageSettingsController do - stub_authorization! - - context "updating image settings" do - it "should be able to update paperclip settings" do - spree_put :update, { :preferences => { - "attachment_path" => "foo/bar", - "attachment_default_url" => "baz/bar" - } - } - Spree::Config[:attachment_path].should == "foo/bar" - Spree::Config[:attachment_default_url].should == "baz/bar" - end - - context "paperclip styles" do - it "should be able to update the paperclip styles" do - spree_put :update, { "attachment_styles" => { "thumb" => "25x25>" } } - updated_styles = ActiveSupport::JSON.decode(Spree::Config[:attachment_styles]) - updated_styles["thumb"].should == "25x25>" - end - - it "should be able to add a new style" do - spree_put :update, { "attachment_styles" => { }, "new_attachment_styles" => { "1" => { "name" => "jumbo", "value" => "2000x2000>" } } } - styles = ActiveSupport::JSON.decode(Spree::Config[:attachment_styles]) - styles["jumbo"].should == "2000x2000>" - end - end - - context "amazon s3" do - after(:all) do - Spree::Image.attachment_definitions[:attachment].delete :storage - end - - it "should be able to update s3 settings" do - spree_put :update, { :preferences => { - "use_s3" => "1", - "s3_access_key" => "a_valid_key", - "s3_secret" => "a_secret", - "s3_bucket" => "some_bucket" - } - } - Spree::Config[:use_s3].should be_true - Spree::Config[:s3_access_key].should == "a_valid_key" - Spree::Config[:s3_secret].should == "a_secret" - Spree::Config[:s3_bucket].should == "some_bucket" - end - - context "headers" do - before(:each) { Spree::Config[:use_s3] = true } - - it "should be able to update the s3 headers" do - spree_put :update, { :preferences => { "use_s3" => "1" }, "s3_headers" => { "Cache-Control" => "max-age=1111" } } - headers = ActiveSupport::JSON.decode(Spree::Config[:s3_headers]) - headers["Cache-Control"].should == "max-age=1111" - end - - it "should be able to add a new header" do - spree_put :update, { "s3_headers" => { }, "new_s3_headers" => { "1" => { "name" => "Charset", "value" => "utf-8" } } } - headers = ActiveSupport::JSON.decode(Spree::Config[:s3_headers]) - headers["Charset"].should == "utf-8" - end - end - end - end -end - diff --git a/core/spec/controllers/spree/admin/mail_methods_controller_spec.rb b/core/spec/controllers/spree/admin/mail_methods_controller_spec.rb deleted file mode 100644 index 19dd44fc2d3..00000000000 --- a/core/spec/controllers/spree/admin/mail_methods_controller_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::MailMethodsController do - stub_authorization! - - let(:mail_method) { mock_model(Spree::MailMethod).as_null_object } - - before do - Spree::MailMethod.stub :find => mail_method - request.env["HTTP_REFERER"] = "/" - end - - context "#create" do - it "should reinitialize the mail settings" do - Spree::Core::MailSettings.should_receive :init - spree_put :create, { :id => "456", :mail_method => {:environment => "foo"}} - end - end - - context "#update" do - it "should reinitialize the mail settings" do - Spree::Core::MailSettings.should_receive :init - spree_put :update, { :id => "456", :mail_method => {:environment => "foo"}} - end - end - - it "can trigger testmail without current_user" do - spree_post :testmail, :id => create(:mail_method).id - flash[:error].should_not include("undefined local variable or method `current_user'") - end -end diff --git a/core/spec/controllers/spree/admin/orders_controller_spec.rb b/core/spec/controllers/spree/admin/orders_controller_spec.rb deleted file mode 100644 index 098db000c8f..00000000000 --- a/core/spec/controllers/spree/admin/orders_controller_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::OrdersController do - stub_authorization! - - let(:order) { mock_model(Spree::Order, :complete? => true, :total => 100) } - - before do - Spree::Order.stub :find_by_number! => order - request.env["HTTP_REFERER"] = "http://localhost:3000" - end - - context "#fire" do - it "should fire the requested event on the payment" do - order.should_receive(:foo).and_return true - spree_put :fire, {:id => "R1234567", :e => "foo"} - end - - it "should respond with a flash message if the event cannot be fired" do - order.stub :foo => false - spree_put :fire, {:id => "R1234567", :e => "foo"} - flash[:error].should_not be_nil - end - end - - context "pagination" do - it "can page through the orders" do - spree_get :index, :page => 2, :per_page => 10 - assigns[:orders].offset_value.should == 10 - assigns[:orders].limit_value.should == 10 - end - end -end diff --git a/core/spec/controllers/spree/admin/payment_methods_controller_spec.rb b/core/spec/controllers/spree/admin/payment_methods_controller_spec.rb deleted file mode 100644 index a1acff2e198..00000000000 --- a/core/spec/controllers/spree/admin/payment_methods_controller_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -module Spree - class GatewayWithPassword < PaymentMethod - attr_accessible :preferred_password - - preference :password, :string, :default => "password" - end -end - -module Spree - describe Admin::PaymentMethodsController do - stub_authorization! - - let(:payment_method) { Spree::GatewayWithPassword.create!(:name => "Bogus", :preferred_password => "haxme") } - - # regression test for #2094 - it "does not clear password on update" do - payment_method.preferred_password.should == "haxme" - spree_put :update, :id => payment_method.id, :payment_method => { :type => payment_method.class.to_s, :preferred_password => "" } - response.should redirect_to(spree.edit_admin_payment_method_path(payment_method)) - - payment_method.reload - payment_method.preferred_password.should == "haxme" - end - - end -end diff --git a/core/spec/controllers/spree/admin/products_controller_spec.rb b/core/spec/controllers/spree/admin/products_controller_spec.rb deleted file mode 100644 index 4109954edb5..00000000000 --- a/core/spec/controllers/spree/admin/products_controller_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::ProductsController do - stub_authorization! - - context "#index" do - # Regression test for #1259 - it "can find a product by SKU" do - product = create(:product, :sku => "ABC123") - spree_get :index, :q => { :sku_start => "ABC123" } - assigns[:collection].should_not be_empty - assigns[:collection].should include(product) - end - end - - context "creating a product" do - - include_context "product prototype" - - it "should create product" do - spree_get :new - response.should render_template("admin/products/new") - end - - it "should create product from prototype" do - spree_post :create, :product => product_attributes.merge(:prototype_id => prototype.id) - product = Spree::Product.last - response.should redirect_to(spree.edit_admin_product_path(product)) - prototype.properties.each do |property| - product.properties.should include(property) - end - prototype.option_types.each do |ot| - product.option_types.should include(ot) - end - product.variants_including_master.length.should == 1 - end - - it "should create product from prototype with option values hash" do - spree_post :create, :product => product_attributes.merge(:prototype_id => prototype.id, :option_values_hash => option_values_hash) - product = Spree::Product.last - response.should redirect_to(spree.edit_admin_product_path(product)) - option_values_hash.each do |option_type_id, option_value_ids| - Spree::ProductOptionType.where(:product_id => product.id, :option_type_id => option_type_id).first.should_not be_nil - end - product.variants.length.should == 3 - end - - end - - # regression test for #1370 - context "adding properties to a product" do - let!(:product) { create(:product) } - specify do - spree_put :update, :id => product.to_param, :product => { :product_properties_attributes => { "1" => { :property_name => "Foo", :value => "bar" } } } - flash[:success].should == "Product #{product.name.inspect} has been successfully updated!" - end - - end - - - # regression test for #801 - context "destroying a product" do - let(:product) do - product = create(:product) - create(:variant, :product => product) - product - end - - it "deletes all the variants (including master) for the product" do - spree_delete :destroy, :id => product - product.reload.deleted_at.should_not be_nil - product.variants_including_master.each do |variant| - varaint.reload.deleted_at.should_not be_nil - end - end - end -end diff --git a/core/spec/controllers/spree/admin/resource_controller_spec.rb b/core/spec/controllers/spree/admin/resource_controller_spec.rb deleted file mode 100644 index 4b2892b6a79..00000000000 --- a/core/spec/controllers/spree/admin/resource_controller_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::ResourceController do - stub_authorization! - - describe 'POST#update_positions' do - - before do - Spree::Admin::ResourceController.any_instance.stub(:model_class).and_return Spree::Variant - end - - it 'returns Ok on json' do - variant = create(:variant) - variant2 = create(:variant) - expect { - spree_post :update_positions, :id => variant.id, :positions => { variant.id => '2', variant2.id => '1' }, :format => "js" - variant.reload - }.to change(variant, :position).from(nil).to(2) - end - - end -end \ No newline at end of file diff --git a/core/spec/controllers/spree/admin/return_authorizations_controller_spec.rb b/core/spec/controllers/spree/admin/return_authorizations_controller_spec.rb deleted file mode 100644 index 02701e2f88a..00000000000 --- a/core/spec/controllers/spree/admin/return_authorizations_controller_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::ReturnAuthorizationsController do - stub_authorization! - - # Regression test for #1370 #3 - let!(:order) { create(:order) } - it "can create a return authorization" do - spree_post :create, :order_id => order.to_param, :return_authorization => { :amount => 0.0, :reason => "" } - response.should be_success - end -end diff --git a/core/spec/controllers/spree/admin/search_controller_spec.rb b/core/spec/controllers/spree/admin/search_controller_spec.rb deleted file mode 100644 index 4dfe2e6a928..00000000000 --- a/core/spec/controllers/spree/admin/search_controller_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::SearchController do - stub_authorization! - let(:user) { create(:user) } - - before do - user.ship_address = create(:address) - user.bill_address = create(:address) - user.save - end - - it "can find a user by their email "do - spree_xhr_get :users, :q => user.email - assigns[:users].should include(user) - end - - it "can find a user by their ship address's first name" do - spree_xhr_get :users, :q => user.ship_address.firstname - assigns[:users].should include(user) - end - - it "can find a user by their ship address's last name" do - spree_xhr_get :users, :q => user.ship_address.lastname - assigns[:users].should include(user) - end - - it "can find a user by their bill address's first name" do - spree_xhr_get :users, :q => user.bill_address.firstname - assigns[:users].should include(user) - end - - it "can find a user by their bill address's last name" do - spree_xhr_get :users, :q => user.bill_address.lastname - assigns[:users].should include(user) - end - -end diff --git a/core/spec/controllers/spree/admin/shipping_methods_controller_spec.rb b/core/spec/controllers/spree/admin/shipping_methods_controller_spec.rb deleted file mode 100644 index caded40be4e..00000000000 --- a/core/spec/controllers/spree/admin/shipping_methods_controller_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::ShippingMethodsController do - stub_authorization! - - # Regression test for #1240 - it "should not hard-delete shipping methods" do - Spree::ShippingMethod.should_receive(:find).and_return(shipping_method = stub_model(Spree::ShippingMethod)) - shipping_method.should_not_receive(:destroy) - shipping_method.deleted_at.should be_nil - spree_delete :destroy, :id => 1 - shipping_method.deleted_at.should_not be_nil - end -end diff --git a/core/spec/controllers/spree/checkout_controller_spec.rb b/core/spec/controllers/spree/checkout_controller_spec.rb deleted file mode 100644 index 23534450d19..00000000000 --- a/core/spec/controllers/spree/checkout_controller_spec.rb +++ /dev/null @@ -1,200 +0,0 @@ -require 'spec_helper' - -describe Spree::CheckoutController do - let(:order) do - mock_model(Spree::Order, :checkout_allowed? => true, - :user => nil, - :email => nil, - :completed? => false, - :update_attributes => true, - :payment? => false, - :insufficient_stock_lines => [], - :coupon_code => nil).as_null_object - end - - before { controller.stub :current_order => order } - - context "#edit" do - - it "should redirect to the cart path unless checkout_allowed?" do - order.stub :checkout_allowed? => false - spree_get :edit, { :state => "delivery" } - response.should redirect_to(spree.cart_path) - end - - it "should redirect to the cart path if current_order is nil" do - controller.stub!(:current_order).and_return(nil) - spree_get :edit, { :state => "delivery" } - response.should redirect_to(spree.cart_path) - end - - it "should change to the requested state" do - order.should_receive(:state=).with("payment").and_return true - spree_get :edit, { :state => "payment" } - end - - it "should redirect to cart if order is completed" do - order.stub(:completed? => true) - spree_get :edit, {:state => "address"} - response.should redirect_to(spree.cart_path) - end - - it "should associate the order with a user" do - user = double("User", :last_incomplete_spree_order => nil) - controller.stub(:spree_current_user => user) - order.should_receive(:associate_user!).with(user) - spree_get :edit, {}, :order_id => 1 - end - - it "should fire the spree.user.signup event if user has just signed up" do - user = double("User", :last_incomplete_spree_order => nil) - controller.stub(:spree_current_user => user) - controller.should_receive(:fire_event).with("spree.user.signup", :user => user, :order => order) - spree_get :edit, {}, :spree_user_signup => true - end - - end - - context "#update" do - - context "save successful" do - before do - order.stub(:update_attribute).and_return true - order.should_receive(:update_attributes).and_return true - end - - it "should assign order" do - order.stub :state => "address" - spree_post :update, {:state => "confirm"} - assigns[:order].should_not be_nil - end - - it "should change to requested state" do - order.stub :state => "address" - order.should_receive(:state=).with('confirm') - spree_post :update, {:state => "confirm"} - end - - context "with next state" do - before { order.stub :next => true } - - it "should advance the state" do - order.stub :state => "address" - order.should_receive(:next).and_return true - spree_post :update, {:state => "delivery"} - end - - it "should redirect the next state" do - order.stub :state => "payment" - spree_post :update, {:state => "delivery"} - response.should redirect_to spree.checkout_state_path("payment") - end - - context "when in the confirm state" do - before { order.stub :state => "complete" } - - it "should redirect to the order view" do - spree_post :update, {:state => "confirm"} - response.should redirect_to spree.order_path(order) - end - - it "should populate the flash message" do - spree_post :update, {:state => "confirm"} - flash.notice.should == I18n.t(:order_processed_successfully) - end - - it "should remove completed order from the session" do - spree_post :update, {:state => "confirm"}, {:order_id => "foofah"} - session[:order_id].should be_nil - end - - end - - end - end - - context "save unsuccessful" do - before { order.should_receive(:update_attributes).and_return false } - - it "should assign order" do - spree_post :update, {:state => "confirm"} - assigns[:order].should_not be_nil - end - - it "should not change the order state" do - order.should_not_receive(:update_attribute) - spree_post :update, { :state => 'confirm' } - end - - it "should render the edit template" do - spree_post :update, { :state => 'confirm' } - response.should render_template :edit - end - end - - context "when current_order is nil" do - before { controller.stub! :current_order => nil } - it "should not change the state if order is completed" do - order.should_not_receive(:update_attribute) - spree_post :update, {:state => "confirm"} - end - - it "should redirect to the cart_path" do - spree_post :update, {:state => "confirm"} - response.should redirect_to spree.cart_path - end - end - - context "Spree::Core::GatewayError" do - - before do - order.stub(:update_attributes).and_raise(Spree::Core::GatewayError) - spree_post :update, {:state => "whatever"} - end - - it "should render the edit template" do - response.should render_template :edit - end - - it "should set appropriate flash message" do - flash[:error].should == I18n.t(:spree_gateway_error_flash_for_checkout) - end - - end - - end - - context "When last inventory item has been purchased" do - let(:product) { mock_model(Spree::Product, :name => "Amazing Object") } - let(:variant) { mock_model(Spree::Variant, :on_hand => 0) } - let(:line_item) { mock_model Spree::LineItem, :insufficient_stock? => true } - let(:order) { create(:order) } - - before do - order.stub(:line_items => [line_item]) - - reset_spree_preferences do |config| - config.track_inventory_levels = true - config.allow_backorders = false - end - - end - - context "and back orders == false" do - before do - spree_post :update, {:state => "payment"} - end - - it "should render edit template" do - response.should redirect_to spree.cart_path - end - - it "should set flash message for no inventory" do - flash[:error].should == I18n.t(:spree_inventory_error_flash_for_insufficient_quantity , :names => "'#{product.name}'" ) - end - - end - - end - -end diff --git a/core/spec/controllers/spree/content_controller_spec.rb b/core/spec/controllers/spree/content_controller_spec.rb deleted file mode 100644 index 779fe6ca52b..00000000000 --- a/core/spec/controllers/spree/content_controller_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -describe Spree::ContentController do - it "should not display a local file" do - spree_get :show, :path => "../../Gemfile" - response.response_code.should == 404 - end - - it "should display CVV page" do - spree_get :cvv - response.response_code.should == 200 - end -end diff --git a/core/spec/controllers/spree/home_controller_spec.rb b/core/spec/controllers/spree/home_controller_spec.rb deleted file mode 100644 index b5342dde718..00000000000 --- a/core/spec/controllers/spree/home_controller_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'spec_helper' - -describe Spree::HomeController do - it "should provide the current user to the searcher class" do - user = stub(:last_incomplete_spree_order => nil) - controller.stub :spree_current_user => user - Spree::Config.searcher_class.any_instance.should_receive(:current_user=).with(user) - spree_get :index - response.status.should == 200 - end -end diff --git a/core/spec/controllers/spree/orders_controller_extension_spec.rb b/core/spec/controllers/spree/orders_controller_extension_spec.rb deleted file mode 100644 index 3bcac3a191b..00000000000 --- a/core/spec/controllers/spree/orders_controller_extension_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'spec_helper' - -describe Spree::OrdersController do - after do - Spree::OrdersController.clear_overrides! - end - - context "extension testing" do - context "update" do - - context "specify symbol for handler instead of Proc" do - before do - @order = create(:order) - Spree::OrdersController.class_eval do - respond_override({:update => {:html => {:success => :success_method}}}) - - private - - def success_method - render :text => 'success!!!' - end - end - end - describe "POST" do - it "has value success" do - spree_put :update, {}, {:order_id => @order.id} - response.should be_success - assert (response.body =~ /success!!!/) - end - end - end - - context "render" do - before do - @order = create(:order) - Spree::OrdersController.instance_eval do - respond_override({:update => {:html => {:success => lambda { render(:text => 'success!!!') }}}}) - respond_override({:update => {:html => {:failure => lambda { render(:text => 'failure!!!') }}}}) - end - end - describe "POST" do - it "has value success" do - spree_put :update, {}, {:order_id => @order.id} - response.should be_success - assert (response.body =~ /success!!!/) - end - end - end - - context "redirect" do - before do - @order = create(:order) - Spree::OrdersController.instance_eval do - respond_override({:update => {:html => {:success => lambda { redirect_to(Spree::Order.first) }}}}) - respond_override({:update => {:html => {:failure => lambda { render(:text => 'failure!!!') }}}}) - end - end - describe "POST" do - it "has value success" do - spree_put :update, {}, {:order_id => @order.id} - response.should be_redirect - end - end - end - - context "validation error" do - before do - @order = create(:order) - Spree::Order.update_all :state => 'address' - Spree::OrdersController.instance_eval do - respond_override({:update => {:html => {:success => lambda { render(:text => 'success!!!') }}}}) - respond_override({:update => {:html => {:failure => lambda { render(:text => 'failure!!!') }}}}) - end - end - describe "POST" do - it "has value success" do - spree_put :update, {:order => {:email => ''}}, {:order_id => @order.id} - response.should be_success - assert (response.body =~ /failure!!!/) - end - end - end - - context 'A different controllers respond_override. Regression test for #1301' do - before do - @order = create(:order) - Spree::Admin::OrdersController.instance_eval do - respond_override({:update => {:html => {:success => lambda { render(:text => 'success!!!') }}}}) - end - end - describe "POST" do - it "should not effect the wrong controller" do - Spree::Responder.should_not_receive(:call) - spree_put :update, {}, {:order_id => @order.id} - response.should redirect_to(spree.cart_path) - end - end - end - - end - end - -end diff --git a/core/spec/controllers/spree/orders_controller_spec.rb b/core/spec/controllers/spree/orders_controller_spec.rb deleted file mode 100644 index 6f3213b88ae..00000000000 --- a/core/spec/controllers/spree/orders_controller_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'spec_helper' - -describe Spree::OrdersController do - let(:user) { create(:user) } - let(:order) { mock_model(Spree::Order, :number => "R123", :reload => nil, :save! => true, :coupon_code => nil, :user => user, :completed? => false, :currency => "USD")} - before do - Spree::Order.stub(:find).with(1).and_return(order) - #ensure no respond_overrides are in effect - if Spree::BaseController.spree_responders[:OrdersController].present? - Spree::BaseController.spree_responders[:OrdersController].clear - end - end - - context "#populate" do - before { Spree::Order.stub(:new).and_return(order) } - - it "should create a new order when none specified" do - Spree::Order.should_receive(:new).and_return order - spree_post :populate, {}, {} - session[:order_id].should == order.id - end - - context "with Variant" do - before do - @variant = mock_model(Spree::Variant) - Spree::Variant.should_receive(:find).and_return @variant - end - - it "should handle single variant/quantity pair" do - order.should_receive(:add_variant).with(@variant, 2, order.currency) - spree_post :populate, {:order_id => 1, :variants => {@variant.id => 2}} - end - it "should handle multiple variant/quantity pairs with shared quantity" do - @variant.stub(:product_id).and_return(10) - order.should_receive(:add_variant).with(@variant, 1, order.currency) - spree_post :populate, {:order_id => 1, :products => {@variant.product_id => @variant.id}, :quantity => 1} - end - it "should handle multiple variant/quantity pairs with specific quantity" do - @variant.stub(:product_id).and_return(10) - order.should_receive(:add_variant).with(@variant, 3, order.currency) - spree_post :populate, {:order_id => 1, :products => {@variant.product_id => @variant.id}, :quantity => {@variant.id.to_s => 3}} - end - end - end - - context "#update" do - before do - order.stub(:update_attributes).and_return true - order.stub(:line_items).and_return([]) - order.stub(:line_items=).with([]) - Spree::Order.stub(:find_by_id_and_currency).and_return(order) - end - - it "should not result in a flash success" do - spree_put :update, {}, {:order_id => 1} - flash[:success].should be_nil - end - - it "should render the edit view (on failure)" do - order.stub(:update_attributes).and_return false - order.stub(:errors).and_return({:number => "has some error"}) - spree_put :update, {}, {:order_id => 1} - response.should render_template :edit - end - - it "should redirect to cart path (on success)" do - order.stub(:update_attributes).and_return true - spree_put :update, {}, {:order_id => 1} - response.should redirect_to(spree.cart_path) - end - end - - context "#empty" do - it "should destroy line items in the current order" do - controller.stub!(:current_order).and_return(order) - order.should_receive(:empty!) - spree_put :empty - response.should redirect_to(spree.cart_path) - end - end - - #TODO - move some of the assigns tests based on session, etc. into a shared example group once new block syntax released -end diff --git a/core/spec/controllers/spree/orders_controller_transitions_spec.rb b/core/spec/controllers/spree/orders_controller_transitions_spec.rb deleted file mode 100644 index 4d24b7952ed..00000000000 --- a/core/spec/controllers/spree/orders_controller_transitions_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -Spree::Order.class_eval do - attr_accessor :did_transition -end - -module Spree - describe OrdersController do - # Regression test for #2004 - context "with a transition callback on first state" do - let(:order) { Spree::Order.new } - - before do - controller.stub :current_order => order - - first_state, _ = Spree::Order.checkout_steps.first - Spree::Order.state_machine.after_transition :to => first_state do |order| - order.did_transition = true - end - end - - it "correctly calls the transition callback" do - order.did_transition.should be_nil - spree_put :update, { :checkout => "checkout" }, { :order_id => 1} - order.did_transition.should be_true - end - end - end -end diff --git a/core/spec/controllers/spree/products_controller_spec.rb b/core/spec/controllers/spree/products_controller_spec.rb deleted file mode 100644 index a6c974e4d15..00000000000 --- a/core/spec/controllers/spree/products_controller_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe Spree::ProductsController do - let!(:product) { create(:product, :available_on => 1.year.from_now) } - - # Regression test for #1390 - it "allows admins to view non-active products" do - controller.stub :spree_current_user => stub(:has_spree_role? => true, :last_incomplete_spree_order => nil) - spree_get :show, :id => product.to_param - response.status.should == 200 - end - - it "cannot view non-active products" do - spree_get :show, :id => product.to_param - response.status.should == 404 - end - - it "should provide the current user to the searcher class" do - user = stub(:last_incomplete_spree_order => nil) - controller.stub :spree_current_user => user - Spree::Config.searcher_class.any_instance.should_receive(:current_user=).with(user) - spree_get :index - response.status.should == 200 - end - - # Regression test for #2249 - it "doesn't error when given an invalid referer" do - controller.stub :spree_current_user => stub(:has_spree_role? => true, :last_incomplete_spree_order => nil) - request.env['HTTP_REFERER'] = "not|a$url" - lambda { spree_get :show, :id => product.to_param }.should_not raise_error(URI::InvalidURIError) - end -end diff --git a/core/spec/controllers/spree/states_controller_spec.rb b/core/spec/controllers/spree/states_controller_spec.rb deleted file mode 100644 index 2253be9a5d7..00000000000 --- a/core/spec/controllers/spree/states_controller_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Spree::StatesController do - before(:each) do - state = create(:state) - end - - it 'should display state mapper' do - spree_get :index, { :format => :js } - assigns[:state_info].should_not be_empty - end -end diff --git a/core/spec/controllers/spree/taxons_controller_spec.rb b/core/spec/controllers/spree/taxons_controller_spec.rb deleted file mode 100644 index 9ca6cb7da74..00000000000 --- a/core/spec/controllers/spree/taxons_controller_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Spree::TaxonsController do - it "should provide the current user to the searcher class" do - taxon = create(:taxon, :permalink => "test") - user = stub(:last_incomplete_spree_order => nil) - controller.stub :spree_current_user => user - Spree::Config.searcher_class.any_instance.should_receive(:current_user=).with(user) - spree_get :show, :id => taxon.permalink - response.status.should == 200 - end -end diff --git a/core/spec/fixtures/thinking-cat.jpg b/core/spec/fixtures/thinking-cat.jpg new file mode 100644 index 00000000000..7e8524d367b Binary files /dev/null and b/core/spec/fixtures/thinking-cat.jpg differ diff --git a/core/spec/helpers/base_helper_spec.rb b/core/spec/helpers/base_helper_spec.rb index bae571bde02..2c3dc3779b5 100644 --- a/core/spec/helpers/base_helper_spec.rb +++ b/core/spec/helpers/base_helper_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' -describe Spree::BaseHelper do +describe Spree::BaseHelper, :type => :helper do include Spree::BaseHelper + let(:current_store){ create :store } + context "available_countries" do let(:country) { create(:country) } @@ -16,7 +18,7 @@ end it "return complete list of countries" do - available_countries.count.should == Spree::Country.count + expect(available_countries.count).to eq(Spree::Country.count) end end @@ -29,7 +31,7 @@ end it "return only the countries defined by the checkout zone" do - available_countries.should == [country] + expect(available_countries).to eq([country]) end end @@ -42,23 +44,15 @@ end it "return complete list of countries" do - available_countries.count.should == Spree::Country.count + expect(available_countries.count).to eq(Spree::Country.count) end end end end - # Regression test for #889 - context "seo_url" do - let(:taxon) { stub(:permalink => "bam") } - it "provides the correct URL" do - seo_url(taxon).should == "/t/bam" - end - end - # Regression test for #1436 context "defining custom image helpers" do - let(:product) { mock_model(Spree::Product, :images => []) } + let(:product) { mock_model(Spree::Product, :images => [], :variant_images => []) } before do Spree::Image.class_eval do attachment_definitions[:attachment][:styles].merge!({:very_strange => '1x1'}) @@ -66,42 +60,110 @@ end it "should not raise errors when style exists" do - lambda { very_strange_image(product) }.should_not raise_error + expect { very_strange_image(product) }.not_to raise_error end it "should raise NoMethodError when style is not exists" do - lambda { another_strange_image(product) }.should raise_error(NoMethodError) + expect { another_strange_image(product) }.to raise_error(NoMethodError) end end # Regression test for #2034 context "flash_message" do - let(:flash) { {:notice => "ok", :foo => "foo", :bar => "bar"} } + let(:flash) { {"notice" => "ok", "foo" => "foo", "bar" => "bar"} } it "should output all flash content" do flash_messages html = Nokogiri::HTML(helper.output_buffer) - html.css(".notice").text.should == "ok" - html.css(".foo").text.should == "foo" - html.css(".bar").text.should == "bar" + expect(html.css(".notice").text).to eq("ok") + expect(html.css(".foo").text).to eq("foo") + expect(html.css(".bar").text).to eq("bar") end it "should output flash content except one key" do flash_messages(:ignore_types => :bar) html = Nokogiri::HTML(helper.output_buffer) - html.css(".notice").text.should == "ok" - html.css(".foo").text.should == "foo" - html.css(".bar").text.should be_empty + expect(html.css(".notice").text).to eq("ok") + expect(html.css(".foo").text).to eq("foo") + expect(html.css(".bar").text).to be_empty end it "should output flash content except some keys" do flash_messages(:ignore_types => [:foo, :bar]) html = Nokogiri::HTML(helper.output_buffer) - html.css(".notice").text.should == "ok" - html.css(".foo").text.should be_empty - html.css(".bar").text.should be_empty - helper.output_buffer.should == "
      ok
      " + expect(html.css(".notice").text).to eq("ok") + expect(html.css(".foo").text).to be_empty + expect(html.css(".bar").text).to be_empty + expect(helper.output_buffer).to eq("
      ok
      ") + end + end + + context "link_to_tracking" do + it "returns tracking link if available" do + a = link_to_tracking_html(shipping_method: true, tracking: '123', tracking_url: 'http://g.c/?t=123').css('a') + + expect(a.text).to eq '123' + expect(a.attr('href').value).to eq 'http://g.c/?t=123' + end + + it "returns tracking without link if link unavailable" do + html = link_to_tracking_html(shipping_method: true, tracking: '123', tracking_url: nil) + expect(html.css('span').text).to eq '123' + end + + it "returns nothing when no shipping method" do + html = link_to_tracking_html(shipping_method: nil, tracking: '123') + expect(html.css('span').text).to eq '' + end + + it "returns nothing when no tracking" do + html = link_to_tracking_html(tracking: nil) + expect(html.css('span').text).to eq '' + end + + def link_to_tracking_html(options = {}) + node = link_to_tracking(double(:shipment, options)) + Nokogiri::HTML(node.to_s) + end + end + + # Regression test for #2396 + context "meta_data_tags" do + it "truncates a product description to 160 characters" do + # Because the controller_name method returns "test" + # controller_name is used by this method to infer what it is supposed + # to be generating meta_data_tags for + text = Faker::Lorem.paragraphs(2).join(" ") + @test = Spree::Product.new(:description => text) + tags = Nokogiri::HTML.parse(meta_data_tags) + content = tags.css("meta[name=description]").first["content"] + assert content.length <= 160, "content length is not truncated to 160 characters" + end + end + + # Regression test for #5384 + context "custom image helpers conflict with inproper statements" do + let(:product) { mock_model(Spree::Product, :images => [], :variant_images => []) } + before do + Spree::Image.class_eval do + attachment_definitions[:attachment][:styles].merge!({:foobar => '1x1'}) + end + end + + it "should not raise errors when helper method called" do + expect { foobar_image(product) }.not_to raise_error + end + + it "should raise NoMethodError when statement with name equal to style name called" do + expect { foobar(product) }.to raise_error(NoMethodError) + end + + end + + context "pretty_time" do + it "prints in a format" do + expect(pretty_time(DateTime.new(2012, 5, 6, 13, 33))).to eq "May 06, 2012 1:33 PM" end end end diff --git a/core/spec/helpers/navigation_helper_spec.rb b/core/spec/helpers/navigation_helper_spec.rb deleted file mode 100644 index 37b01563353..00000000000 --- a/core/spec/helpers/navigation_helper_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# coding: UTF-8 -require 'spec_helper' - -module Spree - describe Admin::NavigationHelper do - describe "#tab" do - context "creating an admin tab" do - it "should capitalize the first letter of each word in the tab's label" do - admin_tab = tab(:orders) - admin_tab.should include("Orders") - end - end - - it "should accept options with label and capitalize each word of it" do - admin_tab = tab(:orders, :label => "delivered orders") - admin_tab.should include("Delivered Orders") - end - - it "should capitalize words with unicode characters" do - admin_tab = tab(:orders, :label => "přehled") # overview - admin_tab.should include("Přehled") - end - end - end -end diff --git a/core/spec/helpers/order_helper_spec.rb b/core/spec/helpers/order_helper_spec.rb new file mode 100644 index 00000000000..58bbf51cc61 --- /dev/null +++ b/core/spec/helpers/order_helper_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +module Spree + describe Spree::OrdersHelper, :type => :helper do + # Regression test for #2518 + #2323 + it "truncates HTML correctly in product description" do + product = double(:description => "" + ("a" * 95) + " This content is invisible.") + expected = "" + ("a" * 95) + "..." + expect(truncated_product_description(product)).to eq(expected) + end + end +end diff --git a/core/spec/helpers/products_helper_spec.rb b/core/spec/helpers/products_helper_spec.rb index 1c9ebfe9db2..0192a8493d3 100644 --- a/core/spec/helpers/products_helper_spec.rb +++ b/core/spec/helpers/products_helper_spec.rb @@ -3,14 +3,14 @@ require 'spec_helper' module Spree - describe ProductsHelper do + describe ProductsHelper, :type => :helper do include ProductsHelper let(:product) { create(:product) } let(:currency) { 'USD' } before do - helper.stub(:current_currency) { currency } + allow(helper).to receive(:current_currency) { currency } end context "#variant_price_diff" do @@ -21,27 +21,35 @@ module Spree @variant = create(:variant, :product => product) product.price = 15 @variant.price = 10 - product.stub(:amount_in) { product_price } - @variant.stub(:amount_in) { variant_price } + allow(product).to receive(:amount_in) { product_price } + allow(@variant).to receive(:amount_in) { variant_price } end subject { helper.variant_price(@variant) } context "when variant is same as master" do - it { should be_nil } + it { is_expected.to be_nil } + end + + context "when the master has no price" do + let(:product_price) { nil } + + it { is_expected.to be_nil } end context "when currency is default" do context "when variant is more than master" do let(:variant_price) { 15 } - it { should == "(Add: $5.00)" } + it { is_expected.to eq("(Add: $5.00)") } + # Regression test for #2737 + it { is_expected.to be_html_safe } end context "when variant is less than master" do let(:product_price) { 15 } - it { should == "(Subtract: $5.00)" } + it { is_expected.to eq("(Subtract: $5.00)") } end end @@ -53,13 +61,13 @@ module Spree context "when variant is more than master" do let(:variant_price) { 150 } - it { should == "(Add: ¥50)" } + it { is_expected.to eq("(Add: ¥50)") } end context "when variant is less than master" do let(:product_price) { 150 } - it { should == "(Subtract: ¥50)" } + it { is_expected.to eq("(Subtract: ¥50)") } end end end @@ -76,8 +84,8 @@ module Spree product.price = 10 @variant1.price = 15 @variant2.price = 20 - helper.variant_price(@variant1).should == "$15.00" - helper.variant_price(@variant2).should == "$20.00" + expect(helper.variant_price(@variant1)).to eq("$15.00") + expect(helper.variant_price(@variant2)).to eq("$20.00") end end @@ -96,7 +104,7 @@ module Spree it "should return the variant price if the price is different than master" do product.price = 100 @variant1.price = 150 - helper.variant_price(@variant1).should == "¥150" + expect(helper.variant_price(@variant1)).to eq("¥150") end end @@ -104,8 +112,8 @@ module Spree product.price = 10 @variant1.default_price.update_column(:amount, 10) @variant2.default_price.update_column(:amount, 10) - helper.variant_price(@variant1).should be_nil - helper.variant_price(@variant2).should be_nil + expect(helper.variant_price(@variant1)).to be_nil + expect(helper.variant_price(@variant2)).to be_nil end end @@ -121,7 +129,7 @@ module Spree
    } description = product_description(product) - description.strip.should == product.description.strip + expect(description.strip).to eq(product.description.strip) end it "renders a product description with automatic paragraph breaks" do @@ -131,9 +139,86 @@ module Spree "IT CHANGED MY LIFE" - Sue, MD} description = product_description(product) - description.strip.should == %Q{

    \nTHIS IS THE BEST PRODUCT EVER!

    "IT CHANGED MY LIFE" - Sue, MD} + expect(description.strip).to eq(%Q{

    \nTHIS IS THE BEST PRODUCT EVER!

    "IT CHANGED MY LIFE" - Sue, MD}) + end + + it "renders a product description without any formatting based on configuration" do + initialDescription = %Q{ +

    hello world

    + +

    tihs is completely awesome and it works

    + +

    why so many spaces in the code. and why some more formatting afterwards?

    + } + + product.description = initialDescription + + Spree::Config[:show_raw_product_description] = true + description = product_description(product) + expect(description).to eq(initialDescription) + end + + end + + shared_examples_for "line item descriptions" do + context 'variant has a blank description' do + let(:description) { nil } + it { is_expected.to eq(Spree.t(:product_has_no_description)) } + end + context 'variant has a description' do + let(:description) { 'test_desc' } + it { is_expected.to eq(description) } + end + context 'description has nonbreaking spaces' do + let(:description) { 'test desc' } + it { is_expected.to eq('test desc') } end + context 'description has line endings' do + let(:description) { "test\n\r\ndesc" } + it { is_expected.to eq('test desc') } + end + end + + context "#line_item_description" do + let(:variant) { create(:variant, :product => product, description: description) } + subject { line_item_description_text(variant.product.description) } + + it_should_behave_like "line item descriptions" + end + context '#line_item_description_text' do + subject { line_item_description_text description } + + it_should_behave_like "line item descriptions" + end + + context '#cache_key_for_products' do + subject { helper.cache_key_for_products } + before(:each) do + @products = double('products collection') + allow(helper).to receive(:params) { {:page => 10} } + end + + context 'when there is a maximum updated date' do + let(:updated_at) { Date.new(2011, 12, 13) } + before :each do + allow(@products).to receive(:count) { 5 } + allow(@products).to receive(:maximum).with(:updated_at) { updated_at } + end + + it { is_expected.to eq('en/USD/spree/products/all-10-20111213-5') } + end + + context 'when there is no considered maximum updated date' do + let(:today) { Date.new(2013, 12, 11) } + before :each do + allow(@products).to receive(:count) { 1234567 } + allow(@products).to receive(:maximum).with(:updated_at) { nil } + allow(Date).to receive(:today) { today } + end + + it { is_expected.to eq('en/USD/spree/products/all-10-20131211-1234567') } + end end end end diff --git a/core/spec/helpers/taxons_helper_spec.rb b/core/spec/helpers/taxons_helper_spec.rb new file mode 100644 index 00000000000..11a4a87ab54 --- /dev/null +++ b/core/spec/helpers/taxons_helper_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Spree::TaxonsHelper, :type => :helper do + # Regression test for #4382 + it "#taxon_preview" do + taxon = create(:taxon) + child_taxon = create(:taxon, parent: taxon) + product_1 = create(:product) + product_2 = create(:product) + product_3 = create(:product) + taxon.products << product_1 + taxon.products << product_2 + child_taxon.products << product_3 + + expect(taxon_preview(taxon.reload)).to eql([product_1, product_2, product_3]) + end +end diff --git a/core/spec/lib/calculated_adjustments_spec.rb b/core/spec/lib/calculated_adjustments_spec.rb index 9c00de09a58..d0e66a0f16f 100644 --- a/core/spec/lib/calculated_adjustments_spec.rb +++ b/core/spec/lib/calculated_adjustments_spec.rb @@ -1,69 +1,7 @@ require 'spec_helper' -# Its pretty difficult to test this module in isolation b/c it needs to work in conjunction with an actual class that -# extends ActiveRecord::Base and has a corresponding table in the database. So we'll just test it using Order and -# ShippingMethod instead since those classes are including the module. -describe Spree::Core::CalculatedAdjustments do - - let(:calculator) { mock_model(Spree::Calculator, :compute => 10, :[]= => nil) } - +describe Spree::CalculatedAdjustments do it "should add has_one :calculator relationship" do assert Spree::ShippingMethod.reflect_on_all_associations(:has_one).map(&:name).include?(:calculator) end - - let(:tax_rate) { Spree::TaxRate.new({:calculator => calculator}, :without_protection => true) } - - context "#create_adjustment and its resulting adjustment" do - let(:order) { Spree::Order.create } - let(:target) { order } - - it "should be associated with the target" do - target.adjustments.should_receive(:create) - tax_rate.create_adjustment("foo", target, order) - end - - it "should have the correct originator and an amount derived from the calculator and supplied calculable" do - adjustment = tax_rate.create_adjustment("foo", target, order) - adjustment.should_not be_nil - adjustment.amount.should == 10 - adjustment.source.should == order - adjustment.originator.should == tax_rate - end - - it "should be mandatory if true is supplied for that parameter" do - adjustment = tax_rate.create_adjustment("foo", target, order, true) - adjustment.should be_mandatory - end - - context "when the calculator returns 0" do - before { calculator.stub :compute => 0 } - - context "when adjustment is mandatory" do - before { tax_rate.create_adjustment("foo", target, order, true) } - - it "should create an adjustment" do - Spree::Adjustment.count.should == 1 - end - end - - context "when adjustment is not mandatory" do - before { tax_rate.create_adjustment("foo", target, order, false) } - - it "should not create an adjustment" do - Spree::Adjustment.count.should == 0 - end - end - end - - end - - context "#update_adjustment" do - it "should update the adjustment using its calculator (and the specified source)" do - adjustment = mock(:adjustment).as_null_object - calculable = mock :calculable - adjustment.should_receive(:update_attribute_without_callbacks).with(:amount, 10) - tax_rate.update_adjustment(adjustment, calculable) - end - end - end diff --git a/core/spec/lib/ext_spec.rb b/core/spec/lib/ext_spec.rb deleted file mode 100644 index 089dd14b343..00000000000 --- a/core/spec/lib/ext_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'spec_helper' - -describe 'Core extensions' do - - describe 'ActiveRecord::Base' do - - let(:order) { Spree::Order.create } - - context "update_attribute_without_callbacks" do - - it "sets the attribute" do - order.update_attribute_without_callbacks 'state', 'address' - order.state.should == 'address' - end - - it "updates the attribute in the database" do - order.update_attribute_without_callbacks 'state', 'address' - order.reload - order.state.should == 'address' - end - - it "doesn't call valid" do - order.should_not_receive(:valid?) - order.update_attribute_without_callbacks 'state', 'address' - end - - end - - context "update_attributes_without_callbacks" do - - it "sets the attributes" do - order.update_attributes_without_callbacks :state => 'address', :email => 'spree@example.com' - order.state.should == 'address' - order.email.should == 'spree@example.com' - end - - it "updates the attributes in the database" do - order.update_attributes_without_callbacks :state => 'address', :email => 'spree@example.com' - order.reload - order.state.should == 'address' - order.email.should == 'spree@example.com' - end - - it "doesn't call valid" do - order.should_not_receive(:valid?) - order.update_attributes_without_callbacks :state => 'address', :email => 'spree@example.com' - end - - end - - end - -end diff --git a/core/spec/lib/i18n_spec.rb b/core/spec/lib/i18n_spec.rb new file mode 100644 index 00000000000..8962749b805 --- /dev/null +++ b/core/spec/lib/i18n_spec.rb @@ -0,0 +1,123 @@ +require 'rspec/expectations' +require 'spree/i18n' +require 'spree/testing_support/i18n' + +describe "i18n" do + before do + I18n.backend.store_translations(:en, + { + :spree => { + :foo => "bar", + :bar => { + :foo => "bar within bar scope", + :invalid => nil, + :legacy_translation => "back in the day..." + }, + :invalid => nil, + :legacy_translation => "back in the day..." + } + }) + end + + it "translates within the spree scope" do + expect(Spree.normal_t(:foo)).to eql("bar") + expect(Spree.translate(:foo)).to eql("bar") + end + + it "translates within the spree scope using a path" do + allow(Spree).to receive(:virtual_path).and_return('bar') + + expect(Spree.normal_t('.legacy_translation')).to eql("back in the day...") + expect(Spree.translate('.legacy_translation')).to eql("back in the day...") + end + + it "raise error without any context when using a path" do + expect { + Spree.normal_t('.legacy_translation') + }.to raise_error + + expect { + Spree.translate('.legacy_translation') + }.to raise_error + end + + it "prepends a string scope" do + expect(Spree.normal_t(:foo, :scope => "bar")).to eql("bar within bar scope") + end + + it "prepends to an array scope" do + expect(Spree.normal_t(:foo, :scope => ["bar"])).to eql("bar within bar scope") + end + + it "returns two translations" do + expect(Spree.normal_t([:foo, 'bar.foo'])).to eql(["bar", "bar within bar scope"]) + end + + it "returns reasonable string for missing translations" do + expect(Spree.t(:missing_entry)).to include(" [:else, :where]) + Spree.check_missing_translations + assert_missing_translation("else") + assert_missing_translation("else.where") + assert_missing_translation("else.where.missing") + end + + it "does not log present translations" do + Spree.t(:foo) + Spree.check_missing_translations + expect(Spree.missing_translation_messages).to be_empty + end + + it "does not break when asked for multiple translations" do + Spree.t [:foo, 'bar.foo'] + Spree.check_missing_translations + expect(Spree.missing_translation_messages).to be_empty + end + end + + context "unused translations" do + def assert_unused_translation(key) + key = key_with_locale(key) + message = Spree.unused_translation_messages.detect { |m| m == key } + expect(message).not_to(be_nil, "expected '#{key}' to be unused, but it was used.") + end + + def assert_used_translation(key) + key = key_with_locale(key) + message = Spree.unused_translation_messages.detect { |m| m == key } + expect(message).to(be_nil, "expected '#{key}' to be used, but it wasn't.") + end + + it "logs translations that aren't used" do + Spree.check_unused_translations + assert_unused_translation("bar.legacy_translation") + assert_unused_translation("legacy_translation") + end + + it "does not log used translations" do + Spree.t(:foo) + Spree.check_unused_translations + assert_used_translation("foo") + end + end + end +end diff --git a/core/spec/lib/mail_interceptor_spec.rb b/core/spec/lib/mail_interceptor_spec.rb deleted file mode 100644 index 0f6cda641e6..00000000000 --- a/core/spec/lib/mail_interceptor_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -# We'll use the OrderMailer as a quick and easy way to test. IF it works here - it works for all email (in theory.) -describe Spree::OrderMailer do - let(:mail_method) { mock("mail_method", :preferred_mails_from => nil, :preferred_intercept_email => nil, :preferred_mail_bcc => nil) } - let(:order) { Spree::Order.new(:email => "customer@example.com") } - let(:message) { Spree::OrderMailer.confirm_email(order) } - #let(:email) { mock "email" } - - before(:all) do - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries.clear - end - - context "#deliver" do - before do - ActionMailer::Base.delivery_method = :test - Spree::MailMethod.stub :current => mail_method - end - after { ActionMailer::Base.deliveries.clear } - - it "should use the from address specified in the preference" do - mail_method.stub :preferred_mails_from => "no-reply@foobar.com" - message.deliver - @email = ActionMailer::Base.deliveries.first - @email.from.should == ["no-reply@foobar.com"] - end - - it "should use the provided from address" do - mail_method.stub :preferred_mails_from => "preference@foobar.com" - message = ActionMailer::Base.mail(:from => "override@foobar.com", :to => "test@test.com") - message.deliver - @email = ActionMailer::Base.deliveries.first - @email.from.should == ["override@foobar.com"] - end - - it "should add the bcc email when provided" do - mail_method.stub :preferred_mail_bcc => "bcc-foo@foobar.com" - message.deliver - @email = ActionMailer::Base.deliveries.first - @email.bcc.should == ["bcc-foo@foobar.com"] - end - - context "when intercept_email is provided" do - it "should strip the bcc recipients" do - message.bcc.should be_blank - end - - it "should strip the cc recipients" do - message.cc.should be_blank - end - - it "should replace the receipient with the specified address" do - mail_method.stub :preferred_intercept_email => "intercept@foobar.com" - message.deliver - @email = ActionMailer::Base.deliveries.first - @email.to.should == ["intercept@foobar.com"] - end - it "should modify the subject to include the original email" do - mail_method.stub :preferred_intercept_email => "intercept@foobar.com" - message.deliver - @email = ActionMailer::Base.deliveries.first - @email.subject.match(/customer@example\.com/).should be_true - end - end - - context "when intercept_mode is not provided" do - before { mail_method.stub :preferred_intercept_email => "" } - - it "should not modify the recipient" do - message.deliver - @email = ActionMailer::Base.deliveries.first - @email.to.should == ["customer@example.com"] - end - end - end -end diff --git a/core/spec/lib/mail_settings_spec.rb b/core/spec/lib/mail_settings_spec.rb deleted file mode 100644 index b3a4de498e1..00000000000 --- a/core/spec/lib/mail_settings_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'spec_helper' - -describe Spree::Core::MailSettings do - let(:mail_method) { Spree::MailMethod.new(:environment => "test") } - - context "init" do - before { Spree::MailMethod.stub :current => mail_method } - - context "perform_delivery preference" do - it "should override the application defaults" do - mail_method.set_preference(:enable_mail_delivery, false) - Spree::Core::MailSettings.init - ActionMailer::Base.perform_deliveries.should be_false - mail_method.set_preference(:enable_mail_delivery, true) - end - end - - context "when delivery is true" do - before { mail_method.set_preference(:enable_mail_delivery, true) } - - context "when mail_auth_type is other than none" do - before { mail_method.set_preference(:mail_auth_type, "login") } - - context "mail_auth_type preference" do - it "should override the application defaults" do - Spree::Core::MailSettings.init - ActionMailer::Base.smtp_settings[:authentication].should == "login" - end - end - - context "mail_host preference" do - it "should override the application defaults" do - mail_method.set_preference(:mail_host, "smtp.example.com") - Spree::Core::MailSettings.init - ActionMailer::Base.smtp_settings[:address].should == "smtp.example.com" - end - end - - context "mail_domain preference" do - it "should override the application defaults" do - mail_method.set_preference(:mail_domain, "example.com") - Spree::Core::MailSettings.init - ActionMailer::Base.smtp_settings[:domain].should == "example.com" - end - end - - context "mail_port preference" do - it "should override the application defaults" do - mail_method.set_preference(:mail_port, 123) - Spree::Core::MailSettings.init - ActionMailer::Base.smtp_settings[:port].should == 123 - end - end - - context "smtp_username preference" do - it "should override the application defaults" do - mail_method.set_preference(:smtp_username, "schof") - Spree::Core::MailSettings.init - ActionMailer::Base.smtp_settings[:user_name].should == "schof" - end - end - - context "smtp_password preference" do - it "should override the application defaults" do - mail_method.set_preference(:smtp_password, "hellospree!") - Spree::Core::MailSettings.init - ActionMailer::Base.smtp_settings[:password].should == "hellospree!" - end - end - - context "secure_connection_type preference" do - it "should override the application defaults" do - mail_method.set_preference(:secure_connection_type, "TLS") - Spree::Core::MailSettings.init - ActionMailer::Base.smtp_settings[:enable_starttls_auto].should be_true - end - end - end - end - - end -end diff --git a/core/spec/lib/money_spec.rb b/core/spec/lib/money_spec.rb deleted file mode 100644 index c7378ab8235..00000000000 --- a/core/spec/lib/money_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -#encoding: UTF-8 -require 'spec_helper' - -module Spree - describe Money do - before do - reset_spree_preferences do |config| - config.currency = "USD" - config.currency_symbol_position = :before - config.display_currency = false - end - end - - it "formats correctly" do - money = Spree::Money.new(10) - money.to_s.should == "$10.00" - end - - context "with currency" do - it "passed in option" do - money = Spree::Money.new(10, :with_currency => true) - money.to_s.should == "$10.00 USD" - end - - it "config option" do - Spree::Config[:display_currency] = true - money = Spree::Money.new(10) - money.to_s.should == "$10.00 USD" - end - end - - context "hide cents" do - it "hides cents suffix" do - Spree::Config[:hide_cents] = true - money = Spree::Money.new(10) - money.to_s.should == "$10" - end - - it "shows cents suffix" do - Spree::Config[:hide_cents] = false - money = Spree::Money.new(10) - money.to_s.should == "$10.00" - end - end - - context "currency parameter" do - context "when currency is specified in Canadian Dollars" do - it "uses the currency param over the global configuration" do - money = Spree::Money.new(10, :currency => 'CAD', :with_currency => true) - money.to_s.should == "$10.00 CAD" - end - end - - context "when currency is specified in Japanese Yen" do - it "uses the currency param over the global configuration" do - money = Spree::Money.new(100, :currency => 'JPY') - money.to_s.should == "¥100" - end - end - end - - context "symbol positioning" do - it "passed in option" do - money = Spree::Money.new(10, :symbol_position => :after) - money.to_s.should == "10.00 $" - end - - it "passed in option string" do - money = Spree::Money.new(10, :symbol_position => "after") - money.to_s.should == "10.00 $" - end - - it "config option" do - Spree::Config[:currency_symbol_position] = :after - money = Spree::Money.new(10) - money.to_s.should == "10.00 $" - end - end - - context "JPY" do - before do - reset_spree_preferences do |config| - config.currency = "JPY" - config.currency_symbol_position = :before - config.display_currency = false - end - end - - it "formats correctly" do - money = Spree::Money.new(1000) - money.to_s.should == "¥1,000" - end - end - end -end diff --git a/core/spec/lib/search/base_spec.rb b/core/spec/lib/search/base_spec.rb index cae3b2e78b3..c1fa3cc19b2 100644 --- a/core/spec/lib/search/base_spec.rb +++ b/core/spec/lib/search/base_spec.rb @@ -3,65 +3,84 @@ describe Spree::Core::Search::Base do before do - include ::Spree::ProductFilters - @product1 = create(:product, :name => "RoR Mug", :price => 9.00, :on_hand => 1) - @product2 = create(:product, :name => "RoR Shirt", :price => 11.00, :on_hand => 1) + include Spree::Core::ProductFilters + @taxon = create(:taxon, name: "Ruby on Rails") + + @product1 = create(:product, name: "RoR Mug", price: 9.00) + @product1.taxons << @taxon + @product2 = create(:product, name: "RoR Shirt", price: 11.00) end it "returns all products by default" do params = { :per_page => "" } searcher = Spree::Core::Search::Base.new(params) - searcher.retrieve_products.count.should == 2 + expect(searcher.retrieve_products.count).to eq(2) + end + + context "when include_images is included in the initalization params" do + let(:params) { { include_images: true, keyword: @product1.name, taxon: @taxon.id } } + subject { described_class.new(params).retrieve_products } + + before do + @product1.master.images.create(attachment_file_name: "Test", position: 2) + @product1.master.images.create(attachment_file_name: "Test1", position: 1) + @product1.reload + end + + it "returns images in correct order" do + expect(subject.first).to eq @product1 + expect(subject.first.images).to eq @product1.master.images + end end it "switches to next page according to the page parameter" do - @product3 = create(:product, :name => "RoR Pants", :price => 14.00, :on_hand => 1) + @product3 = create(:product, :name => "RoR Pants", :price => 14.00) params = { :per_page => "2" } searcher = Spree::Core::Search::Base.new(params) - searcher.retrieve_products.count.should == 2 + expect(searcher.retrieve_products.count).to eq(2) params.merge! :page => "2" searcher = Spree::Core::Search::Base.new(params) - searcher.retrieve_products.count.should == 1 + expect(searcher.retrieve_products.count).to eq(1) end it "maps search params to named scopes" do params = { :per_page => "", :search => { "price_range_any" => ["Under $10.00"] }} searcher = Spree::Core::Search::Base.new(params) - searcher.send(:get_base_scope).to_sql.should match /<= 10/ - searcher.retrieve_products.count.should == 1 + expect(searcher.send(:get_base_scope).to_sql).to match /<= 10/ + expect(searcher.retrieve_products.count).to eq(1) end it "maps multiple price_range_any filters" do params = { :per_page => "", :search => { "price_range_any" => ["Under $10.00", "$10.00 - $15.00"] }} searcher = Spree::Core::Search::Base.new(params) - searcher.send(:get_base_scope).to_sql.should match /<= 10/ - searcher.send(:get_base_scope).to_sql.should match /between 10 and 15/i - searcher.retrieve_products.count.should == 2 + expect(searcher.send(:get_base_scope).to_sql).to match /<= 10/ + expect(searcher.send(:get_base_scope).to_sql).to match /between 10 and 15/i + expect(searcher.retrieve_products.count).to eq(2) end it "uses ransack if scope not found" do params = { :per_page => "", :search => { "name_not_cont" => "Shirt" }} searcher = Spree::Core::Search::Base.new(params) - searcher.retrieve_products.count.should == 1 + expect(searcher.retrieve_products.count).to eq(1) end it "accepts a current user" do - user = stub + user = double searcher = Spree::Core::Search::Base.new({}) searcher.current_user = user - searcher.current_user.should eql(user) + expect(searcher.current_user).to eql(user) end it "finds products in alternate currencies" do price = create(:price, :currency => 'EUR', :variant => @product1.master) searcher = Spree::Core::Search::Base.new({}) searcher.current_currency = 'EUR' - searcher.retrieve_products.should == [@product1] + expect(searcher.retrieve_products).to eq([@product1]) end end diff --git a/core/spec/lib/spree/core/controller_helpers/auth_spec.rb b/core/spec/lib/spree/core/controller_helpers/auth_spec.rb new file mode 100644 index 00000000000..4d2d94483e7 --- /dev/null +++ b/core/spec/lib/spree/core/controller_helpers/auth_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +class FakesController < ApplicationController + include Spree::Core::ControllerHelpers::Auth + def index; render text: 'index'; end +end + +describe Spree::Core::ControllerHelpers::Auth, type: :controller do + controller(FakesController) {} + + describe '#current_ability' do + it 'returns Spree::Ability instance' do + expect(controller.current_ability.class).to eq Spree::Ability + end + end + + describe '#redirect_back_or_default' do + controller(FakesController) do + def index; redirect_back_or_default('/'); end + end + it 'redirects to session url' do + session[:spree_user_return_to] = '/redirect' + get :index + expect(response).to redirect_to('/redirect') + end + it 'redirects to default page' do + get :index + expect(response).to redirect_to('/') + end + end + + describe '#set_guest_token' do + controller(FakesController) do + def index + set_guest_token + render text: 'index' + end + end + it 'sends cookie header' do + get :index + expect(response.cookies['guest_token']).not_to be_nil + end + end + + describe '#store_location' do + it 'sets session return url' do + allow(controller).to receive_messages(request: double(fullpath: '/redirect')) + controller.store_location + expect(session[:spree_user_return_to]).to eq '/redirect' + end + end + + describe '#try_spree_current_user' do + it 'calls spree_current_user when define spree_current_user method' do + expect(controller).to receive(:spree_current_user) + controller.try_spree_current_user + end + it 'calls current_spree_user when define current_spree_user method' do + expect(controller).to receive(:current_spree_user) + controller.try_spree_current_user + end + it 'returns nil' do + expect(controller.try_spree_current_user).to eq nil + end + end + + describe '#redirect_unauthorized_access' do + controller(FakesController) do + def index; redirect_unauthorized_access; end + end + context 'when logged in' do + before do + allow(controller).to receive_messages(try_spree_current_user: double('User', id: 1, last_incomplete_spree_order: nil)) + end + it 'redirects unauthorized path' do + get :index + expect(response).to redirect_to('/unauthorized') + end + end + context 'when guest user' do + before do + allow(controller).to receive_messages(try_spree_current_user: nil) + end + it 'redirects login path' do + allow(controller).to receive_messages(spree_login_path: '/login') + get :index + expect(response).to redirect_to('/login') + end + it 'redirects root path' do + allow(controller).to receive_message_chain(:spree, :root_path).and_return('/root_path') + get :index + expect(response).to redirect_to('/root_path') + end + end + end +end diff --git a/core/spec/lib/spree/core/controller_helpers/order_spec.rb b/core/spec/lib/spree/core/controller_helpers/order_spec.rb new file mode 100644 index 00000000000..7254bcdd424 --- /dev/null +++ b/core/spec/lib/spree/core/controller_helpers/order_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +class FakesController < ApplicationController + include Spree::Core::ControllerHelpers::Order +end + +describe Spree::Core::ControllerHelpers::Order, type: :controller do + controller(FakesController) {} + + let(:user) { create(:user) } + let(:order) { create(:order, user: user) } + let(:store) { create(:store) } + + describe '#simple_current_order' do + before { allow(controller).to receive_messages(try_spree_current_user: user) } + it "returns an empty order" do + expect(controller.simple_current_order.item_count).to eq 0 + end + it 'returns Spree::Order instance' do + allow(controller).to receive_messages(cookies: double(signed: { guest_token: order.guest_token })) + expect(controller.simple_current_order).to eq order + end + end + + describe '#current_order' do + before { + allow(controller).to receive_messages(current_store: store) + allow(controller).to receive_messages(try_spree_current_user: user) + } + context 'create_order_if_necessary option is false' do + let!(:order) { create :order, user: user } + it 'returns current order' do + expect(controller.current_order).to eq order + end + end + context 'create_order_if_necessary option is true' do + it 'creates new order' do + expect { + controller.current_order(create_order_if_necessary: true) + }.to change(Spree::Order, :count).to(1) + end + + it 'assigns the current_store id' do + controller.current_order(create_order_if_necessary: true) + expect(Spree::Order.last.store_id).to eq store.id + end + end + end + + describe '#associate_user' do + before do + allow(controller).to receive_messages(current_order: order, try_spree_current_user: user) + end + context "user's email is blank" do + let(:user) { create(:user, email: '') } + it 'calls Spree::Order#associate_user! method' do + expect_any_instance_of(Spree::Order).to receive(:associate_user!) + controller.associate_user + end + end + context "user isn't blank" do + it 'does not calls Spree::Order#associate_user! method' do + expect_any_instance_of(Spree::Order).not_to receive(:associate_user!) + controller.associate_user + end + end + end + + describe '#set_current_order' do + let(:incomplete_order) { create(:order, user: user) } + before { allow(controller).to receive_messages(try_spree_current_user: user) } + + context 'when current order not equal to users incomplete orders' do + before { allow(controller).to receive_messages(current_order: order, last_incomplete_order: incomplete_order, cookies: double(signed: { guest_token: 'guest_token' })) } + + it 'calls Spree::Order#merge! method' do + expect(order).to receive(:merge!).with(incomplete_order, user) + controller.set_current_order + end + end + end + + describe '#current_currency' do + it 'returns current currency' do + Spree::Config[:currency] = 'USD' + expect(controller.current_currency).to eq 'USD' + end + end + + describe '#ip_address' do + it 'returns remote ip' do + expect(controller.ip_address).to eq request.remote_ip + end + end +end diff --git a/core/spec/lib/spree/core/controller_helpers/search_spec.rb b/core/spec/lib/spree/core/controller_helpers/search_spec.rb new file mode 100644 index 00000000000..84b7fadda5a --- /dev/null +++ b/core/spec/lib/spree/core/controller_helpers/search_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +class FakesController < ApplicationController + include Spree::Core::ControllerHelpers::Search +end + +describe Spree::Core::ControllerHelpers::Search, type: :controller do + controller(FakesController) {} + + describe '#build_searcher' do + it 'returns Spree::Core::Search::Base instance' do + allow(controller).to receive_messages(try_spree_current_user: create(:user), + current_currency: 'USD') + expect(controller.build_searcher({}).class).to eq Spree::Core::Search::Base + end + end +end diff --git a/core/spec/lib/spree/core/controller_helpers/ssl_spec.rb b/core/spec/lib/spree/core/controller_helpers/ssl_spec.rb new file mode 100644 index 00000000000..3ef3cb3ec62 --- /dev/null +++ b/core/spec/lib/spree/core/controller_helpers/ssl_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +class FakesController < ApplicationController + include Spree::Core::ControllerHelpers::SSL + def index; render text: 'index'; end + def create; end + def ssl_supported?; true; end +end + +describe Spree::Core::ControllerHelpers::SSL, type: :controller do + + describe 'redirect to http' do + before { Spree::Config[:redirect_https_to_http] = true } + after { Spree::Config[:redirect_https_to_http] = false } + before { request.env['HTTPS'] = 'on' } + + describe 'allowed two actions' do + controller(FakesController) do + ssl_allowed :index + ssl_allowed :foobar + end + + it '#ssl_allowed_actions returns both' do + expect(controller.ssl_allowed_actions).to eq [:index, :foobar] + end + + it 'should allow https access' do + expect(get(:index)).to be_success + end + end + + context 'allowed a single action' do + controller(FakesController) do + ssl_allowed :index + end + specify{ expect(controller.ssl_allowed_actions).to eq([:index]) } + specify{ expect(get(:index)).to be_success } + end + + context 'allowed all actions' do + controller(FakesController) do + ssl_allowed + end + specify{ expect(controller.ssl_allowed_actions).to eq([]) } + specify{ expect(get(:index)).to be_success } + end + + context 'ssl not allowed' do + controller(FakesController) { } + specify{ expect(get(:index)).to be_redirect } + end + + context 'using a post returns a HTTP status 426' do + controller(FakesController) { } + specify do + post(:create) + expect(response.body).to eq("Please switch to using HTTP (rather than HTTPS) and retry this request.") + expect(response.status).to eq(426) + end + end + end + + describe 'redirect to https' do + context 'required a single action' do + controller(FakesController) do + ssl_required :index + end + specify{ expect(controller.ssl_allowed_actions).to eq([:index]) } + specify{ expect(get(:index)).to be_redirect } + end + + context 'required all actions' do + controller(FakesController) do + ssl_required + end + specify{ expect(controller.ssl_allowed_actions).to eq([]) } + specify{ expect(get(:index)).to be_redirect } + end + + context 'not required' do + controller(FakesController) { } + specify{ expect(get(:index)).to be_success } + end + end +end diff --git a/core/spec/lib/spree/core/controller_helpers/store_spec.rb b/core/spec/lib/spree/core/controller_helpers/store_spec.rb new file mode 100644 index 00000000000..03c47a7b7f1 --- /dev/null +++ b/core/spec/lib/spree/core/controller_helpers/store_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +class FakesController < ApplicationController + include Spree::Core::ControllerHelpers::Store +end + +describe Spree::Core::ControllerHelpers::Store, type: :controller do + controller(FakesController) {} + + describe '#current_store' do + let!(:store) { create :store, default: true } + it 'returns current store' do + expect(controller.current_store).to eq store + end + end +end diff --git a/core/spec/lib/spree/core/controller_helpers/strong_parameters_spec.rb b/core/spec/lib/spree/core/controller_helpers/strong_parameters_spec.rb new file mode 100644 index 00000000000..f72f5fe733f --- /dev/null +++ b/core/spec/lib/spree/core/controller_helpers/strong_parameters_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +class FakesController < ApplicationController + include Spree::Core::ControllerHelpers::StrongParameters +end + +describe Spree::Core::ControllerHelpers::StrongParameters, type: :controller do + controller(FakesController) {} + + describe '#permitted_attributes' do + it 'returns Spree::PermittedAttributes module' do + expect(controller.permitted_attributes).to eq Spree::PermittedAttributes + end + end + + describe '#permitted_payment_attributes' do + it 'returns Array class' do + expect(controller.permitted_payment_attributes.class).to eq Array + end + end + + describe '#permitted_checkout_attributes' do + it 'returns Array class' do + expect(controller.permitted_checkout_attributes.class).to eq Array + end + end + + describe '#permitted_order_attributes' do + it 'returns Array class' do + expect(controller.permitted_order_attributes.class).to eq Array + end + end + + describe '#permitted_product_attributes' do + it 'returns Array class' do + expect(controller.permitted_product_attributes.class).to eq Array + end + end +end diff --git a/core/spec/lib/spree/core/delegate_belongs_to_spec.rb b/core/spec/lib/spree/core/delegate_belongs_to_spec.rb new file mode 100644 index 00000000000..0d053f6bfd6 --- /dev/null +++ b/core/spec/lib/spree/core/delegate_belongs_to_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +# This is a bit of a insane spec I have to admit +# Chosed the spree_payment_methods table because it has a `name` column +# already. Stubs wouldn't work here (the delegation runs before this spec is +# loaded) and adding a column here might make the test even crazy so here we go +module Spree + class DelegateBelongsToStubModel < Spree::Base + self.table_name = "spree_payment_methods" + belongs_to :product + delegate_belongs_to :product, :name + end + + describe DelegateBelongsToStubModel do + context "model has column attr delegated to associated object" do + it "doesnt touch the associated object" do + expect(subject).not_to receive(:product) + subject.name + end + end + end +end diff --git a/core/spec/lib/spree/core/importer/order_spec.rb b/core/spec/lib/spree/core/importer/order_spec.rb new file mode 100644 index 00000000000..a1dc9d3ca36 --- /dev/null +++ b/core/spec/lib/spree/core/importer/order_spec.rb @@ -0,0 +1,472 @@ +require 'spec_helper' + +module Spree + module Core + describe Importer::Order do + + let!(:country) { create(:country) } + let!(:state) { country.states.first || create(:state, :country => country) } + let!(:stock_location) { create(:stock_location, admin_name: 'Admin Name') } + + let(:user) { stub_model(LegacyUser, :email => 'fox@mudler.com') } + let(:shipping_method) { create(:shipping_method) } + let(:payment_method) { create(:check_payment_method) } + + let(:product) { product = Spree::Product.create(:name => 'Test', + :sku => 'TEST-1', + :price => 33.22) + product.shipping_category = create(:shipping_category) + product.save + product } + + let(:variant) { variant = product.master + variant.stock_items.each { |si| si.update_attribute(:count_on_hand, 10) } + variant } + + let(:sku) { variant.sku } + let(:variant_id) { variant.id } + + let(:line_items) {{ "0" => { :variant_id => variant.id, :quantity => 5 }}} + let(:ship_address) {{ + :address1 => '123 Testable Way', + :firstname => 'Fox', + :lastname => 'Mulder', + :city => 'Washington', + :country_id => country.id, + :state_id => state.id, + :zipcode => '66666', + :phone => '666-666-6666' + }} + + it 'can import an order number' do + params = { number: '123-456-789' } + order = Importer::Order.import(user, params) + expect(order.number).to eq '123-456-789' + end + + it 'optionally add completed at' do + params = { email: 'test@test.com', + completed_at: Time.now, + line_items_attributes: line_items } + + order = Importer::Order.import(user,params) + expect(order).to be_completed + expect(order.state).to eq 'complete' + end + + it "assigns order[email] over user email to order" do + params = { email: 'wooowww@test.com' } + order = Importer::Order.import(user,params) + expect(order.email).to eq params[:email] + end + + context "assigning a user to an order" do + let(:other_user) { stub_model(LegacyUser, :email => 'dana@scully.com') } + + context "as an admin" do + before { allow(user).to receive_messages :has_spree_role? => true } + + context "a user's id is not provided" do + # this is a regression spec for an issue we ran into at Bonobos + it "doesn't unassociate the admin from the order" do + params = { } + order = Importer::Order.import(user, params) + expect(order.user_id).to eq(user.id) + end + end + end + + context "as a user" do + before { allow(user).to receive_messages :has_spree_role? => false } + it "does not assign the order to the other user" do + params = { user_id: other_user.id } + order = Importer::Order.import(user, params) + expect(order.user_id).to eq(user.id) + end + end + end + + it 'can build an order from API with just line items' do + params = { :line_items_attributes => line_items } + + expect(Importer::Order).to receive(:ensure_variant_id_from_params).and_return({variant_id: variant.id, quantity: 5}) + order = Importer::Order.import(user,params) + expect(order.user).to eq(nil) + line_item = order.line_items.first + expect(line_item.quantity).to eq(5) + expect(line_item.variant_id).to eq(variant_id) + end + + it 'handles line_item building exceptions' do + line_items['0'][:variant_id] = 'XXX' + params = { :line_items_attributes => line_items } + + expect { + order = Importer::Order.import(user,params) + }.to raise_error /XXX/ + end + + it 'handles line_item updating exceptions' do + line_items['0'][:currency] = 'GBP' + params = { :line_items_attributes => line_items } + + expect { + order = Importer::Order.import(user, params) + }.to raise_error /Validation failed/ + end + + it 'can build an order from API with variant sku' do + params = { :line_items_attributes => { + "0" => { :sku => sku, :quantity => 5 } }} + + order = Importer::Order.import(user,params) + + line_item = order.line_items.first + expect(line_item.variant_id).to eq(variant_id) + expect(line_item.quantity).to eq(5) + end + + it 'handles exceptions when sku is not found' do + params = { :line_items_attributes => { + "0" => { :sku => 'XXX', :quantity => 5 } }} + expect { + order = Importer::Order.import(user,params) + }.to raise_error /XXX/ + end + + it 'can build an order from API shipping address' do + params = { :ship_address_attributes => ship_address, + :line_items_attributes => line_items } + + order = Importer::Order.import(user,params) + expect(order.ship_address.address1).to eq '123 Testable Way' + end + + it 'can build an order from API with country attributes' do + ship_address.delete(:country_id) + ship_address[:country] = { 'iso' => 'US' } + params = { :ship_address_attributes => ship_address, + :line_items_attributes => line_items } + + order = Importer::Order.import(user,params) + expect(order.ship_address.country.iso).to eq 'US' + end + + it 'handles country lookup exceptions' do + ship_address.delete(:country_id) + ship_address[:country] = { 'iso' => 'XXX' } + params = { :ship_address_attributes => ship_address, + :line_items_attributes => line_items } + + expect { + order = Importer::Order.import(user,params) + }.to raise_error /XXX/ + end + + it 'can build an order from API with state attributes' do + ship_address.delete(:state_id) + ship_address[:state] = { 'name' => state.name } + params = { :ship_address_attributes => ship_address, + :line_items_attributes => line_items } + + order = Importer::Order.import(user,params) + expect(order.ship_address.state.name).to eq 'Alabama' + end + + context "with a different currency" do + before { variant.price_in("GBP").update_attribute(:price, 18.99) } + + it "sets the order currency" do + params = { + currency: "GBP" + } + order = Importer::Order.import(user,params) + expect(order.currency).to eq "GBP" + end + + it "can handle it when a line order price is specified" do + params = { + currency: "GBP", + line_items_attributes: line_items + } + line_items["0"].merge! currency: "GBP", price: 1.99 + order = Importer::Order.import(user, params) + expect(order.currency).to eq "GBP" + expect(order.line_items.first.price).to eq 1.99 + expect(order.line_items.first.currency).to eq "GBP" + end + end + + context "state passed is not associated with country" do + let(:params) do + params = { :ship_address_attributes => ship_address, + :line_items_attributes => line_items } + end + + let(:other_state) { create(:state, name: "Uhuhuh", country: create(:country)) } + + before do + ship_address.delete(:state_id) + ship_address[:state] = { 'name' => other_state.name } + end + + it 'sets states name instead of state id' do + order = Importer::Order.import(user,params) + expect(order.ship_address.state_name).to eq other_state.name + end + end + + it 'sets state name if state record not found' do + ship_address.delete(:state_id) + ship_address[:state] = { 'name' => 'XXX' } + params = { :ship_address_attributes => ship_address, + :line_items_attributes => line_items } + + order = Importer::Order.import(user,params) + expect(order.ship_address.state_name).to eq 'XXX' + end + + context 'variant not deleted' do + it 'ensures variant id from api' do + hash = { sku: variant.sku } + Importer::Order.ensure_variant_id_from_params(hash) + expect(hash[:variant_id]).to eq variant.id + end + end + + context 'variant was deleted' do + it 'raise error as variant shouldnt be found' do + variant.product.destroy + hash = { sku: variant.sku } + expect { + Importer::Order.ensure_variant_id_from_params(hash) + }.to raise_error + end + end + + it 'ensures_country_id for country fields' do + [:name, :iso, :iso_name, :iso3].each do |field| + address = { :country => { field => country.send(field) }} + Importer::Order.ensure_country_id_from_params(address) + expect(address[:country_id]).to eq country.id + end + end + + it "raises with proper message when cant find country" do + address = { :country => { "name" => "NoNoCountry" } } + expect { + Importer::Order.ensure_country_id_from_params(address) + }.to raise_error /NoNoCountry/ + end + + it 'ensures_state_id for state fields' do + [:name, :abbr].each do |field| + address = { country_id: country.id, :state => { field => state.send(field) }} + Importer::Order.ensure_state_id_from_params(address) + expect(address[:state_id]).to eq state.id + end + end + + context "shipments" do + let(:params) do + { :shipments_attributes => [ + { :tracking => '123456789', + :cost => '14.99', + :shipping_method => shipping_method.name, + :stock_location => stock_location.name, + :inventory_units => [{ :sku => sku }] + } + ] } + end + + it 'ensures variant exists and is not deleted' do + expect(Importer::Order).to receive(:ensure_variant_id_from_params) + order = Importer::Order.import(user,params) + end + + it 'builds them properly' do + order = Importer::Order.import(user, params) + shipment = order.shipments.first + + expect(shipment.cost.to_f).to eq 14.99 + expect(shipment.inventory_units.first.variant_id).to eq product.master.id + expect(shipment.tracking).to eq '123456789' + expect(shipment.shipping_rates.first.cost).to eq 14.99 + expect(shipment.selected_shipping_rate).to eq(shipment.shipping_rates.first) + expect(shipment.stock_location).to eq stock_location + expect(order.shipment_total.to_f).to eq 14.99 + end + + it "accepts admin name for stock location" do + params[:shipments_attributes][0][:stock_location] = stock_location.admin_name + order = Importer::Order.import(user, params) + shipment = order.shipments.first + + expect(shipment.stock_location).to eq stock_location + end + + it "raises if cant find stock location" do + params[:shipments_attributes][0][:stock_location] = "doesnt exist" + expect { + order = Importer::Order.import(user,params) + }.to raise_error + end + + context 'when completed_at and shipped_at present' do + let(:params) do + { + :completed_at => 2.days.ago, + :shipments_attributes => [ + { :tracking => '123456789', + :cost => '4.99', + :shipped_at => 1.day.ago, + :shipping_method => shipping_method.name, + :stock_location => stock_location.name, + :inventory_units => [{ :sku => sku }] + } + ] + } + end + + it 'builds them properly' do + order = Importer::Order.import(user, params) + shipment = order.shipments.first + + expect(shipment.cost.to_f).to eq 4.99 + expect(shipment.inventory_units.first.variant_id).to eq product.master.id + expect(shipment.tracking).to eq '123456789' + expect(shipment.shipped_at).to be_present + expect(shipment.shipping_rates.first.cost).to eq 4.99 + expect(shipment.selected_shipping_rate).to eq(shipment.shipping_rates.first) + expect(shipment.stock_location).to eq stock_location + expect(shipment.state).to eq('shipped') + expect(shipment.inventory_units.all?(&:shipped?)).to be true + expect(order.shipment_state).to eq('shipped') + expect(order.shipment_total.to_f).to eq 4.99 + end + end + end + + it 'handles shipment building exceptions' do + params = { :shipments_attributes => [{ tracking: '123456789', + cost: '4.99', + shipping_method: 'XXX', + inventory_units: [{ sku: sku }] + }] } + expect { + order = Importer::Order.import(user,params) + }.to raise_error /XXX/ + end + + it 'adds adjustments' do + params = { :adjustments_attributes => [ + { label: 'Shipping Discount', amount: -4.99 }, + { label: 'Promotion Discount', amount: -3.00 }] } + + order = Importer::Order.import(user,params) + expect(order.adjustments.all?(&:closed?)).to be true + expect(order.adjustments.first.label).to eq 'Shipping Discount' + expect(order.adjustments.first.amount).to eq -4.99 + end + + it "calculates final order total correctly" do + params = { + adjustments_attributes: [ + { label: 'Promotion Discount', amount: -3.00 } + ], + line_items_attributes: { + "0" => { + variant_id: variant.id, + quantity: 5 + } + } + } + + order = Importer::Order.import(user,params) + expect(order.item_total).to eq(166.1) + expect(order.total).to eq(163.1) # = item_total (166.1) - adjustment_total (3.00) + + end + + it 'handles adjustment building exceptions' do + params = { :adjustments_attributes => [ + { amount: 'XXX' }, + { label: 'Promotion Discount', amount: '-3.00' }] } + + expect { + order = Importer::Order.import(user,params) + }.to raise_error /XXX/ + end + + it 'builds a payment using state' do + params = { :payments_attributes => [{ amount: '4.99', + payment_method: payment_method.name, + state: 'completed' }] } + order = Importer::Order.import(user,params) + expect(order.payments.first.amount).to eq 4.99 + end + + it 'builds a payment using status as fallback' do + params = { :payments_attributes => [{ amount: '4.99', + payment_method: payment_method.name, + status: 'completed' }] } + order = Importer::Order.import(user,params) + expect(order.payments.first.amount).to eq 4.99 + end + + it 'handles payment building exceptions' do + params = { :payments_attributes => [{ amount: '4.99', + payment_method: 'XXX' }] } + expect { + order = Importer::Order.import(user, params) + }.to raise_error /XXX/ + end + + it 'build a source payment using years and month' do + params = { :payments_attributes => [{ + amount: '4.99', + payment_method: payment_method.name, + status: 'completed', + source: { + name: 'Fox', + last_digits: "7424", + cc_type: "visa", + year: '2022', + month: "5" + } + }]} + + order = Importer::Order.import(user, params) + expect(order.payments.first.source.last_digits).to eq '7424' + end + + it 'handles source building exceptions when do not have years and month' do + params = { :payments_attributes => [{ + amount: '4.99', + payment_method: payment_method.name, + status: 'completed', + source: { + name: 'Fox', + last_digits: "7424", + cc_type: "visa" + } + }]} + + expect { + order = Importer::Order.import(user, params) + }.to raise_error /Validation failed: Credit card Month is not a number, Credit card Year is not a number/ + end + + context "raises error" do + it "clears out order from db" do + params = { :payments_attributes => [{ payment_method: "XXX" }] } + count = Order.count + + expect { order = Importer::Order.import(user,params) }.to raise_error + expect(Order.count).to eq count + end + end + + end + end +end diff --git a/core/spec/lib/spree/core/validators/email_spec.rb b/core/spec/lib/spree/core/validators/email_spec.rb new file mode 100644 index 00000000000..68e0a4a78bc --- /dev/null +++ b/core/spec/lib/spree/core/validators/email_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe EmailValidator do + + class Tester + include ActiveModel::Validations + attr_accessor :email_address + validates :email_address, email: true + end + + let(:valid_emails) {[ + 'valid@email.com', + 'valid@email.com.uk', + 'e@email.com', + 'valid+email@email.com', + 'valid-email@email.com', + 'valid_email@email.com', + 'valid.email@email.com' + ]} + let(:invalid_emails) {[ + 'invalid email@email.com', + '.invalid.email@email.com', + 'invalid.email.@email.com', + '@email.com', + '.@email.com', + 'invalidemailemail.com', + '@invalid.email@email.com', + 'invalid@email@email.com', + 'invalid.email@@email.com' + ]} + + it 'validates valid email addresses' do + tester = Tester.new + valid_emails.each do |email| + tester.email_address = email + expect(tester.valid?).to be true + end + end + + it 'validates invalid email addresses' do + tester = Tester.new + invalid_emails.each do |email| + tester.email_address = email + expect(tester.valid?).to be false + end + end + +end diff --git a/core/spec/lib/spree/localized_number_spec.rb b/core/spec/lib/spree/localized_number_spec.rb new file mode 100644 index 00000000000..9e789179f9a --- /dev/null +++ b/core/spec/lib/spree/localized_number_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Spree::LocalizedNumber do + + context ".parse" do + before do + I18n.enforce_available_locales = false + I18n.locale = I18n.default_locale + I18n.backend.store_translations(:de, { :number => { :currency => { :format => { :delimiter => '.', :separator => ',' } } } }) + end + + after do + I18n.locale = I18n.default_locale + I18n.enforce_available_locales = true + end + + context "with decimal point" do + it "captures the proper amount for a formatted price" do + expect(subject.class.parse('1,599.99')).to eql 1599.99 + end + end + + context "with decimal comma" do + it "captures the proper amount for a formatted price" do + I18n.locale = :de + expect(subject.class.parse('1.599,99')).to eql 1599.99 + end + end + + context "with a numeric price" do + it "uses the price as is" do + I18n.locale = :de + expect(subject.class.parse(1599.99)).to eql 1599.99 + end + end + end + +end diff --git a/core/spec/lib/spree/migrations_spec.rb b/core/spec/lib/spree/migrations_spec.rb new file mode 100644 index 00000000000..11835df8ad5 --- /dev/null +++ b/core/spec/lib/spree/migrations_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +module Spree + describe Migrations do + let(:app_migrations) { [".", "34_add_title.rb", "52_add_text.rb"] } + let(:engine_migrations) { [".", "334_create_orders.spree.rb", "777_create_products.spree.rb"] } + + let(:config) { double("Config", root: "dir") } + + subject { described_class.new(config, "spree") } + + before do + expect(File).to receive(:exists?).with("config/spree.yml").and_return true + expect(File).to receive(:directory?).with("db/migrate").and_return true + end + + it "warns about missing migrations" do + expect(Dir).to receive(:entries).with("db/migrate").and_return app_migrations + expect(Dir).to receive(:entries).with("dir/db/migrate").and_return engine_migrations + + silence_stream(STDOUT) { + expect(subject.check).to eq true + } + end + + context "no missing migrations" do + it "says nothing" do + expect(Dir).to receive(:entries).with("dir/db/migrate").and_return engine_migrations + expect(Dir).to receive(:entries).with("db/migrate").and_return (app_migrations + engine_migrations) + expect(subject.check).to eq nil + end + end + end +end diff --git a/core/spec/lib/spree/money_spec.rb b/core/spec/lib/spree/money_spec.rb new file mode 100644 index 00000000000..b3fc1b38485 --- /dev/null +++ b/core/spec/lib/spree/money_spec.rb @@ -0,0 +1,168 @@ +# coding: utf-8 +require 'spec_helper' + +describe Spree::Money do + before do + configure_spree_preferences do |config| + config.currency = "USD" + config.currency_symbol_position = :before + config.display_currency = false + end + end + + it "formats correctly" do + money = Spree::Money.new(10) + expect(money.to_s).to eq("$10.00") + end + + it "can get cents" do + money = Spree::Money.new(10) + expect(money.cents).to eq(1000) + end + + context "with currency" do + it "passed in option" do + money = Spree::Money.new(10, :with_currency => true, :html => false) + expect(money.to_s).to eq("$10.00 USD") + end + + it "config option" do + Spree::Config[:display_currency] = true + money = Spree::Money.new(10, :html => false) + expect(money.to_s).to eq("$10.00 USD") + end + end + + context "hide cents" do + it "hides cents suffix" do + Spree::Config[:hide_cents] = true + money = Spree::Money.new(10) + expect(money.to_s).to eq("$10") + end + + it "shows cents suffix" do + Spree::Config[:hide_cents] = false + money = Spree::Money.new(10) + expect(money.to_s).to eq("$10.00") + end + end + + context "currency parameter" do + context "when currency is specified in Canadian Dollars" do + it "uses the currency param over the global configuration" do + money = Spree::Money.new(10, :currency => 'CAD', :with_currency => true, :html => false) + expect(money.to_s).to eq("$10.00 CAD") + end + end + + context "when currency is specified in Japanese Yen" do + it "uses the currency param over the global configuration" do + money = Spree::Money.new(100, :currency => 'JPY', :html => false) + expect(money.to_s).to eq("¥100") + end + end + end + + context "symbol positioning" do + it "passed in option" do + money = Spree::Money.new(10, :symbol_position => :after, :html => false) + expect(money.to_s).to eq("10.00 $") + end + + it "passed in option string" do + money = Spree::Money.new(10, :symbol_position => "after", :html => false) + expect(money.to_s).to eq("10.00 $") + end + + it "config option" do + Spree::Config[:currency_symbol_position] = :after + money = Spree::Money.new(10, :html => false) + expect(money.to_s).to eq("10.00 $") + end + end + + context "sign before symbol" do + it "defaults to -$10.00" do + money = Spree::Money.new(-10) + expect(money.to_s).to eq("-$10.00") + end + + it "passed in option" do + money = Spree::Money.new(-10, :sign_before_symbol => false) + expect(money.to_s).to eq("$-10.00") + end + + it "config option" do + Spree::Config[:currency_sign_before_symbol] = false + money = Spree::Money.new(-10) + expect(money.to_s).to eq("$-10.00") + end + end + + context "JPY" do + before do + configure_spree_preferences do |config| + config.currency = "JPY" + config.currency_symbol_position = :before + config.display_currency = false + end + end + + it "formats correctly" do + money = Spree::Money.new(1000, :html => false) + expect(money.to_s).to eq("¥1,000") + end + end + + context "EUR" do + before do + configure_spree_preferences do |config| + config.currency = "EUR" + config.currency_symbol_position = :after + config.display_currency = false + end + end + + # Regression test for #2634 + it "formats as plain by default" do + money = Spree::Money.new(10) + expect(money.to_s).to eq("10.00 €") + end + + # Regression test for #2632 + it "acknowledges decimal mark option" do + Spree::Config[:currency_decimal_mark] = "," + money = Spree::Money.new(10) + expect(money.to_s).to eq("10,00 €") + end + + # Regression test for #2632 + it "acknowledges thousands separator option" do + Spree::Config[:currency_thousands_separator] = "." + money = Spree::Money.new(1000) + expect(money.to_s).to eq("1.000.00 €") + end + + it "formats as HTML if asked (nicely) to" do + money = Spree::Money.new(10) + # The HTML'ified version of "10.00 €" + expect(money.to_html).to eq("10.00 €") + end + + it "formats as HTML with currency" do + Spree::Config[:display_currency] = true + money = Spree::Money.new(10) + # The HTML'ified version of "10.00 €" + expect(money.to_html).to eq("10.00 € EUR") + end + end + + describe "#as_json" do + let(:options) { double('options') } + + it "returns the expected string" do + money = Spree::Money.new(10) + expect(money.as_json(options)).to eq("$10.00") + end + end +end diff --git a/core/spec/lib/tasks/exchanges_spec.rb b/core/spec/lib/tasks/exchanges_spec.rb new file mode 100644 index 00000000000..deb51bc31a4 --- /dev/null +++ b/core/spec/lib/tasks/exchanges_spec.rb @@ -0,0 +1,136 @@ +require 'spec_helper' + +describe "exchanges:charge_unreturned_items" do + include_context "rake" + + describe '#prerequisites' do + it { expect(subject.prerequisites).to include("environment") } + end + + context "there are no unreturned items" do + it { expect { subject.invoke }.not_to change { Spree::Order.count } } + end + + context "there are unreturned items" do + let!(:order) { create(:shipped_order, line_items_count: 2) } + let(:return_item_1) { create(:exchange_return_item, inventory_unit: order.inventory_units.first) } + let(:return_item_2) { create(:exchange_return_item, inventory_unit: order.inventory_units.last) } + let!(:rma) { create(:return_authorization, order: order, return_items: [return_item_1, return_item_2]) } + let!(:tax_rate) { create(:tax_rate, zone: order.tax_zone, tax_category: return_item_2.exchange_variant.tax_category) } + + before do + @original_expedited_exchanges_pref = Spree::Config[:expedited_exchanges] + Spree::Config[:expedited_exchanges] = true + Spree::StockItem.update_all(count_on_hand: 10) + rma.save! + Spree::Shipment.last.ship! + return_item_1.receive! + Timecop.travel travel_time + end + + after do + Timecop.return + Spree::Config[:expedited_exchanges] = @original_expedited_exchanges_pref + end + + context "fewer than the config allowed days have passed" do + let(:travel_time) { (Spree::Config[:expedited_exchanges_days_window] - 1).days } + + it "does not create a new order" do + expect { subject.invoke }.not_to change { Spree::Order.count } + end + end + + context "more than the config allowed days have passed" do + + let(:travel_time) { (Spree::Config[:expedited_exchanges_days_window] + 1).days } + + it "creates a new completed order" do + expect { subject.invoke }.to change { Spree::Order.count } + expect(Spree::Order.last).to be_completed + end + + it "moves the shipment for the unreturned items to the new order" do + subject.invoke + new_order = Spree::Order.last + expect(new_order.shipments.count).to eq 1 + expect(return_item_2.reload.exchange_shipment.order).to eq Spree::Order.last + end + + it "creates line items on the order for the unreturned items" do + subject.invoke + expect(Spree::Order.last.line_items.map(&:variant)).to eq [return_item_2.exchange_variant] + end + + it "associates the exchanges inventory units with the new line items" do + subject.invoke + expect(return_item_2.reload.exchange_inventory_unit.try(:line_item).try(:order)).to eq Spree::Order.last + end + + it "uses the credit card from the previous order" do + subject.invoke + new_order = Spree::Order.last + expect(new_order.credit_cards).to be_present + expect(new_order.credit_cards.first).to eq order.valid_credit_cards.first + end + + it "authorizes the order for the full amount of the unreturned items including taxes" do + expect { subject.invoke }.to change { Spree::Payment.count }.by(1) + new_order = Spree::Order.last + expected_amount = return_item_2.reload.exchange_variant.price + new_order.additional_tax_total + new_order.included_tax_total + expect(new_order.total).to eq expected_amount + payment = new_order.payments.first + expect(payment.amount).to eq expected_amount + expect(payment).to be_pending + expect(new_order.item_total).to eq return_item_2.reload.exchange_variant.price + end + + it "does not attempt to create a new order for the item more than once" do + subject.invoke + subject.reenable + expect { subject.invoke }.not_to change { Spree::Order.count } + end + + it "associates the store of the original order with the exchange order" do + allow_any_instance_of(Spree::Order).to receive(:store_id).and_return(123) + + expect(Spree::Order).to receive(:create!).once.with(hash_including({store_id: 123})) { |attrs| Spree::Order.new(attrs.except(:store_id)).tap(&:save!) } + subject.invoke + end + + context "there is no card from the previous order" do + let!(:credit_card) { create(:credit_card, user: order.user, default: true, gateway_customer_profile_id: "BGS-123") } + before { allow_any_instance_of(Spree::Order).to receive(:valid_credit_cards) { [] } } + + it "attempts to use the user's default card" do + expect { subject.invoke }.to change { Spree::Payment.count }.by(1) + new_order = Spree::Order.last + expect(new_order.credit_cards).to be_present + expect(new_order.credit_cards.first).to eq credit_card + end + end + + context "it is unable to authorize the credit card" do + before { allow_any_instance_of(Spree::Payment).to receive(:authorize!).and_raise(RuntimeError) } + + it "raises an error with the order" do + expect { subject.invoke }.to raise_error(UnableToChargeForUnreturnedItems) + end + end + + context "the exchange inventory unit is not shipped" do + before { return_item_2.reload.exchange_inventory_unit.update_columns(state: "on hand") } + it "does not create a new order" do + expect { subject.invoke }.not_to change { Spree::Order.count } + end + end + + context "the exchange inventory unit has been returned" do + before { return_item_2.reload.exchange_inventory_unit.update_columns(state: "returned") } + it "does not create a new order" do + expect { subject.invoke }.not_to change { Spree::Order.count } + end + end + end + end +end diff --git a/core/spec/lib/token_resource_spec.rb b/core/spec/lib/token_resource_spec.rb deleted file mode 100644 index 40ac8f838ab..00000000000 --- a/core/spec/lib/token_resource_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -# Its pretty difficult to test this module in isolation b/c it needs to work in conjunction with an actual class that -# extends ActiveRecord::Base and has a corresponding table in the database. So we'll just test it using Order instead -# since those classes are including the module. -describe Spree::Core::TokenResource do - let(:order) { Spree::Order.new } - let(:permission) { mock_model(Spree::TokenizedPermission) } - - it 'should add has_one :tokenized_permission relationship' do - assert Spree::Order.reflect_on_all_associations(:has_one).map(&:name).include?(:tokenized_permission) - end - - context '#token' do - it 'should return the token of the associated permission' do - order.stub :tokenized_permission => permission - permission.stub :token => 'foo' - order.token.should == 'foo' - end - - it 'should return nil if there is no associated permission' do - order.token.should be_nil - end - end - - context '#create_token' do - it 'should create a randomized 16 character token' do - token = order.create_token - token.size.should == 16 - end - end -end diff --git a/core/spec/mailers/order_mailer_spec.rb b/core/spec/mailers/order_mailer_spec.rb index 295488d48a6..cec201d0dc3 100644 --- a/core/spec/mailers/order_mailer_spec.rb +++ b/core/spec/mailers/order_mailer_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'email_spec' -describe Spree::OrderMailer do +describe Spree::OrderMailer, :type => :mailer do include EmailSpec::Helpers include EmailSpec::Matchers @@ -9,70 +9,82 @@ order = stub_model(Spree::Order) product = stub_model(Spree::Product, :name => %Q{The "BEST" product}) variant = stub_model(Spree::Variant, :product => product) - price = stub_model(Spree::Price, :variant => variant) - line_item = stub_model(Spree::LineItem, :variant => variant, :order => order, :quantity => 1, :price => 5) - variant.stub(:default_price => price) - order.stub(:line_items => [line_item]) + price = stub_model(Spree::Price, :variant => variant, :amount => 5.00) + line_item = stub_model(Spree::LineItem, :variant => variant, :order => order, :quantity => 1, :price => 4.99) + allow(variant).to receive_messages(:default_price => price) + allow(order).to receive_messages(:line_items => [line_item]) order end + context ":from not set explicitly" do + it "falls back to spree config" do + message = Spree::OrderMailer.confirm_email(order) + expect(message.from).to eq([Spree::Config[:mails_from]]) + end + end + it "doesn't aggressively escape double quotes in confirmation body" do confirmation_email = Spree::OrderMailer.confirm_email(order) - confirmation_email.body.should_not include(""") + expect(confirmation_email.body).not_to include(""") + end + + it "confirm_email accepts an order id as an alternative to an Order object" do + expect(Spree::Order).to receive(:find).with(order.id).and_return(order) + expect { + confirmation_email = Spree::OrderMailer.confirm_email(order.id) + }.not_to raise_error + end + + it "cancel_email accepts an order id as an alternative to an Order object" do + expect(Spree::Order).to receive(:find).with(order.id).and_return(order) + expect { + cancel_email = Spree::OrderMailer.cancel_email(order.id) + }.not_to raise_error end context "only shows eligible adjustments in emails" do before do - order.adjustments.create({:label => "Eligible Adjustment", - :amount => 10, - :eligible => true}, :without_protection => true) - - order.adjustments.create!({:label => "Ineligible Adjustment", - :amount => -10, - :eligible => false}, :without_protection => true) + create(:adjustment, :order => order, :eligible => true, :label => "Eligible Adjustment") + create(:adjustment, :order => order, :eligible => false, :label => "Ineligible Adjustment") end let!(:confirmation_email) { Spree::OrderMailer.confirm_email(order) } let!(:cancel_email) { Spree::OrderMailer.cancel_email(order) } specify do - confirmation_email.body.should_not include("Ineligible Adjustment") + expect(confirmation_email.body).not_to include("Ineligible Adjustment") end specify do - cancel_email.body.should_not include("Ineligible Adjustment") + expect(cancel_email.body).not_to include("Ineligible Adjustment") end end - context "emails must be translatable" do - context "en locale" do - before do - en_confirm_mail = { :order_mailer => { :confirm_email => { :dear_customer => 'Dear Customer,' } } } - en_cancel_mail = { :order_mailer => { :cancel_email => { :order_summary_canceled => 'Order Summary [CANCELED]' } } } - I18n.backend.store_translations :en, en_confirm_mail - I18n.backend.store_translations :en, en_cancel_mail - I18n.locale = :en - end + context "displays unit costs from line item" do + # Regression test for #2772 - context "confirm_email" do - specify do - confirmation_email = Spree::OrderMailer.confirm_email(order) - confirmation_email.body.should include("Dear Customer,") - end - end + # Tests mailer view spree/order_mailer/confirm_email.text.erb + specify do + confirmation_email = Spree::OrderMailer.confirm_email(order) + expect(confirmation_email).to have_body_text("4.99") + expect(confirmation_email).to_not have_body_text("5.00") + end - context "cancel_email" do - specify do - cancel_email = Spree::OrderMailer.cancel_email(order) - cancel_email.body.should include("Order Summary [CANCELED]") - end - end + # Tests mailer view spree/order_mailer/cancel_email.text.erb + specify do + cancel_email = Spree::OrderMailer.cancel_email(order) + expect(cancel_email).to have_body_text("4.99") + expect(cancel_email).to_not have_body_text("5.00") end + end + + context "emails must be translatable" do context "pt-BR locale" do before do - pt_br_confirm_mail = { :order_mailer => { :confirm_email => { :dear_customer => 'Caro Cliente,' } } } - pt_br_cancel_mail = { :order_mailer => { :cancel_email => { :order_summary_canceled => 'Resumo da Pedido [CANCELADA]' } } } + I18n.enforce_available_locales = false + pt_br_confirm_mail = { :spree => { :order_mailer => { :confirm_email => { :dear_customer => 'Caro Cliente,' } } } } + pt_br_cancel_mail = { :spree => { :order_mailer => { :cancel_email => { :order_summary_canceled => 'Resumo da Pedido [CANCELADA]' } } } } I18n.backend.store_translations :'pt-BR', pt_br_confirm_mail I18n.backend.store_translations :'pt-BR', pt_br_cancel_mail I18n.locale = :'pt-BR' @@ -80,21 +92,31 @@ after do I18n.locale = I18n.default_locale + I18n.enforce_available_locales = true end context "confirm_email" do specify do confirmation_email = Spree::OrderMailer.confirm_email(order) - confirmation_email.body.should include("Caro Cliente,") + expect(confirmation_email).to have_body_text("Caro Cliente,") end end context "cancel_email" do specify do cancel_email = Spree::OrderMailer.cancel_email(order) - cancel_email.body.should include("Resumo da Pedido [CANCELADA]") + expect(cancel_email).to have_body_text("Resumo da Pedido [CANCELADA]") end end end end + + context "with preference :send_core_emails set to false" do + it "sends no email" do + Spree::Config.set(:send_core_emails, false) + message = Spree::OrderMailer.confirm_email(order) + expect(message.body).to be_blank + end + end + end diff --git a/core/spec/mailers/reimbursement_mailer_spec.rb b/core/spec/mailers/reimbursement_mailer_spec.rb new file mode 100644 index 00000000000..063160ad738 --- /dev/null +++ b/core/spec/mailers/reimbursement_mailer_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' +require 'email_spec' + +describe Spree::ReimbursementMailer, :type => :mailer do + include EmailSpec::Helpers + include EmailSpec::Matchers + + let(:reimbursement) { create(:reimbursement) } + + context ":from not set explicitly" do + it "falls back to spree config" do + message = Spree::ReimbursementMailer.reimbursement_email(reimbursement) + expect(message.from).to eq [Spree::Config[:mails_from]] + end + end + + it "accepts a reimbursement id as an alternative to a Reimbursement object" do + expect(Spree::Reimbursement).to receive(:find).with(reimbursement.id).and_return(reimbursement) + + expect { + reimbursement_email = Spree::ReimbursementMailer.reimbursement_email(reimbursement.id) + }.not_to raise_error + end + + context "emails must be translatable" do + context "reimbursement_email" do + context "pt-BR locale" do + before do + I18n.enforce_available_locales = false + pt_br_shipped_email = { :spree => { :reimbursement_mailer => { :reimbursement_email => { :dear_customer => 'Caro Cliente,' } } } } + I18n.backend.store_translations :'pt-BR', pt_br_shipped_email + I18n.locale = :'pt-BR' + end + + after do + I18n.locale = I18n.default_locale + I18n.enforce_available_locales = true + end + + specify do + reimbursement_email = Spree::ReimbursementMailer.reimbursement_email(reimbursement) + expect(reimbursement_email.body).to include("Caro Cliente,") + end + end + end + end +end diff --git a/core/spec/mailers/shipment_mailer_spec.rb b/core/spec/mailers/shipment_mailer_spec.rb index a4882e4cfbc..39939347cca 100644 --- a/core/spec/mailers/shipment_mailer_spec.rb +++ b/core/spec/mailers/shipment_mailer_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'email_spec' -describe Spree::ShipmentMailer do +describe Spree::ShipmentMailer, :type => :mailer do include EmailSpec::Helpers include EmailSpec::Matchers @@ -9,47 +9,51 @@ order = stub_model(Spree::Order) product = stub_model(Spree::Product, :name => %Q{The "BEST" product}) variant = stub_model(Spree::Variant, :product => product) - variant.stub(:in_stock? => false) line_item = stub_model(Spree::LineItem, :variant => variant, :order => order, :quantity => 1, :price => 5) shipment = stub_model(Spree::Shipment) - shipment.stub(:line_items => [line_item], :order => order) + allow(shipment).to receive_messages(:line_items => [line_item], :order => order) + allow(shipment).to receive_messages(:tracking_url => "TRACK_ME") shipment end + context ":from not set explicitly" do + it "falls back to spree config" do + message = Spree::ShipmentMailer.shipped_email(shipment) + expect(message.from).to eq([Spree::Config[:mails_from]]) + end + end + # Regression test for #2196 it "doesn't include out of stock in the email body" do shipment_email = Spree::ShipmentMailer.shipped_email(shipment) - shipment_email.body.should_not include(%Q{Out of Stock}) + expect(shipment_email.body).not_to include(%Q{Out of Stock}) + end + + it "shipment_email accepts an shipment id as an alternative to an Shipment object" do + expect(Spree::Shipment).to receive(:find).with(shipment.id).and_return(shipment) + expect { + shipped_email = Spree::ShipmentMailer.shipped_email(shipment.id) + }.not_to raise_error end context "emails must be translatable" do context "shipped_email" do - context "en locale" do - before do - en_shipped_email = { :shipment_mailer => { :shipped_email => { :dear_customer => 'Dear Customer,' } } } - I18n.backend.store_translations :en, en_shipped_email - I18n.locale = :en - end - - specify do - shipped_email = Spree::ShipmentMailer.shipped_email(shipment) - shipped_email.body.should include("Dear Customer,") - end - end context "pt-BR locale" do before do - pt_br_shipped_email = { :shipment_mailer => { :shipped_email => { :dear_customer => 'Caro Cliente,' } } } + I18n.enforce_available_locales = false + pt_br_shipped_email = { :spree => { :shipment_mailer => { :shipped_email => { :dear_customer => 'Caro Cliente,' } } } } I18n.backend.store_translations :'pt-BR', pt_br_shipped_email I18n.locale = :'pt-BR' end after do I18n.locale = I18n.default_locale + I18n.enforce_available_locales = true end specify do shipped_email = Spree::ShipmentMailer.shipped_email(shipment) - shipped_email.body.should include("Caro Cliente,") + expect(shipped_email).to have_body_text("Caro Cliente,") end end end diff --git a/core/spec/mailers/test_mailer_spec.rb b/core/spec/mailers/test_mailer_spec.rb new file mode 100644 index 00000000000..ef5601d44ff --- /dev/null +++ b/core/spec/mailers/test_mailer_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' +require 'email_spec' + +describe Spree::TestMailer, :type => :mailer do + include EmailSpec::Helpers + include EmailSpec::Matchers + + let(:user) { create(:user) } + + context ":from not set explicitly" do + it "falls back to spree config" do + message = Spree::TestMailer.test_email('test@example.com') + expect(message.from).to eq([Spree::Config[:mails_from]]) + end + end + + it "confirm_email accepts a user id as an alternative to a User object" do + expect { + test_email = Spree::TestMailer.test_email('test@example.com') + }.not_to raise_error + end +end diff --git a/core/spec/models/activator_spec.rb b/core/spec/models/activator_spec.rb deleted file mode 100644 index 96a63d6124d..00000000000 --- a/core/spec/models/activator_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Spree::Activator do - - context "register_event_name" do - it "adds the name to event_names" do - Spree::Activator.register_event_name('spree.new_event') - Spree::Activator.event_names.should include('spree.new_event') - end - end - -end diff --git a/core/spec/models/address_spec.rb b/core/spec/models/address_spec.rb deleted file mode 100644 index ee229d6a359..00000000000 --- a/core/spec/models/address_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -require 'spec_helper' - -describe Spree::Address do - describe "clone" do - it "creates a copy of the address with the exception of the id, updated_at and created_at attributes" do - state = create(:state) - original = create(:address, - :address1 => 'address1', - :address2 => 'address2', - :alternative_phone => 'alternative_phone', - :city => 'city', - :country => Spree::Country.first, - :firstname => 'firstname', - :lastname => 'lastname', - :company => 'company', - :phone => 'phone', - :state_id => state.id, - :state_name => state.name, - :zipcode => 'zip_code') - - cloned = original.clone - - cloned.address1.should == original.address1 - cloned.address2.should == original.address2 - cloned.alternative_phone.should == original.alternative_phone - cloned.city.should == original.city - cloned.country_id.should == original.country_id - cloned.firstname.should == original.firstname - cloned.lastname.should == original.lastname - cloned.company.should == original.company - cloned.phone.should == original.phone - cloned.state_id.should == original.state_id - cloned.state_name.should == original.state_name - cloned.zipcode.should == original.zipcode - - cloned.id.should_not == original.id - cloned.created_at.should_not == original.created_at - cloned.updated_at.should_not == original.updated_at - end - end - - context "validation" do - before do - reset_spree_preferences do |config| - config.address_requires_state = true - end - end - - let(:country) { mock_model(Spree::Country, :states => [state], :states_required => true) } - let(:state) { stub_model(Spree::State, :name => 'maryland', :abbr => 'md') } - let(:address) { FactoryGirl.build(:address, :country => country) } - - before do - country.states.stub :find_all_by_name_or_abbr => [state] - end - - it "state_name is not nil and country does not have any states" do - address.state = nil - address.state_name = 'alabama' - address.should be_valid - end - - it "errors when state_name is nil" do - address.state_name = nil - address.state = nil - address.should_not be_valid - end - - it "full state name is in state_name and country does contain that state" do - address.state_name = 'alabama' - # called by state_validate to set up state_id. - # Perhaps this should be a before_validation instead? - address.should be_valid - address.state.should_not be_nil - address.state_name.should be_nil - end - - it "state abbr is in state_name and country does contain that state" do - address.state_name = state.abbr - address.should be_valid - address.state_id.should_not be_nil - address.state_name.should be_nil - end - - it "state is entered but country does not contain that state" do - address.state = state - address.country = stub_model(Spree::Country) - address.valid? - address.errors["state"].should == ['is invalid'] - end - - it "both state and state_name are entered but country does not contain the state" do - address.state = state - address.state_name = 'maryland' - address.country = stub_model(Spree::Country) - address.should be_valid - address.state_id.should be_nil - end - - it "both state and state_name are entered and country does contain the state" do - address.state = state - address.state_name = 'maryland' - address.should be_valid - address.state_name.should be_nil - end - - it "address_requires_state preference is false" do - Spree::Config.set :address_requires_state => false - address.state = nil - address.state_name = nil - address.should be_valid - end - - end - - context ".default" do - before do - @default_country_id = Spree::Config[:default_country_id] - new_country = create(:country) - Spree::Config[:default_country_id] = new_country.id - end - - after do - Spree::Config[:default_country_id] = @default_country_id - end - it "sets up a new record with Spree::Config[:default_country_id]" do - Spree::Address.default.country.should == Spree::Country.find(Spree::Config[:default_country_id]) - end - - # Regression test for #1142 - it "uses the first available country if :default_country_id is set to an invalid value" do - Spree::Config[:default_country_id] = "0" - Spree::Address.default.country.should == Spree::Country.first - end - end - - context '#full_name' do - context 'both first and last names are present' do - let(:address) { stub_model(Spree::Address, :firstname => 'Michael', :lastname => 'Jackson') } - specify { address.full_name.should == 'Michael Jackson' } - end - - context 'first name is blank' do - let(:address) { stub_model(Spree::Address, :firstname => nil, :lastname => 'Jackson') } - specify { address.full_name.should == 'Jackson' } - end - - context 'last name is blank' do - let(:address) { stub_model(Spree::Address, :firstname => 'Michael', :lastname => nil) } - specify { address.full_name.should == 'Michael' } - end - - context 'both first and last names are blank' do - let(:address) { stub_model(Spree::Address, :firstname => nil, :lastname => nil) } - specify { address.full_name.should == '' } - end - - end - - context '#state_text' do - context 'state is blank' do - let(:address) { stub_model(Spree::Address, :state => nil, :state_name => 'virginia') } - specify { address.state_text.should == 'virginia' } - end - - context 'both name and abbr is present' do - let(:state) { stub_model(Spree::State, :name => 'virginia', :abbr => 'va') } - let(:address) { stub_model(Spree::Address, :state => state) } - specify { address.state_text.should == 'va' } - end - - context 'only name is present' do - let(:state) { stub_model(Spree::State, :name => 'virginia', :abbr => nil) } - let(:address) { stub_model(Spree::Address, :state => state) } - specify { address.state_text.should == 'virginia' } - end - - end -end diff --git a/core/spec/models/adjustment_spec.rb b/core/spec/models/adjustment_spec.rb deleted file mode 100644 index de67186751c..00000000000 --- a/core/spec/models/adjustment_spec.rb +++ /dev/null @@ -1,150 +0,0 @@ -# encoding: utf-8 -# - -require 'spec_helper' - -describe Spree::Adjustment do - - let(:order) { mock_model(Spree::Order, :update! => nil) } - let(:adjustment) { Spree::Adjustment.new } - - context "#update!" do - context "when originator present" do - let(:originator) { mock("originator", :update_adjustment => nil) } - before do - originator.stub :update_amount => true - adjustment.stub :originator => originator - end - it "should do nothing when locked" do - adjustment.locked = true - originator.should_not_receive(:update_adjustment) - adjustment.update! - end - it "should set the eligibility" do - adjustment.should_receive(:set_eligibility) - adjustment.update! - end - it "should ask the originator to update_adjustment" do - originator.should_receive(:update_adjustment) - adjustment.update! - end - end - it "should do nothing when originator is nil" do - adjustment.stub :originator => nil - adjustment.should_not_receive(:amount=) - adjustment.update! - end - end - - context "#eligible? after #set_eligibility" do - context "when amount is 0" do - before { adjustment.amount = 0 } - it "should be eligible if mandatory?" do - adjustment.mandatory = true - adjustment.set_eligibility - adjustment.should be_eligible - end - it "should not be eligible unless mandatory?" do - adjustment.mandatory = false - adjustment.set_eligibility - adjustment.should_not be_eligible - end - end - context "when amount is greater than 0" do - before { adjustment.amount = 25.00 } - it "should be eligible if mandatory?" do - adjustment.mandatory = true - adjustment.set_eligibility - adjustment.should be_eligible - end - it "should be eligible if not mandatory and eligible for the originator" do - adjustment.mandatory = false - adjustment.stub(:eligible_for_originator? => true) - adjustment.set_eligibility - adjustment.should be_eligible - end - it "should not be eligible if not mandatory not eligible for the originator" do - adjustment.mandatory = false - adjustment.stub(:eligible_for_originator? => false) - adjustment.set_eligibility - adjustment.should_not be_eligible - end - end - end - - context "#save" do - it "should call order#update!" do - adjustment = Spree::Adjustment.new({:adjustable => order, :amount => 10, :label => "Foo"}, :without_protection => true) - order.should_receive(:update!) - adjustment.save - end - end - - - context "#eligible_for_originator?" do - context "with no originator" do - specify { adjustment.should be_eligible_for_originator } - end - context "with originator that doesn't have 'eligible?'" do - before { adjustment.originator = mock_model(Spree::TaxRate) } - specify { adjustment.should be_eligible_for_originator } - end - context "with originator that has 'eligible?'" do - let(:originator) { Spree::TaxRate.new } - before { adjustment.originator = originator } - context "and originator is eligible for order" do - before { originator.stub(:eligible? => true) } - specify { adjustment.should be_eligible_for_originator } - end - context "and originator is not eligible for order" do - before { originator.stub(:eligible? => false) } - specify { adjustment.should_not be_eligible_for_originator } - end - end - end - - context "#display_amount" do - before { adjustment.amount = 10.55 } - - context "with display_currency set to true" do - before { Spree::Config[:display_currency] = true } - - it "shows the currency" do - adjustment.display_amount.should == "$10.55 USD" - end - end - - context "with display_currency set to false" do - before { Spree::Config[:display_currency] = false } - - it "does not include the currency" do - adjustment.display_amount.should == "$10.55" - end - end - - context "with currency set to JPY" do - context "when adjustable is set to an order" do - before do - order.stub(:currency) { 'JPY' } - adjustment.adjustable = order - end - - it "displays in JPY" do - adjustment.display_amount.should == "¥11" - end - end - - context "when adjustable is nil" do - it "displays in the default currency" do - adjustment.display_amount.should == "$10.55" - end - end - end - end - - context '#currency' do - it 'returns the globally configured currency' do - adjustment.currency.should == 'USD' - end - end -end diff --git a/core/spec/models/app_configuration_spec.rb b/core/spec/models/app_configuration_spec.rb deleted file mode 100644 index bca2f74a66b..00000000000 --- a/core/spec/models/app_configuration_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Spree::AppConfiguration do - - let (:prefs) { Rails.application.config.spree.preferences } - - it "should be available from the environment" do - prefs.site_name = "TEST SITE NAME" - prefs.site_name.should eq "TEST SITE NAME" - end - - it "should be available as Spree::Config for legacy access" do - Spree::Config.site_name = "Spree::Config TEST SITE NAME" - Spree::Config.site_name.should eq "Spree::Config TEST SITE NAME" - end - - it "uses base searcher class by default" do - prefs.searcher_class = nil - prefs.searcher_class.should eq Spree::Core::Search::Base - end - -end - diff --git a/core/spec/models/calculator/default_tax_spec.rb b/core/spec/models/calculator/default_tax_spec.rb deleted file mode 100644 index da0ec7dfcaf..00000000000 --- a/core/spec/models/calculator/default_tax_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -require 'spec_helper' - -describe Spree::Calculator::DefaultTax do - let!(:tax_category) { create(:tax_category, :tax_rates => []) } - let!(:rate) { mock_model(Spree::TaxRate, :tax_category => tax_category, :amount => 0.05) } - let!(:calculator) { Spree::Calculator::DefaultTax.new({:calculable => rate}, :without_protection => true) } - let!(:order) { create(:order) } - let!(:product_1) { create(:product) } - let!(:product_2) { create(:product) } - let!(:line_item_1) { create(:line_item, :product => product_1, :price => 10, :quantity => 3) } - let!(:line_item_2) { create(:line_item, :product => product_2, :price => 5, :quantity => 1) } - - context "#compute" do - context "when given an order" do - before do - order.stub :line_items => [line_item_1, line_item_2] - end - - context "when no line items match the tax category" do - before do - product_1.tax_category = nil - product_2.tax_category = nil - end - - it "should be 0" do - calculator.compute(order).should == 0 - end - end - - context "when one item matches the tax category" do - before do - product_1.tax_category = tax_category - product_2.tax_category = nil - end - - it "should be equal to the item total * rate" do - calculator.compute(order).should == 1.5 - end - - context "correctly rounds to within two decimal places" do - before do - line_item_1.price = 10.333 - line_item_1.quantity = 1 - end - - specify do - # Amount is 0.51665, which will be rounded to... - calculator.compute(order).should == 0.52 - end - - end - end - - - context "when more than one item matches the tax category" do - it "should be equal to the sum of the item totals * rate" do - calculator.compute(order).should == 1.75 - end - end - end - - context "when given a line item" do - context "when the variant matches the tax category" do - it "should be equal to the item total * rate" do - calculator.compute(line_item_1).should == 1.43 - end - end - - context "when the variant does not match the tax category" do - before do - line_item_2.product.tax_category = nil - end - - it "should be 0" do - calculator.compute(line_item_2).should == 0 - end - end - end - end -end diff --git a/core/spec/models/calculator/flat_percent_item_total_spec.rb b/core/spec/models/calculator/flat_percent_item_total_spec.rb deleted file mode 100644 index c8f516310bb..00000000000 --- a/core/spec/models/calculator/flat_percent_item_total_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe Spree::Calculator::FlatPercentItemTotal do - let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new } - let(:order) { mock_model Spree::Order, :line_items => [mock_model(Spree::LineItem, :amount => 10), mock_model(Spree::LineItem, :amount => 20)] } - - before { calculator.stub :preferred_flat_percent => 10 } - - context "compute" do - it "should compute amount correctly" do - calculator.compute(order).should == 3.0 - end - - it "should round result correctly" do - order.stub :line_items => [mock_model(Spree::LineItem, :amount => 10.56), mock_model(Spree::LineItem, :amount => 20.49)] - calculator.compute(order).should == 3.11 - - order.stub :line_items => [mock_model(Spree::LineItem, :amount => 10.56), mock_model(Spree::LineItem, :amount => 20.48)] - calculator.compute(order).should == 3.10 - end - end -end diff --git a/core/spec/models/calculator/flexi_rate_spec.rb b/core/spec/models/calculator/flexi_rate_spec.rb deleted file mode 100644 index cc7b342a8bb..00000000000 --- a/core/spec/models/calculator/flexi_rate_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -describe Spree::Calculator::FlexiRate do - let(:calculator) { Spree::Calculator::FlexiRate.new } - let(:order) { mock_model Spree::Order, :line_items => [mock_model(Spree::LineItem, :amount => 10, :quantity => 4), mock_model(Spree::LineItem, :amount => 20, :quantity => 6)] } - - context "compute" do - it "should compute amount correctly when all fees are 0" do - calculator.compute(order).round(2).should == 0.0 - end - - it "should compute amount correctly when first_item has a value" do - calculator.stub :preferred_first_item => 1.0 - calculator.compute(order).round(2).should == 1.0 - end - - it "should compute amount correctly when additional_items has a value" do - calculator.stub :preferred_additional_item => 1.0 - calculator.compute(order).round(2).should == 9.0 - end - - it "should compute amount correctly when additional_items and first_item have values" do - calculator.stub :preferred_first_item => 5.0, :preferred_additional_item => 1.0 - calculator.compute(order).round(2).should == 14.0 - end - - it "should compute amount correctly when additional_items and first_item have values AND max items has value" do - calculator.stub :preferred_first_item => 5.0, :preferred_additional_item => 1.0, :preferred_max_items => 3 - calculator.compute(order).round(2).should == 26.0 - end - - it "should allow creation of new object with all the attributes" do - Spree::Calculator::FlexiRate.new(:preferred_first_item => 1, :preferred_additional_item => 1, :preferred_max_items => 1) - end - end -end diff --git a/core/spec/models/calculator/per_item_spec.rb b/core/spec/models/calculator/per_item_spec.rb deleted file mode 100644 index 211dc0e7b79..00000000000 --- a/core/spec/models/calculator/per_item_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe Spree::Calculator::PerItem do - # Like an order object, but not quite... - let!(:product1) { double("Product") } - let!(:product2) { double("Product") } - let!(:line_items) { [double("LineItem", :quantity => 5, :product => product1), double("LineItem", :quantity => 3, :product => product2)] } - let!(:object) { double("Order", :line_items => line_items) } - - let!(:shipping_calculable) { double("Calculable") } - let!(:promotion_calculable) { double("Calculable", :promotion => promotion) } - - let!(:promotion) { double("Promotion", :rules => [double("Rule", :products => [product1])]) } - - let!(:calculator) { Spree::Calculator::PerItem.new(:preferred_amount => 10) } - - # regression test for #1414 - it "correctly calculates per item shipping" do - calculator.stub(:calculable => shipping_calculable) - calculator.compute(object).to_f.should == 80 # 5 x 10 + 3 x 10 - end - - it "correctly calculates per item promotion" do - calculator.stub(:calculable => promotion_calculable) - calculator.compute(object).to_f.should == 50 # 5 x 10 - end - - it "returns 0 when no object passed" do - calculator.stub(:calculable => shipping_calculable) - calculator.compute.should == 0 - end - - it "returns 0 when no object passed" do - calculator.stub(:calculable => promotion_calculable) - calculator.compute.should == 0 - end - -end diff --git a/core/spec/models/calculator/price_sack_spec.rb b/core/spec/models/calculator/price_sack_spec.rb deleted file mode 100644 index 21d6cabe13f..00000000000 --- a/core/spec/models/calculator/price_sack_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -describe Spree::Calculator::PriceSack do - let(:calculator) do - calculator = Spree::Calculator::PriceSack.new - calculator.preferred_minimal_amount = 5 - calculator.preferred_normal_amount = 10 - calculator.preferred_discount_amount = 1 - calculator - end - - let(:order) { stub_model(Spree::Order) } - let(:shipment) { stub_model(Spree::Shipment) } - - # Regression test for #714 and #739 - it "computes with an order object" do - calculator.compute(order) - end - - # Regression test for #1156 - it "computes with a shipment object" do - calculator.compute(shipment) - end - - # Regression test for #2055 - it "computes the correct amount" do - calculator.compute(2).should == calculator.preferred_normal_amount - calculator.compute(6).should == calculator.preferred_discount_amount - end -end diff --git a/core/spec/models/configuration_spec.rb b/core/spec/models/configuration_spec.rb deleted file mode 100644 index 7ebc496cda3..00000000000 --- a/core/spec/models/configuration_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Spree::Configuration do - -end diff --git a/core/spec/models/country_spec.rb b/core/spec/models/country_spec.rb deleted file mode 100644 index c147f236b6f..00000000000 --- a/core/spec/models/country_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe Spree::Country do - it "can find all countries group by states required" do - country_states_required= Spree::Country.create({:name => "Canada", :iso_name => "CAN", :states_required => true}) - country_states_not_required= Spree::Country.create({:name => "France", :iso_name => "FR", :states_required => false}) - states_required = Spree::Country.states_required_by_country_id - states_required[country_states_required.id.to_s].should be_true - states_required[country_states_not_required.id.to_s].should be_false - end - - it "returns that the states are required for an invalid country" do - Spree::Country.states_required_by_country_id['i do not exit'].should be_true - end -end diff --git a/core/spec/models/credit_card_spec.rb b/core/spec/models/credit_card_spec.rb deleted file mode 100644 index 01517a48e46..00000000000 --- a/core/spec/models/credit_card_spec.rb +++ /dev/null @@ -1,216 +0,0 @@ -require 'spec_helper' - -describe Spree::CreditCard do - - let(:valid_credit_card_attributes) { {:number => '4111111111111111', :verification_value => '123', :month => 12, :year => 2014} } - - def stub_rails_env(environment) - Rails.stub(:env => ActiveSupport::StringInquirer.new(environment)) - end - - let(:credit_card) { Spree::CreditCard.new } - - before(:each) do - - @order = create(:order) - @payment = Spree::Payment.create({:amount => 100, :order => @order}, :without_protection => true) - - @success_response = mock('gateway_response', :success? => true, :authorization => '123', :avs_result => {'code' => 'avs-code'}) - @fail_response = mock('gateway_response', :success? => false) - - @payment_gateway = mock_model(Spree::PaymentMethod, - :payment_profiles_supported? => true, - :authorize => @success_response, - :purchase => @success_response, - :capture => @success_response, - :void => @success_response, - :credit => @success_response, - :environment => 'test' - ) - - @payment.stub :payment_method => @payment_gateway - end - - context "#can_capture?" do - it "should be true if payment state is pending" do - payment = mock_model(Spree::Payment, :state => 'pending', :created_at => Time.now) - credit_card.can_capture?(payment).should be_true - end - end - - context "when transaction is more than 12 hours old" do - let(:payment) { mock_model(Spree::Payment, :state => "completed", - :created_at => Time.now - 14.hours, - :amount => 99.00, - :credit_allowed => 100.00, - :order => mock_model(Spree::Order, :payment_state => 'credit_owed')) } - - context "#can_credit?" do - - it "should be true when payment state is 'completed' and order payment_state is 'credit_owed' and credit_allowed is greater than amount" do - credit_card.can_credit?(payment).should be_true - end - - it "should be false when order payment_state is not 'credit_owed'" do - payment.order.stub(:payment_state => 'paid') - credit_card.can_credit?(payment).should be_false - end - - it "should be false when credit_allowed is zero" do - payment.stub(:credit_allowed => 0) - credit_card.can_credit?(payment).should be_false - end - - (PAYMENT_STATES - ['completed']).each do |state| - it "should be false if payment state is #{state}" do - payment.stub :state => state - credit_card.can_credit?(payment).should be_false - end - end - - end - - context "#can_void?" do - (PAYMENT_STATES - ['void']).each do |state| - it "should be true if payment state is #{state}" do - payment.stub :state => state - payment.stub :void? => false - credit_card.can_void?(payment).should be_true - end - end - - it "should be valse if payment state is void" do - payment.stub :state => 'void' - credit_card.can_void?(payment).should be_false - end - end - end - - context "when transaction is less than 12 hours old" do - let(:payment) { mock_model(Spree::Payment, :state => 'completed') } - - context "#can_void?" do - (PAYMENT_STATES - ['void']).each do |state| - it "should be true if payment state is #{state}" do - payment.stub :state => state - credit_card.can_void?(payment).should be_true - end - end - - it "should be false if payment state is void" do - payment.stub :state => 'void' - credit_card.can_void?(payment).should be_false - end - - end - end - - context "#valid?" do - it "should validate presence of number" do - credit_card.attributes = valid_credit_card_attributes.except(:number) - credit_card.should_not be_valid - credit_card.errors[:number].should == ["can't be blank"] - end - - it "should validate presence of security code" do - credit_card.attributes = valid_credit_card_attributes.except(:verification_value) - credit_card.should_not be_valid - credit_card.errors[:verification_value].should == ["can't be blank"] - end - - it "should only validate on create" do - credit_card.attributes = valid_credit_card_attributes - credit_card.save - credit_card.should be_valid - end - end - - context "#save" do - before do - credit_card.attributes = valid_credit_card_attributes - credit_card.save! - end - - let!(:persisted_card) { Spree::CreditCard.find(credit_card.id) } - - it "should not actually store the number" do - persisted_card.number.should be_blank - end - - it "should not actually store the security code" do - persisted_card.verification_value.should be_blank - end - end - - context "#spree_cc_type" do - before do - credit_card.attributes = valid_credit_card_attributes - end - - context "in development mode" do - before do - stub_rails_env("production") - end - - it "should return visa" do - credit_card.save - credit_card.spree_cc_type.should == "visa" - end - end - - context "in production mode" do - before do - stub_rails_env("production") - end - - it "should return the actual cc_type for a valid number" do - credit_card.number = "378282246310005" - credit_card.save - credit_card.spree_cc_type.should == "american_express" - end - end - end - - context "#set_card_type" do - before :each do - stub_rails_env("production") - credit_card.attributes = valid_credit_card_attributes - end - - it "stores the credit card type after validation" do - credit_card.number = "6011000990139424" - credit_card.save - credit_card.spree_cc_type.should == "discover" - end - - it "does not overwrite the credit card type when loaded and saved" do - credit_card.number = "5105105105105100" - credit_card.save - credit_card.number = "XXXXXXXXXXXX5100" - credit_card.save - credit_card.spree_cc_type.should == "master" - end - end - - context "#number=" do - it "should strip non-numeric characters from card input" do - credit_card.number = "6011000990139424" - credit_card.number.should == "6011000990139424" - - credit_card.number = " 6011-0009-9013-9424 " - credit_card.number.should == "6011000990139424" - end - - it "should not raise an exception on non-string input" do - credit_card.number = Hash.new - credit_card.number.should be_nil - end - end - - context "#associations" do - it "should be able to access its payments" do - lambda { credit_card.payments.all }.should_not raise_error ActiveRecord::StatementInvalid - end - end -end - diff --git a/core/spec/models/image_spec.rb b/core/spec/models/image_spec.rb deleted file mode 100644 index 886dcbaf72c..00000000000 --- a/core/spec/models/image_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Spree::Image do - -end diff --git a/core/spec/models/inventory_unit_spec.rb b/core/spec/models/inventory_unit_spec.rb deleted file mode 100644 index a6a08ece3d4..00000000000 --- a/core/spec/models/inventory_unit_spec.rb +++ /dev/null @@ -1,315 +0,0 @@ -require 'spec_helper' - -describe Spree::InventoryUnit do - before(:each) do - reset_spree_preferences - end - - let(:variant) { mock_model(Spree::Variant, :on_hand => 95, :on_demand => false) } - let(:line_item) { mock_model(Spree::LineItem, :variant => variant, :quantity => 5) } - let(:order) { mock_model(Spree::Order, :line_items => [line_item], :inventory_units => [], :shipments => mock('shipments'), :completed? => true) } - - context "#assign_opening_inventory" do - context "when order is complete" do - - it "should increase inventory" do - Spree::InventoryUnit.should_receive(:increase).with(order, variant, 5).and_return([]) - Spree::InventoryUnit.assign_opening_inventory(order) - end - - end - - context "when order is not complete" do - before { order.stub(:completed?).and_return(false) } - - it "should not do anything" do - Spree::InventoryUnit.should_not_receive(:increase) - Spree::InventoryUnit.assign_opening_inventory(order).should == [] - end - - end - end - - context "#increase" do - context "when :track_inventory_levels is true" do - before do - Spree::Config.set :track_inventory_levels => true - Spree::InventoryUnit.stub(:create_units) - end - - it "should decrement count_on_hand" do - variant.should_receive(:decrement!).with(:count_on_hand, 5) - Spree::InventoryUnit.increase(order, variant, 5) - end - - end - - context "when :track_inventory_levels is false" do - before do - Spree::Config.set :track_inventory_levels => false - Spree::InventoryUnit.stub(:create_units) - end - - it "should decrement count_on_hand" do - variant.should_not_receive(:decrement!) - Spree::InventoryUnit.increase(order, variant, 5) - end - - end - - context "when on_demand is true" do - before do - variant.stub(:on_demand).and_return(true) - Spree::InventoryUnit.stub(:create_units) - end - - it "should decrement count_on_hand" do - variant.should_not_receive(:decrement!) - Spree::InventoryUnit.increase(order, variant, 5) - end - - end - - context "when :create_inventory_units is true" do - before do - Spree::Config.set :create_inventory_units => true - variant.stub(:decrement!) - end - - it "should create units" do - Spree::InventoryUnit.should_receive(:create_units) - Spree::InventoryUnit.increase(order, variant, 5) - end - - end - - context "when :create_inventory_units is false" do - before do - Spree::Config.set :create_inventory_units => false - variant.stub(:decrement!) - end - - it "should not create units" do - Spree::InventoryUnit.should_not_receive(:create_units) - Spree::InventoryUnit.increase(order, variant, 5) - end - - end - - end - - context "#decrease" do - context "when :track_inventory_levels is true" do - before do - Spree::Config.set :track_inventory_levels => true - Spree::InventoryUnit.stub(:destroy_units) - end - - it "should decrement count_on_hand" do - variant.should_receive(:increment!).with(:count_on_hand, 5) - Spree::InventoryUnit.decrease(order, variant, 5) - end - - end - - context "when :track_inventory_levels is false" do - before do - Spree::Config.set :track_inventory_levels => false - Spree::InventoryUnit.stub(:destroy_units) - end - - it "should decrement count_on_hand" do - variant.should_not_receive(:increment!) - Spree::InventoryUnit.decrease(order, variant, 5) - end - - end - - context "when on_demand is true" do - before do - variant.stub(:on_demand).and_return(true) - Spree::InventoryUnit.stub(:destroy_units) - end - - it "should decrement count_on_hand" do - variant.should_not_receive(:increment!) - Spree::InventoryUnit.decrease(order, variant, 5) - end - - end - - context "when :create_inventory_units is true" do - before do - Spree::Config.set :create_inventory_units => true - variant.stub(:increment!) - end - - it "should destroy units" do - Spree::InventoryUnit.should_receive(:destroy_units).with(order, variant, 5) - Spree::InventoryUnit.decrease(order, variant, 5) - end - - end - - context "when :create_inventory_units is false" do - before do - Spree::Config.set :create_inventory_units => false - variant.stub(:increment!) - end - - it "should destroy units" do - Spree::InventoryUnit.should_not_receive(:destroy_units) - Spree::InventoryUnit.decrease(order, variant, 5) - end - - end - - end - - context "#determine_backorder" do - context "when :track_inventory_levels is true" do - before { Spree::Config.set :create_inventory_units => true } - - context "and all units are in stock" do - it "should return zero back orders" do - Spree::InventoryUnit.determine_backorder(order, variant, 5).should == 0 - end - end - - context "and partial units are in stock" do - before { variant.stub(:on_hand).and_return(2) } - - it "should return correct back order amount" do - Spree::InventoryUnit.determine_backorder(order, variant, 5).should == 3 - end - end - - context "and zero units are in stock" do - before { variant.stub(:on_hand).and_return(0) } - - it "should return correct back order amount" do - Spree::InventoryUnit.determine_backorder(order, variant, 5).should == 5 - end - end - - context "and less than zero units are in stock" do - before { variant.stub(:on_hand).and_return(-9) } - - it "should return entire amount as back order" do - Spree::InventoryUnit.determine_backorder(order, variant, 5).should == 5 - end - end - end - - context "when :track_inventory_levels is false" do - before { Spree::Config.set :track_inventory_levels => false } - - it "should return zero back orders" do - variant.stub(:on_hand).and_return(nil) - Spree::InventoryUnit.determine_backorder(order, variant, 5).should == 0 - end - end - - context "when :on_demand is true" do - before { variant.stub(:on_demand).and_return(true) } - - it "should return zero back orders" do - variant.stub(:on_hand).and_return(nil) - Spree::InventoryUnit.determine_backorder(order, variant, 5).should == 0 - end - end - - end - - context "#create_units" do - let(:shipment) { mock_model(Spree::Shipment) } - before { order.shipments.stub :detect => shipment } - - context "when :allow_backorders is true" do - before { Spree::Config.set :allow_backorders => true } - - it "should create both sold and backordered units" do - order.inventory_units.should_receive(:create).with({:variant => variant, :state => "sold", :shipment => shipment}, :without_protection => true).exactly(2).times - order.inventory_units.should_receive(:create).with({:variant => variant, :state => "backordered", :shipment => shipment}, :without_protection => true).exactly(3).times - Spree::InventoryUnit.create_units(order, variant, 2, 3) - end - - end - - context "when :allow_backorders is false" do - before { Spree::Config.set :allow_backorders => false } - - it "should create sold items" do - order.inventory_units.should_receive(:create).with({:variant => variant, :state => "sold", :shipment => shipment}, :without_protection => true).exactly(2).times - Spree::InventoryUnit.create_units(order, variant, 2, 0) - end - - end - - end - - context "#destroy_units" do - before { order.stub(:inventory_units => [mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => "sold")]) } - - it "should call destroy correct number of units" do - order.inventory_units.each { |unit| unit.should_receive(:destroy) } - Spree::InventoryUnit.destroy_units(order, variant, 1) - end - - context "when inventory_units contains backorders" do - before { order.stub(:inventory_units => [ mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'backordered'), - mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'sold'), - mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'backordered') ]) } - - it "should destroy backordered units first" do - order.inventory_units[0].should_receive(:destroy) - order.inventory_units[1].should_not_receive(:destroy) - order.inventory_units[2].should_receive(:destroy) - Spree::InventoryUnit.destroy_units(order, variant, 2) - end - end - - context "when inventory_units contains sold and shipped" do - before { order.stub(:inventory_units => [ mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'shipped'), - mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'sold') ]) } - # Regression test for #1652 - it "should not destroy shipped" do - order.inventory_units[0].should_not_receive(:destroy) - order.inventory_units[1].should_receive(:destroy) - Spree::InventoryUnit.destroy_units(order, variant, 1) - end - end - end - - context "return!" do - let(:inventory_unit) { Spree::InventoryUnit.create({:state => "shipped", :variant => mock_model(Spree::Variant, :on_hand => 95, :on_demand => false)}, :without_protection => true) } - - it "should update on_hand for variant" do - inventory_unit.variant.should_receive(:on_hand=).with(96) - inventory_unit.variant.should_receive(:save) - inventory_unit.return! - end - - # Regression test for #2074 - context "with inventory tracking disabled" do - before { Spree::Config[:track_inventory_levels] = false } - - it "does not update on_hand for variant" do - inventory_unit.variant.should_not_receive(:on_hand=).with(96) - inventory_unit.variant.should_not_receive(:save) - inventory_unit.return! - end - end - context "when on_demand is true" do - before { inventory_unit.variant.stub(:on_demand).and_return(true) } - - it "does not update on_hand for variant" do - inventory_unit.variant.should_not_receive(:on_hand=).with(96) - inventory_unit.variant.should_not_receive(:save) - inventory_unit.return! - end - end - - end -end - diff --git a/core/spec/models/line_item_spec.rb b/core/spec/models/line_item_spec.rb deleted file mode 100644 index fa9c377fa0d..00000000000 --- a/core/spec/models/line_item_spec.rb +++ /dev/null @@ -1,262 +0,0 @@ -require 'spec_helper' - -describe Spree::LineItem do - before(:each) do - reset_spree_preferences - end - - let(:variant) { mock_model(Spree::Variant, :count_on_hand => 95, :price => 9.99) } - let(:line_item) { Spree::LineItem.new(:quantity => 5) } - let(:order) do - shipments = mock(:shipments, :reduce => 0) - mock_model(Spree::Order, :line_items => [line_item], - :inventory_units => [], - :shipments => shipments, - :completed? => true, - :update! => true) - end - - before do - line_item.stub(:order => order, :variant => variant, :new_record? => false) - variant.stub(:currency => "USD") - Spree::Config.set :allow_backorders => true - end - - context '#save' do - it 'should update inventory, totals, and tax' do - Spree::InventoryUnit.stub(:increase) - line_item.should_receive(:update_inventory) - # Regression check for #1481 - order.should_receive(:create_tax_charge!) - order.should_receive(:update!) - line_item.save - end - - context 'when order#completed? is true' do - # We don't care about this method for these tests - before { line_item.stub(:update_order) } - - context 'and line_item is a new record' do - before { line_item.stub(:new_record? => true) } - - it 'should increase inventory' do - Spree::InventoryUnit.stub(:increase) - Spree::InventoryUnit.should_receive(:increase).with(order, variant, 5) - # We don't care about this method for this test - line_item.stub(:update_order) - line_item.save - end - end - - context 'and quantity is increased' do - before { line_item.stub(:changed_attributes => {'quantity' => 5}, :quantity => 6) } - - it 'should increase inventory' do - Spree::InventoryUnit.should_not_receive(:decrease) - Spree::InventoryUnit.should_receive(:increase).with(order, variant, 1) - line_item.save - end - end - - context 'and quantity is decreased' do - before { line_item.stub(:changed_attributes => {'quantity' => 5}, :quantity => 3) } - - it 'should decrease inventory' do - Spree::InventoryUnit.should_not_receive(:increase) - Spree::InventoryUnit.should_receive(:decrease).with(order, variant, 2) - line_item.save - end - end - - context 'and quantity is not changed' do - - it 'should not manager inventory' do - Spree::InventoryUnit.should_not_receive(:increase) - Spree::InventoryUnit.should_not_receive(:decrease) - line_item.save - end - end - end - - context 'when order#completed? is false' do - before do - order.stub(:completed? => false) - # We don't care about this method for this test - line_item.stub(:update_order) - end - - it 'should not manage inventory' do - Spree::InventoryUnit.should_not_receive(:increase) - Spree::InventoryUnit.should_not_receive(:decrease) - line_item.save - end - end - end - - context '#destroy' do - # Regression test for #1481 - it "applies tax adjustments" do - # We don't care about this method for this test - line_item.stub(:remove_inventory) - order.should_receive(:create_tax_charge!) - line_item.destroy - end - - context 'when order.completed? is true' do - it 'should remove inventory' do - # We don't care about this method for this test - line_item.stub(:update_order) - Spree::InventoryUnit.should_receive(:decrease).with(order, variant, 5) - line_item.destroy - end - end - - context 'when order.completed? is false' do - before { order.stub(:completed? => false) } - - it 'should not remove inventory' do - Spree::InventoryUnit.should_not_receive(:decrease) - end - end - - context 'with inventory units' do - let(:inventory_unit) { mock_model(Spree::InventoryUnit, :variant_id => variant.id, :shipped? => false) } - before do - order.stub(:inventory_units => [inventory_unit]) - line_item.stub(:order => order, :variant_id => variant.id) - end - - it 'should allow destroy when no units have shipped' do - # We don't care about this method for this test - line_item.stub(:update_order) - line_item.should_receive(:remove_inventory) - line_item.destroy.should be_true - end - - it 'should not allow destroy when units have shipped' do - inventory_unit.stub(:shipped? => true) - line_item.should_not_receive(:remove_inventory) - line_item.destroy.should be_false - end - end - end - - context '(in)sufficient_stock?' do - context 'when backordering is disabled' do - before { Spree::Config.set :allow_backorders => false } - - it 'should report insufficient stock when variant is out of stock' do - line_item.stub_chain :variant, :on_hand => 0 - line_item.insufficient_stock?.should be_true - line_item.sufficient_stock?.should be_false - end - - it 'should report insufficient stock when variant has less on_hand that line_item quantity' do - line_item.stub_chain :variant, :on_hand => 3 - line_item.insufficient_stock?.should be_true - line_item.sufficient_stock?.should be_false - end - - it 'should report sufficient stock when variant has enough on_hand' do - line_item.stub_chain :variant, :on_hand => 300 - line_item.insufficient_stock?.should be_false - line_item.sufficient_stock?.should be_true - end - - context 'when line item has been saved' do - before { line_item.stub(:new_record? => false) } - - it 'should report sufficient stock when reducing purchased quantity' do - line_item.stub(:changed_attributes => {'quantity' => 6}, :quantity => 5) - line_item.stub_chain :variant, :on_hand => 0 - line_item.insufficient_stock?.should be_false - line_item.sufficient_stock?.should be_true - end - - it 'should report sufficient stock when increasing purchased quantity and variant has enough on_hand' do - line_item.stub(:changed_attributes => {'quantity' => 5}, :quantity => 6) - line_item.stub_chain :variant, :on_hand => 1 - line_item.insufficient_stock?.should be_false - line_item.sufficient_stock?.should be_true - end - - it 'should report insufficient stock when increasing purchased quantity and new units is more than variant on_hand' do - line_item.stub(:changed_attributes => {'quantity' => 5}, :quantity => 7) - line_item.stub_chain :variant, :on_hand => 1 - line_item.insufficient_stock?.should be_true - line_item.sufficient_stock?.should be_false - end - end - end - - context 'when backordering is enabled' do - before { Spree::Config.set :allow_backorders => true } - - it 'should report sufficient stock regardless of on_hand value' do - [-99,0,99].each do |i| - line_item.stub_chain :variant, :on_hand => i - line_item.insufficient_stock?.should be_false - line_item.sufficient_stock?.should be_true - end - end - end - end - - context 'after shipment made' do - before do - shipping_method = mock_model(Spree::ShippingMethod, :calculator => mock(:calculator)) - shipment = Spree::Shipment.new :order => order, :shipping_method => shipping_method - shipment.stub(:state => 'shipped') - shipped_inventory_units = 5.times.map { Spree::InventoryUnit.new({ :variant => line_item.variant, :state => 'shipped' }, :without_protection => true) } - unshipped_inventory_units = 2.times.map { Spree::InventoryUnit.new({ :variant => line_item.variant, :state => 'sold' }, :without_protection => true) } - inventory_units = shipped_inventory_units + unshipped_inventory_units - order.stub(:shipments => [shipment]) - shipment.stub(:inventory_units => inventory_units) - inventory_units.stub(:shipped => shipped_inventory_units) - shipped_inventory_units.stub(:where).with(:variant_id => line_item.variant_id).and_return(shipped_inventory_units) - # We don't care about this method for these test - line_item.stub(:update_order) - end - - it 'should not allow quantity to be adjusted lower than already shipped units' do - line_item.quantity = 4 - line_item.save.should be_false - line_item.errors.size.should == 1 - end - - it "should allow quantity to be adjusted higher than already shipped units" do - line_item.quantity = 6 - line_item.save.should be_true - end - end - - context "destroying" do - # Regression test for #1233 - it "removes related adjustments" do - line_item = create(:line_item) - adjustment = line_item.adjustments.create(:amount => 10, :label => "test") - line_item.destroy - lambda { adjustment.reload }.should raise_error(ActiveRecord::RecordNotFound) - end - end - - describe '.currency' do - it 'returns the globally configured currency' do - line_item.currency == 'USD' - end - end - - describe ".money" do - before { line_item.price = 3.50 } - it "returns a Spree::Money representing the total for this line item" do - line_item.money.to_s.should == "$17.50" - end - end - - describe '.single_money' do - before { line_item.price = 3.50 } - it "returns a Spree::Money representing the price for one variant" do - line_item.single_money.to_s.should == "$3.50" - end - end -end diff --git a/core/spec/models/mail_method_spec.rb b/core/spec/models/mail_method_spec.rb deleted file mode 100644 index 1659d699f8f..00000000000 --- a/core/spec/models/mail_method_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Spree::MailMethod do - context "current" do - it "should return the first active mail method corresponding to the current environment" do - method = Spree::MailMethod.create(:environment => "test") - Spree::MailMethod.current.should == method - end - end - - context "valid?" do - it "should be false when missing an environment value" do - method = Spree::MailMethod.new - method.valid?.should be_false - end - it "should be valid if it has an environment" do - method = Spree::MailMethod.new(:environment => "foo") - method.valid?.should be_true - end - end -end diff --git a/core/spec/models/order/address_spec.rb b/core/spec/models/order/address_spec.rb deleted file mode 100644 index 2efbc20be6d..00000000000 --- a/core/spec/models/order/address_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe Spree::Order do - let(:order) { Spree::Order.new } - - context 'validation' do - context "when @use_billing is populated" do - before do - order.bill_address = stub_model(Spree::Address) - order.ship_address = nil - end - - context "with true" do - before { order.use_billing = true } - - it "clones the bill address to the ship address" do - order.valid? - order.ship_address.should == order.bill_address - end - end - - context "with 'true'" do - before { order.use_billing = 'true' } - - it "clones the bill address to the shipping" do - order.valid? - order.ship_address.should == order.bill_address - end - end - - context "with '1'" do - before { order.use_billing = '1' } - - it "clones the bill address to the shipping" do - order.valid? - order.ship_address.should == order.bill_address - end - end - - context "with something other than a 'truthful' value" do - before { order.use_billing = '0' } - - it "does not clone the bill address to the shipping" do - order.valid? - order.ship_address.should be_nil - end - end - end - end -end diff --git a/core/spec/models/order/adjustments_spec.rb b/core/spec/models/order/adjustments_spec.rb deleted file mode 100644 index 0ed7e00fc47..00000000000 --- a/core/spec/models/order/adjustments_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -require 'spec_helper' -describe Spree::Order do - let(:order) { Spree::Order.new } - - context "clear_adjustments" do - it "should destroy all previous tax adjustments" do - adjustment = stub - adjustment.should_receive :destroy - - order.stub_chain :adjustments, :tax => [adjustment] - order.clear_adjustments! - end - - it "should destroy all price adjustments" do - adjustment = stub - adjustment.should_receive :destroy - - order.stub :price_adjustments => [adjustment] - order.clear_adjustments! - end - end - - context "totaling adjustments" do - let(:adjustment1) { mock_model(Spree::Adjustment, :amount => 5) } - let(:adjustment2) { mock_model(Spree::Adjustment, :amount => 10) } - - context "#ship_total" do - it "should return the correct amount" do - order.stub_chain :adjustments, :shipping => [adjustment1, adjustment2] - order.ship_total.should == 15 - end - end - - context "#tax_total" do - it "should return the correct amount" do - order.stub_chain :adjustments, :tax => [adjustment1, adjustment2] - order.tax_total.should == 15 - end - end - end - - - context "#price_adjustment_totals" do - before { @order = Spree::Order.create! } - - - context "when there are no price adjustments" do - before { @order.stub :price_adjustments => [] } - - it "should return an empty hash" do - @order.price_adjustment_totals.should == {} - end - end - - context "when there are two adjustments with different labels" do - let(:adj1) { mock_model Spree::Adjustment, :amount => 10, :label => "Foo" } - let(:adj2) { mock_model Spree::Adjustment, :amount => 20, :label => "Bar" } - - before do - @order.stub :price_adjustments => [adj1, adj2] - end - - it "should return exactly two totals" do - @order.price_adjustment_totals.size.should == 2 - end - - it "should return the correct totals" do - @order.price_adjustment_totals["Foo"].should == 10 - @order.price_adjustment_totals["Bar"].should == 20 - end - end - - context "when there are two adjustments with one label and a single adjustment with another" do - let(:adj1) { mock_model Spree::Adjustment, :amount => 10, :label => "Foo" } - let(:adj2) { mock_model Spree::Adjustment, :amount => 20, :label => "Bar" } - let(:adj3) { mock_model Spree::Adjustment, :amount => 40, :label => "Bar" } - - before do - @order.stub :price_adjustments => [adj1, adj2, adj3] - end - - it "should return exactly two totals" do - @order.price_adjustment_totals.size.should == 2 - end - it "should return the correct totals" do - @order.price_adjustment_totals["Foo"].should == 10 - @order.price_adjustment_totals["Bar"].should == 60 - end - end - end - - context "#price_adjustments" do - before do - @order = Spree::Order.create! - @order.stub :line_items => [line_item1, line_item2] - end - - let(:line_item1) { create(:line_item, :order => @order) } - let(:line_item2) { create(:line_item, :order => @order) } - - context "when there are no line item adjustments" do - it "should return nothing if line items have no adjustments" do - @order.price_adjustments.should be_empty - end - end - - context "when only one line item has adjustments" do - before do - @adj1 = line_item1.adjustments.create({:amount => 2, :source => line_item1, :label => "VAT 5%"}, :without_protection => true) - @adj2 = line_item1.adjustments.create({:amount => 5, :source => line_item1, :label => "VAT 10%"}, :without_protection => true) - end - - it "should return the adjustments for that line item" do - @order.price_adjustments.should =~ [@adj1, @adj2] - end - end - - context "when more than one line item has adjustments" do - before do - @adj1 = line_item1.adjustments.create({:amount => 2, :source => line_item1, :label => "VAT 5%"}, :without_protection => true) - @adj2 = line_item2.adjustments.create({:amount => 5, :source => line_item2, :label => "VAT 10%"}, :without_protection => true) - end - - it "should return the adjustments for each line item" do - @order.price_adjustments.should == [@adj1, @adj2] - end - end - end -end - diff --git a/core/spec/models/order/callbacks_spec.rb b/core/spec/models/order/callbacks_spec.rb deleted file mode 100644 index f7f676a3257..00000000000 --- a/core/spec/models/order/callbacks_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' - -describe Spree::Order do - let(:order) { stub_model(Spree::Order) } - before do - Spree::Order.define_state_machine! - end - - context "validations" do - context "email validation" do - # Regression test for #1238 - it "o'brien@gmail.com is a valid email address" do - order.state = 'address' - order.email = "o'brien@gmail.com" - order.should be_valid - end - end - end - - context "#save" do - context "when associated with a registered user" do - let(:user) { stub(:user, :email => "test@example.com") } - - before do - order.stub :user => user - end - - it "should assign the email address of the user" do - order.run_callbacks(:create) - order.email.should == user.email - end - end - end - - context "in the cart state" do - it "should not validate email address" do - order.state = "cart" - order.email = nil - order.should be_valid - end - end -end diff --git a/core/spec/models/order/checkout_spec.rb b/core/spec/models/order/checkout_spec.rb deleted file mode 100644 index a18f7ed2213..00000000000 --- a/core/spec/models/order/checkout_spec.rb +++ /dev/null @@ -1,207 +0,0 @@ -require 'spec_helper' - -describe Spree::Order do - let(:order) { Spree::Order.new } - - context "with default state machine" do - it "has the following transitions" do - transitions = [ - { :address => :delivery }, - { :delivery => :payment }, - { :payment => :confirm }, - { :confirm => :complete }, - { :payment => :complete }, - { :delivery => :complete } - ] - transitions.each do |transition| - transition = Spree::Order.find_transition(:from => transition.keys.first, :to => transition.values.first) - transition.should_not be_nil - end - end - - it "does not have a transition from delivery to confirm" do - transition = Spree::Order.find_transition(:from => :delivery, :to => :confirm) - transition.should be_nil - end - - context "#checkout_steps" do - context "when confirmation not required" do - before do - order.stub :confirmation_required? => false - order.stub :payment_required? => true - end - - specify do - order.checkout_steps.should == %w(address delivery payment) - end - end - - context "when confirmation required" do - before do - order.stub :confirmation_required? => true - order.stub :payment_required? => true - end - - specify do - order.checkout_steps.should == %w(address delivery payment confirm) - end - end - - context "when payment not required" do - before { order.stub :payment_required? => false } - specify do - order.checkout_steps.should == %w(address delivery complete) - end - end - - context "when payment required" do - before { order.stub :payment_required? => true } - specify do - order.checkout_steps.should == %w(address delivery payment) - end - end - end - - it "starts out at cart" do - order.state.should == "cart" - end - - it "transitions to address" do - order.next! - order.state.should == "address" - end - - context "from address" do - before do - order.state = 'address' - end - - it "transitions to delivery" do - order.stub(:has_available_payment) - order.next! - order.state.should == "delivery" - end - end - - context "from delivery" do - before do - order.state = 'delivery' - end - - context "with payment required" do - before do - order.stub :payment_required? => true - end - - it "transitions to payment" do - order.next! - order.state.should == 'payment' - end - end - - context "without payment required" do - before do - order.stub :payment_required? => false - end - - it "transitions to complete" do - order.next! - order.state.should == "complete" - end - end - end - - context "from payment" do - before do - order.state = 'payment' - end - - context "with confirmation required" do - before do - order.stub :confirmation_required? => true - end - - it "transitions to confirm" do - order.next! - order.state.should == "confirm" - end - end - - context "without confirmation required" do - before do - order.stub :confirmation_required? => false - order.stub :payment_required? => true - order.stub :paid? => true - end - - it "transitions to complete" do - order.should_receive(:process_payments!).once - order.next! - order.state.should == "complete" - end - end - - # Regression test for #2028 - context "when payment is not required" do - before do - order.stub :payment_required? => false - end - - it "does not call process payments" do - order.should_not_receive(:process_payments!) - order.next! - order.state.should == "complete" - end - end - end - end - - context "subclassed order" do - # This causes another test above to fail, but fixing this test should make - # the other test pass - class SubclassedOrder < Spree::Order - checkout_flow do - go_to_state :payment - go_to_state :complete - end - end - - it "should only call default transitions once when checkout_flow is redefined" do - order = SubclassedOrder.new - order.stub :payment_required? => true - order.should_receive(:process_payments!).once - order.state = "payment" - order.next! - order.state.should == "complete" - end - end - - context "re-define checkout flow" do - before do - @old_checkout_flow = Spree::Order.checkout_flow - Spree::Order.class_eval do - checkout_flow do - go_to_state :payment - go_to_state :complete - end - end - end - - after do - Spree::Order.checkout_flow = @old_checkout_flow - end - - it "should not keep old event transitions when checkout_flow is redefined" do - Spree::Order.next_event_transitions.should == [{:cart=>:payment}, {:payment=>:complete}] - end - - it "should not keep old events when checkout_flow is redefined" do - state_machine = Spree::Order.state_machine - state_machine.states.any? { |s| s.name == :address }.should be_false - known_states = state_machine.events[:next].branches.map(&:known_states).flatten - known_states.should_not include(:address) - known_states.should_not include(:delivery) - known_states.should_not include(:confirm) - end - end -end diff --git a/core/spec/models/order/payment_spec.rb b/core/spec/models/order/payment_spec.rb deleted file mode 100644 index ef20ca31572..00000000000 --- a/core/spec/models/order/payment_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -module Spree - describe Order do - let(:order) { stub_model(Order) } - let(:updater) { Spree::OrderUpdater.new(order) } - - before do - # So that Payment#purchase! is called during processing - Spree::Config[:auto_capture] = true - - order.stub_chain(:line_items, :empty?).and_return(false) - order.stub :total => 100 - end - - it 'processes all payments' do - payment_1 = create(:payment, :amount => 50) - payment_2 = create(:payment, :amount => 50) - order.stub(:pending_payments).and_return([payment_1, payment_2]) - - order.process_payments! - updater.update_payment_state - order.payment_state.should == 'paid' - - payment_1.should be_completed - payment_2.should be_completed - end - - it 'does not go over total for order' do - payment_1 = create(:payment, :amount => 50) - payment_2 = create(:payment, :amount => 50) - payment_3 = create(:payment, :amount => 50) - order.stub(:pending_payments).and_return([payment_1, payment_2, payment_3]) - - order.process_payments! - updater.update_payment_state - order.payment_state.should == 'paid' - - payment_1.should be_completed - payment_2.should be_completed - payment_3.should be_pending - end - - it "does not use failed payments" do - payment_1 = create(:payment, :amount => 50) - payment_2 = create(:payment, :amount => 50, :state => 'failed') - order.stub(:pending_payments).and_return([payment_1]) - - payment_2.should_not_receive(:process!) - - order.process_payments! - end - end -end diff --git a/core/spec/models/order/state_machine_spec.rb b/core/spec/models/order/state_machine_spec.rb deleted file mode 100644 index 3f9f34db2e5..00000000000 --- a/core/spec/models/order/state_machine_spec.rb +++ /dev/null @@ -1,214 +0,0 @@ -require 'spec_helper' - -describe Spree::Order do - let(:order) { Spree::Order.new } - before do - # Ensure state machine has been re-defined correctly - Spree::Order.define_state_machine! - # We don't care about this validation here - order.stub(:require_email) - end - - context "#next!" do - context "when current state is confirm" do - before { order.state = "confirm" } - it "should finalize order when transitioning to complete state" do - order.run_callbacks(:create) - order.should_receive(:finalize!) - order.next! - end - - context "when credit card payment fails" do - before do - order.stub(:process_payments!).and_raise(Spree::Core::GatewayError) - order.stub :payment_required? => true - end - - context "when not configured to allow failed payments" do - before do - Spree::Config.set :allow_checkout_on_gateway_error => false - end - - it "should not complete the order" do - order.next - order.state.should == "confirm" - end - end - - context "when configured to allow failed payments" do - before do - Spree::Config.set :allow_checkout_on_gateway_error => true - end - - it "should complete the order" do - order.stub :paid? => true - order.next! - order.state.should == "complete" - end - - end - - end - end - - context "when current state is address" do - before do - order.stub(:has_available_payment) - order.state = "address" - end - - it "adjusts tax rates when transitioning to delivery" do - # Once because the record is being saved - # Twice because it is transitioning to the delivery state - Spree::TaxRate.should_receive(:adjust).twice - order.next! - end - end - - context "when current state is delivery" do - before do - order.state = "delivery" - order.stub :total => 10.0 - end - - context "when transitioning to payment state" do - it "should create a shipment" do - order.should_receive(:create_shipment!) - order.next! - order.state.should == 'payment' - end - end - end - - end - - context "#can_cancel?" do - - %w(pending backorder ready).each do |shipment_state| - it "should be true if shipment_state is #{shipment_state}" do - order.stub :completed? => true - order.shipment_state = shipment_state - order.can_cancel?.should be_true - end - end - - (SHIPMENT_STATES - %w(pending backorder ready)).each do |shipment_state| - it "should be false if shipment_state is #{shipment_state}" do - order.stub :completed? => true - order.shipment_state = shipment_state - order.can_cancel?.should be_false - end - end - - end - - context "#cancel" do - let!(:variant) { stub_model(Spree::Variant, :on_hand => 0) } - let!(:inventory_units) { [stub_model(Spree::InventoryUnit, :variant => variant), - stub_model(Spree::InventoryUnit, :variant => variant) ]} - let!(:shipment) do - shipment = stub_model(Spree::Shipment) - shipment.stub :inventory_units => inventory_units - order.stub :shipments => [shipment] - shipment - end - - before do - order.stub :line_items => [stub_model(Spree::LineItem, :variant => variant, :quantity => 2)] - order.line_items.stub :find_by_variant_id => order.line_items.first - - order.stub :completed? => true - order.stub :allow_cancel? => true - end - - it "should send a cancel email" do - # Stub methods that cause side-effects in this test - order.stub :has_available_shipment - order.stub :restock_items! - mail_message = mock "Mail::Message" - Spree::OrderMailer.should_receive(:cancel_email).with(order).and_return mail_message - mail_message.should_receive :deliver - order.cancel! - end - - context "restocking inventory" do - before do - shipment.stub(:ensure_correct_adjustment) - shipment.stub(:update_order) - Spree::OrderMailer.stub(:cancel_email).and_return(mail_message = stub) - mail_message.stub :deliver - - order.stub :has_available_shipment - end - - # Regression fix for #729 - specify do - Spree::InventoryUnit.should_receive(:decrease).with(order, variant, 2).once - order.cancel! - end - end - - context "resets payment state" do - before do - # TODO: This is ugly :( - # Stubs methods that cause unwanted side effects in this test - Spree::OrderMailer.stub(:cancel_email).and_return(mail_message = stub) - mail_message.stub :deliver - order.stub :has_available_shipment - order.stub :restock_items! - end - - context "without shipped items" do - it "should set payment state to 'credit owed'" do - order.cancel! - order.payment_state.should == 'credit_owed' - end - end - - context "with shipped items" do - before do - order.stub :shipment_state => 'partial' - end - - it "should not alter the payment state" do - order.cancel! - order.payment_state.should be_nil - end - end - end - end - - - # Another regression test for #729 - context "#resume" do - before do - order.stub :email => "user@spreecommerce.com" - order.stub :state => "canceled" - order.stub :allow_resume? => true - - # Stubs method that cause unwanted side effects in this test - order.stub :has_available_shipment - end - - context "unstocks inventory" do - let(:variant) { stub_model(Spree::Variant) } - - before do - shipment = stub_model(Spree::Shipment) - line_item = stub_model(Spree::LineItem, :variant => variant, :quantity => 2) - order.stub :line_items => [line_item] - order.line_items.stub :find_by_variant_id => line_item - - order.stub :shipments => [shipment] - shipment.stub :inventory_units => [stub_model(Spree::InventoryUnit, :variant => variant), - stub_model(Spree::InventoryUnit, :variant => variant) ] - end - - specify do - Spree::InventoryUnit.should_receive(:increase).with(order, variant, 2).once - order.resume! - end - end - - end -end diff --git a/core/spec/models/order/tax_spec.rb b/core/spec/models/order/tax_spec.rb deleted file mode 100644 index 0b097411bbd..00000000000 --- a/core/spec/models/order/tax_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -require 'spec_helper' - -module Spree - describe Order do - let(:order) { stub_model(Spree::Order) } - - context "#tax_zone" do - let(:bill_address) { Factory :address } - let(:ship_address) { Factory :address } - let(:order) { Spree::Order.create(:ship_address => ship_address, :bill_address => bill_address) } - let(:zone) { Factory :zone } - - context "when no zones exist" do - before { Spree::Zone.destroy_all } - - it "should return nil" do - order.tax_zone.should be_nil - end - end - - context "when :tax_using_ship_address => true" do - before { Spree::Config.set(:tax_using_ship_address => true) } - - it "should calculate using ship_address" do - Spree::Zone.should_receive(:match).at_least(:once).with(ship_address) - Spree::Zone.should_not_receive(:match).with(bill_address) - order.tax_zone - end - end - - context "when :tax_using_ship_address => false" do - before { Spree::Config.set(:tax_using_ship_address => false) } - - it "should calculate using bill_address" do - Spree::Zone.should_receive(:match).at_least(:once).with(bill_address) - Spree::Zone.should_not_receive(:match).with(ship_address) - order.tax_zone - end - end - - context "when there is a default tax zone" do - before do - @default_zone = create(:zone, :name => "foo_zone") - Spree::Zone.stub :default_tax => @default_zone - end - - context "when there is a matching zone" do - before { Spree::Zone.stub(:match => zone) } - - it "should return the matching zone" do - order.tax_zone.should == zone - end - end - - context "when there is no matching zone" do - before { Spree::Zone.stub(:match => nil) } - - it "should return the default tax zone" do - order.tax_zone.should == @default_zone - end - end - end - - context "when no default tax zone" do - before { Spree::Zone.stub :default_tax => nil } - - context "when there is a matching zone" do - before { Spree::Zone.stub(:match => zone) } - - it "should return the matching zone" do - order.tax_zone.should == zone - end - end - - context "when there is no matching zone" do - before { Spree::Zone.stub(:match => nil) } - - it "should return nil" do - order.tax_zone.should be_nil - end - end - end - end - - - context "#exclude_tax?" do - before do - @order = create(:order) - @default_zone = create(:zone) - Spree::Zone.stub :default_tax => @default_zone - end - - context "when prices include tax" do - before { Spree::Config.set(:prices_inc_tax => true) } - - it "should be true when tax_zone is not the same as the default" do - @order.stub :tax_zone => create(:zone, :name => "other_zone") - @order.exclude_tax?.should be_true - end - - it "should be false when tax_zone is the same as the default" do - @order.stub :tax_zone => @default_zone - @order.exclude_tax?.should be_false - end - end - - context "when prices do not include tax" do - before { Spree::Config.set(:prices_inc_tax => false) } - - it "should be false" do - @order.exclude_tax?.should be_false - end - end - end - end -end - - diff --git a/core/spec/models/order/validations_spec.rb b/core/spec/models/order/validations_spec.rb deleted file mode 100644 index f96e6bac44f..00000000000 --- a/core/spec/models/order/validations_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -module Spree - describe Order do - context "validations" do - # Regression test for #2214 - it "does not return two error messages when email is blank" do - order = Order.new - order.stub(:require_email => true) - order.valid? - order.errors[:email].should == ["can't be blank"] - end - end - end -end diff --git a/core/spec/models/order_spec.rb b/core/spec/models/order_spec.rb deleted file mode 100644 index 1425a5115bd..00000000000 --- a/core/spec/models/order_spec.rb +++ /dev/null @@ -1,462 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -class FakeCalculator < Spree::Calculator - def compute(computable) - 5 - end -end - -describe Spree::Order do - before(:each) do - reset_spree_preferences - end - - let(:user) { stub_model(Spree::LegacyUser, :email => "spree@example.com") } - let(:order) { stub_model(Spree::Order, :user => user) } - - before do - Spree::LegacyUser.stub(:current => mock_model(Spree::LegacyUser, :id => 123)) - end - - context "#products" do - before :each do - @variant1 = mock_model(Spree::Variant, :product => "product1") - @variant2 = mock_model(Spree::Variant, :product => "product2") - @line_items = [mock_model(Spree::LineItem, :variant => @variant1, :variant_id => @variant1.id, :quantity => 1), - mock_model(Spree::LineItem, :variant => @variant2, :variant_id => @variant2.id, :quantity => 2)] - order.stub(:line_items => @line_items) - end - - it "should return ordered products" do - order.products.should == ['product1', 'product2'] - end - - it "contains?" do - order.contains?(@variant1).should be_true - end - - it "gets the quantity of a given variant" do - order.quantity_of(@variant1).should == 1 - - @variant3 = mock_model(Spree::Variant, :product => "product3") - order.quantity_of(@variant3).should == 0 - end - - it "can find a line item matching a given variant" do - order.find_line_item_by_variant(@variant1).should_not be_nil - order.find_line_item_by_variant(mock_model(Spree::Variant)).should be_nil - end - end - - context "#generate_order_number" do - it "should generate a random string" do - order.generate_order_number.is_a?(String).should be_true - (order.generate_order_number.to_s.length > 0).should be_true - end - end - - context "#associate_user!" do - it "should associate a user with this order" do - order.user = nil - order.email = nil - order.associate_user!(user) - order.user.should == user - order.email.should == user.email - end - end - - context "#create" do - it "should assign an order number" do - order = Spree::Order.create - order.number.should_not be_nil - end - end - - context "#finalize!" do - let(:order) { Spree::Order.create } - it "should set completed_at" do - order.should_receive(:touch).with(:completed_at) - order.finalize! - end - - it "should sell inventory units" do - Spree::InventoryUnit.should_receive(:assign_opening_inventory).with(order) - order.finalize! - end - - it "should change the shipment state to ready if order is paid" do - order.stub :shipping_method => mock_model(Spree::ShippingMethod, :create_adjustment => true) - order.create_shipment! - order.stub(:paid? => true, :complete? => true) - order.finalize! - order.reload # reload so we're sure the changes are persisted - order.shipment.state.should == 'ready' - order.shipment_state.should == 'ready' - end - - after { Spree::Config.set :track_inventory_levels => true } - it "should not sell inventory units if track_inventory_levels is false" do - Spree::Config.set :track_inventory_levels => false - Spree::InventoryUnit.should_not_receive(:sell_units) - order.finalize! - end - - it "should send an order confirmation email" do - mail_message = mock "Mail::Message" - Spree::OrderMailer.should_receive(:confirm_email).with(order).and_return mail_message - mail_message.should_receive :deliver - order.finalize! - end - - it "should continue even if confirmation email delivery fails" do - Spree::OrderMailer.should_receive(:confirm_email).with(order).and_raise 'send failed!' - order.finalize! - end - - it "should freeze all adjustments" do - # Stub this method as it's called due to a callback - # and it's irrelevant to this test - order.stub :has_available_shipment - - Spree::OrderMailer.stub_chain :confirm_email, :deliver - adjustment1 = mock_model(Spree::Adjustment, :mandatory => true) - adjustment2 = mock_model(Spree::Adjustment, :mandatory => false) - order.stub :adjustments => [adjustment1, adjustment2] - adjustment1.should_receive(:update_column).with("locked", true) - adjustment2.should_receive(:update_column).with("locked", true) - order.finalize! - end - - it "should log state event" do - order.state_changes.should_receive(:create).exactly(3).times #order, shipment & payment state changes - order.finalize! - end - end - - context "#process_payments!" do - it "should process the payments" do - order.stub(:total).and_return(10) - payment = stub_model(Spree::Payment) - payments = [payment] - order.stub(:payments).and_return(payments) - payments.first.should_receive(:process!) - order.process_payments! - end - end - - context "#outstanding_balance" do - it "should return positive amount when payment_total is less than total" do - order.payment_total = 20.20 - order.total = 30.30 - order.outstanding_balance.should == 10.10 - end - it "should return negative amount when payment_total is greater than total" do - order.total = 8.20 - order.payment_total = 10.20 - order.outstanding_balance.should be_within(0.001).of(-2.00) - end - - end - - context "#outstanding_balance?" do - it "should be true when total greater than payment_total" do - order.total = 10.10 - order.payment_total = 9.50 - order.outstanding_balance?.should be_true - end - it "should be true when total less than payment_total" do - order.total = 8.25 - order.payment_total = 10.44 - order.outstanding_balance?.should be_true - end - it "should be false when total equals payment_total" do - order.total = 10.10 - order.payment_total = 10.10 - order.outstanding_balance?.should be_false - end - end - - context "#complete?" do - it "should indicate if order is complete" do - order.completed_at = nil - order.complete?.should be_false - - order.completed_at = Time.now - order.completed?.should be_true - end - end - - context "#backordered?" do - it "should indicate whether any units in the order are backordered" do - order.stub_chain(:inventory_units, :backordered).and_return [] - order.backordered?.should be_false - order.stub_chain(:inventory_units, :backordered).and_return [mock_model(Spree::InventoryUnit)] - order.backordered?.should be_true - end - - it "should always be false when inventory tracking is disabled" do - Spree::Config.set :track_inventory_levels => false - order.stub_chain(:inventory_units, :backordered).and_return [mock_model(Spree::InventoryUnit)] - order.backordered?.should be_false - end - end - - context "#payment_method" do - it "should return payment.payment_method if payment is present" do - payments = [create(:payment)] - payments.stub(:completed => payments) - order.stub(:payments => payments) - order.payment_method.should == order.payments.first.payment_method - end - - it "should return the first payment method from available_payment_methods if payment is not present" do - create(:payment_method, :environment => 'test') - order.payment_method.should == order.available_payment_methods.first - end - end - - context "#allow_checkout?" do - it "should be true if there are line_items in the order" do - order.stub_chain(:line_items, :count => 1) - order.checkout_allowed?.should be_true - end - it "should be false if there are no line_items in the order" do - order.stub_chain(:line_items, :count => 0) - order.checkout_allowed?.should be_false - end - end - - context "#item_count" do - before do - @order = create(:order, :user => user) - @order.line_items = [ create(:line_item, :quantity => 2), create(:line_item, :quantity => 1) ] - end - it "should return the correct number of items" do - @order.item_count.should == 3 - end - end - - context "#amount" do - before do - @order = create(:order, :user => user) - @order.line_items = [ create(:line_item, :price => 1.0, :quantity => 2), create(:line_item, :price => 1.0, :quantity => 1) ] - end - it "should return the correct lum sum of items" do - @order.amount.should == 3.0 - end - end - - context "#can_cancel?" do - it "should be false for completed order in the canceled state" do - order.state = 'canceled' - order.shipment_state = 'ready' - order.completed_at = Time.now - order.can_cancel?.should be_false - end - - it "should be true for completed order with no shipment" do - order.state = 'complete' - order.shipment_state = nil - order.completed_at = Time.now - order.can_cancel?.should be_true - end - end - - context "rate_hash" do - let(:shipping_method_1) { mock_model Spree::ShippingMethod, :name => 'Air Shipping', :id => 1, :calculator => mock('calculator') } - let(:shipping_method_2) { mock_model Spree::ShippingMethod, :name => 'Ground Shipping', :id => 2, :calculator => mock('calculator') } - - before do - shipping_method_1.calculator.stub(:compute).and_return(10.0) - shipping_method_2.calculator.stub(:compute).and_return(0.0) - order.stub(:available_shipping_methods => [ shipping_method_1, shipping_method_2 ]) - end - - it "should return shipping methods sorted by cost" do - rate_1, rate_2 = order.rate_hash - - rate_1.shipping_method.should == shipping_method_2 - rate_1.cost.should == 0.0 - rate_1.name.should == "Ground Shipping" - rate_1.id.should == 2 - - rate_2.shipping_method.should == shipping_method_1 - rate_2.cost.should == 10.0 - rate_2.name.should == "Air Shipping" - rate_2.id.should == 1 - end - - it "should not return shipping methods with nil cost" do - shipping_method_1.calculator.stub(:compute).and_return(nil) - order.rate_hash.count.should == 1 - rate_1 = order.rate_hash.first - - rate_1.shipping_method.should == shipping_method_2 - rate_1.cost.should == 0 - rate_1.name.should == "Ground Shipping" - rate_1.id.should == 2 - end - - end - - context "insufficient_stock_lines" do - let(:line_item) { mock_model Spree::LineItem, :insufficient_stock? => true } - - before { order.stub(:line_items => [line_item]) } - - it "should return line_item that has insufficent stock on hand" do - order.insufficient_stock_lines.size.should == 1 - order.insufficient_stock_lines.include?(line_item).should be_true - end - - end - - context "#add_variant" do - it "should update order totals" do - order = Spree::Order.create - - order.item_total.to_f.should == 0.00 - order.total.to_f.should == 0.00 - - product = Spree::Product.create!(:name => 'Test', :sku => 'TEST-1', :price => 22.25) - order.add_variant(product.master) - - order.item_total.to_f.should == 22.25 - order.total.to_f.should == 22.25 - end - end - - context "empty!" do - it "should clear out all line items and adjustments" do - order = stub_model(Spree::Order) - order.stub(:line_items => line_items = []) - order.stub(:adjustments => adjustments = []) - order.line_items.should_receive(:destroy_all) - order.adjustments.should_receive(:destroy_all) - - order.empty! - end - end - - context "#display_outstanding_balance" do - it "returns the value as a spree money" do - order.stub(:outstanding_balance) { 10.55 } - order.display_outstanding_balance.should == Spree::Money.new(10.55) - end - end - - context "#display_item_total" do - it "returns the value as a spree money" do - order.stub(:item_total) { 10.55 } - order.display_item_total.should == Spree::Money.new(10.55) - end - end - - context "#display_adjustment_total" do - it "returns the value as a spree money" do - order.adjustment_total = 10.55 - order.display_adjustment_total.should == Spree::Money.new(10.55) - end - end - - context "#display_total" do - it "returns the value as a spree money" do - order.total = 10.55 - order.display_total.should == Spree::Money.new(10.55) - end - end - - context "#currency" do - context "when object currency is ABC" do - before { order.currency = "ABC" } - - it "returns the currency from the object" do - order.currency.should == "ABC" - end - end - - context "when object currency is nil" do - before { order.currency = nil } - - it "returns the globally configured currency" do - order.currency.should == "USD" - end - end - end - - # Regression tests for #2179 - context "#merge!" do - let(:variant) { Factory(:variant) } - let(:order_1) { Spree::Order.create } - let(:order_2) { Spree::Order.create } - - it "destroys the other order" do - order_1.merge!(order_2) - lambda { order_2.reload }.should raise_error(ActiveRecord::RecordNotFound) - end - - context "merging together two orders with line items for the same variant" do - before do - order_1.add_variant(variant) - order_2.add_variant(variant) - end - - specify do - order_1.merge!(order_2) - order_1.line_items.count.should == 1 - - line_item = order_1.line_items.first - line_item.quantity.should == 2 - line_item.variant_id.should == variant.id - end - end - - context "merging together two orders with different line items" do - let(:variant_2) { Factory(:variant) } - - before do - order_1.add_variant(variant) - order_2.add_variant(variant_2) - end - - specify do - order_1.merge!(order_2) - line_items = order_1.line_items - line_items.count.should == 2 - - # No guarantee on ordering of line items, so we do this: - line_items.map(&:quantity).should =~ [1,1] - line_items.map(&:variant_id).should =~ [variant.id, variant_2.id] - end - end - end - - # Regression test for #2191 - context "when an order has an adjustment that zeroes the total, but another adjustment for shipping that raises it above zero" do - let!(:persisted_order) { create(:order) } - let!(:line_item) { create(:line_item) } - let!(:shipping_method) do - sm = create(:shipping_method) - sm.calculator.preferred_amount = 10 - sm.save - sm - end - - before do - # Don't care about available payment methods in this test - persisted_order.stub(:has_available_payment => false) - persisted_order.line_items << line_item - persisted_order.adjustments.create(:amount => -line_item.amount, :label => "Promotion") - persisted_order.state = 'delivery' - persisted_order.save # To ensure new state_change event - end - - it "transitions from delivery to payment" do - persisted_order.shipping_method = shipping_method - persisted_order.next! - persisted_order.state.should == "payment" - end - end -end diff --git a/core/spec/models/order_updater_spec.rb b/core/spec/models/order_updater_spec.rb deleted file mode 100644 index a44c494af04..00000000000 --- a/core/spec/models/order_updater_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -require 'spec_helper' - -module Spree - describe OrderUpdater do - let(:order) { stub_model(Spree::Order) } - let(:updater) { Spree::OrderUpdater.new(order) } - - it "updates totals" do - payments = [stub(:amount => 5), stub(:amount => 5)] - order.stub_chain(:payments, :completed).and_return(payments) - - line_items = [stub(:amount => 10), stub(:amount => 20)] - order.stub :line_items => line_items - - adjustments = [stub(:amount => 10), stub(:amount => -20)] - order.stub_chain(:adjustments, :eligible).and_return(adjustments) - - updater.update_totals - order.payment_total.should == 10 - order.item_total.should == 30 - order.adjustment_total.should == -10 - order.total.should == 20 - end - - context "updating shipment state" do - before do - order.stub_chain(:shipments, :shipped, :count).and_return(0) - order.stub_chain(:shipments, :ready, :count).and_return(0) - order.stub_chain(:shipments, :pending, :count).and_return(0) - end - - it "is backordered" do - order.stub :backordered? => true - updater.update_shipment_state - - order.shipment_state.should == 'backorder' - end - - it "is nil" do - order.stub_chain(:shipments, :count).and_return(0) - - updater.update_shipment_state - order.shipment_state.should be_nil - end - - - [:shipped, :ready, :pending].each do |state| - it "is #{state}" do - order.stub_chain(:shipments, :count).and_return(1) - order.stub_chain(:shipments, state, :count).and_return(1) - - updater.update_shipment_state - order.shipment_state.should == state.to_s - end - end - - it "is partial" do - order.stub_chain(:shipments, :count).and_return(2) - order.stub_chain(:shipments, :ready, :count).and_return(1) - order.stub_chain(:shipments, :pending, :count).and_return(1) - - updater.update_shipment_state - order.shipment_state.should == 'partial' - end - end - - context "updating payment state" do - it "is failed if last payment failed" do - order.stub_chain(:payments, :last, :state).and_return('failed') - - updater.update_payment_state - order.payment_state.should == 'failed' - end - - it "is balance due with no line items" do - order.stub_chain(:line_items, :empty?).and_return(true) - - updater.update_payment_state - order.payment_state.should == 'balance_due' - end - - it "is credit owed if payment is above total" do - order.stub_chain(:line_items, :empty?).and_return(false) - order.stub :payment_total => 31 - order.stub :total => 30 - - updater.update_payment_state - order.payment_state.should == 'credit_owed' - end - - it "is paid if order is paid in full" do - order.stub_chain(:line_items, :empty?).and_return(false) - order.stub :payment_total => 30 - order.stub :total => 30 - - updater.update_payment_state - order.payment_state.should == 'paid' - end - end - - - it "state change" do - order.shipment_state = 'shipped' - state_changes = stub - order.stub :state_changes => state_changes - state_changes.should_receive(:create).with({ - :previous_state => nil, - :next_state => 'shipped', - :name => 'shipment', - :user_id => nil - }, :without_protection => true) - - order.state_changed('shipment') - end - - it "updates each shipment" do - shipment = stub_model(Shipment) - shipments = [shipment] - order.stub :shipments => shipments - shipments.stub :ready => [] - shipments.stub :pending => [] - shipments.stub :shipped => [] - - shipment.should_receive(:update!).with(order) - - updater.update - end - - it "updates totals twice" do - updater.should_receive(:update_totals).twice - - updater.update - end - end -end diff --git a/core/spec/models/payment_spec.rb b/core/spec/models/payment_spec.rb deleted file mode 100644 index fab72b765df..00000000000 --- a/core/spec/models/payment_spec.rb +++ /dev/null @@ -1,557 +0,0 @@ -require 'spec_helper' - -describe Spree::Payment do - let(:order) do - order = Spree::Order.new(:bill_address => Spree::Address.new, - :ship_address => Spree::Address.new) - end - - let(:gateway) do - gateway = Spree::Gateway::Bogus.new({:environment => 'test', :active => true}, :without_protection => true) - gateway.stub :source_required => true - gateway - end - - let(:card) do - mock_model(Spree::CreditCard, :number => "4111111111111111", - :has_payment_profile? => true) - end - - let(:payment) do - payment = Spree::Payment.new - payment.source = card - payment.order = order - payment.payment_method = gateway - payment - end - - let(:amount_in_cents) { payment.amount.to_f * 100 } - - let!(:success_response) do - mock('success_response', :success? => true, - :authorization => '123', - :avs_result => { 'code' => 'avs-code' }) - end - - let(:failed_response) { mock('gateway_response', :success? => false) } - - before(:each) do - # So it doesn't create log entries every time a processing method is called - payment.log_entries.stub(:create) - end - - # Regression test for https://github.com/spree/spree/pull/2224 - context 'failure' do - - it 'should transition to failed from pending state' do - payment.state = 'pending' - payment.failure - payment.state.should eql('failed') - end - - it 'should transition to failed from processing state' do - payment.state = 'processing' - payment.failure - payment.state.should eql('failed') - end - - end - - context "processing" do - before do - payment.stub(:update_order) - payment.stub(:create_payment_profile) - end - - context "#process!" do - it "should purchase if with auto_capture" do - Spree::Config[:auto_capture] = true - payment.should_receive(:purchase!) - payment.process! - end - - it "should authorize without auto_capture" do - Spree::Config[:auto_capture] = false - payment.should_receive(:authorize!) - payment.process! - end - - it "should make the state 'processing'" do - payment.should_receive(:started_processing!) - payment.process! - end - - end - - context "#authorize" do - it "should call authorize on the gateway with the payment amount" do - payment.payment_method.should_receive(:authorize).with(amount_in_cents, - card, - anything).and_return(success_response) - payment.authorize! - end - - it "should call authorize on the gateway with the currency code" do - payment.stub :currency => 'GBP' - payment.payment_method.should_receive(:authorize).with(amount_in_cents, - card, - hash_including({:currency => "GBP"})).and_return(success_response) - payment.authorize! - end - - it "should log the response" do - payment.log_entries.should_receive(:create).with({:details => anything}, {:without_protection => true}) - payment.authorize! - end - - context "when gateway does not match the environment" do - it "should raise an exception" do - gateway.stub :environment => "foo" - lambda { payment.authorize! }.should raise_error(Spree::Core::GatewayError) - end - end - - context "if successful" do - before do - payment.payment_method.should_receive(:authorize).with(amount_in_cents, - card, - anything).and_return(success_response) - end - - it "should store the response_code and avs_response" do - payment.authorize! - payment.response_code.should == '123' - payment.avs_response.should == 'avs-code' - end - - it "should make payment pending" do - payment.should_receive(:pend!) - payment.authorize! - end - end - - context "if unsuccessful" do - it "should mark payment as failed" do - gateway.stub(:authorize).and_return(failed_response) - payment.should_receive(:failure) - payment.should_not_receive(:pend) - lambda { - payment.authorize! - }.should raise_error(Spree::Core::GatewayError) - end - end - end - - context "purchase" do - it "should call purchase on the gateway with the payment amount" do - gateway.should_receive(:purchase).with(amount_in_cents, card, anything).and_return(success_response) - payment.purchase! - end - - it "should log the response" do - payment.log_entries.should_receive(:create).with({:details => anything}, {:without_protection => true}) - payment.purchase! - end - - context "when gateway does not match the environment" do - it "should raise an exception" do - gateway.stub :environment => "foo" - lambda { payment.purchase! }.should raise_error(Spree::Core::GatewayError) - end - end - - context "if successful" do - before do - payment.payment_method.should_receive(:purchase).with(amount_in_cents, - card, - anything).and_return(success_response) - end - - it "should store the response_code and avs_response" do - payment.purchase! - payment.response_code.should == '123' - payment.avs_response.should == 'avs-code' - end - - it "should make payment complete" do - payment.should_receive(:complete!) - payment.purchase! - end - end - - context "if unsuccessful" do - it "should make payment failed" do - gateway.stub(:purchase).and_return(failed_response) - payment.should_receive(:failure) - payment.should_not_receive(:pend) - lambda { payment.purchase! }.should raise_error(Spree::Core::GatewayError) - end - end - end - - context "#capture" do - before do - payment.stub(:complete).and_return(true) - end - - context "when payment is pending" do - before do - payment.state = 'pending' - end - - context "if successful" do - before do - payment.payment_method.should_receive(:capture).with(payment, card, anything).and_return(success_response) - end - - it "should make payment complete" do - payment.should_receive(:complete) - payment.capture! - end - - it "should store the response_code" do - gateway.stub :capture => success_response - payment.capture! - payment.response_code.should == '123' - end - end - - context "if unsuccessful" do - it "should not make payment complete" do - gateway.stub :capture => failed_response - payment.should_receive(:failure) - payment.should_not_receive(:complete) - lambda { payment.capture! }.should raise_error(Spree::Core::GatewayError) - end - end - end - - # Regression test for #2119 - context "when payment is completed" do - before do - payment.state = 'completed' - end - - it "should do nothing" do - payment.should_not_receive(:complete) - payment.payment_method.should_not_receive(:capture) - payment.log_entries.should_not_receive(:create) - payment.capture! - end - end - end - - context "#void" do - before do - payment.response_code = '123' - payment.state = 'pending' - end - - context "when profiles are supported" do - it "should call payment_gateway.void with the payment's response_code" do - gateway.stub :payment_profiles_supported? => true - gateway.should_receive(:void).with('123', card, anything).and_return(success_response) - payment.void_transaction! - end - end - - context "when profiles are not supported" do - it "should call payment_gateway.void with the payment's response_code" do - gateway.stub :payment_profiles_supported? => false - gateway.should_receive(:void).with('123', anything).and_return(success_response) - payment.void_transaction! - end - end - - it "should log the response" do - payment.log_entries.should_receive(:create).with({:details => anything}, {:without_protection => true}) - payment.void_transaction! - end - - context "when gateway does not match the environment" do - it "should raise an exception" do - gateway.stub :environment => "foo" - lambda { payment.void_transaction! }.should raise_error(Spree::Core::GatewayError) - end - end - - context "if successful" do - it "should update the response_code with the authorization from the gateway" do - # Change it to something different - payment.response_code = 'abc' - payment.void_transaction! - payment.response_code.should == '12345' - end - end - - context "if unsuccessful" do - it "should not void the payment" do - gateway.stub :void => failed_response - payment.should_not_receive(:void) - lambda { payment.void_transaction! }.should raise_error(Spree::Core::GatewayError) - end - end - - # Regression test for #2119 - context "if payment is already voided" do - before do - payment.state = 'void' - end - - it "should not void the payment" do - payment.payment_method.should_not_receive(:void) - payment.void_transaction! - end - end - end - - context "#credit" do - before do - payment.state = 'complete' - payment.response_code = '123' - end - - context "when outstanding_balance is less than payment amount" do - before do - payment.order.stub :outstanding_balance => 10 - payment.stub :credit_allowed => 1000 - end - - it "should call credit on the gateway with the credit amount and response_code" do - gateway.should_receive(:credit).with(1000, card, '123', anything).and_return(success_response) - payment.credit! - end - end - - context "when outstanding_balance is equal to payment amount" do - before do - payment.order.stub :outstanding_balance => payment.amount - end - - it "should call credit on the gateway with the credit amount and response_code" do - gateway.should_receive(:credit).with(amount_in_cents, card, '123', anything).and_return(success_response) - payment.credit! - end - end - - context "when outstanding_balance is greater than payment amount" do - before do - payment.order.stub :outstanding_balance => 101 - end - - it "should call credit on the gateway with the original payment amount and response_code" do - gateway.should_receive(:credit).with(amount_in_cents.to_f, card, '123', anything).and_return(success_response) - payment.credit! - end - end - - it "should log the response" do - payment.log_entries.should_receive(:create).with({:details => anything}, {:without_protection => true}) - payment.credit! - end - - context "when gateway does not match the environment" do - it "should raise an exception" do - gateway.stub :environment => "foo" - lambda { payment.credit! }.should raise_error(Spree::Core::GatewayError) - end - end - - context "when response is successful" do - it "should create an offsetting payment" do - Spree::Payment.should_receive(:create) - payment.credit! - end - - it "resulting payment should have correct values" do - payment.order.stub :outstanding_balance => 100 - payment.stub :credit_allowed => 10 - - offsetting_payment = payment.credit! - offsetting_payment.amount.to_f.should == -10 - offsetting_payment.should be_completed - offsetting_payment.response_code.should == '12345' - offsetting_payment.source.should == payment - end - end - end - end - - context "when response is unsuccessful" do - it "should not create a payment" do - gateway.stub :credit => failed_response - Spree::Payment.should_not_receive(:create) - lambda { payment.credit! }.should raise_error(Spree::Core::GatewayError) - end - end - - context "when already processing" do - it "should return nil without trying to process the source" do - payment.state = 'processing' - - payment.should_not_receive(:authorize!) - payment.should_not_receive(:purchase!) - payment.process!.should == nil - end - end - - context "with source required" do - context "raises an error if no source is specified" do - before do - payment.source = nil - end - - specify do - lambda { payment.process! }.should raise_error(Spree::Core::GatewayError, I18n.t(:payment_processing_failed)) - end - end - end - - context "with source optional" do - context "raises no error if source is not specified" do - before do - payment.source = nil - payment.payment_method.stub(:source_required? => false) - end - - specify do - lambda { payment.process! }.should_not raise_error(Spree::Core::GatewayError) - end - end - end - - context "#credit_allowed" do - it "is the difference between offsets total and payment amount" do - payment.amount = 100 - payment.stub(:offsets_total).and_return(0) - payment.credit_allowed.should == 100 - payment.stub(:offsets_total).and_return(80) - payment.credit_allowed.should == 20 - end - end - - context "#can_credit?" do - it "is true if credit_allowed > 0" do - payment.stub(:credit_allowed).and_return(100) - payment.can_credit?.should be_true - end - it "is false if credit_allowed is 0" do - payment.stub(:credit_allowed).and_return(0) - payment.can_credit?.should be_false - end - end - - context "#credit" do - context "when amount <= credit_allowed" do - it "makes the state processing" do - payment.state = 'completed' - payment.stub(:credit_allowed).and_return(10) - payment.partial_credit(10) - payment.should be_processing - end - it "calls credit on the source with the payment and amount" do - payment.state = 'completed' - payment.stub(:credit_allowed).and_return(10) - payment.should_receive(:credit!).with(10) - payment.partial_credit(10) - end - end - context "when amount > credit_allowed" do - it "should not call credit on the source" do - payment.state = 'completed' - payment.stub(:credit_allowed).and_return(10) - payment.partial_credit(20) - payment.should be_completed - end - end - end - - context "#save" do - it "should call order#update!" do - payment = Spree::Payment.create({:amount => 100, :order => order}, :without_protection => true) - order.should_receive(:update!) - payment.save - end - - context "when profiles are supported" do - before do - gateway.stub :payment_profiles_supported? => true - payment.source.stub :has_payment_profile? => false - end - - - context "when there is an error connecting to the gateway" do - it "should call gateway_error " do - gateway.should_receive(:create_profile).and_raise(ActiveMerchant::ConnectionError) - lambda { Spree::Payment.create({:amount => 100, :order => order, :source => card, :payment_method => gateway}, :without_protection => true) }.should raise_error(Spree::Core::GatewayError) - end - end - - context "when successfully connecting to the gateway" do - it "should create a payment profile" do - payment.payment_method.should_receive :create_profile - payment = Spree::Payment.create({:amount => 100, :order => order, :source => card, :payment_method => gateway}, :without_protection => true) - end - end - - - end - - context "when profiles are not supported" do - before { gateway.stub :payment_profiles_supported? => false } - - it "should not create a payment profile" do - gateway.should_not_receive :create_profile - payment = Spree::Payment.create({:amount => 100, :order => order, :source => card, :payment_method => gateway}, :without_protection => true) - end - end - end - - context "#build_source" do - it "should build the payment's source" do - params = { :amount => 100, :payment_method => gateway, - :source_attributes => {:year=>"2012", :month =>"1", :number => '1234567890123',:verification_value => '123'}} - - payment = Spree::Payment.new(params, :without_protection => true) - payment.should be_valid - payment.source.should_not be_nil - end - - context "with the params hash ordered differently" do - it "should build the payment's source" do - params = { - :source_attributes => {:year=>"2012", :month =>"1", :number => '1234567890123',:verification_value => '123'}, - :amount => 100, :payment_method => gateway - } - - payment = Spree::Payment.new(params, :without_protection => true) - payment.should be_valid - payment.source.should_not be_nil - end - end - - it "errors when payment source not valid" do - params = { :amount => 100, :payment_method => gateway, - :source_attributes => {:year=>"2012", :month =>"1" }} - - payment = Spree::Payment.new(params, :without_protection => true) - payment.should_not be_valid - payment.source.should_not be_nil - payment.source.should have(1).error_on(:number) - payment.source.should have(1).error_on(:verification_value) - end - end - - context "#currency" do - before { order.stub(:currency) { "ABC" } } - it "returns the order currency" do - payment.currency.should == "ABC" - end - end - - context "#display_amount" do - it "returns a Spree::Money for this amount" do - payment.display_amount.should == Spree::Money.new(payment.amount) - end - end -end diff --git a/core/spec/models/preference_spec.rb b/core/spec/models/preference_spec.rb deleted file mode 100644 index 2b2aab754bd..00000000000 --- a/core/spec/models/preference_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'spec_helper' - -describe Spree::Preference do - - it "should require a key" do - @preference = Spree::Preference.new - @preference.key = :test - @preference.value_type = :boolean - @preference.value = true - @preference.should be_valid - end - - describe "type coversion for values" do - def round_trip_preference(key, value, value_type) - p = Spree::Preference.new - p.value = value - p.value_type = value_type - p.key = key - p.save - - Spree::Preference.find_by_key(key) - end - - it ":boolean" do - value_type = :boolean - value = true - key = "boolean_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - it "false :boolean" do - value_type = :boolean - value = false - key = "boolean_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - it ":integer" do - value_type = :integer - value = 10 - key = "integer_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - it ":decimal" do - value_type = :decimal - value = 1.5 - key = "decimal_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - it ":string" do - value_type = :string - value = "This is a string" - key = "string_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - it ":text" do - value_type = :text - value = "This is a string stored as text" - key = "text_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - it ":password" do - value_type = :password - value = "This is a password" - key = "password_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - it ":any" do - value_type = :any - value = [1, 2] - key = "any_key" - pref = round_trip_preference(key, value, value_type) - pref.value.should eq value - pref.value_type.should == value_type.to_s - end - - end - - describe "converting old values" do - - it "converts true" do - p = Spree::Preference.new - p.value = 't' - p.value_type = TrueClass.to_s - Spree::Preference.convert_old_value_types(p) - p.value_type.should == 'boolean' - p.value.should == true - end - - it "converts false" do - p = Spree::Preference.new - p.value = 'f' - p.value_type = FalseClass.to_s - Spree::Preference.convert_old_value_types(p) - p.value_type.should == 'boolean' - p.value.should == false - end - - end - -end diff --git a/core/spec/models/preferences/configuration_spec.rb b/core/spec/models/preferences/configuration_spec.rb deleted file mode 100644 index a1b45d05341..00000000000 --- a/core/spec/models/preferences/configuration_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -describe Spree::Preferences::Configuration do - - before :all do - class AppConfig < Spree::Preferences::Configuration - preference :color, :string, :default => :blue - end - @config = AppConfig.new - end - - it "has named methods to access preferences" do - @config.color = 'orange' - @config.color.should eq 'orange' - end - - it "uses [ ] to access preferences" do - @config[:color] = 'red' - @config[:color].should eq 'red' - end - - it "uses set/get to access preferences" do - @config.set :color, 'green' - @config.get(:color).should eq 'green' - end - -end - - - diff --git a/core/spec/models/preferences/preferable_spec.rb b/core/spec/models/preferences/preferable_spec.rb deleted file mode 100644 index 0961f570963..00000000000 --- a/core/spec/models/preferences/preferable_spec.rb +++ /dev/null @@ -1,326 +0,0 @@ -require 'spec_helper' - -describe Spree::Preferences::Preferable do - - before :all do - class A - include Spree::Preferences::Preferable - attr_reader :id - - def initialize - @id = rand(999) - end - - preference :color, :string, :default => 'green', :description => "My Favorite Color" - end - - class B < A - preference :flavor, :string - end - end - - before :each do - @a = A.new - @a.stub(:persisted? => true) - @b = B.new - @b.stub(:persisted? => true) - end - - describe "preference definitions" do - it "parent should not see child definitions" do - @a.has_preference?(:color).should be_true - @a.has_preference?(:flavor).should_not be_true - end - - it "child should have parent and own definitions" do - @b.has_preference?(:color).should be_true - @b.has_preference?(:flavor).should be_true - end - - it "instances have defaults" do - @a.preferred_color.should eq 'green' - @b.preferred_color.should eq 'green' - @b.preferred_flavor.should be_nil - end - - it "can be asked if it has a preference definition" do - @a.has_preference?(:color).should be_true - @a.has_preference?(:bad).should be_false - end - - it "can be asked and raises" do - lambda { - @a.has_preference! :flavor - }.should raise_error(NoMethodError, "flavor preference not defined") - end - - it "has a type" do - @a.preferred_color_type.should eq :string - @a.preference_type(:color).should eq :string - end - - it "has a default" do - @a.preferred_color_default.should eq 'green' - @a.preference_default(:color).should eq 'green' - end - - it "has a description" do - @a.preferred_color_description.should eq "My Favorite Color" - @a.preference_description(:color).should eq "My Favorite Color" - end - - it "raises if not defined" do - lambda { - @a.get_preference :flavor - }.should raise_error(NoMethodError, "flavor preference not defined") - end - - end - - describe "preference access" do - it "handles ghost methods for preferences" do - @a.preferred_color = 'blue' - @a.preferred_color.should eq 'blue' - - @a.prefers_color = 'green' - @a.prefers_color?.should eq 'green' - end - - it "has genric readers" do - @a.preferred_color = 'red' - @a.prefers?(:color).should eq 'red' - @a.preferred(:color).should eq 'red' - end - - it "parent and child instances have their own prefs" do - @a.preferred_color = 'red' - @b.preferred_color = 'blue' - - @a.preferred_color.should eq 'red' - @b.preferred_color.should eq 'blue' - end - - it "raises when preference not defined" do - lambda { - @a.set_preference(:bad, :bone) - }.should raise_exception(NoMethodError, "bad preference not defined") - end - - it "builds a hash of preferences" do - @b.preferred_flavor = :strawberry - @b.preferences[:flavor].should eq 'strawberry' - @b.preferences[:color].should eq 'green' #default from A - end - - context "database fallback" do - before do - @a.instance_variable_set("@pending_preferences", {}) - end - - it "retrieves a preference from the database before falling back to default" do - preference = mock(:value => "chatreuse") - Spree::Preference.should_receive(:find_by_key).with("color").and_return(preference) - @a.preferred_color.should == 'chatreuse' - end - - it "defaults if no database key exists" do - Spree::Preference.should_receive(:find_by_key).and_return(nil) - @a.preferred_color.should == 'green' - end - end - - - context "converts integer preferences to integer values" do - before do - A.preference :is_integer, :integer - end - - it "with strings" do - @a.set_preference(:is_integer, '3') - @a.preferences[:is_integer].should == 3 - - @a.set_preference(:is_integer, '') - @a.preferences[:is_integer].should == 0 - end - - end - - context "converts decimal preferences to BigDecimal values" do - before do - A.preference :if_decimal, :decimal - end - - it "returns a BigDecimal" do - @a.set_preference(:if_decimal, 3.3) - @a.preferences[:if_decimal].class.should == BigDecimal - end - - it "with strings" do - @a.set_preference(:if_decimal, '3.3') - @a.preferences[:if_decimal].should == 3.3 - - @a.set_preference(:if_decimal, '') - @a.preferences[:if_decimal].should == 0.0 - end - end - - context "converts boolean preferences to boolean values" do - before do - A.preference :is_boolean, :boolean, :default => true - end - - it "with strings" do - @a.set_preference(:is_boolean, '0') - @a.preferences[:is_boolean].should be_false - @a.set_preference(:is_boolean, 'f') - @a.preferences[:is_boolean].should be_false - @a.set_preference(:is_boolean, 't') - @a.preferences[:is_boolean].should be_true - end - - it "with integers" do - @a.set_preference(:is_boolean, 0) - @a.preferences[:is_boolean].should be_false - @a.set_preference(:is_boolean, 1) - @a.preferences[:is_boolean].should be_true - end - - it "with an empty string" do - @a.set_preference(:is_boolean, '') - @a.preferences[:is_boolean].should be_false - end - - it "with an empty hash" do - @a.set_preference(:is_boolean, []) - @a.preferences[:is_boolean].should be_false - end - end - - context "converts any preferences to any values" do - before do - A.preference :product_ids, :any, :default => [] - A.preference :product_attributes, :any, :default => {} - end - - it "with array" do - @a.preferences[:product_ids].should == [] - @a.set_preference(:product_ids, [1, 2]) - @a.preferences[:product_ids].should == [1, 2] - end - - it "with hash" do - @a.preferences[:product_attributes].should == {} - @a.set_preference(:product_attributes, {:id => 1, :name => 2}) - @a.preferences[:product_attributes].should == {:id => 1, :name => 2} - end - end - - end - - describe "persisted preferables" do - before(:all) do - class CreatePrefTest < ActiveRecord::Migration - def self.up - create_table :pref_tests do |t| - t.string :col - end - end - - def self.down - drop_table :pref_tests - end - end - - @migration_verbosity = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - CreatePrefTest.migrate(:up) - - class PrefTest < ActiveRecord::Base - preference :pref_test_pref, :string, :default => 'abc' - preference :pref_test_any, :any, :default => [] - end - end - - after(:all) do - CreatePrefTest.migrate(:down) - ActiveRecord::Migration.verbose = @migration_verbosity - end - - before(:each) do - @pt = PrefTest.create - end - - describe "pending preferences for new activerecord objects" do - it "saves preferences after record is saved" do - pr = PrefTest.new - pr.set_preference(:pref_test_pref, 'XXX') - pr.get_preference(:pref_test_pref).should == 'XXX' - pr.save! - pr.get_preference(:pref_test_pref).should == 'XXX' - end - - it "saves preferences for serialized object" do - pr = PrefTest.new - pr.set_preference(:pref_test_any, [1, 2]) - pr.get_preference(:pref_test_any).should == [1, 2] - pr.save! - pr.get_preference(:pref_test_any).should == [1, 2] - end - end - - describe "requires a valid id" do - it "for cache_key" do - pref_test = PrefTest.new - pref_test.preference_cache_key(:pref_test_pref).should be_nil - - pref_test.save - pref_test.preference_cache_key(:pref_test_pref).should_not be_nil - end - - it "but returns default values" do - pref_test = PrefTest.new - pref_test.get_preference(:pref_test_pref).should == 'abc' - end - - it "adds prefs in a pending hash until after_create" do - pref_test = PrefTest.new - pref_test.should_receive(:add_pending_preference).with(:pref_test_pref, 'XXX') - pref_test.set_preference(:pref_test_pref, 'XXX') - end - end - - it "clear preferences" do - @pt.set_preference(:pref_test_pref, 'xyz') - @pt.preferred_pref_test_pref.should == 'xyz' - @pt.clear_preferences - @pt.preferred_pref_test_pref.should == 'abc' - end - - it "clear preferences when record is deleted" do - @pt.save! - @pt.preferred_pref_test_pref = 'lmn' - @pt.save! - @pt.destroy - @pt1 = PrefTest.new({:col => 'aaaa'}, :without_protection => true) - @pt1.id = @pt.id - @pt1.save! - @pt1.get_preference(:pref_test_pref).should_not == 'lmn' - @pt1.get_preference(:pref_test_pref).should == 'abc' - end - end - - it "builds cache keys" do - @a.preference_cache_key(:color).should match /a\/color\/\d+/ - end - - it "can add and remove preferences" do - A.preference :test_temp, :boolean, :default => true - @a.preferred_test_temp.should be_true - A.remove_preference :test_temp - @a.has_preference?(:test_temp).should be_false - @a.respond_to?(:preferred_test_temp).should be_false - end - -end - - diff --git a/core/spec/models/preferences/store_spec.rb b/core/spec/models/preferences/store_spec.rb deleted file mode 100644 index 2d5a091934e..00000000000 --- a/core/spec/models/preferences/store_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' - -describe Spree::Preferences::Store do - before :each do - @store = Spree::Preferences::StoreInstance.new - end - - it "sets and gets a key" do - @store.set :test, 1, :integer - @store.exist?(:test).should be_true - @store.get(:test).should eq 1 - end - - it "can set and get false values when cache return nil" do - @store.set :test, false, :boolean - @store.get(:test).should be_false - end - - it "returns the correct preference value when the cache is empty" do - @store.set :test, "1", :string - Rails.cache.clear - @store.get(:test).should == "1" - end -end diff --git a/core/spec/models/product/scopes_spec.rb b/core/spec/models/product/scopes_spec.rb deleted file mode 100644 index cff66ad7ad5..00000000000 --- a/core/spec/models/product/scopes_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe "Product scopes" do - let!(:product) { create(:product) } - - context "A product assigned to parent and child taxons" do - before do - @taxonomy = create(:taxonomy) - @root_taxon = @taxonomy.root - - @parent_taxon = create(:taxon, :name => 'Parent', :taxonomy_id => @taxonomy.id, :parent => @root_taxon) - @child_taxon = create(:taxon, :name =>'Child 1', :taxonomy_id => @taxonomy.id, :parent => @parent_taxon) - @parent_taxon.reload # Need to reload for descendents to show up - - product.taxons << @parent_taxon - product.taxons << @child_taxon - end - - it "calling Product.in_taxon should not return duplicate records" do - Spree::Product.in_taxon(@parent_taxon).to_a.count.should == 1 - end - end - - context "on_hand" do - # Regression test for #2111 - context "A product with a deleted variant" do - before do - variant = product.variants.create!({:price => 10, :count_on_hand => 300}, :without_protection => true) - variant.update_column(:deleted_at, Time.now) - end - - it "does not include the deleted variant in on_hand summary" do - Spree::Product.on_hand.should be_empty - end - end - end -end diff --git a/core/spec/models/product_filter_spec.rb b/core/spec/models/product_filter_spec.rb deleted file mode 100644 index fc965744b14..00000000000 --- a/core/spec/models/product_filter_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' -require 'spree/product_filters' - -describe 'product filters' do - # Regression test for #1709 - context 'finds products filtered by brand' do - let(:product) { create(:product) } - before do - property = Spree::Property.create!(:name => "brand", :presentation => "brand") - product.set_property("brand", "Nike") - end - - it "does not attempt to call value method on Arel::Table" do - lambda { Spree::ProductFilters.brand_filter }.should_not raise_error(NoMethodError) - end - - it "can find products in the 'Nike' brand" do - Spree::Product.brand_any("Nike").should include(product) - end - end -end diff --git a/core/spec/models/product_option_type_spec.rb b/core/spec/models/product_option_type_spec.rb deleted file mode 100644 index 8bf18700137..00000000000 --- a/core/spec/models/product_option_type_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Spree::ProductOptionType do - -end diff --git a/core/spec/models/product_property_spec.rb b/core/spec/models/product_property_spec.rb deleted file mode 100644 index e979efb7699..00000000000 --- a/core/spec/models/product_property_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' - -describe Spree::ProductProperty do - - context "validations" do - it "should validate length of value" do - pp = create(:product_property) - pp.value = "x" * 256 - pp.should_not be_valid - end - - end - -end diff --git a/core/spec/models/product_spec.rb b/core/spec/models/product_spec.rb deleted file mode 100644 index 2d5a70f4564..00000000000 --- a/core/spec/models/product_spec.rb +++ /dev/null @@ -1,422 +0,0 @@ -# coding: UTF-8 - -require 'spec_helper' - -describe Spree::Product do - before(:each) do - reset_spree_preferences - end - - context "#on_hand=" do - it "should not complain of a missing master" do - product = Spree::Product.new - product.on_hand = 5 - end - end - - it "should always have a master variant" do - product = Spree::Product.new - product.master.should_not be_nil - end - - context 'product instance' do - let(:product) { create(:product) } - - context '#duplicate' do - before do - product.stub :taxons => [create(:taxon)] - end - - it 'duplicates product' do - clone = product.duplicate - clone.name.should == 'COPY OF ' + product.name - clone.master.sku.should == 'COPY OF ' + product.master.sku - clone.taxons.should == product.taxons - clone.images.size.should == product.images.size - end - end - - context "product has no variants" do - context "#delete" do - it "should set deleted_at value" do - product.delete - product.reload - product.deleted_at.should_not be_nil - product.master.deleted_at.should_not be_nil - end - end - end - - context "product has variants" do - before do - create(:variant, :product => product) - end - - context "#delete" do - it "should set deleted_at value" do - product.delete - product.deleted_at.should_not be_nil - product.variants_including_master.all? { |v| !v.deleted_at.nil? }.should be_true - end - end - end - - context "#on_hand" do - # Regression test for #898 - context 'returns the correct number of products on hand' do - before do - Spree::Config.set :track_inventory_levels => true - product.master.stub :on_hand => 2 - end - specify { product.on_hand.should == 2 } - end - end - - # Test for #2167 - context "#on_display?" do - it "is on display if product has stock" do - product.stub :has_stock? => true - assert product.on_display? - end - - it "is on display if show_zero_stock_products preference is set to true" do - Spree::Config[:show_zero_stock_products] = true - assert product.on_display? - end - end - - # Test for #2167 - context "#on_sale?" do - it "is on sale if the product has stock" do - product.stub :has_stock? => true - assert product.on_sale? - end - - it "is on sale if allow_backorders preference is set to true" do - Spree::Config[:allow_backorders] = true - assert product.on_sale? - end - end - - context "#price" do - # Regression test for #1173 - it 'strips non-price characters' do - product.price = "$10" - product.price.should == 10.0 - end - end - - context "#display_price" do - before { product.price = 10.55 } - - context "with display_currency set to true" do - before { Spree::Config[:display_currency] = true } - - it "shows the currency" do - product.display_price.should == "$10.55 USD" - end - end - - context "with display_currency set to false" do - before { Spree::Config[:display_currency] = false } - - it "does not include the currency" do - product.display_price.should == "$10.55" - end - end - - context "with currency set to JPY" do - before do - product.master.default_price.currency = 'JPY' - product.master.default_price.save! - Spree::Config[:currency] = 'JPY' - end - - it "displays the currency in yen" do - product.display_price.to_s.should == "¥11" - end - end - end - - context "#available?" do - it "should be available if date is in the past" do - product.available_on = 1.day.ago - product.should be_available - end - - it "should not be available if date is nil or in the future" do - product.available_on = nil - product.should_not be_available - - product.available_on = 1.day.from_now - product.should_not be_available - end - end - end - - context "validations" do - context "find_by_param" do - - context "permalink should be incremented until the value is not taken" do - before do - @other_product = create(:product, :name => 'zoo') - @product1 = create(:product, :name => 'foo') - @product2 = create(:product, :name => 'foo') - @product3 = create(:product, :name => 'foo') - end - it "should have valid permalink" do - @product1.permalink.should == 'foo' - @product2.permalink.should == 'foo-1' - @product3.permalink.should == 'foo-2' - end - end - - context "permalink should be incremented until the value is not taken when there are more than 10 products" do - before do - @products = 0.upto(11).map do - create(:product, :name => 'foo') - end - end - it "should have valid permalink" do - @products[11].permalink.should == 'foo-11' - end - end - - context "permalink should be incremented until the value is not taken for similar names" do - before do - @other_product = create(:product, :name => 'foo bar') - @product1 = create(:product, :name => 'foo') - @product2 = create(:product, :name => 'foo') - @product3 = create(:product, :name => 'foo') - end - it "should have valid permalink" do - @product1.permalink.should == 'foo-1' - @product2.permalink.should == 'foo-2' - @product3.permalink.should == 'foo-3' - end - end - - context "permalink should be incremented until the value is not taken for similar names when there are more than 10 products" do - before do - @other_product = create(:product, :name => 'foo a') - @products = 0.upto(11).map do - create(:product, :name => 'foo') - end - end - it "should have valid permalink" do - @products[11].permalink.should == 'foo-12' - end - end - - context "permalink with quotes" do - it "should be saved correctly" do - product = create(:product, :name => "Joe's", :permalink => "joe's") - product.permalink.should == "joe's" - end - - context "existing" do - before do - create(:product, :name => "Joe's", :permalink => "joe's") - end - - it "should be detected" do - product = create(:product, :name => "Joe's", :permalink => "joe's") - product.permalink.should == "joe's-1" - end - end - end - - context "make_permalink should declare validates_uniqueness_of" do - before do - @product1 = create(:product, :name => 'foo') - @product2 = create(:product, :name => 'foo') - @product2.update_attributes(:permalink => 'foo') - end - - it "should have an error" do - @product2.errors.size.should == 1 - end - - it "should have error message that permalink is already taken" do - @product2.errors.full_messages.first.should == 'Permalink has already been taken' - end - end - - end - end - - context "permalink generation" do - it "supports Chinese" do - @product = create(:product, :name => "你好") - @product.permalink.should == "ni-hao" - end - end - - context "manual permalink override" do - it "calling save_permalink with a parameter" do - @product = create(:product, :name => "foo") - @product.permalink.should == "foo" - @product.name = "foobar" - @product.save - @product.permalink.should == "foo" - @product.save_permalink(@product.name) - @product.permalink.should == "foobar" - end - - it "should be incremented until not taken with a parameter" do - @product = create(:product, :name => "foo") - @product2 = create(:product, :name => "foobar") - @product.permalink.should == "foo" - @product.name = "foobar" - @product.save - @product.permalink.should == "foo" - @product.save_permalink(@product.name) - @product.permalink.should == "foobar-1" - end - end - - context "properties" do - it "should properly assign properties" do - product = FactoryGirl.create :product - product.set_property('the_prop', 'value1') - product.property('the_prop').should == 'value1' - - product.set_property('the_prop', 'value2') - product.property('the_prop').should == 'value2' - end - - it "should not create duplicate properties when set_property is called" do - product = FactoryGirl.create :product - - lambda { - product.set_property('the_prop', 'value2') - product.save - product.reload - }.should_not change(product.properties, :length) - - lambda { - product.set_property('the_prop_new', 'value') - product.save - product.reload - product.property('the_prop_new').should == 'value' - }.should change { product.properties.length }.by(1) - end - end - - context '#create' do - before do - @prototype = create(:prototype) - @product = Spree::Product.new(:name => "Foo", :price => 1.99) - end - - context "when prototype is supplied" do - before { @product.prototype_id = @prototype.id } - - it "should create properties based on the prototype" do - @product.save - @product.properties.count.should == 1 - end - - end - - context "when prototype with option types is supplied" do - - include_context "product prototype" - - before { @product.prototype_id = prototype.id } - - it "should create option types based on the prototype" do - @product.save - @product.option_type_ids.length.should == 1 - @product.option_type_ids.should == prototype.option_type_ids - end - - it "should create product option types based on the prototype" do - @product.save - @product.product_option_types.map(&:option_type_id).should == prototype.option_type_ids - end - - it "should create variants from an option values hash with one option type" do - @product.option_values_hash = option_values_hash - @product.save - @product.variants.length.should == 3 - end - - it "should still create variants when option_values_hash is given but prototype id is nil" do - @product.option_values_hash = option_values_hash - @product.prototype_id = nil - @product.save - @product.option_type_ids.length.should == 1 - @product.option_type_ids.should == prototype.option_type_ids - @product.variants.length.should == 3 - end - - it "should create variants from an option values hash with multiple option types" do - color = build_option_type_with_values("color", %w(Red Green Blue)) - logo = build_option_type_with_values("logo", %w(Ruby Rails Nginx)) - option_values_hash[color.id.to_s] = color.option_value_ids - option_values_hash[logo.id.to_s] = logo.option_value_ids - @product.option_values_hash = option_values_hash - @product.save - @product = @product.reload - @product.option_type_ids.length.should == 3 - @product.variants.length.should == 27 - end - end - - end - - context '#has_stock?' do - let(:product) do - product = stub_model(Spree::Product) - product.stub :master => stub_model(Spree::Variant) - product - end - - context 'nothing in stock' do - before do - Spree::Config.set :track_inventory_levels => true - product.master.stub :on_hand => 0 - end - specify { product.has_stock?.should be_false } - end - - context 'master variant has items in stock' do - before do - product.master.on_hand = 100 - end - specify { product.has_stock?.should be_true } - end - - context 'variant has items in stock' do - before do - Spree::Config.set :track_inventory_levels => true - product.master.stub :on_hand => 0 - product.variants.build(:on_hand => 100) - product.stub :has_variants? => true - end - specify { product.has_stock?.should be_true } - end - end - - context "#images" do - let(:product) { create(:product) } - - before do - image = File.open(File.expand_path('../../../app/assets/images/noimage/product.png', __FILE__)) - Spree::Image.create({:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :alt => "position 2", :attachment => image, :position => 2}) - Spree::Image.create({:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :alt => "position 1", :attachment => image, :position => 1}) - Spree::Image.create({:viewable_id => product.master.id, :viewable_type => 'ThirdParty::Extension', :alt => "position 1", :attachment => image, :position => 2}) - end - - it "should only look for variant images to support third-party extensions" do - product.images.size.should == 2 - end - - it "should be sorted by position" do - product.images.map(&:alt).should eq(["position 1", "position 2"]) - end - - end - -end diff --git a/core/spec/models/return_authorization_spec.rb b/core/spec/models/return_authorization_spec.rb deleted file mode 100644 index d14e9f61332..00000000000 --- a/core/spec/models/return_authorization_spec.rb +++ /dev/null @@ -1,125 +0,0 @@ -require 'spec_helper' - -describe Spree::ReturnAuthorization do - let(:inventory_unit) { Spree::InventoryUnit.create({:variant => mock_model(Spree::Variant)}, :without_protection => true) } - let(:order) { mock_model(Spree::Order, :inventory_units => [inventory_unit], :awaiting_return? => false) } - let(:return_authorization) { Spree::ReturnAuthorization.new({:order => order}, :without_protection => true) } - - before { inventory_unit.stub(:shipped?).and_return(true) } - - context "save" do - it "should be invalid when order has no inventory units" do - inventory_unit.stub(:shipped?).and_return(false) - return_authorization.save - return_authorization.errors[:order].should == ["has no shipped units"] - end - - it "should generate RMA number" do - return_authorization.should_receive(:generate_number) - return_authorization.save - end - end - - context "add_variant" do - context "on empty rma" do - it "should associate inventory unit" do - order.stub(:authorize_return!) - return_authorization.add_variant(inventory_unit.variant.id, 1) - return_authorization.inventory_units.size.should == 1 - inventory_unit.return_authorization.should == return_authorization - end - - it "should update order state" do - order.should_receive(:authorize_return!) - return_authorization.add_variant(inventory_unit.variant.id, 1) - end - end - - context "on rma that already has inventory_units" do - let(:inventory_unit_2) { Spree::InventoryUnit.create({:variant => inventory_unit.variant}, :without_protection => true) } - before { order.stub(:inventory_units => [inventory_unit, inventory_unit_2], :awaiting_return? => true) } - - it "should associate inventory unit" do - order.stub(:authorize_return!) - return_authorization.add_variant(inventory_unit.variant.id, 2) - return_authorization.inventory_units.size.should == 2 - inventory_unit_2.return_authorization.should == return_authorization - end - - it "should not update order state" do - order.should_not_receive(:authorize_return!) - return_authorization.add_variant(inventory_unit.variant.id, 1) - end - - end - - end - - context "can_receive?" do - it "should allow_receive when inventory units assigned" do - return_authorization.stub(:inventory_units => [inventory_unit]) - return_authorization.can_receive?.should be_true - end - - it "should not allow_receive with no inventory units" do - return_authorization.can_receive?.should be_false - end - end - - context "receive!" do - before do - inventory_unit.stub(:state => "shipped", :return! => true) - return_authorization.stub(:inventory_units => [inventory_unit], :amount => -20) - Spree::Adjustment.stub(:create) - order.stub(:update!) - end - - it "should mark all inventory units are returned" do - inventory_unit.should_receive(:return!) - return_authorization.receive! - end - - it "should add credit for specified amount" do - mock_adjustment = mock - mock_adjustment.should_receive(:source=).with(return_authorization) - mock_adjustment.should_receive(:adjustable=).with(order) - mock_adjustment.should_receive(:save) - Spree::Adjustment.should_receive(:new).with(:amount => -20, :label => I18n.t(:rma_credit)).and_return(mock_adjustment) - return_authorization.receive! - end - - it "should update order state" do - order.should_receive :update! - return_authorization.receive! - end - end - - context "force_positive_amount" do - it "should ensure the amount is always positive" do - return_authorization.amount = -10 - return_authorization.send :force_positive_amount - return_authorization.amount.should == 10 - end - end - - context "after_save" do - it "should run correct callbacks" do - return_authorization.should_receive(:force_positive_amount) - return_authorization.run_callbacks(:save, :after) - end - end - - context "currency" do - before { order.stub(:currency) { "ABC" } } - it "returns the order currency" do - return_authorization.currency.should == "ABC" - end - end - - context "display_amount" do - it "retuns a Spree::Money" do - return_authorization.amount = 21.22 - return_authorization.display_amount.should == Spree::Money.new(21.22) - end - end -end diff --git a/core/spec/models/shipment_spec.rb b/core/spec/models/shipment_spec.rb deleted file mode 100644 index ecc76d89f8d..00000000000 --- a/core/spec/models/shipment_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -require 'spec_helper' - -describe Spree::Shipment do - before(:each) do - reset_spree_preferences - end - - let(:order) { mock_model Spree::Order, :backordered? => false, :complete? => true, :currency => "USD" } - let(:shipping_method) { mock_model Spree::ShippingMethod, :calculator => mock('calculator') } - let(:shipment) do - shipment = Spree::Shipment.new :order => order, :shipping_method => shipping_method - shipment.state = 'pending' - shipment - end - - let(:charge) { mock_model Spree::Adjustment, :amount => 10, :source => shipment } - - context "#cost" do - it "should return the amount of any shipping charges that it originated" do - shipment.stub_chain :adjustment, :amount => 10 - shipment.cost.should == 10 - end - - it "should return 0 if there are no relevant shipping adjustments" do - shipment.cost.should == 0 - end - end - - context "#update!" do - - shared_examples_for "immutable once shipped" do - it "should remain in shipped state once shipped" do - shipment.state = "shipped" - shipment.should_receive(:update_column).with("state", "shipped") - shipment.update!(order) - end - end - - shared_examples_for "pending if backordered" do - it "should have a state of pending if backordered" do - shipment.stub(:inventory_units => [mock_model(Spree::InventoryUnit, :backordered? => true)] ) - shipment.should_receive(:update_column).with("state", "pending") - shipment.update!(order) - end - end - - context "when order is incomplete" do - before { order.stub :complete? => false } - it "should result in a 'pending' state" do - shipment.should_receive(:update_column).with("state", "pending") - shipment.update!(order) - end - end - - context "when order is paid" do - before { order.stub :paid? => true } - it "should result in a 'ready' state" do - shipment.should_receive(:update_column).with("state", "ready") - shipment.update!(order) - end - it_should_behave_like "immutable once shipped" - it_should_behave_like "pending if backordered" - end - - context "when order has balance due" do - before { order.stub :paid? => false } - it "should result in a 'pending' state" do - shipment.state = 'ready' - shipment.should_receive(:update_column).with("state", "pending") - shipment.update!(order) - end - it_should_behave_like "immutable once shipped" - it_should_behave_like "pending if backordered" - end - - context "when order has a credit owed" do - before { order.stub :payment_state => 'credit_owed', :paid? => true } - it "should result in a 'ready' state" do - shipment.state = 'pending' - shipment.should_receive(:update_column).with("state", "ready") - shipment.update!(order) - end - it_should_behave_like "immutable once shipped" - it_should_behave_like "pending if backordered" - end - - context "when shipment state changes to shipped" do - it "should call after_ship" do - shipment.state = "pending" - shipment.should_receive :after_ship - shipment.stub :determine_state => 'shipped' - shipment.should_receive(:update_column).with("state", "shipped") - shipment.update!(order) - end - end - end - - context "when track_inventory is false" do - - before { Spree::Config.set :track_inventory_levels => false } - after { Spree::Config.set :track_inventory_levels => true } - - it "should not use the line items from order when track_inventory_levels is false" do - line_items = [mock_model(Spree::LineItem)] - order.stub :complete? => true - order.stub :line_items => line_items - shipment.line_items.should == line_items - end - - end - - context "when order is completed" do - after { Spree::Config.set :track_inventory_levels => true } - - before do - order.stub :completed? => true - order.stub :canceled? => false - end - - - context "with inventory tracking" do - before { Spree::Config.set :track_inventory_levels => true } - - it "should not validate without inventory" do - shipment.valid?.should be_false - end - - it "should validate with inventory" do - shipment.inventory_units = [create(:inventory_unit)] - shipment.valid?.should be_true - end - - end - - context "without inventory tracking" do - before { Spree::Config.set :track_inventory_levels => false } - - it "should validate with no inventory" do - shipment.valid?.should be_true - end - end - - end - - context "#ship" do - before do - order.stub(:update!) - shipment.stub(:require_inventory => false, :update_order => true, :state => 'ready') - shipping_method.stub(:create_adjustment) - end - - it "should update shipped_at timestamp" do - shipment.stub(:send_shipped_email) - shipment.ship! - shipment.shipped_at.should_not be_nil - # Ensure value is persisted - shipment.reload - shipment.shipped_at.should_not be_nil - end - - it "should send a shipment email" do - mail_message = mock "Mail::Message" - Spree::ShipmentMailer.should_receive(:shipped_email).with(shipment).and_return mail_message - mail_message.should_receive :deliver - shipment.ship! - end - end - - context "#ready" do - # Regression test for #2040 - it "cannot ready a shipment for an order if the order is unpaid" do - order.stub(:paid? => false) - assert !shipment.can_ready? - end - end - - context "ensure_correct_adjustment" do - before { shipment.stub(:reload) } - - it "should create adjustment when not present" do - shipping_method.should_receive(:create_adjustment).with(I18n.t(:shipping), order, shipment, true) - shipment.send(:ensure_correct_adjustment) - end - - it "should update originator when adjustment is present" do - shipment.stub_chain(:adjustment, :originator) - shipment.adjustment.should_receive(:originator=).with(shipping_method) - shipment.adjustment.should_receive(:save) - shipment.send(:ensure_correct_adjustment) - end - end - - context "update_order" do - it "should update order" do - order.should_receive(:update!) - shipment.send(:update_order) - end - end - - context "after_save" do - it "should run correct callbacks" do - shipment.should_receive(:ensure_correct_adjustment) - shipment.should_receive(:update_order) - shipment.run_callbacks(:save, :after) - end - end - - context "currency" do - it "returns the order currency" do - shipment.currency.should == order.currency - end - end - - context "display_cost" do - it "retuns a Spree::Money" do - shipment.stub(:cost) { 21.22 } - shipment.display_cost.should == Spree::Money.new(21.22) - end - end -end diff --git a/core/spec/models/shipping_category_spec.rb b/core/spec/models/shipping_category_spec.rb deleted file mode 100644 index 089f5afc159..00000000000 --- a/core/spec/models/shipping_category_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -describe Spree::ShippingCategory do - -end diff --git a/core/spec/models/shipping_method_spec.rb b/core/spec/models/shipping_method_spec.rb deleted file mode 100644 index 7b9375d8194..00000000000 --- a/core/spec/models/shipping_method_spec.rb +++ /dev/null @@ -1,221 +0,0 @@ -require 'spec_helper' - -describe Spree::ShippingMethod do - context 'factory' do - let(:shipping_method){ Factory :shipping_method } - - it "should set calculable correctly" do - shipping_method.calculator.calculable.should == shipping_method - end - end - - context 'available?' do - before(:each) do - @shipping_method = create(:shipping_method) - variant = create(:variant, :product => create(:product)) - @order = create(:order, :line_items => [create(:line_item, :variant => variant)]) - end - - context "when the calculator is not available" do - before { @shipping_method.calculator.stub(:available? => false) } - - it "should be false" do - @shipping_method.available?(@order).should be_false - end - end - - context "when the calculator is available" do - before { @shipping_method.calculator.stub(:available? => true) } - - it "should be true" do - @shipping_method.available?(@order).should be_true - end - end - - context "whe the calculator is front_end" do - before { @shipping_method.display_on = 'front_end' } - - context "and the order is processed through the front_end" do - it "should be true" do - @shipping_method.available?(@order, :front_end).should be_true - end - - it "should be false" do - @shipping_method.available?(@order, :back_end).should be_false - end - end - end - - context "whe the calculator is back_end" do - before { @shipping_method.display_on = 'back_end' } - - context "and the order is processed through the back_end" do - it "should be false" do - @shipping_method.available?(@order, :front_end).should be_false - end - - it "should be true" do - @shipping_method.available?(@order, :back_end).should be_true - end - end - end - end - - context 'available_to_order?' do - before(:each) do - @shipping_method = create(:shipping_method) - @shipping_method.zone.stub(:include? => true) - @shipping_method.stub(:available? => true) - variant = create(:variant, :product => create(:product)) - @order = create(:order, :line_items => [create(:line_item, :variant => variant)]) - end - - context "when availability_check is false" do - before { @shipping_method.stub(:available? => false) } - - it "should be false" do - @shipping_method.available_to_order?(@order).should be_false - end - end - - context "when zone_check is false" do - before { @shipping_method.zone.stub(:include? => false) } - - it "should be false" do - @shipping_method.available_to_order?(@order).should be_false - end - end - - context "when category_check is false" do - before { @shipping_method.stub(:category_match? => false) } - - it "should be false" do - @shipping_method.available_to_order?(@order).should be_false - end - end - - context "when currency_check is false" do - before { @shipping_method.stub(:currency_match? => false) } - - it "should be false" do - @shipping_method.available_to_order?(@order).should be_false - end - end - - context "when all checks are true" do - it "should be true" do - @shipping_method.available_to_order?(@order).should be_true - end - end - end - - context "#category_match?" do - context "when no category is specified" do - before(:each) do - @shipping_method = create(:shipping_method) - end - - it "should return true" do - @shipping_method.category_match?(create(:order)).should be_true - end - end - - context "when a category is specified" do - before { @shipping_method = create(:shipping_method_with_category) } - - context "when all products match" do - before(:each) do - variant = create(:variant, :product => create(:product, :shipping_category => @shipping_method.shipping_category)) - @order = create(:order, :line_items => [create(:line_item, :variant => variant)]) - end - - context "when rule is every match" do - before { @shipping_method.match_all = true } - - it "should return true" do - @shipping_method.category_match?(@order).should be_true - end - end - - context "when rule is at least one match" do - before { @shipping_method.match_one = true } - - it "should return true" do - @shipping_method.category_match?(@order).should be_true - end - end - - context "when rule is none match" do - before { @shipping_method.match_none = true } - - it "should return false" do - @shipping_method.category_match?(@order).should be_false - end - end - end - - context "when no products match" do - before(:each) do - variant = create(:variant, :product => create(:product, :shipping_category => create(:shipping_category))) - @order = create(:order, :line_items => [create(:line_item, :variant => variant)]) - end - - context "when rule is every match" do - before { @shipping_method.match_all = true } - - it "should return false" do - @shipping_method.category_match?(@order).should be_false - end - end - - context "when rule is at least one match" do - before { @shipping_method.match_one = true } - - it "should return false" do - @shipping_method.category_match?(@order).should be_false - end - end - - context "when rule is none match" do - before { @shipping_method.match_none = true } - - it "should return true" do - @shipping_method.category_match?(@order).should be_true - end - end - end - - context "when some products match" do - before(:each) do - variant1 = create(:variant, :product => create(:product, :shipping_category => @shipping_method.shipping_category)) - variant2 = create(:variant, :product => create(:product, :shipping_category => create(:shipping_category))) - @order = create(:order, :line_items => [create(:line_item, :variant => variant1), create(:line_item, :variant => variant2)]) - end - - context "when rule is every match" do - before { @shipping_method.match_all = true } - - it "should return false" do - @shipping_method.category_match?(@order).should be_false - end - end - - context "when rule is at least one match" do - before { @shipping_method.match_one = true } - - it "should return true" do - @shipping_method.category_match?(@order).should be_true - end - end - - context "when rule is none match" do - before { @shipping_method.match_none = true } - - it "should return false" do - @shipping_method.category_match?(@order).should be_false - end - end - end - end - end -end diff --git a/core/spec/models/shipping_rate_spec.rb b/core/spec/models/shipping_rate_spec.rb deleted file mode 100644 index e30524864cb..00000000000 --- a/core/spec/models/shipping_rate_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -describe Spree::ShippingRate do - let(:shipping_rate) { Spree::ShippingRate.new(:cost => 10.55) } - before { Spree::TaxRate.stub(:default => 0.05) } - - context "#display_price" do - context "when shipment includes VAT" do - before { Spree::Config[:shipment_inc_vat] = true } - it "displays the correct price" do - shipping_rate.display_price.to_s.should == "$11.08" # $10.55 * 1.05 == $11.08 - end - end - - context "when shipment does not include VAT" do - before { Spree::Config[:shipment_inc_vat] = false } - it "displays the correct price" do - shipping_rate.display_price.to_s.should == "$10.55" - end - end - - context "when the currency is JPY" do - let(:shipping_rate) { Spree::ShippingRate.new(:cost => 205, :currency => "JPY") } - - it "displays the price in yen" do - shipping_rate.display_price.to_s.should == "¥205" - end - end - end -end diff --git a/core/spec/models/spree/ability_spec.rb b/core/spec/models/spree/ability_spec.rb new file mode 100644 index 00000000000..9007a7cfca6 --- /dev/null +++ b/core/spec/models/spree/ability_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' +require 'cancan/matchers' +require 'spree/testing_support/ability_helpers' +require 'spree/testing_support/bar_ability' + +# Fake ability for testing registration of additional abilities +class FooAbility + include CanCan::Ability + + def initialize(user) + # allow anyone to perform index on Order + can :index, Spree::Order + # allow anyone to update an Order with id of 1 + can :update, Spree::Order do |order| + order.id == 1 + end + end +end + +describe Spree::Ability, :type => :model do + let(:user) { create(:user) } + let(:ability) { Spree::Ability.new(user) } + let(:token) { nil } + + before do + user.spree_roles.clear + end + + TOKEN = 'token123' + + after(:each) { + Spree::Ability.abilities = Set.new + user.spree_roles = [] + } + + context 'register_ability' do + it 'should add the ability to the list of abilties' do + Spree::Ability.register_ability(FooAbility) + expect(Spree::Ability.new(user).abilities).not_to be_empty + end + + it 'should apply the registered abilities permissions' do + Spree::Ability.register_ability(FooAbility) + expect(Spree::Ability.new(user).can?(:update, mock_model(Spree::Order, :id => 1))).to be true + end + end + + context 'for general resource' do + let(:resource) { Object.new } + + context 'with admin user' do + before(:each) { allow(user).to receive(:has_spree_role?).and_return(true) } + it_should_behave_like 'access granted' + it_should_behave_like 'index allowed' + end + + context 'with customer' do + it_should_behave_like 'access denied' + it_should_behave_like 'no index allowed' + end + end + + context 'for admin protected resources' do + let(:resource) { Object.new } + let(:resource_shipment) { Spree::Shipment.new } + let(:resource_product) { Spree::Product.new } + let(:resource_user) { Spree.user_class.new } + let(:resource_order) { Spree::Order.new } + let(:fakedispatch_user) { Spree.user_class.new } + let(:fakedispatch_ability) { Spree::Ability.new(fakedispatch_user) } + + context 'with admin user' do + it 'should be able to admin' do + user.spree_roles << Spree::Role.find_or_create_by(name: 'admin') + expect(ability).to be_able_to :admin, resource + expect(ability).to be_able_to :index, resource_order + expect(ability).to be_able_to :show, resource_product + expect(ability).to be_able_to :create, resource_user + end + end + + context 'with fakedispatch user' do + it 'should be able to admin on the order and shipment pages' do + user.spree_roles << Spree::Role.find_or_create_by(name: 'bar') + + Spree::Ability.register_ability(BarAbility) + + expect(ability).not_to be_able_to :admin, resource + + expect(ability).to be_able_to :admin, resource_order + expect(ability).to be_able_to :index, resource_order + expect(ability).not_to be_able_to :update, resource_order + # ability.should_not be_able_to :create, resource_order # Fails + + expect(ability).to be_able_to :admin, resource_shipment + expect(ability).to be_able_to :index, resource_shipment + expect(ability).to be_able_to :create, resource_shipment + + expect(ability).not_to be_able_to :admin, resource_product + expect(ability).not_to be_able_to :update, resource_product + # ability.should_not be_able_to :show, resource_product # Fails + + expect(ability).not_to be_able_to :admin, resource_user + expect(ability).not_to be_able_to :update, resource_user + expect(ability).to be_able_to :update, user + # ability.should_not be_able_to :create, resource_user # Fails + # It can create new users if is has access to the :admin, User!! + + # TODO change the Ability class so only users and customers get the extra premissions? + + Spree::Ability.remove_ability(BarAbility) + end + end + + context 'with customer' do + it 'should not be able to admin' do + expect(ability).not_to be_able_to :admin, resource + expect(ability).not_to be_able_to :admin, resource_order + expect(ability).not_to be_able_to :admin, resource_product + expect(ability).not_to be_able_to :admin, resource_user + end + end + end + + context 'as Guest User' do + + context 'for Country' do + let(:resource) { Spree::Country.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for OptionType' do + let(:resource) { Spree::OptionType.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for OptionValue' do + let(:resource) { Spree::OptionType.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for Order' do + let(:resource) { Spree::Order.new } + + context 'requested by same user' do + before(:each) { resource.user = user } + it_should_behave_like 'access granted' + it_should_behave_like 'no index allowed' + end + + context 'requested by other user' do + before(:each) { resource.user = Spree.user_class.new } + it_should_behave_like 'create only' + end + + context 'requested with proper token' do + let(:token) { 'TOKEN123' } + before(:each) { allow(resource).to receive_messages guest_token: 'TOKEN123' } + it_should_behave_like 'access granted' + it_should_behave_like 'no index allowed' + end + + context 'requested with inproper token' do + let(:token) { 'FAIL' } + before(:each) { allow(resource).to receive_messages guest_token: 'TOKEN123' } + it_should_behave_like 'create only' + end + end + + context 'for Product' do + let(:resource) { Spree::Product.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for ProductProperty' do + let(:resource) { Spree::Product.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for Property' do + let(:resource) { Spree::Product.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for State' do + let(:resource) { Spree::State.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for Taxons' do + let(:resource) { Spree::Taxon.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for Taxonomy' do + let(:resource) { Spree::Taxonomy.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for User' do + context 'requested by same user' do + let(:resource) { user } + it_should_behave_like 'access granted' + it_should_behave_like 'no index allowed' + end + context 'requested by other user' do + let(:resource) { Spree.user_class.new } + it_should_behave_like 'create only' + end + end + + context 'for Variant' do + let(:resource) { Spree::Variant.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + context 'for Zone' do + let(:resource) { Spree::Zone.new } + context 'requested by any user' do + it_should_behave_like 'read only' + end + end + + end + +end diff --git a/core/spec/models/spree/address_spec.rb b/core/spec/models/spree/address_spec.rb new file mode 100644 index 00000000000..9622b8f65a5 --- /dev/null +++ b/core/spec/models/spree/address_spec.rb @@ -0,0 +1,284 @@ +require 'spec_helper' + +describe Spree::Address, :type => :model do + + subject { Spree::Address } + + describe "clone" do + it "creates a copy of the address with the exception of the id, updated_at and created_at attributes" do + state = create(:state) + original = create(:address, + :address1 => 'address1', + :address2 => 'address2', + :alternative_phone => 'alternative_phone', + :city => 'city', + :country => Spree::Country.first, + :firstname => 'firstname', + :lastname => 'lastname', + :company => 'company', + :phone => 'phone', + :state_id => state.id, + :state_name => state.name, + :zipcode => '10001') + + cloned = original.clone + + expect(cloned.address1).to eq(original.address1) + expect(cloned.address2).to eq(original.address2) + expect(cloned.alternative_phone).to eq(original.alternative_phone) + expect(cloned.city).to eq(original.city) + expect(cloned.country_id).to eq(original.country_id) + expect(cloned.firstname).to eq(original.firstname) + expect(cloned.lastname).to eq(original.lastname) + expect(cloned.company).to eq(original.company) + expect(cloned.phone).to eq(original.phone) + expect(cloned.state_id).to eq(original.state_id) + expect(cloned.state_name).to eq(original.state_name) + expect(cloned.zipcode).to eq(original.zipcode) + + expect(cloned.id).not_to eq(original.id) + expect(cloned.created_at).not_to eq(original.created_at) + expect(cloned.updated_at).not_to eq(original.updated_at) + end + end + + context "aliased attributes" do + let(:address) { Spree::Address.new } + + it "first_name" do + address.firstname = "Ryan" + expect(address.first_name).to eq("Ryan") + end + + it "last_name" do + address.lastname = "Bigg" + expect(address.last_name).to eq("Bigg") + end + end + + context "validation" do + before do + configure_spree_preferences do |config| + config.address_requires_state = true + end + end + + let(:country) { mock_model(Spree::Country, :states => [state], :states_required => true) } + let(:state) { stub_model(Spree::State, :name => 'maryland', :abbr => 'md') } + let(:address) { build(:address, :country => country) } + + before do + allow(country.states).to receive_messages :find_all_by_name_or_abbr => [state] + end + + it "state_name is not nil and country does not have any states" do + address.state = nil + address.state_name = 'alabama' + expect(address).to be_valid + end + + it "errors when state_name is nil" do + address.state_name = nil + address.state = nil + expect(address).not_to be_valid + end + + it "full state name is in state_name and country does contain that state" do + address.state_name = 'alabama' + # called by state_validate to set up state_id. + # Perhaps this should be a before_validation instead? + expect(address).to be_valid + expect(address.state).not_to be_nil + expect(address.state_name).to be_nil + end + + it "state abbr is in state_name and country does contain that state" do + address.state_name = state.abbr + expect(address).to be_valid + expect(address.state_id).not_to be_nil + expect(address.state_name).to be_nil + end + + it "state is entered but country does not contain that state" do + address.state = state + address.country = stub_model(Spree::Country, :states_required => true) + address.valid? + expect(address.errors["state"]).to eq(['is invalid']) + end + + it "both state and state_name are entered but country does not contain the state" do + address.state = state + address.state_name = 'maryland' + address.country = stub_model(Spree::Country, :states_required => true) + expect(address).to be_valid + expect(address.state_id).to be_nil + end + + it "both state and state_name are entered and country does contain the state" do + address.state = state + address.state_name = 'maryland' + expect(address).to be_valid + expect(address.state_name).to be_nil + end + + it "address_requires_state preference is false" do + Spree::Config.set :address_requires_state => false + address.state = nil + address.state_name = nil + expect(address).to be_valid + end + + it "requires phone" do + address.phone = "" + address.valid? + expect(address.errors["phone"]).to eq(["can't be blank"]) + end + + it "requires zipcode" do + address.zipcode = "" + address.valid? + expect(address.errors['zipcode']).to include("can't be blank") + end + + context "zipcode validation" do + it "validates the zipcode" do + allow(address.country).to receive(:iso).and_return('US') + address.zipcode = 'abc' + address.valid? + expect(address.errors['zipcode']).to include('is invalid') + end + + context 'does not validate' do + it 'does not have a country' do + address.country = nil + address.valid? + expect(address.errors['zipcode']).not_to include('is invalid') + end + + it 'does not have an iso' do + allow(address.country).to receive(:iso).and_return(nil) + address.valid? + expect(address.errors['zipcode']).not_to include('is invalid') + end + + it 'does not have a zipcode' do + address.zipcode = "" + address.valid? + expect(address.errors['zipcode']).not_to include('is invalid') + end + + it 'does not have a supported country iso' do + allow(address.country).to receive(:iso).and_return('BO') + address.valid? + expect(address.errors['zipcode']).not_to include('is invalid') + end + end + end + + context "phone not required" do + before { allow(address).to receive_messages require_phone?: false } + + it "shows no errors when phone is blank" do + address.phone = "" + address.valid? + expect(address.errors[:phone].size).to eq 0 + end + end + + context "zipcode not required" do + before { allow(address).to receive_messages require_zipcode?: false } + + it "shows no errors when phone is blank" do + address.zipcode = "" + address.valid? + expect(address.errors[:zipcode].size).to eq 0 + end + end + end + + context ".default" do + context "no user given" do + before do + @default_country_id = Spree::Config[:default_country_id] + new_country = create(:country) + Spree::Config[:default_country_id] = new_country.id + end + + after do + Spree::Config[:default_country_id] = @default_country_id + end + + it "sets up a new record with Spree::Config[:default_country_id]" do + expect(Spree::Address.default.country).to eq(Spree::Country.find(Spree::Config[:default_country_id])) + end + + # Regression test for #1142 + it "uses the first available country if :default_country_id is set to an invalid value" do + Spree::Config[:default_country_id] = "0" + expect(Spree::Address.default.country).to eq(Spree::Country.first) + end + end + + context "user given" do + let(:bill_address) { Spree::Address.new(phone: Time.now.to_i) } + let(:ship_address) { double("ShipAddress") } + let(:user) { double("User", bill_address: bill_address, ship_address: ship_address) } + + it "returns a copy of that user bill address" do + expect(subject.default(user).phone).to eq bill_address.phone + end + + it "falls back to build default when user has no address" do + allow(user).to receive_messages(bill_address: nil) + expect(subject.default(user)).to eq subject.build_default + end + end + end + + context '#full_name' do + context 'both first and last names are present' do + let(:address) { stub_model(Spree::Address, :firstname => 'Michael', :lastname => 'Jackson') } + specify { expect(address.full_name).to eq('Michael Jackson') } + end + + context 'first name is blank' do + let(:address) { stub_model(Spree::Address, :firstname => nil, :lastname => 'Jackson') } + specify { expect(address.full_name).to eq('Jackson') } + end + + context 'last name is blank' do + let(:address) { stub_model(Spree::Address, :firstname => 'Michael', :lastname => nil) } + specify { expect(address.full_name).to eq('Michael') } + end + + context 'both first and last names are blank' do + let(:address) { stub_model(Spree::Address, :firstname => nil, :lastname => nil) } + specify { expect(address.full_name).to eq('') } + end + + end + + context '#state_text' do + context 'state is blank' do + let(:address) { stub_model(Spree::Address, :state => nil, :state_name => 'virginia') } + specify { expect(address.state_text).to eq('virginia') } + end + + context 'both name and abbr is present' do + let(:state) { stub_model(Spree::State, :name => 'virginia', :abbr => 'va') } + let(:address) { stub_model(Spree::Address, :state => state) } + specify { expect(address.state_text).to eq('va') } + end + + context 'only name is present' do + let(:state) { stub_model(Spree::State, :name => 'virginia', :abbr => nil) } + let(:address) { stub_model(Spree::Address, :state => state) } + specify { expect(address.state_text).to eq('virginia') } + end + end + + context "defines require_phone? helper method" do + let(:address) { stub_model(Spree::Address) } + specify { expect(address.instance_eval{ require_phone? }).to be true} + end +end diff --git a/core/spec/models/spree/adjustment_spec.rb b/core/spec/models/spree/adjustment_spec.rb new file mode 100644 index 00000000000..49cfd813c76 --- /dev/null +++ b/core/spec/models/spree/adjustment_spec.rb @@ -0,0 +1,140 @@ +# encoding: utf-8 +# + +require 'spec_helper' + +describe Spree::Adjustment, :type => :model do + + let(:order) { Spree::Order.new } + + before do + allow(order).to receive(:update!) + end + + let(:adjustment) { Spree::Adjustment.create!(label: 'Adjustment', adjustable: order, order: order, amount: 5) } + + context '#create & #destroy' do + let(:adjustment) { Spree::Adjustment.new(label: "Adjustment", amount: 5, order: order, adjustable: create(:line_item)) } + + it 'calls #update_adjustable_adjustment_total' do + expect(adjustment).to receive(:update_adjustable_adjustment_total).twice + adjustment.save + adjustment.destroy + end + end + + context '#save' do + let(:adjustment) { Spree::Adjustment.create(label: "Adjustment", amount: 5, order: order, adjustable: create(:line_item)) } + + it 'touches the adjustable' do + expect(adjustment.adjustable).to receive(:touch) + adjustment.save + end + end + + describe 'non_tax scope' do + subject do + Spree::Adjustment.non_tax.to_a + end + + let!(:tax_adjustment) { create(:adjustment, order: order, source: create(:tax_rate)) } + let!(:non_tax_adjustment_with_source) { create(:adjustment, order: order, source_type: 'Spree::Order', source_id: nil) } + let!(:non_tax_adjustment_without_source) { create(:adjustment, order: order, source: nil) } + + it 'select non-tax adjustments' do + expect(subject).to_not include tax_adjustment + expect(subject).to include non_tax_adjustment_with_source + expect(subject).to include non_tax_adjustment_without_source + end + end + + context "adjustment state" do + let(:adjustment) { create(:adjustment, order: order, state: 'open') } + + context "#closed?" do + it "is true when adjustment state is closed" do + adjustment.state = "closed" + expect(adjustment).to be_closed + end + + it "is false when adjustment state is open" do + adjustment.state = "open" + expect(adjustment).to_not be_closed + end + end + end + + context '#currency' do + it 'returns the globally configured currency' do + expect(adjustment.currency).to eq 'USD' + end + end + + context "#display_amount" do + before { adjustment.amount = 10.55 } + + context "with display_currency set to true" do + before { Spree::Config[:display_currency] = true } + + it "shows the currency" do + expect(adjustment.display_amount.to_s).to eq "$10.55 USD" + end + end + + context "with display_currency set to false" do + before { Spree::Config[:display_currency] = false } + + it "does not include the currency" do + expect(adjustment.display_amount.to_s).to eq "$10.55" + end + end + + context "with currency set to JPY" do + context "when adjustable is set to an order" do + before do + expect(order).to receive(:currency).and_return('JPY') + adjustment.adjustable = order + end + + it "displays in JPY" do + expect(adjustment.display_amount.to_s).to eq "¥11" + end + end + + context "when adjustable is nil" do + it "displays in the default currency" do + expect(adjustment.display_amount.to_s).to eq "$10.55" + end + end + end + end + + context '#update!' do + # Regression test for #6689 + it "correctly calculates for adjustments with no source" do + expect(adjustment.update!).to eq 5 + end + + context "when adjustment is closed" do + before { expect(adjustment).to receive(:closed?).and_return(true) } + + it "does not update the adjustment" do + expect(adjustment).to_not receive(:update_column) + adjustment.update! + end + end + + context "when adjustment is open" do + before { expect(adjustment).to receive(:closed?).and_return(false) } + + it "updates the amount" do + expect(adjustment).to receive(:adjustable).and_return(double("Adjustable")).at_least(1).times + expect(adjustment).to receive(:source).and_return(double("Source")).at_least(1).times + expect(adjustment.source).to receive("compute_amount").with(adjustment.adjustable).and_return(5) + expect(adjustment).to receive(:update_columns).with(amount: 5, updated_at: kind_of(Time)) + adjustment.update! + end + end + end + +end diff --git a/core/spec/models/spree/app_configuration_spec.rb b/core/spec/models/spree/app_configuration_spec.rb new file mode 100644 index 00000000000..880b51c6d85 --- /dev/null +++ b/core/spec/models/spree/app_configuration_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Spree::AppConfiguration, :type => :model do + + let (:prefs) { Rails.application.config.spree.preferences } + + it "should be available from the environment" do + prefs.layout = "my/layout" + expect(prefs.layout).to eq "my/layout" + end + + it "should be available as Spree::Config for legacy access" do + Spree::Config.layout = "my/layout" + expect(Spree::Config.layout).to eq "my/layout" + end + + it "uses base searcher class by default" do + prefs.searcher_class = nil + expect(prefs.searcher_class).to eq Spree::Core::Search::Base + end + +end + diff --git a/core/spec/models/spree/asset_spec.rb b/core/spec/models/spree/asset_spec.rb new file mode 100644 index 00000000000..f88bdf88018 --- /dev/null +++ b/core/spec/models/spree/asset_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Spree::Asset, :type => :model do + describe "#viewable" do + it "touches association" do + product = create(:custom_product) + asset = Spree::Asset.create! { |a| a.viewable = product.master } + + expect do + asset.save + end.to change { product.reload.updated_at } + end + end + + describe "#acts_as_list scope" do + it "should start from first position for different viewables" do + asset1 = Spree::Asset.create(viewable_type: 'Spree::Image', viewable_id: 1) + asset2 = Spree::Asset.create(viewable_type: 'Spree::LineItem', viewable_id: 1) + + expect(asset1.position).to eq 1 + expect(asset2.position).to eq 1 + end + end + +end diff --git a/core/spec/models/spree/calculator/default_tax_spec.rb b/core/spec/models/spree/calculator/default_tax_spec.rb new file mode 100644 index 00000000000..57467fd78ce --- /dev/null +++ b/core/spec/models/spree/calculator/default_tax_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' + +describe Spree::Calculator::DefaultTax, :type => :model do + let!(:country) { create(:country) } + let!(:zone) { create(:zone, :name => "Country Zone", :default_tax => true, :zone_members => []) } + let!(:tax_category) { create(:tax_category, :tax_rates => []) } + let!(:rate) { create(:tax_rate, :tax_category => tax_category, :amount => 0.05, :included_in_price => included_in_price) } + let(:included_in_price) { false } + let!(:calculator) { Spree::Calculator::DefaultTax.new(:calculable => rate ) } + let!(:order) { create(:order) } + let!(:line_item) { create(:line_item, :price => 10, :quantity => 3, :tax_category => tax_category) } + let!(:shipment) { create(:shipment, :cost => 15) } + + context "#compute" do + context "when given an order" do + let!(:line_item_1) { line_item } + let!(:line_item_2) { create(:line_item, :price => 10, :quantity => 3, :tax_category => tax_category) } + + before do + allow(order).to receive_messages :line_items => [line_item_1, line_item_2] + end + + context "when no line items match the tax category" do + before do + line_item_1.tax_category = nil + line_item_2.tax_category = nil + end + + it "should be 0" do + expect(calculator.compute(order)).to eq(0) + end + end + + context "when one item matches the tax category" do + before do + line_item_1.tax_category = tax_category + line_item_2.tax_category = nil + end + + it "should be equal to the item total * rate" do + expect(calculator.compute(order)).to eq(1.5) + end + + context "correctly rounds to within two decimal places" do + before do + line_item_1.price = 10.333 + line_item_1.quantity = 1 + end + + specify do + # Amount is 0.51665, which will be rounded to... + expect(calculator.compute(order)).to eq(0.52) + end + + end + end + + context "when more than one item matches the tax category" do + it "should be equal to the sum of the item totals * rate" do + expect(calculator.compute(order)).to eq(3) + end + end + + context "when tax is included in price" do + let(:included_in_price) { true } + + it "will return the deducted amount from the totals" do + # total price including 5% tax = $60 + # ex pre-tax = $57.14 + # 57.14 + %5 = 59.997 (or "close enough" to $60) + # 60 - 57.14 = $2.86 + expect(calculator.compute(order).to_f).to eql 2.86 + end + end + end + + context "when tax is included in price" do + let(:included_in_price) { true } + context "when the variant matches the tax category" do + + context "when line item is discounted" do + before do + line_item.promo_total = -1 + Spree::TaxRate.store_pre_tax_amount(line_item, [rate]) + end + + it "should be equal to the item's discounted total * rate" do + expect(calculator.compute(line_item)).to eql 1.38 + end + end + + it "should be equal to the item's full price * rate" do + Spree::TaxRate.store_pre_tax_amount(line_item, [rate]) + expect(calculator.compute(line_item)).to eql 1.43 + end + end + end + + context "when tax is not included in price" do + context "when the line item is discounted" do + before { line_item.promo_total = -1 } + + it "should be equal to the item's pre-tax total * rate" do + expect(calculator.compute(line_item)).to eq(1.45) + end + end + + context "when the variant matches the tax category" do + it "should be equal to the item pre-tax total * rate" do + expect(calculator.compute(line_item)).to eq(1.50) + end + end + end + + context "when given a shipment" do + it "should be 5% of 15" do + expect(calculator.compute(shipment)).to eq(0.75) + end + + it "takes discounts into consideration" do + shipment.promo_total = -1 + # 5% of 14 + expect(calculator.compute(shipment)).to eq(0.7) + end + end + end +end diff --git a/core/spec/models/spree/calculator/flat_percent_item_total_spec.rb b/core/spec/models/spree/calculator/flat_percent_item_total_spec.rb new file mode 100644 index 00000000000..404f38aa2c1 --- /dev/null +++ b/core/spec/models/spree/calculator/flat_percent_item_total_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Spree::Calculator::FlatPercentItemTotal, :type => :model do + let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new } + let(:line_item) { mock_model Spree::LineItem } + + before { allow(calculator).to receive_messages preferred_flat_percent: 10 } + + context "compute" do + it "should round result correctly" do + allow(line_item).to receive_messages amount: 31.08 + expect(calculator.compute(line_item)).to eq 3.11 + + allow(line_item).to receive_messages amount: 31.00 + expect(calculator.compute(line_item)).to eq 3.10 + end + + it 'returns object.amount if computed amount is greater' do + allow(calculator).to receive_messages preferred_flat_percent: 110 + allow(line_item).to receive_messages amount: 30.00 + + expect(calculator.compute(line_item)).to eq 30.0 + end + end +end diff --git a/core/spec/models/spree/calculator/flat_rate_spec.rb b/core/spec/models/spree/calculator/flat_rate_spec.rb new file mode 100644 index 00000000000..40bb97e8ef8 --- /dev/null +++ b/core/spec/models/spree/calculator/flat_rate_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Spree::Calculator::FlatRate, type: :model do + let(:calculator) { Spree::Calculator::FlatRate.new } + + let(:order) do + mock_model( + Spree::Order, quantity: 10, currency: "USD" + ) + end + + context "compute" do + it "should compute the amount as the rate when currency matches the order's currency" do + calculator.preferred_amount = 25.0 + calculator.preferred_currency = "GBP" + allow(order).to receive_messages currency: "GBP" + expect(calculator.compute(order).round(2)).to eq(25.0) + end + + it "should compute the amount as 0 when currency does not match the order's currency" do + calculator.preferred_amount = 100.0 + calculator.preferred_currency = "GBP" + allow(order).to receive_messages currency: "USD" + expect(calculator.compute(order).round(2)).to eq(0.0) + end + + it "should compute the amount as 0 when currency is blank" do + calculator.preferred_amount = 100.0 + calculator.preferred_currency = "" + allow(order).to receive_messages currency: "GBP" + expect(calculator.compute(order).round(2)).to eq(0.0) + end + + it "should compute the amount as the rate when the currencies use different casing" do + calculator.preferred_amount = 100.0 + calculator.preferred_currency = "gBp" + allow(order).to receive_messages currency: "GBP" + expect(calculator.compute(order).round(2)).to eq(100.0) + end + + it "should compute the amount as 0 when there is no object" do + calculator.preferred_amount = 100.0 + calculator.preferred_currency = "GBP" + expect(calculator.compute.round(2)).to eq(0.0) + end + end +end diff --git a/core/spec/models/spree/calculator/flexi_rate_spec.rb b/core/spec/models/spree/calculator/flexi_rate_spec.rb new file mode 100644 index 00000000000..1d720a814fa --- /dev/null +++ b/core/spec/models/spree/calculator/flexi_rate_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Spree::Calculator::FlexiRate, :type => :model do + let(:calculator) { Spree::Calculator::FlexiRate.new } + + let(:order) do + mock_model( + Spree::Order, quantity: 10 + ) + end + + context "compute" do + it "should compute amount correctly when all fees are 0" do + expect(calculator.compute(order).round(2)).to eq(0.0) + end + + it "should compute amount correctly when first_item has a value" do + allow(calculator).to receive_messages :preferred_first_item => 1.0 + expect(calculator.compute(order).round(2)).to eq(1.0) + end + + it "should compute amount correctly when additional_items has a value" do + allow(calculator).to receive_messages :preferred_additional_item => 1.0 + expect(calculator.compute(order).round(2)).to eq(9.0) + end + + it "should compute amount correctly when additional_items and first_item have values" do + allow(calculator).to receive_messages :preferred_first_item => 5.0, :preferred_additional_item => 1.0 + expect(calculator.compute(order).round(2)).to eq(14.0) + end + + it "should compute amount correctly when additional_items and first_item have values AND max items has value" do + allow(calculator).to receive_messages :preferred_first_item => 5.0, :preferred_additional_item => 1.0, :preferred_max_items => 3 + expect(calculator.compute(order).round(2)).to eq(7.0) + end + + it "should allow creation of new object with all the attributes" do + Spree::Calculator::FlexiRate.new(:preferred_first_item => 1, :preferred_additional_item => 1, :preferred_max_items => 1) + end + end +end diff --git a/core/spec/models/spree/calculator/percent_on_line_item_spec.rb b/core/spec/models/spree/calculator/percent_on_line_item_spec.rb new file mode 100644 index 00000000000..88f8aa095af --- /dev/null +++ b/core/spec/models/spree/calculator/percent_on_line_item_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +module Spree + class Calculator + describe PercentOnLineItem, :type => :model do + let(:line_item) { double("LineItem", amount: 100) } + + before { subject.preferred_percent = 15 } + + it "computes based on item price and quantity" do + expect(subject.compute(line_item)).to eq 15 + end + end + end +end diff --git a/core/spec/models/spree/calculator/price_sack_spec.rb b/core/spec/models/spree/calculator/price_sack_spec.rb new file mode 100644 index 00000000000..d4105c50089 --- /dev/null +++ b/core/spec/models/spree/calculator/price_sack_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Spree::Calculator::PriceSack, :type => :model do + let(:calculator) do + calculator = Spree::Calculator::PriceSack.new + calculator.preferred_minimal_amount = 5 + calculator.preferred_normal_amount = 10 + calculator.preferred_discount_amount = 1 + calculator + end + + let(:order) { stub_model(Spree::Order) } + let(:shipment) { stub_model(Spree::Shipment, :amount => 10) } + + # Regression test for #714 and #739 + it "computes with an order object" do + calculator.compute(order) + end + + # Regression test for #1156 + it "computes with a shipment object" do + calculator.compute(shipment) + end + + # Regression test for #2055 + it "computes the correct amount" do + expect(calculator.compute(2)).to eq(calculator.preferred_normal_amount) + expect(calculator.compute(6)).to eq(calculator.preferred_discount_amount) + end +end diff --git a/core/spec/models/spree/calculator/refunds/default_refund_amount_spec.rb b/core/spec/models/spree/calculator/refunds/default_refund_amount_spec.rb new file mode 100644 index 00000000000..4c9a94eebbc --- /dev/null +++ b/core/spec/models/spree/calculator/refunds/default_refund_amount_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Spree::Calculator::Returns::DefaultRefundAmount, :type => :model do + let(:order) { create(:order) } + let(:line_item_quantity) { 2 } + let(:pre_tax_amount) { 100.0 } + let(:line_item) { create(:line_item, price: 100.0, quantity: line_item_quantity, pre_tax_amount: pre_tax_amount) } + let(:inventory_unit) { build(:inventory_unit, order: order, line_item: line_item) } + let(:return_item) { build(:return_item, inventory_unit: inventory_unit ) } + let(:calculator) { Spree::Calculator::Returns::DefaultRefundAmount.new } + + before { order.line_items << line_item } + + subject { calculator.compute(return_item) } + + context "not an exchange" do + context "no promotions or taxes" do + it { is_expected.to eq pre_tax_amount / line_item_quantity } + end + + context "order adjustments" do + let(:adjustment_amount) { -10.0 } + + before do + order.adjustments << create(:adjustment, order: order, amount: adjustment_amount, eligible: true, label: 'Adjustment', source_type: 'Spree::Order') + order.adjustments.first.update_attributes(amount: adjustment_amount) + end + + it { is_expected.to eq (pre_tax_amount - adjustment_amount.abs) / line_item_quantity } + end + + context "shipping adjustments" do + let(:adjustment_total) { -50.0 } + + before { order.shipments << Spree::Shipment.new(adjustment_total: adjustment_total) } + + it { is_expected.to eq pre_tax_amount / line_item_quantity } + end + end + + context "an exchange" do + let(:return_item) { build(:exchange_return_item) } + + it { is_expected.to eq 0.0 } + end + + context "pre_tax_amount is zero" do + let(:pre_tax_amount) { 0.0 } + it { should eq 0.0 } + end +end diff --git a/core/spec/models/spree/calculator/shipping.rb b/core/spec/models/spree/calculator/shipping.rb new file mode 100644 index 00000000000..cb1e78a6b3d --- /dev/null +++ b/core/spec/models/spree/calculator/shipping.rb @@ -0,0 +1,8 @@ +require_dependency 'spree/calculator' + +module Spree + class Calculator::Shipping < Calculator + def compute(content_items) + end + end +end diff --git a/core/spec/models/spree/calculator/shipping/flat_percent_item_total_spec.rb b/core/spec/models/spree/calculator/shipping/flat_percent_item_total_spec.rb new file mode 100644 index 00000000000..a0e5323bff3 --- /dev/null +++ b/core/spec/models/spree/calculator/shipping/flat_percent_item_total_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +module Spree + module Calculator::Shipping + describe FlatPercentItemTotal, :type => :model do + let(:variant1) { build(:variant, :price => 10.11) } + let(:variant2) { build(:variant, :price => 20.2222) } + + let(:line_item1) { build(:line_item, variant: variant1) } + let(:line_item2) { build(:line_item, variant: variant2) } + + let(:package) do + build(:stock_package, variants_contents: { variant1 => 2, variant2 => 1 }) + end + + subject { FlatPercentItemTotal.new(:preferred_flat_percent => 10) } + + it "should round result correctly" do + expect(subject.compute(package)).to eq(4.04) + end + end + end +end diff --git a/core/spec/models/spree/calculator/shipping/flat_rate_spec.rb b/core/spec/models/spree/calculator/shipping/flat_rate_spec.rb new file mode 100644 index 00000000000..41b84ed9c9b --- /dev/null +++ b/core/spec/models/spree/calculator/shipping/flat_rate_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +module Spree + module Calculator::Shipping + describe FlatRate, :type => :model do + subject { Calculator::Shipping::FlatRate.new(:preferred_amount => 4.00) } + + it 'always returns the same rate' do + expect(subject.compute(build(:stock_package_fulfilled))).to eql 4.00 + end + end + end +end diff --git a/core/spec/models/spree/calculator/shipping/flexi_rate_spec.rb b/core/spec/models/spree/calculator/shipping/flexi_rate_spec.rb new file mode 100644 index 00000000000..f4bda317135 --- /dev/null +++ b/core/spec/models/spree/calculator/shipping/flexi_rate_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +module Spree + module Calculator::Shipping + describe FlexiRate, :type => :model do + let(:variant1) { build(:variant, :price => 10) } + let(:variant2) { build(:variant, :price => 20) } + + let(:package) do + build(:stock_package, variants_contents: { variant1 => 4, variant2 => 6 }) + end + + let(:subject) { FlexiRate.new } + + context "compute" do + it "should compute amount correctly when all fees are 0" do + expect(subject.compute(package).round(2)).to eq(0.0) + end + + it "should compute amount correctly when first_item has a value" do + subject.preferred_first_item = 1.0 + expect(subject.compute(package).round(2)).to eq(1.0) + end + + it "should compute amount correctly when additional_items has a value" do + subject.preferred_additional_item = 1.0 + expect(subject.compute(package).round(2)).to eq(9.0) + end + + it "should compute amount correctly when additional_items and first_item have values" do + subject.preferred_first_item = 5.0 + subject.preferred_additional_item = 1.0 + expect(subject.compute(package).round(2)).to eq(14.0) + end + + it "should compute amount correctly when additional_items and first_item have values AND max items has value" do + subject.preferred_first_item = 5.0 + subject.preferred_additional_item = 1.0 + subject.preferred_max_items = 3 + expect(subject.compute(package).round(2)).to eq(26.0) + end + + it "should allow creation of new object with all the attributes" do + FlexiRate.new(:preferred_first_item => 1, + :preferred_additional_item => 1, + :preferred_max_items => 1) + end + end + end + end +end + diff --git a/core/spec/models/spree/calculator/shipping/per_item_spec.rb b/core/spec/models/spree/calculator/shipping/per_item_spec.rb new file mode 100644 index 00000000000..91446f37e88 --- /dev/null +++ b/core/spec/models/spree/calculator/shipping/per_item_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +module Spree + module Calculator::Shipping + describe PerItem, :type => :model do + let(:variant1) { build(:variant) } + let(:variant2) { build(:variant) } + + let(:package) do + build(:stock_package, variants_contents: { variant1 => 5, variant2 => 3 }) + end + + subject { PerItem.new(:preferred_amount => 10) } + + it "correctly calculates per item shipping" do + expect(subject.compute(package).to_f).to eq(80) # 5 x 10 + 3 x 10 + end + end + end +end diff --git a/core/spec/models/spree/calculator/shipping/price_sack_spec.rb b/core/spec/models/spree/calculator/shipping/price_sack_spec.rb new file mode 100644 index 00000000000..d4105c50089 --- /dev/null +++ b/core/spec/models/spree/calculator/shipping/price_sack_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Spree::Calculator::PriceSack, :type => :model do + let(:calculator) do + calculator = Spree::Calculator::PriceSack.new + calculator.preferred_minimal_amount = 5 + calculator.preferred_normal_amount = 10 + calculator.preferred_discount_amount = 1 + calculator + end + + let(:order) { stub_model(Spree::Order) } + let(:shipment) { stub_model(Spree::Shipment, :amount => 10) } + + # Regression test for #714 and #739 + it "computes with an order object" do + calculator.compute(order) + end + + # Regression test for #1156 + it "computes with a shipment object" do + calculator.compute(shipment) + end + + # Regression test for #2055 + it "computes the correct amount" do + expect(calculator.compute(2)).to eq(calculator.preferred_normal_amount) + expect(calculator.compute(6)).to eq(calculator.preferred_discount_amount) + end +end diff --git a/core/spec/models/spree/calculator/tiered_flat_rate_spec.rb b/core/spec/models/spree/calculator/tiered_flat_rate_spec.rb new file mode 100644 index 00000000000..8c974ed8a7b --- /dev/null +++ b/core/spec/models/spree/calculator/tiered_flat_rate_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Spree::Calculator::TieredFlatRate, :type => :model do + let(:calculator) { Spree::Calculator::TieredFlatRate.new } + + describe "#valid?" do + subject { calculator.valid? } + context "when tiers is not a hash" do + before { calculator.preferred_tiers = ["nope", 0] } + it { is_expected.to be false } + end + context "when tiers is a hash" do + context "and one of the keys is not a positive number" do + before { calculator.preferred_tiers = { "nope" => 20 } } + it { is_expected.to be false } + end + end + end + + describe "#compute" do + let(:line_item) { mock_model Spree::LineItem, amount: amount } + before do + calculator.preferred_base_amount = 10 + calculator.preferred_tiers = { + 100 => 15, + 200 => 20 + } + end + subject { calculator.compute(line_item) } + context "when amount falls within the first tier" do + let(:amount) { 50 } + it { is_expected.to eq 10 } + end + context "when amount falls within the second tier" do + let(:amount) { 150 } + it { is_expected.to eq 15 } + end + end +end + diff --git a/core/spec/models/spree/calculator/tiered_percent_spec.rb b/core/spec/models/spree/calculator/tiered_percent_spec.rb new file mode 100644 index 00000000000..9b2bd4b45be --- /dev/null +++ b/core/spec/models/spree/calculator/tiered_percent_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Spree::Calculator::TieredPercent, :type => :model do + let(:calculator) { Spree::Calculator::TieredPercent.new } + + describe "#valid?" do + subject { calculator.valid? } + context "when base percent is less than zero" do + before { calculator.preferred_base_percent = -1 } + it { is_expected.to be false } + end + context "when base percent is greater than 100" do + before { calculator.preferred_base_percent = 110 } + it { is_expected.to be false } + end + context "when tiers is not a hash" do + before { calculator.preferred_tiers = ["nope", 0] } + it { is_expected.to be false } + end + context "when tiers is a hash" do + context "and one of the keys is not a positive number" do + before { calculator.preferred_tiers = { "nope" => 20 } } + it { is_expected.to be false } + end + context "and one of the values is not a percent" do + before { calculator.preferred_tiers = { 10 => 110 } } + it { is_expected.to be false } + end + end + end + + describe "#compute" do + let(:line_item) { mock_model Spree::LineItem, amount: amount } + before do + calculator.preferred_base_percent = 10 + calculator.preferred_tiers = { + 100 => 15, + 200 => 20 + } + end + subject { calculator.compute(line_item) } + context "when amount falls within the first tier" do + let(:amount) { 50 } + it { is_expected.to eq 5 } + end + context "when amount falls within the second tier" do + let(:amount) { 150 } + it { is_expected.to eq 22 } + end + end +end diff --git a/core/spec/models/spree/calculator_spec.rb b/core/spec/models/spree/calculator_spec.rb new file mode 100644 index 00000000000..6d68089472b --- /dev/null +++ b/core/spec/models/spree/calculator_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Spree::Calculator, :type => :model do + + let(:order) { create(:order) } + let!(:line_item) { create(:line_item, :order => order) } + let(:shipment) { create(:shipment, :order => order, :stock_location => create(:stock_location_with_items)) } + + context "with computable" do + context "and compute methods stubbed out" do + context "with a Spree::LineItem" do + it "calls compute_line_item" do + expect(subject).to receive(:compute_line_item).with(line_item) + subject.compute(line_item) + end + end + + context "with a Spree::Order" do + it "calls compute_order" do + expect(subject).to receive(:compute_order).with(order) + subject.compute(order) + end + end + + context "with a Spree::Shipment" do + it "calls compute_shipment" do + expect(subject).to receive(:compute_shipment).with(shipment) + subject.compute(shipment) + end + end + + context "with a arbitray object" do + it "calls the correct compute" do + s = "Calculator can all" + expect(subject).to receive(:compute_string).with(s) + subject.compute(s) + end + end + end + + context "with no stubbing" do + context "with a Spree::LineItem" do + it "raises NotImplementedError" do + expect{subject.compute(line_item)}.to raise_error NotImplementedError, /Please implement \'compute_line_item\(line_item\)\' in your calculator/ + end + end + + context "with a Spree::Order" do + it "raises NotImplementedError" do + expect{subject.compute(order)}.to raise_error NotImplementedError, /Please implement \'compute_order\(order\)\' in your calculator/ + end + end + + context "with a Spree::Shipment" do + it "raises NotImplementedError" do + expect{subject.compute(shipment)}.to raise_error NotImplementedError, /Please implement \'compute_shipment\(shipment\)\' in your calculator/ + end + end + + context "with a arbitray object" do + it "raises NotImplementedError" do + s = "Calculator can all" + expect{subject.compute(s)}.to raise_error NotImplementedError, /Please implement \'compute_string\(string\)\' in your calculator/ + end + end + end + end + +end diff --git a/core/spec/models/spree/classification_spec.rb b/core/spec/models/spree/classification_spec.rb new file mode 100644 index 00000000000..1ff5ea6bef1 --- /dev/null +++ b/core/spec/models/spree/classification_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +module Spree + describe Classification, :type => :model do + # Regression test for #3494 + it "cannot link the same taxon to the same product more than once" do + product = create(:product) + taxon = create(:taxon) + add_taxon = lambda { product.taxons << taxon } + expect(add_taxon).not_to raise_error + expect(add_taxon).to raise_error(ActiveRecord::RecordInvalid) + end + + let(:taxon_with_5_products) do + products = [] + 5.times do + products << create(:base_product) + end + + create(:taxon, products: products) + end + + def positions_to_be_valid(taxon) + positions = taxon.reload.classifications.map(&:position) + expect(positions).to eq((1..taxon.classifications.count).to_a) + end + + it "has a valid fixtures" do + expect positions_to_be_valid(taxon_with_5_products) + expect(Spree::Classification.count).to eq 5 + end + + context "removing product from taxon" do + before :each do + p = taxon_with_5_products.products[1] + expect(p.classifications.first.position).to eq(2) + taxon_with_5_products.products.destroy(p) + end + + it "resets positions" do + expect positions_to_be_valid(taxon_with_5_products) + end + end + + context "replacing taxon's products" do + before :each do + products = taxon_with_5_products.products.to_a + products.pop(1) + taxon_with_5_products.products = products + taxon_with_5_products.save! + end + + it "resets positions" do + expect positions_to_be_valid(taxon_with_5_products) + end + end + + context "removing taxon from product" do + before :each do + p = taxon_with_5_products.products[1] + p.taxons.destroy(taxon_with_5_products) + p.save! + end + + it "resets positions" do + expect positions_to_be_valid(taxon_with_5_products) + end + end + + context "replacing product's taxons" do + before :each do + p = taxon_with_5_products.products[1] + p.taxons = [] + p.save! + end + + it "resets positions" do + expect positions_to_be_valid(taxon_with_5_products) + end + end + + context "destroying classification" do + before :each do + classification = taxon_with_5_products.classifications[1] + classification.destroy + end + + it "resets positions" do + expect positions_to_be_valid(taxon_with_5_products) + end + end + end +end \ No newline at end of file diff --git a/core/spec/models/spree/configuration_spec.rb b/core/spec/models/spree/configuration_spec.rb new file mode 100644 index 00000000000..826a213b1e6 --- /dev/null +++ b/core/spec/models/spree/configuration_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Spree::Configuration, :type => :model do + +end diff --git a/core/spec/models/spree/country_spec.rb b/core/spec/models/spree/country_spec.rb new file mode 100644 index 00000000000..3074a2678c6 --- /dev/null +++ b/core/spec/models/spree/country_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Spree::Country, :type => :model do + it "can find all countries group by states required" do + country_states_required= Spree::Country.create({:name => "Canada", :iso_name => "CAN", :states_required => true}) + country_states_not_required= Spree::Country.create({:name => "France", :iso_name => "FR", :states_required => false}) + states_required = Spree::Country.states_required_by_country_id + expect(states_required[country_states_required.id.to_s]).to be true + expect(states_required[country_states_not_required.id.to_s]).to be false + end + + it "returns that the states are required for an invalid country" do + expect(Spree::Country.states_required_by_country_id['i do not exit']).to be true + end +end diff --git a/core/spec/models/spree/credit_card_spec.rb b/core/spec/models/spree/credit_card_spec.rb new file mode 100644 index 00000000000..5f9bbb8f098 --- /dev/null +++ b/core/spec/models/spree/credit_card_spec.rb @@ -0,0 +1,382 @@ +require 'spec_helper' + +describe Spree::CreditCard, type: :model do + let(:valid_credit_card_attributes) do + { + number: '4111111111111111', + verification_value: '123', + expiry: "12 / #{(Time.now.year + 1).to_s.last(2)}", + name: 'Spree Commerce' + } + end + + def self.payment_states + Spree::Payment.state_machine.states.keys + end + + def stub_rails_env(environment) + allow(Rails).to receive_messages(env: ActiveSupport::StringInquirer.new(environment)) + end + + let(:credit_card) { Spree::CreditCard.new } + + before(:each) do + + @order = create(:order) + @payment = Spree::Payment.create(:amount => 100, :order => @order) + + @success_response = double('gateway_response', success?: true, authorization: '123', avs_result: { 'code' => 'avs-code' }) + @fail_response = double('gateway_response', success?: false) + + @payment_gateway = mock_model(Spree::PaymentMethod, + payment_profiles_supported?: true, + authorize: @success_response, + purchase: @success_response, + capture: @success_response, + void: @success_response, + credit: @success_response, + environment: 'test' + ) + + allow(@payment).to receive_messages payment_method: @payment_gateway + end + + context "#can_capture?" do + it "should be true if payment is pending" do + payment = mock_model(Spree::Payment, pending?: true, created_at: Time.now) + expect(credit_card.can_capture?(payment)).to be true + end + + it "should be true if payment is checkout" do + payment = mock_model(Spree::Payment, pending?: false, checkout?: true, created_at: Time.now) + expect(credit_card.can_capture?(payment)).to be true + end + end + + context "#can_void?" do + it "should be true if payment is not void" do + payment = mock_model(Spree::Payment, failed?: false, void?: false) + expect(credit_card.can_void?(payment)).to be true + end + end + + context "#can_credit?" do + it "should be false if payment is not completed" do + payment = mock_model(Spree::Payment, completed?: false) + expect(credit_card.can_credit?(payment)).to be false + end + + it "should be false when credit_allowed is zero" do + payment = mock_model(Spree::Payment, completed?: true, credit_allowed: 0, order: mock_model(Spree::Order, payment_state: 'credit_owed')) + expect(credit_card.can_credit?(payment)).to be false + end + end + + context "#valid?" do + it "should validate presence of number" do + credit_card.attributes = valid_credit_card_attributes.except(:number) + expect(credit_card).not_to be_valid + expect(credit_card.errors[:number]).to eq(["can't be blank"]) + end + + it "should validate presence of security code" do + credit_card.attributes = valid_credit_card_attributes.except(:verification_value) + expect(credit_card).not_to be_valid + expect(credit_card.errors[:verification_value]).to eq(["can't be blank"]) + end + + it "validates name presence" do + credit_card.valid? + expect(credit_card.error_on(:name).size).to eq(1) + end + + # Regression spec for #4971 + it "should not bomb out when given an invalid expiry" do + credit_card.month = 13 + credit_card.year = Time.now.year + 1 + expect(credit_card).not_to be_valid + expect(credit_card.errors[:base]).to eq(["Card expiration is invalid"]) + end + + it "should validate expiration is not in the past" do + credit_card.month = 1.month.ago.month + credit_card.year = 1.month.ago.year + expect(credit_card).not_to be_valid + expect(credit_card.errors[:base]).to eq(["Card has expired"]) + end + + it "should not be expired expiring on the current month" do + credit_card.attributes = valid_credit_card_attributes + credit_card.month = Time.zone.now.month + credit_card.year = Time.zone.now.year + expect(credit_card).to be_valid + end + + it "should handle TZ correctly" do + # The card is valid according to the system clock's local time + # (Time.now). + # However it has expired in rails's configured time zone (Time.current), + # which is the value we should be respecting. + time = Time.new(2014, 04, 30, 23, 0, 0, "-07:00") + Timecop.freeze(time) do + credit_card.month = 1.month.ago.month + credit_card.year = 1.month.ago.year + expect(credit_card).not_to be_valid + expect(credit_card.errors[:base]).to eq(["Card has expired"]) + end + end + + it "does not run expiration in the past validation if month is not set" do + credit_card.month = nil + credit_card.year = Time.now.year + expect(credit_card).not_to be_valid + expect(credit_card.errors[:base]).to be_blank + end + + it "does not run expiration in the past validation if year is not set" do + credit_card.month = Time.now.month + credit_card.year = nil + expect(credit_card).not_to be_valid + expect(credit_card.errors[:base]).to be_blank + end + + it "does not run expiration in the past validation if year and month are empty" do + credit_card.year = "" + credit_card.month = "" + expect(credit_card).not_to be_valid + expect(credit_card.errors[:card]).to be_blank + end + + it "should only validate on create" do + credit_card.attributes = valid_credit_card_attributes + credit_card.save + expect(credit_card).to be_valid + end + + context "encrypted data is present" do + it "does not validate presence of number or cvv" do + credit_card.encrypted_data = "$fdgsfgdgfgfdg&gfdgfdgsf-" + credit_card.valid? + expect(credit_card.errors[:number]).to be_empty + expect(credit_card.errors[:verification_value]).to be_empty + end + end + + context "imported is true" do + it "does not validate presence of number or cvv" do + credit_card.imported = true + credit_card.valid? + expect(credit_card.errors[:number]).to be_empty + expect(credit_card.errors[:verification_value]).to be_empty + end + end + end + + context "#save" do + before do + credit_card.attributes = valid_credit_card_attributes + credit_card.save! + end + + let!(:persisted_card) { Spree::CreditCard.find(credit_card.id) } + + it "should not actually store the number" do + expect(persisted_card.number).to be_blank + end + + it "should not actually store the security code" do + expect(persisted_card.verification_value).to be_blank + end + end + + context "#number=" do + it "should strip non-numeric characters from card input" do + credit_card.number = "6011000990139424" + expect(credit_card.number).to eq("6011000990139424") + + credit_card.number = " 6011-0009-9013-9424 " + expect(credit_card.number).to eq("6011000990139424") + end + + it "should not raise an exception on non-string input" do + credit_card.number = Hash.new + expect(credit_card.number).to be_nil + end + end + + # Regression test for #3847 & #3896 + context "#expiry=" do + it "can set with a 2-digit month and year" do + credit_card.expiry = '04 / 14' + expect(credit_card.month).to eq(4) + expect(credit_card.year).to eq(2014) + end + + it "can set with a 2-digit month and 4-digit year" do + credit_card.expiry = '04 / 2014' + expect(credit_card.month).to eq(4) + expect(credit_card.year).to eq(2014) + end + + it "can set with a 2-digit month and 4-digit year without whitespace" do + credit_card.expiry = '04/14' + expect(credit_card.month).to eq(4) + expect(credit_card.year).to eq(2014) + end + + it "can set with a 2-digit month and 4-digit year without whitespace" do + credit_card.expiry = '04/2014' + expect(credit_card.month).to eq(4) + expect(credit_card.year).to eq(2014) + end + + it "can set with a 2-digit month and 4-digit year without whitespace and slash" do + credit_card.expiry = '042014' + expect(credit_card.month).to eq(4) + expect(credit_card.year).to eq(2014) + end + + it "can set with a 2-digit month and 2-digit year without whitespace and slash" do + credit_card.expiry = '0414' + expect(credit_card.month).to eq(4) + expect(credit_card.year).to eq(2014) + end + + it "does not blow up when passed an empty string" do + expect { credit_card.expiry = '' }.not_to raise_error + end + + # Regression test for #4725 + it "does not blow up when passed one number" do + expect { credit_card.expiry = '12' }.not_to raise_error + end + + end + + context "#cc_type=" do + it "converts between the different types" do + credit_card.cc_type = 'mastercard' + expect(credit_card.cc_type).to eq('master') + + credit_card.cc_type = 'maestro' + expect(credit_card.cc_type).to eq('master') + + credit_card.cc_type = 'amex' + expect(credit_card.cc_type).to eq('american_express') + + credit_card.cc_type = 'dinersclub' + expect(credit_card.cc_type).to eq('diners_club') + + credit_card.cc_type = 'some_outlandish_cc_type' + expect(credit_card.cc_type).to eq('some_outlandish_cc_type') + end + + it "assigns the type based on card number in the event of js failure" do + credit_card.number = '4242424242424242' + credit_card.cc_type = '' + expect(credit_card.cc_type).to eq('visa') + + credit_card.number = '5555555555554444' + credit_card.cc_type = '' + expect(credit_card.cc_type).to eq('master') + + credit_card.number = '378282246310005' + credit_card.cc_type = '' + expect(credit_card.cc_type).to eq('american_express') + + credit_card.number = '30569309025904' + credit_card.cc_type = '' + expect(credit_card.cc_type).to eq('diners_club') + + credit_card.number = '3530111333300000' + credit_card.cc_type = '' + expect(credit_card.cc_type).to eq('jcb') + + credit_card.number = '' + credit_card.cc_type = '' + expect(credit_card.cc_type).to eq('') + + credit_card.number = nil + credit_card.cc_type = '' + expect(credit_card.cc_type).to eq('') + end + end + + context "#associations" do + it "should be able to access its payments" do + expect { credit_card.payments.to_a }.not_to raise_error + end + end + + context "#first_name" do + before do + credit_card.name = "Ludwig van Beethoven" + end + + it "extracts the first name" do + expect(credit_card.first_name).to eq "Ludwig" + end + end + + context "#last_name" do + before do + credit_card.name = "Ludwig van Beethoven" + end + + it "extracts the last name" do + expect(credit_card.last_name).to eq "van Beethoven" + end + end + + context "#to_active_merchant" do + before do + credit_card.number = "4111111111111111" + credit_card.year = Time.now.year + credit_card.month = Time.now.month + credit_card.name = "Ludwig van Beethoven" + credit_card.verification_value = 123 + end + + it "converts to an ActiveMerchant::Billing::CreditCard object" do + am_card = credit_card.to_active_merchant + expect(am_card.number).to eq("4111111111111111") + expect(am_card.year).to eq(Time.now.year) + expect(am_card.month).to eq(Time.now.month) + expect(am_card.first_name).to eq("Ludwig") + expect(am_card.last_name).to eq("van Beethoven") + expect(am_card.verification_value).to eq(123) + end + end + + it 'ensures only one credit card per user is default at a time' do + user = FactoryGirl.create(:user) + first = FactoryGirl.create(:credit_card, user: user, default: true) + second = FactoryGirl.create(:credit_card, user: user, default: true) + + expect(first.reload.default).to eq false + expect(second.reload.default).to eq true + + first.default = true + first.save! + + expect(first.reload.default).to eq true + expect(second.reload.default).to eq false + end + + it 'allows default credit cards for different users' do + first = FactoryGirl.create(:credit_card, user: FactoryGirl.create(:user), default: true) + second = FactoryGirl.create(:credit_card, user: FactoryGirl.create(:user), default: true) + + expect(first.reload.default).to eq true + expect(second.reload.default).to eq true + end + + it 'allows this card to save even if the previously default card has expired' do + user = FactoryGirl.create(:user) + first = FactoryGirl.create(:credit_card, user: user, default: true) + second = FactoryGirl.create(:credit_card, user: user, default: false) + first.update_columns(year: DateTime.now.year, month: 1.month.ago.month) + + expect { second.update_attributes!(default: true) }.not_to raise_error + end +end diff --git a/core/spec/models/spree/customer_return_spec.rb b/core/spec/models/spree/customer_return_spec.rb new file mode 100644 index 00000000000..5f75acd9875 --- /dev/null +++ b/core/spec/models/spree/customer_return_spec.rb @@ -0,0 +1,262 @@ +require 'spec_helper' + +describe Spree::CustomerReturn, :type => :model do + before do + allow_any_instance_of(Spree::Order).to receive_messages(return!: true) + end + + describe ".validation" do + describe "#must_have_return_authorization" do + let(:customer_return) { build(:customer_return) } + + let(:inventory_unit) { build(:inventory_unit) } + let(:return_item) { build(:return_item, inventory_unit: inventory_unit) } + + subject { customer_return.valid? } + + before do + customer_return.return_items << return_item + end + + context "return item does not belong to return authorization" do + before do + return_item.return_authorization = nil + end + + it "is not valid" do + expect(subject).to eq false + end + + it "adds an error message" do + subject + expect(customer_return.errors.full_messages).to include(Spree.t(:missing_return_authorization, item_name: inventory_unit.variant.name)) + end + end + + context "return item belongs to return authorization" do + it "is valid" do + expect(subject).to eq true + end + end + end + + describe "#return_items_belong_to_same_order" do + let(:customer_return) { build(:customer_return) } + + let(:first_inventory_unit) { build(:inventory_unit) } + let(:first_return_item) { build(:return_item, inventory_unit: first_inventory_unit) } + + let(:second_inventory_unit) { build(:inventory_unit, order: second_order) } + let(:second_return_item) { build(:return_item, inventory_unit: second_inventory_unit) } + + subject { customer_return.valid? } + + before do + customer_return.return_items << first_return_item + customer_return.return_items << second_return_item + end + + context "return items are part of different orders" do + let(:second_order) { create(:order) } + + it "is not valid" do + expect(subject).to eq false + end + + it "adds an error message" do + subject + expect(customer_return.errors.full_messages).to include(Spree.t(:return_items_cannot_be_associated_with_multiple_orders)) + end + end + + context "return items are part of the same order" do + let(:second_order) { first_inventory_unit.order } + + it "is valid" do + expect(subject).to eq true + end + end + end + end + + describe ".before_create" do + describe "#generate_number" do + context "number is assigned" do + let(:customer_return) { Spree::CustomerReturn.new(number: '123') } + + it "should return the assigned number" do + customer_return.save + expect(customer_return.number).to eq('123') + end + end + + context "number is not assigned" do + let(:customer_return) { Spree::CustomerReturn.new(number: nil) } + + before do + allow(customer_return).to receive_messages(valid?: true, process_return!: true) + end + + it "should assign number with random CR number" do + customer_return.save + expect(customer_return.number).to match(/CR\d{9}/) + end + end + end + end + + describe "#pre_tax_total" do + let(:pre_tax_amount) { 15.0 } + let(:customer_return) { create(:customer_return, line_items_count: 2) } + + before do + Spree::ReturnItem.where(customer_return_id: customer_return.id).update_all(pre_tax_amount: pre_tax_amount) + end + + subject { customer_return.pre_tax_total } + + it "returns the sum of the return item's pre_tax_amount" do + expect(subject).to eq (pre_tax_amount * 2) + end + end + + describe "#display_pre_tax_total" do + let(:customer_return) { Spree::CustomerReturn.new } + + it "returns a Spree::Money" do + allow(customer_return).to receive_messages(pre_tax_total: 21.22) + expect(customer_return.display_pre_tax_total).to eq(Spree::Money.new(21.22)) + end + end + + describe "#order" do + let(:return_item) { create(:return_item) } + let(:customer_return) { build(:customer_return, return_items: [return_item]) } + + subject { customer_return.order } + + it "returns the order associated with the return item's inventory unit" do + expect(subject).to eq return_item.inventory_unit.order + end + end + + describe "#order_id" do + subject { customer_return.order_id } + + context "return item is not associated yet" do + let(:customer_return) { build(:customer_return) } + + it "is nil" do + expect(subject).to be_nil + end + end + + context "has an associated return item" do + let(:return_item) { create(:return_item) } + let(:customer_return) { build(:customer_return, return_items: [return_item]) } + + it "is the return item's inventory unit's order id" do + expect(subject).to eq return_item.inventory_unit.order.id + end + end + end + + context ".after_save" do + let(:inventory_unit) { create(:inventory_unit, state: 'shipped', order: create(:shipped_order)) } + let(:return_item) { create(:return_item, inventory_unit: inventory_unit) } + + context "to the initial stock location" do + + it "should mark all inventory units are returned" do + create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id) + expect(inventory_unit.reload.state).to eq 'returned' + end + + it "should update the stock item counts in the stock location" do + expect do + create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id) + end.to change { inventory_unit.find_stock_item.count_on_hand }.by(1) + end + + context 'with Config.track_inventory_levels == false' do + before do + Spree::Config.track_inventory_levels = false + expect(Spree::StockItem).not_to receive(:find_by) + expect(Spree::StockMovement).not_to receive(:create!) + end + + it "should NOT update the stock item counts in the stock location" do + count_on_hand = inventory_unit.find_stock_item.count_on_hand + create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id) + expect(inventory_unit.find_stock_item.count_on_hand).to eql count_on_hand + end + end + end + + context "to a different stock location" do + let(:new_stock_location) { create(:stock_location, :name => "other") } + + it "should update the stock item counts in new stock location" do + expect { + create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: new_stock_location.id) + }.to change { + Spree::StockItem.where(variant_id: inventory_unit.variant_id, stock_location_id: new_stock_location.id).first.count_on_hand + }.by(1) + end + + it "should NOT raise an error when no stock item exists in the stock location" do + inventory_unit.find_stock_item.destroy + expect { create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: new_stock_location.id) }.not_to raise_error + end + + it "should not update the stock item counts in the original stock location" do + count_on_hand = inventory_unit.find_stock_item.count_on_hand + create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: new_stock_location.id) + expect(inventory_unit.find_stock_item.count_on_hand).to eq(count_on_hand) + end + end + end + + describe '#fully_reimbursed?' do + let(:customer_return) { create(:customer_return) } + + let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) } + + subject { customer_return.fully_reimbursed? } + + context 'when some return items are undecided' do + it { is_expected.to be false } + end + + context 'when all return items are decided' do + + context 'when all return items are rejected' do + before { customer_return.return_items.each(&:reject!) } + + it { is_expected.to be true } + end + + context 'when all return items are accepted' do + before { customer_return.return_items.each(&:accept!) } + + context 'when some return items have no reimbursement' do + it { is_expected.to be false } + end + + context 'when all return items have a reimbursement' do + let!(:reimbursement) { create(:reimbursement, customer_return: customer_return) } + + context 'when some reimbursements are not reimbursed' do + it { is_expected.to be false } + end + + context 'when all reimbursements are reimbursed' do + before { reimbursement.perform! } + + it { is_expected.to be true } + end + end + end + end + end +end diff --git a/core/spec/models/spree/exchange_spec.rb b/core/spec/models/spree/exchange_spec.rb new file mode 100644 index 00000000000..66c895b09a9 --- /dev/null +++ b/core/spec/models/spree/exchange_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +module Spree + describe Exchange, :type => :model do + let(:order) { Spree::Order.new } + + let(:return_item_1) { build(:exchange_return_item) } + let(:return_item_2) { build(:exchange_return_item) } + let(:return_items) { [return_item_1, return_item_2] } + let(:exchange) { Exchange.new(order, return_items) } + + describe "#description" do + before do + allow(return_item_1).to receive(:variant) { double(options_text: "foo") } + allow(return_item_1).to receive(:exchange_variant) { double(options_text: "bar") } + allow(return_item_2).to receive(:variant) { double(options_text: "baz") } + allow(return_item_2).to receive(:exchange_variant) { double(options_text: "qux") } + end + + it "describes the return items' change in options" do + expect(exchange.description).to match /foo => bar/ + expect(exchange.description).to match /baz => qux/ + end + end + + describe "#display_amount" do + it "is the total amount of all return items" do + expect(exchange.display_amount).to eq Spree::Money.new(0.0) + end + end + + describe "#perform!" do + let(:return_item) { create(:exchange_return_item) } + let(:return_items) { [return_item] } + let(:order) { return_item.return_authorization.order } + subject { exchange.perform! } + before { return_item.exchange_variant.stock_items.first.adjust_count_on_hand(20) } + + it "creates shipments for the order with the return items exchange inventory units" do + expect { subject }.to change { order.shipments.count }.by(1) + new_shipment = order.shipments.last + expect(new_shipment).to be_ready + new_inventory_units = new_shipment.inventory_units + expect(new_inventory_units.count).to eq 1 + expect(new_inventory_units.first.original_return_item).to eq return_item + expect(new_inventory_units.first.line_item).to eq return_item.inventory_unit.line_item + end + + context "when it cannot create shipments for all items" do + before do + StockItem.where(:variant_id => return_item.exchange_variant_id).destroy_all + end + + it 'raises an UnableToCreateShipments error' do + expect { + subject + }.to raise_error(Spree::Exchange::UnableToCreateShipments) + end + end + end + + describe "#to_key" do # for dom_id + it { expect(Exchange.new(nil, nil).to_key).to be_nil } + end + + describe ".param_key" do # for dom_id + it { expect(Exchange.param_key).to eq "spree_exchange" } + end + + describe ".model_name" do # for dom_id + it { expect(Exchange.model_name).to eq Spree::Exchange } + end + + end +end diff --git a/core/spec/models/spree/gateway/bogus_simple.rb b/core/spec/models/spree/gateway/bogus_simple.rb new file mode 100644 index 00000000000..1d632fce4b1 --- /dev/null +++ b/core/spec/models/spree/gateway/bogus_simple.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Spree::Gateway::BogusSimple, :type => :model do + + subject { Spree::Gateway::BogusSimple.new } + + # regression test for #3824 + describe "#capture" do + it "returns success with the right response code" do + response = subject.capture(123, '12345', {}) + expect(response.message).to include("success") + end + + it "returns failure with the wrong response code" do + response = subject.capture(123, 'wrong', {}) + expect(response.message).to include("failure") + end + end + +end \ No newline at end of file diff --git a/core/spec/models/spree/gateway/bogus_spec.rb b/core/spec/models/spree/gateway/bogus_spec.rb new file mode 100644 index 00000000000..89ca1dd045a --- /dev/null +++ b/core/spec/models/spree/gateway/bogus_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +module Spree + describe Gateway::Bogus, :type => :model do + let(:bogus) { create(:credit_card_payment_method) } + let!(:cc) { create(:credit_card, payment_method: bogus, gateway_customer_profile_id: "BGS-RERTERT") } + + it "disable recurring contract by destroying payment source" do + bogus.disable_customer_profile(cc) + expect(cc.gateway_customer_profile_id).to be_nil + end + end +end diff --git a/core/spec/models/spree/gateway_spec.rb b/core/spec/models/spree/gateway_spec.rb new file mode 100644 index 00000000000..02007473282 --- /dev/null +++ b/core/spec/models/spree/gateway_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Spree::Gateway, :type => :model do + class Provider + def initialize(options) + end + + def imaginary_method + + end + end + + class TestGateway < Spree::Gateway + def provider_class + Provider + end + end + + it "passes through all arguments on a method_missing call" do + gateway = TestGateway.new + expect(gateway.provider).to receive(:imaginary_method).with('foo') + gateway.imaginary_method('foo') + end + + context "fetching payment sources" do + let(:order) { Spree::Order.create(user_id: 1) } + + let(:has_card) { create(:credit_card_payment_method) } + let(:no_card) { create(:credit_card_payment_method) } + + let(:cc) do + create(:credit_card, payment_method: has_card, gateway_customer_profile_id: "EFWE") + end + + let(:payment) do + create(:payment, order: order, source: cc, payment_method: has_card) + end + + it "finds credit cards associated on a order completed" do + allow(payment.order).to receive_messages completed?: true + + expect(no_card.reusable_sources(payment.order)).to be_empty + expect(has_card.reusable_sources(payment.order)).not_to be_empty + end + + it "finds credit cards associated with the order user" do + cc.update_column :user_id, 1 + allow(payment.order).to receive_messages completed?: false + + expect(no_card.reusable_sources(payment.order)).to be_empty + expect(has_card.reusable_sources(payment.order)).not_to be_empty + end + end +end diff --git a/core/spec/models/spree/image_spec.rb b/core/spec/models/spree/image_spec.rb new file mode 100644 index 00000000000..394de852b8d --- /dev/null +++ b/core/spec/models/spree/image_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Spree::Image, :type => :model do + +end diff --git a/core/spec/models/spree/inventory_unit_spec.rb b/core/spec/models/spree/inventory_unit_spec.rb new file mode 100644 index 00000000000..8f8285f18bd --- /dev/null +++ b/core/spec/models/spree/inventory_unit_spec.rb @@ -0,0 +1,242 @@ +require 'spec_helper' + +describe Spree::InventoryUnit, :type => :model do + let(:stock_location) { create(:stock_location_with_items) } + let(:stock_item) { stock_location.stock_items.order(:id).first } + + context "#backordered_for_stock_item" do + let(:order) do + order = create(:order, state: 'complete', ship_address: create(:ship_address)) + order.completed_at = Time.now + create(:shipment, order: order, stock_location: stock_location) + order.shipments.reload + create(:line_item, order: order, variant: stock_item.variant) + order.line_items.reload + order.tap(&:save!) + end + + let(:shipment) do + order.shipments.first + end + + let(:shipping_method) do + shipment.shipping_methods.first + end + + let!(:unit) do + unit = shipment.inventory_units.first + unit.state = 'backordered' + unit.tap(&:save!) + end + + before do + stock_item.set_count_on_hand(-2) + end + + # Regression for #3066 + it "returns modifiable objects" do + units = Spree::InventoryUnit.backordered_for_stock_item(stock_item) + expect { units.first.save! }.to_not raise_error + end + + it "finds inventory units from its stock location when the unit's variant matches the stock item's variant" do + expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).to match_array([unit]) + end + + it "does not find inventory units that aren't backordered" do + on_hand_unit = shipment.inventory_units.build + on_hand_unit.state = 'on_hand' + on_hand_unit.variant_id = 1 + on_hand_unit.save! + + expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).not_to include(on_hand_unit) + end + + it "does not find inventory units that don't match the stock item's variant" do + other_variant_unit = shipment.inventory_units.build + other_variant_unit.state = 'backordered' + other_variant_unit.variant = create(:variant) + other_variant_unit.save! + + expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).not_to include(other_variant_unit) + end + + it "does not change shipping cost when fulfilling the order" do + current_shipment_cost = shipment.cost + shipping_method.calculator.set_preference(:amount, current_shipment_cost + 5.0) + stock_item.set_count_on_hand(0) + expect(shipment.reload.cost).to eq(current_shipment_cost) + end + + context "other shipments" do + let(:other_order) do + order = create(:order) + order.state = 'payment' + order.completed_at = nil + order.tap(&:save!) + end + + let(:other_shipment) do + shipment = Spree::Shipment.new + shipment.stock_location = stock_location + shipment.shipping_methods << create(:shipping_method) + shipment.order = other_order + # We don't care about this in this test + allow(shipment).to receive(:ensure_correct_adjustment) + shipment.tap(&:save!) + end + + let!(:other_unit) do + unit = other_shipment.inventory_units.build + unit.state = 'backordered' + unit.variant_id = stock_item.variant.id + unit.order_id = other_order.id + unit.tap(&:save!) + end + + it "does not find inventory units belonging to incomplete orders" do + expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).not_to include(other_unit) + end + + end + + end + + context "variants deleted" do + let!(:unit) do + Spree::InventoryUnit.create(variant: stock_item.variant) + end + + it "can still fetch variant" do + unit.variant.destroy + expect(unit.reload.variant).to be_a Spree::Variant + end + + it "can still fetch variants by eager loading (remove default_scope)" do + skip "find a way to remove default scope when eager loading associations" + unit.variant.destroy + expect(Spree::InventoryUnit.joins(:variant).includes(:variant).first.variant).to be_a Spree::Variant + end + end + + context "#finalize_units!" do + let!(:stock_location) { create(:stock_location) } + let(:variant) { create(:variant) } + let(:inventory_units) { [ + create(:inventory_unit, variant: variant), + create(:inventory_unit, variant: variant) + ] } + + it "should create a stock movement" do + Spree::InventoryUnit.finalize_units!(inventory_units) + expect(inventory_units.any?(&:pending)).to be false + end + end + + describe "#current_or_new_return_item" do + before { allow(inventory_unit).to receive_messages(pre_tax_amount: 100.0) } + + subject { inventory_unit.current_or_new_return_item } + + context "associated with a return item" do + let(:return_item) { create(:return_item) } + let(:inventory_unit) { return_item.inventory_unit } + + it "returns a persisted return item" do + expect(subject).to be_persisted + end + + it "returns it's associated return_item" do + expect(subject).to eq return_item + end + end + + context "no associated return item" do + let(:inventory_unit) { create(:inventory_unit) } + + it "returns a new return item" do + expect(subject).to_not be_persisted + end + + it "associates itself to the new return_item" do + expect(subject.inventory_unit).to eq inventory_unit + end + end + end + + describe '#additional_tax_total' do + let(:quantity) { 2 } + let(:line_item_additional_tax_total) { 10.00 } + let(:line_item) do + build(:line_item, { + quantity: quantity, + additional_tax_total: line_item_additional_tax_total, + }) + end + + subject do + build(:inventory_unit, line_item: line_item) + end + + it 'is the correct amount' do + expect(subject.additional_tax_total).to eq line_item_additional_tax_total / quantity + end + end + + describe '#included_tax_total' do + let(:quantity) { 2 } + let(:line_item_included_tax_total) { 10.00 } + let(:line_item) do + build(:line_item, { + quantity: quantity, + included_tax_total: line_item_included_tax_total, + }) + end + + subject do + build(:inventory_unit, line_item: line_item) + end + + it 'is the correct amount' do + expect(subject.included_tax_total).to eq line_item_included_tax_total / quantity + end + end + + describe '#additional_tax_total' do + let(:quantity) { 2 } + let(:line_item_additional_tax_total) { 10.00 } + let(:line_item) do + build(:line_item, { + quantity: quantity, + additional_tax_total: line_item_additional_tax_total, + }) + end + + subject do + build(:inventory_unit, line_item: line_item) + end + + it 'is the correct amount' do + expect(subject.additional_tax_total).to eq line_item_additional_tax_total / quantity + end + end + + describe '#included_tax_total' do + let(:quantity) { 2 } + let(:line_item_included_tax_total) { 10.00 } + let(:line_item) do + build(:line_item, { + quantity: quantity, + included_tax_total: line_item_included_tax_total, + }) + end + + subject do + build(:inventory_unit, line_item: line_item) + end + + it 'is the correct amount' do + expect(subject.included_tax_total).to eq line_item_included_tax_total / quantity + end + end +end diff --git a/core/spec/models/spree/item_adjustments_spec.rb b/core/spec/models/spree/item_adjustments_spec.rb new file mode 100644 index 00000000000..3f874118e6c --- /dev/null +++ b/core/spec/models/spree/item_adjustments_spec.rb @@ -0,0 +1,273 @@ +require 'spec_helper' + +module Spree + describe ItemAdjustments, :type => :model do + let(:order) { create :order_with_line_items, line_items_count: 1 } + let(:line_item) { order.line_items.first } + + let(:subject) { ItemAdjustments.new(line_item) } + let(:order_subject) { ItemAdjustments.new(order) } + + context '#update' do + it "updates a linked adjustment" do + tax_rate = create(:tax_rate, :amount => 0.05) + adjustment = create(:adjustment, order: order, source: tax_rate, adjustable: line_item) + line_item.price = 10 + line_item.tax_category = tax_rate.tax_category + + subject.update + expect(line_item.adjustment_total).to eq(0.5) + expect(line_item.additional_tax_total).to eq(0.5) + end + end + + context "taxes and promotions" do + let!(:tax_rate) do + create(:tax_rate, :amount => 0.05) + end + + let!(:promotion) do + Spree::Promotion.create(:name => "$10 off") + end + + let!(:promotion_action) do + calculator = Calculator::FlatRate.new(:preferred_amount => 10) + Promotion::Actions::CreateItemAdjustments.create calculator: calculator, promotion: promotion + end + + before do + line_item.price = 20 + line_item.tax_category = tax_rate.tax_category + line_item.save + create(:adjustment, order: order, source: promotion_action, adjustable: line_item) + end + + context "tax included in price" do + before do + create(:adjustment, + :source => tax_rate, + :adjustable => line_item, + :order => order, + :included => true + ) + end + + it "tax has no bearing on final price" do + subject.update_adjustments + line_item.reload + expect(line_item.included_tax_total).to eq(0.5) + expect(line_item.additional_tax_total).to eq(0) + expect(line_item.promo_total).to eq(-10) + expect(line_item.adjustment_total).to eq(-10) + end + + it "tax linked to order" do + order_subject.update_adjustments + order.reload + expect(order.included_tax_total).to eq(0.5) + expect(order.additional_tax_total).to eq(00) + end + end + + context "tax excluded from price" do + before do + create(:adjustment, + :source => tax_rate, + :adjustable => line_item, + :order => order, + :included => false + ) + end + + it "tax applies to line item" do + subject.update_adjustments + line_item.reload + # Taxable amount is: $20 (base) - $10 (promotion) = $10 + # Tax rate is 5% (of $10). + expect(line_item.included_tax_total).to eq(0) + expect(line_item.additional_tax_total).to eq(0.5) + expect(line_item.promo_total).to eq(-10) + expect(line_item.adjustment_total).to eq(-9.5) + end + + it "tax linked to order" do + order_subject.update_adjustments + order.reload + expect(order.included_tax_total).to eq(0) + expect(order.additional_tax_total).to eq(0.5) + end + end + end + + context "best promotion is always applied" do + let(:calculator) { Calculator::FlatRate.new(:preferred_amount => 10) } + + let(:source) { Promotion::Actions::CreateItemAdjustments.create calculator: calculator } + + def create_adjustment(label, amount) + create(:adjustment, :order => order, + :adjustable => line_item, + :source => source, + :amount => amount, + :state => "closed", + :label => label, + :mandatory => false) + end + + it "should make all but the most valuable promotion adjustment ineligible, leaving non promotion adjustments alone" do + create_adjustment("Promotion A", -100) + create_adjustment("Promotion B", -200) + create_adjustment("Promotion C", -300) + create(:adjustment, :order => order, + :adjustable => line_item, + :source => nil, + :amount => -500, + :state => "closed", + :label => "Some other credit") + line_item.adjustments.each {|a| a.update_column(:eligible, true)} + + subject.choose_best_promotion_adjustment + + expect(line_item.adjustments.promotion.eligible.count).to eq(1) + expect(line_item.adjustments.promotion.eligible.first.label).to eq('Promotion C') + end + + it "should choose the most recent promotion adjustment when amounts are equal" do + # Using Timecop is a regression test + Timecop.freeze do + create_adjustment("Promotion A", -200) + create_adjustment("Promotion B", -200) + end + line_item.adjustments.each {|a| a.update_column(:eligible, true)} + + subject.choose_best_promotion_adjustment + + expect(line_item.adjustments.promotion.eligible.count).to eq(1) + expect(line_item.adjustments.promotion.eligible.first.label).to eq('Promotion B') + end + + context "when previously ineligible promotions become available" do + let(:order_promo1) { create(:promotion, :with_order_adjustment, :with_item_total_rule, weighted_order_adjustment_amount: 5, item_total_threshold_amount: 10) } + let(:order_promo2) { create(:promotion, :with_order_adjustment, :with_item_total_rule, weighted_order_adjustment_amount: 10, item_total_threshold_amount: 20) } + let(:order_promos) { [ order_promo1, order_promo2 ] } + let(:line_item_promo1) { create(:promotion, :with_line_item_adjustment, :with_item_total_rule, adjustment_rate: 2.5, item_total_threshold_amount: 10) } + let(:line_item_promo2) { create(:promotion, :with_line_item_adjustment, :with_item_total_rule, adjustment_rate: 5, item_total_threshold_amount: 20) } + let(:line_item_promos) { [ line_item_promo1, line_item_promo2 ] } + let(:order) { create(:order_with_line_items, line_items_count: 1) } + + # Apply promotions in different sequences. Results should be the same. + promo_sequences = [ + [ 0, 1 ], + [ 1, 0 ] + ] + + promo_sequences.each do |promo_sequence| + it "should pick the best order-level promo according to current eligibility" do + # apply both promos to the order, even though only promo1 is eligible + order_promos[promo_sequence[0]].activate order: order + order_promos[promo_sequence[1]].activate order: order + + order.reload + expect(order.all_adjustments.count).to eq(2), "Expected two adjustments (using sequence #{promo_sequence})" + expect(order.all_adjustments.eligible.count).to eq(1), "Expected one elegible adjustment (using sequence #{promo_sequence})" + expect(order.all_adjustments.eligible.first.source.promotion).to eq(order_promo1), "Expected promo1 to be used (using sequence #{promo_sequence})" + + order.contents.add create(:variant, price: 10), 1 + order.save + + order.reload + expect(order.all_adjustments.count).to eq(2), "Expected two adjustments (using sequence #{promo_sequence})" + expect(order.all_adjustments.eligible.count).to eq(1), "Expected one elegible adjustment (using sequence #{promo_sequence})" + expect(order.all_adjustments.eligible.first.source.promotion).to eq(order_promo2), "Expected promo2 to be used (using sequence #{promo_sequence})" + end + end + + promo_sequences.each do |promo_sequence| + it "should pick the best line-item-level promo according to current eligibility" do + # apply both promos to the order, even though only promo1 is eligible + line_item_promos[promo_sequence[0]].activate order: order + line_item_promos[promo_sequence[1]].activate order: order + + order.reload + expect(order.all_adjustments.count).to eq(1), "Expected one adjustment (using sequence #{promo_sequence})" + expect(order.all_adjustments.eligible.count).to eq(1), "Expected one elegible adjustment (using sequence #{promo_sequence})" + # line_item_promo1 is the only one that has thus far met the order total threshold, it is the only promo which should be applied. + expect(order.all_adjustments.first.source.promotion).to eq(line_item_promo1), "Expected line_item_promo1 to be used (using sequence #{promo_sequence})" + + order.contents.add create(:variant, price: 10), 1 + order.save + + order.reload + expect(order.all_adjustments.count).to eq(4), "Expected four adjustments (using sequence #{promo_sequence})" + expect(order.all_adjustments.eligible.count).to eq(2), "Expected two elegible adjustments (using sequence #{promo_sequence})" + order.all_adjustments.eligible.each do |adjustment| + expect(adjustment.source.promotion).to eq(line_item_promo2), "Expected line_item_promo2 to be used (using sequence #{promo_sequence})" + end + end + end + end + + context "multiple adjustments and the best one is not eligible" do + let!(:promo_a) { create_adjustment("Promotion A", -100) } + let!(:promo_c) { create_adjustment("Promotion C", -300) } + + before do + promo_a.update_column(:eligible, true) + promo_c.update_column(:eligible, false) + end + + # regression for #3274 + it "still makes the previous best eligible adjustment valid" do + subject.choose_best_promotion_adjustment + expect(line_item.adjustments.promotion.eligible.first.label).to eq('Promotion A') + end + end + + it "should only leave one adjustment even if 2 have the same amount" do + create_adjustment("Promotion A", -100) + create_adjustment("Promotion B", -200) + create_adjustment("Promotion C", -200) + + subject.choose_best_promotion_adjustment + + expect(line_item.adjustments.promotion.eligible.count).to eq(1) + expect(line_item.adjustments.promotion.eligible.first.amount.to_i).to eq(-200) + end + end + + # For #4483 + context "callbacks" do + class SuperItemAdjustments < Spree::ItemAdjustments + attr_accessor :before_promo_adjustments_called, + :after_promo_adjustments_called, + :before_tax_adjustments_called, + :after_tax_adjustments_called + + set_callback :promo_adjustments, :before do |object| + @before_promo_adjustments_called = true + end + + set_callback :promo_adjustments, :after do |object| + @after_promo_adjustments_called = true + end + + set_callback :tax_adjustments, :before do |object| + @before_tax_adjustments_called = true + end + + set_callback :tax_adjustments, :after do |object| + @after_tax_adjustments_called = true + end + end + let(:subject) { SuperItemAdjustments.new(line_item) } + + it "calls all the callbacks" do + subject.update_adjustments + expect(subject.before_promo_adjustments_called).to be true + expect(subject.after_promo_adjustments_called).to be true + expect(subject.before_tax_adjustments_called).to be true + expect(subject.after_tax_adjustments_called).to be true + end + end + end +end diff --git a/core/spec/models/spree/line_item_spec.rb b/core/spec/models/spree/line_item_spec.rb new file mode 100644 index 00000000000..107d6b17567 --- /dev/null +++ b/core/spec/models/spree/line_item_spec.rb @@ -0,0 +1,257 @@ +require 'spec_helper' + +describe Spree::LineItem, :type => :model do + let(:order) { create :order_with_line_items, line_items_count: 1 } + let(:line_item) { order.line_items.first } + + context '#save' do + it 'touches the order' do + expect(line_item.order).to receive(:touch) + line_item.save + end + end + + context '#destroy' do + it "fetches deleted products" do + line_item.product.destroy + expect(line_item.reload.product).to be_a Spree::Product + end + + it "fetches deleted variants" do + line_item.variant.destroy + expect(line_item.reload.variant).to be_a Spree::Variant + end + + it "returns inventory when a line item is destroyed" do + expect_any_instance_of(Spree::OrderInventory).to receive(:verify) + line_item.destroy + end + + it "deletes inventory units" do + expect { line_item.destroy }.to change { line_item.inventory_units.count }.from(1).to(0) + end + end + + context "#save" do + context "line item changes" do + before do + line_item.quantity = line_item.quantity + 1 + end + + it "triggers adjustment total recalculation" do + expect(line_item).to receive(:update_tax_charge) # Regression test for https://github.com/spree/spree/issues/4671 + expect(line_item).to receive(:recalculate_adjustments) + line_item.save + end + end + + context "line item does not change" do + it "does not trigger adjustment total recalculation" do + expect(line_item).not_to receive(:recalculate_adjustments) + line_item.save + end + end + + context "target_shipment is provided" do + it "verifies inventory" do + line_item.target_shipment = Spree::Shipment.new + expect_any_instance_of(Spree::OrderInventory).to receive(:verify) + line_item.save + end + end + end + + context "#create" do + let(:variant) { create(:variant) } + + before do + create(:tax_rate, :zone => order.tax_zone, :tax_category => variant.tax_category) + end + + context "when order has a tax zone" do + before do + expect(order.tax_zone).to be_present + end + + it "creates a tax adjustment" do + order.contents.add(variant) + line_item = order.find_line_item_by_variant(variant) + expect(line_item.adjustments.tax.count).to eq(1) + end + end + + context "when order does not have a tax zone" do + before do + order.bill_address = nil + order.ship_address = nil + order.save + expect(order.reload.tax_zone).to be_nil + end + + it "does not create a tax adjustment" do + order.contents.add(variant) + line_item = order.find_line_item_by_variant(variant) + expect(line_item.adjustments.tax.count).to eq(0) + end + end + end + + # Test for #3391 + context '#copy_price' do + it "copies over a variant's prices" do + line_item.price = nil + line_item.cost_price = nil + line_item.currency = nil + line_item.copy_price + variant = line_item.variant + expect(line_item.price).to eq(variant.price) + expect(line_item.cost_price).to eq(variant.cost_price) + expect(line_item.currency).to eq(variant.currency) + end + end + + # Test for #3481 + context '#copy_tax_category' do + it "copies over a variant's tax category" do + line_item.tax_category = nil + line_item.copy_tax_category + expect(line_item.tax_category).to eq(line_item.variant.tax_category) + end + end + + describe '.discounted_amount' do + it "returns the amount minus any discounts" do + line_item.price = 10 + line_item.quantity = 2 + line_item.promo_total = -5 + expect(line_item.discounted_amount).to eq(15) + end + end + + describe "#discounted_money" do + it "should return a money object with the discounted amount" do + expect(line_item.discounted_money.to_s).to eq "$10.00" + end + end + + describe '.currency' do + it 'returns the globally configured currency' do + line_item.currency == 'USD' + end + end + + describe ".money" do + before do + line_item.price = 3.50 + line_item.quantity = 2 + end + + it "returns a Spree::Money representing the total for this line item" do + expect(line_item.money.to_s).to eq("$7.00") + end + end + + describe '.single_money' do + before { line_item.price = 3.50 } + it "returns a Spree::Money representing the price for one variant" do + expect(line_item.single_money.to_s).to eq("$3.50") + end + end + + context "has inventory (completed order so items were already unstocked)" do + let(:order) { Spree::Order.create(email: 'spree@example.com') } + let(:variant) { create(:variant) } + + context "nothing left on stock" do + before do + variant.stock_items.update_all count_on_hand: 5, backorderable: false + order.contents.add(variant, 5) + order.create_proposed_shipments + order.finalize! + end + + it "allows to decrease item quantity" do + line_item = order.line_items.first + line_item.quantity -= 1 + line_item.target_shipment = order.shipments.first + + line_item.save + expect(line_item.errors_on(:quantity).size).to eq(0) + end + + it "doesnt allow to increase item quantity" do + line_item = order.line_items.first + line_item.quantity += 2 + line_item.target_shipment = order.shipments.first + + line_item.save + expect(line_item.errors_on(:quantity).size).to eq(1) + end + end + + context "2 items left on stock" do + before do + variant.stock_items.update_all count_on_hand: 7, backorderable: false + order.contents.add(variant, 5) + order.create_proposed_shipments + order.finalize! + end + + it "allows to increase quantity up to stock availability" do + line_item = order.line_items.first + line_item.quantity += 2 + line_item.target_shipment = order.shipments.first + + line_item.save + expect(line_item.errors_on(:quantity).size).to eq(0) + end + + it "doesnt allow to increase quantity over stock availability" do + line_item = order.line_items.first + line_item.quantity += 3 + line_item.target_shipment = order.shipments.first + + line_item.save + expect(line_item.errors_on(:quantity).size).to eq(1) + end + end + end + + context "currency same as order.currency" do + it "is a valid line item" do + line_item = order.line_items.first + line_item.currency = order.currency + line_item.valid? + + expect(line_item.error_on(:currency).size).to eq(0) + end + end + + context "currency different than order.currency" do + it "is not a valid line item" do + line_item = order.line_items.first + line_item.currency = "no currency" + line_item.valid? + + expect(line_item.error_on(:currency).size).to eq(1) + end + end + + describe "#options=" do + it "can handle updating a blank line item with no order" do + line_item.options = { price: 123 } + end + + it "updates the data provided in the options" do + line_item.options = { price: 123 } + expect(line_item.price).to eq 123 + end + + it "updates the price based on the options provided" do + expect(line_item).to receive(:gift_wrap=).with(true) + expect(line_item.variant).to receive(:gift_wrap_price_modifier_amount_in).with("USD", true).and_return 1.99 + line_item.options = { gift_wrap: true } + expect(line_item.price).to eq 21.98 + end + end +end diff --git a/core/spec/models/spree/option_type_spec.rb b/core/spec/models/spree/option_type_spec.rb new file mode 100644 index 00000000000..ddd1f225d24 --- /dev/null +++ b/core/spec/models/spree/option_type_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Spree::OptionType, :type => :model do + context "touching" do + it "should touch a product" do + product_option_type = create(:product_option_type) + option_type = product_option_type.option_type + product = product_option_type.product + product.update_column(:updated_at, 1.day.ago) + option_type.touch + expect(product.reload.updated_at).to be_within(3.seconds).of(Time.now) + end + end +end \ No newline at end of file diff --git a/core/spec/models/spree/option_value_spec.rb b/core/spec/models/spree/option_value_spec.rb new file mode 100644 index 00000000000..b79f61b9266 --- /dev/null +++ b/core/spec/models/spree/option_value_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Spree::OptionValue, :type => :model do + context "touching" do + it "should touch a variant" do + variant = create(:variant) + option_value = variant.option_values.first + variant.update_column(:updated_at, 1.day.ago) + option_value.touch + expect(variant.reload.updated_at).to be_within(3.seconds).of(Time.now) + end + end +end \ No newline at end of file diff --git a/core/spec/models/spree/order/address_spec.rb b/core/spec/models/spree/order/address_spec.rb new file mode 100644 index 00000000000..9f95df5bb23 --- /dev/null +++ b/core/spec/models/spree/order/address_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Spree::Order, :type => :model do + let(:order) { Spree::Order.new } + + context 'validation' do + context "when @use_billing is populated" do + before do + order.bill_address = stub_model(Spree::Address) + order.ship_address = nil + end + + context "with true" do + before { order.use_billing = true } + + it "clones the bill address to the ship address" do + order.valid? + expect(order.ship_address).to eq(order.bill_address) + end + end + + context "with 'true'" do + before { order.use_billing = 'true' } + + it "clones the bill address to the shipping" do + order.valid? + expect(order.ship_address).to eq(order.bill_address) + end + end + + context "with '1'" do + before { order.use_billing = '1' } + + it "clones the bill address to the shipping" do + order.valid? + expect(order.ship_address).to eq(order.bill_address) + end + end + + context "with something other than a 'truthful' value" do + before { order.use_billing = '0' } + + it "does not clone the bill address to the shipping" do + order.valid? + expect(order.ship_address).to be_nil + end + end + end + end +end diff --git a/core/spec/models/spree/order/adjustments_spec.rb b/core/spec/models/spree/order/adjustments_spec.rb new file mode 100644 index 00000000000..690d88e4786 --- /dev/null +++ b/core/spec/models/spree/order/adjustments_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Spree::Order do + context "when an order has an adjustment that zeroes the total, but another adjustment for shipping that raises it above zero" do + let!(:persisted_order) { create(:order) } + let!(:line_item) { create(:line_item) } + let!(:shipping_method) do + sm = create(:shipping_method) + sm.calculator.preferred_amount = 10 + sm.save + sm + end + + before do + # Don't care about available payment methods in this test + allow(persisted_order).to receive_messages(:has_available_payment => false) + persisted_order.line_items << line_item + create(:adjustment, amount: -line_item.amount, label: "Promotion", adjustable: line_item, order: persisted_order) + persisted_order.state = 'delivery' + persisted_order.save # To ensure new state_change event + end + + it "transitions from delivery to payment" do + allow(persisted_order).to receive_messages(payment_required?: true) + persisted_order.next! + expect(persisted_order.state).to eq("payment") + end + end +end diff --git a/core/spec/models/spree/order/callbacks_spec.rb b/core/spec/models/spree/order/callbacks_spec.rb new file mode 100644 index 00000000000..43eecb3abac --- /dev/null +++ b/core/spec/models/spree/order/callbacks_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Spree::Order, :type => :model do + let(:order) { stub_model(Spree::Order) } + before do + Spree::Order.define_state_machine! + end + + context "validations" do + context "email validation" do + # Regression test for #1238 + it "o'brien@gmail.com is a valid email address" do + order.state = 'address' + order.email = "o'brien@gmail.com" + expect(order.error_on(:email).size).to eq(0) + end + end + end + + context "#save" do + context "when associated with a registered user" do + let(:user) { double(:user, :email => "test@example.com") } + + before do + allow(order).to receive_messages :user => user + end + + it "should assign the email address of the user" do + order.run_callbacks(:create) + expect(order.email).to eq(user.email) + end + end + end + + context "in the cart state" do + it "should not validate email address" do + order.state = "cart" + order.email = nil + expect(order.error_on(:email).size).to eq(0) + end + end +end diff --git a/core/spec/models/spree/order/checkout_spec.rb b/core/spec/models/spree/order/checkout_spec.rb new file mode 100644 index 00000000000..bd390bfb5ea --- /dev/null +++ b/core/spec/models/spree/order/checkout_spec.rb @@ -0,0 +1,749 @@ +require 'spec_helper' +require 'spree/testing_support/order_walkthrough' + +describe Spree::Order, :type => :model do + let(:order) { Spree::Order.new } + + def assert_state_changed(order, from, to) + state_change_exists = order.state_changes.where(:previous_state => from, :next_state => to).exists? + assert state_change_exists, "Expected order to transition from #{from} to #{to}, but didn't." + end + + context "with default state machine" do + let(:transitions) do + [ + { :address => :delivery }, + { :delivery => :payment }, + { :payment => :confirm }, + { :confirm => :complete }, + { :payment => :complete }, + { :delivery => :complete } + ] + end + + it "has the following transitions" do + transitions.each do |transition| + transition = Spree::Order.find_transition(:from => transition.keys.first, :to => transition.values.first) + expect(transition).not_to be_nil + end + end + + it "does not have a transition from delivery to confirm" do + transition = Spree::Order.find_transition(:from => :delivery, :to => :confirm) + expect(transition).to be_nil + end + + it '.find_transition when contract was broken' do + expect(Spree::Order.find_transition({foo: :bar, baz: :dog})).to be_falsey + end + + it '.remove_transition' do + options = {:from => transitions.first.keys.first, :to => transitions.first.values.first} + allow(Spree::Order).to receive(:next_event_transition).and_return([options]) + expect(Spree::Order.remove_transition(options)).to be_truthy + end + + it '.remove_transition when contract was broken' do + expect(Spree::Order.remove_transition(nil)).to be_falsey + end + + it "always return integer on checkout_step_index" do + expect(order.checkout_step_index("imnotthere")).to be_a Integer + expect(order.checkout_step_index("delivery")).to be > 0 + end + + it "passes delivery state when transitioning from address over delivery to payment" do + allow(order).to receive_messages :payment_required? => true + order.state = "address" + expect(order.passed_checkout_step?("delivery")).to be false + order.state = "delivery" + expect(order.passed_checkout_step?("delivery")).to be false + order.state = "payment" + expect(order.passed_checkout_step?("delivery")).to be true + end + + context "#checkout_steps" do + context "when confirmation not required" do + before do + allow(order).to receive_messages :confirmation_required? => false + allow(order).to receive_messages :payment_required? => true + end + + specify do + expect(order.checkout_steps).to eq(%w(address delivery payment complete)) + end + end + + context "when confirmation required" do + before do + allow(order).to receive_messages :confirmation_required? => true + allow(order).to receive_messages :payment_required? => true + end + + specify do + expect(order.checkout_steps).to eq(%w(address delivery payment confirm complete)) + end + end + + context "when payment not required" do + before { allow(order).to receive_messages :payment_required? => false } + specify do + expect(order.checkout_steps).to eq(%w(address delivery complete)) + end + end + + context "when payment required" do + before { allow(order).to receive_messages :payment_required? => true } + specify do + expect(order.checkout_steps).to eq(%w(address delivery payment complete)) + end + end + end + + it "starts out at cart" do + expect(order.state).to eq("cart") + end + + context "to address" do + before do + order.email = "user@example.com" + order.save! + end + + context "with a line item" do + before do + order.line_items << FactoryGirl.create(:line_item) + end + + it "transitions to address" do + order.next! + assert_state_changed(order, 'cart', 'address') + expect(order.state).to eq("address") + end + + it "doesn't raise an error if the default address is invalid" do + order.user = mock_model(Spree::LegacyUser, ship_address: Spree::Address.new, bill_address: Spree::Address.new) + expect { order.next! }.to_not raise_error + end + + context "with default addresses" do + let(:default_address) { FactoryGirl.create(:address) } + + before do + order.user = FactoryGirl.create(:user, "#{address_kind}_address" => default_address) + order.next! + order.reload + end + + shared_examples "it cloned the default address" do + it do + default_attributes = default_address.attributes + order_attributes = order.send("#{address_kind}_address".to_sym).try(:attributes) || {} + + expect(order_attributes.except('id', 'created_at', 'updated_at')).to eql(default_attributes.except('id', 'created_at', 'updated_at')) + end + end + + it_behaves_like "it cloned the default address" do + let(:address_kind) { 'ship' } + end + + it_behaves_like "it cloned the default address" do + let(:address_kind) { 'bill' } + end + end + end + + it "cannot transition to address without any line items" do + expect(order.line_items).to be_blank + expect { order.next! }.to raise_error(StateMachine::InvalidTransition, /#{Spree.t(:there_are_no_items_for_this_order)}/) + end + end + + context "from address" do + before do + order.state = 'address' + allow(order).to receive(:has_available_payment) + shipment = FactoryGirl.create(:shipment, :order => order) + order.email = "user@example.com" + order.save! + end + + it "updates totals" do + allow(order).to receive_messages(:ensure_available_shipping_rates => true) + line_item = FactoryGirl.create(:line_item, :price => 10, :adjustment_total => 10) + order.line_items << line_item + tax_rate = create(:tax_rate, :tax_category => line_item.tax_category, :amount => 0.05) + allow(Spree::TaxRate).to receive_messages :match => [tax_rate] + FactoryGirl.create(:tax_adjustment, :adjustable => line_item, :source => tax_rate, order: order) + order.email = "user@example.com" + order.next! + expect(order.adjustment_total).to eq(0.5) + expect(order.additional_tax_total).to eq(0.5) + expect(order.included_tax_total).to eq(0) + expect(order.total).to eq(10.5) + end + + it "transitions to delivery" do + allow(order).to receive_messages(:ensure_available_shipping_rates => true) + order.next! + assert_state_changed(order, 'address', 'delivery') + expect(order.state).to eq("delivery") + end + + it "does not call persist_order_address if there is no address on the order" do + # otherwise, it will crash + allow(order).to receive_messages(:ensure_available_shipping_rates => true) + + order.user = FactoryGirl.create(:user) + order.save! + + expect(order.user).to_not receive(:persist_order_address).with(order) + order.next! + end + + it "calls persist_order_address on the order's user" do + allow(order).to receive_messages(:ensure_available_shipping_rates => true) + + order.user = FactoryGirl.create(:user) + order.ship_address = FactoryGirl.create(:address) + order.bill_address = FactoryGirl.create(:address) + order.save! + + expect(order.user).to receive(:persist_order_address).with(order) + order.next! + end + + it "does not call persist_order_address on the order's user for a temporary address" do + allow(order).to receive_messages(:ensure_available_shipping_rates => true) + + order.user = FactoryGirl.create(:user) + order.temporary_address = true + order.save! + + expect(order.user).to_not receive(:persist_order_address) + order.next! + end + + context "cannot transition to delivery" do + context "with an existing shipment" do + before do + line_item = FactoryGirl.create(:line_item, :price => 10) + order.line_items << line_item + end + + context "if there are no shipping rates for any shipment" do + it "raises an InvalidTransitionError" do + transition = lambda { order.next! } + expect(transition).to raise_error(StateMachine::InvalidTransition, /#{Spree.t(:items_cannot_be_shipped)}/) + end + + it "deletes all the shipments" do + order.next + expect(order.shipments).to be_empty + end + end + end + end + end + + context "to delivery" do + context 'when order has default selected_shipping_rate_id' do + let(:shipment) { create(:shipment, order: order) } + let(:shipping_method) { create(:shipping_method) } + let(:shipping_rate) { [ + Spree::ShippingRate.create!(shipping_method: shipping_method, cost: 10.00, shipment: shipment) + ] } + + before do + order.state = 'address' + shipment.selected_shipping_rate_id = shipping_rate.first.id + order.email = "user@example.com" + order.save! + + allow(order).to receive(:has_available_payment) + allow(order).to receive(:create_proposed_shipments) + allow(order).to receive(:ensure_available_shipping_rates) { true } + end + + it 'should invoke set_shipment_cost' do + expect(order).to receive(:set_shipments_cost) + order.next! + end + + it 'should update shipment_total' do + expect { order.next! }.to change{ order.shipment_total }.by(10.00) + end + end + end + + context "from delivery" do + before do + order.state = 'delivery' + allow(order).to receive(:apply_free_shipping_promotions) + end + + it "attempts to apply free shipping promotions" do + expect(order).to receive(:apply_free_shipping_promotions) + order.next! + end + + context "with payment required" do + before do + allow(order).to receive_messages :payment_required? => true + end + + it "transitions to payment" do + expect(order).to receive(:set_shipments_cost) + order.next! + assert_state_changed(order, 'delivery', 'payment') + expect(order.state).to eq('payment') + end + end + + context "without payment required" do + before do + allow(order).to receive_messages :payment_required? => false + end + + it "transitions to complete" do + order.next! + expect(order.state).to eq("complete") + end + end + + context "correctly determining payment required based on shipping information" do + let(:shipment) do + FactoryGirl.create(:shipment) + end + + before do + # Needs to be set here because we're working with a persisted order object + order.email = "test@example.com" + order.save! + order.shipments << shipment + end + + context "with a shipment that has a price" do + before do + shipment.shipping_rates.first.update_column(:cost, 10) + order.set_shipments_cost + end + + it "transitions to payment" do + order.next! + expect(order.state).to eq("payment") + end + end + + context "with a shipment that is free" do + before do + shipment.shipping_rates.first.update_column(:cost, 0) + order.set_shipments_cost + end + + it "skips payment, transitions to complete" do + order.next! + expect(order.state).to eq("complete") + end + end + end + end + + context "to payment" do + before do + @default_credit_card = FactoryGirl.create(:credit_card) + order.user = mock_model(Spree::LegacyUser, default_credit_card: @default_credit_card, email: 'spree@example.org') + + allow(order).to receive_messages(payment_required?: true) + order.state = 'delivery' + order.save! + end + + it "assigns the user's default credit card" do + order.next! + order.reload + + expect(order.state).to eq 'payment' + expect(order.payments.count).to eq 1 + expect(order.payments.first.source).to eq @default_credit_card + end + end + + context "from payment" do + before do + order.state = 'payment' + end + + context "with confirmation required" do + before do + allow(order).to receive_messages :confirmation_required? => true + end + + it "transitions to confirm" do + order.next! + assert_state_changed(order, 'payment', 'confirm') + expect(order.state).to eq("confirm") + end + end + + context "without confirmation required" do + before do + order.email = "spree@example.com" + allow(order).to receive_messages :confirmation_required? => false + allow(order).to receive_messages :payment_required? => true + order.payments << FactoryGirl.create(:payment, state: payment_state, order: order) + end + + context 'when there is at least one valid payment' do + let(:payment_state) { 'checkout' } + + before do + expect(order).to receive(:process_payments!).once { true } + end + + it "transitions to complete" do + order.next! + assert_state_changed(order, 'payment', 'complete') + expect(order.state).to eq('complete') + end + end + + context 'when there is only an invalid payment' do + let(:payment_state) { 'failed' } + + it "raises a StateMachine::InvalidTransition" do + expect { + order.next! + }.to raise_error(StateMachine::InvalidTransition, /#{Spree.t(:no_payment_found)}/) + + expect(order.errors[:base]).to include(Spree.t(:no_payment_found)) + end + end + end + + # Regression test for #2028 + context "when payment is not required" do + before do + allow(order).to receive_messages :payment_required? => false + end + + it "does not call process payments" do + expect(order).not_to receive(:process_payments!) + order.next! + assert_state_changed(order, 'payment', 'complete') + expect(order.state).to eq("complete") + end + end + end + end + + context "to complete" do + before do + order.state = 'confirm' + order.save! + end + + context "default credit card" do + before do + order.user = FactoryGirl.create(:user) + order.email = 'spree@example.org' + order.payments << FactoryGirl.create(:payment) + + # make sure we will actually capture a payment + allow(order).to receive_messages(payment_required?: true) + order.line_items << FactoryGirl.create(:line_item) + Spree::OrderUpdater.new(order).update + + order.save! + end + + it "makes the current credit card a user's default credit card" do + order.next! + expect(order.state).to eq 'complete' + expect(order.user.reload.default_credit_card.try(:id)).to eq(order.credit_cards.first.id) + end + + it "does not assign a default credit card if temporary_credit_card is set" do + order.temporary_credit_card = true + order.next! + expect(order.user.reload.default_credit_card).to be_nil + end + end + end + + context "subclassed order" do + # This causes another test above to fail, but fixing this test should make + # the other test pass + class SubclassedOrder < Spree::Order + checkout_flow do + go_to_state :payment + go_to_state :complete + end + end + + skip "should only call default transitions once when checkout_flow is redefined" do + order = SubclassedOrder.new + allow(order).to receive_messages :payment_required? => true + expect(order).to receive(:process_payments!).once + order.state = "payment" + order.next! + assert_state_changed(order, 'payment', 'complete') + expect(order.state).to eq("complete") + end + end + + context "re-define checkout flow" do + before do + @old_checkout_flow = Spree::Order.checkout_flow + Spree::Order.class_eval do + checkout_flow do + go_to_state :payment + go_to_state :complete + end + end + end + + after do + Spree::Order.checkout_flow(&@old_checkout_flow) + end + + it "should not keep old event transitions when checkout_flow is redefined" do + expect(Spree::Order.next_event_transitions).to eq([{:cart=>:payment}, {:payment=>:complete}]) + end + + it "should not keep old events when checkout_flow is redefined" do + state_machine = Spree::Order.state_machine + expect(state_machine.states.any? { |s| s.name == :address }).to be false + known_states = state_machine.events[:next].branches.map(&:known_states).flatten + expect(known_states).not_to include(:address) + expect(known_states).not_to include(:delivery) + expect(known_states).not_to include(:confirm) + end + end + + # Regression test for #3665 + context "with only a complete step" do + before do + @old_checkout_flow = Spree::Order.checkout_flow + Spree::Order.class_eval do + checkout_flow do + go_to_state :complete + end + end + end + + after do + Spree::Order.checkout_flow(&@old_checkout_flow) + end + + it "does not attempt to process payments" do + allow(order).to receive_message_chain(:line_items, :present?) { true } + allow(order).to receive(:ensure_line_items_are_in_stock) { true } + allow(order).to receive(:ensure_line_item_variants_are_not_deleted) { true } + expect(order).not_to receive(:payment_required?) + expect(order).not_to receive(:process_payments!) + order.next! + assert_state_changed(order, 'cart', 'complete') + end + + end + + context "insert checkout step" do + before do + @old_checkout_flow = Spree::Order.checkout_flow + Spree::Order.class_eval do + insert_checkout_step :new_step, before: :address + end + end + + after do + Spree::Order.checkout_flow(&@old_checkout_flow) + end + + it "should maintain removed transitions" do + transition = Spree::Order.find_transition(:from => :delivery, :to => :confirm) + expect(transition).to be_nil + end + + context "before" do + before do + Spree::Order.class_eval do + insert_checkout_step :before_address, before: :address + end + end + + specify do + order = Spree::Order.new + expect(order.checkout_steps).to eq(%w(new_step before_address address delivery complete)) + end + end + + context "after" do + before do + Spree::Order.class_eval do + insert_checkout_step :after_address, after: :address + end + end + + specify do + order = Spree::Order.new + expect(order.checkout_steps).to eq(%w(new_step address after_address delivery complete)) + end + end + end + + context "remove checkout step" do + before do + @old_checkout_flow = Spree::Order.checkout_flow + Spree::Order.class_eval do + remove_checkout_step :address + end + end + + after do + Spree::Order.checkout_flow(&@old_checkout_flow) + end + + it "should maintain removed transitions" do + transition = Spree::Order.find_transition(:from => :delivery, :to => :confirm) + expect(transition).to be_nil + end + + specify do + order = Spree::Order.new + expect(order.checkout_steps).to eq(%w(delivery complete)) + end + end + + describe "payment processing" do + self.use_transactional_fixtures = false + before do + Spree::PaymentMethod.destroy_all # TODO data is leaking between specs as database_cleaner or rspec 3 was broken in Rails 4.1.6 & 4.0.10 + # Turn off transactional fixtures so that we can test that + # processing state is persisted. + DatabaseCleaner.strategy = :truncation + end + + after do + DatabaseCleaner.clean + # Turn on transactional fixtures again. + self.use_transactional_fixtures = true + end + + let(:order) { OrderWalkthrough.up_to(:payment) } + let(:creditcard) { create(:credit_card) } + let!(:payment_method) { create(:credit_card_payment_method, environment: 'test') } + + it "does not process payment within transaction" do + # Make sure we are not already in a transaction + expect(ActiveRecord::Base.connection.open_transactions).to eq 0 + + expect_any_instance_of(Spree::Payment).to receive(:authorize!) do + expect(ActiveRecord::Base.connection.open_transactions).to eq 0 + end + + order.next! + end + end + + describe 'update_from_params' do + let(:permitted_params) { {} } + let(:params) { {} } + + it 'calls update_atributes without order params' do + expect(order).to receive(:update_attributes).with({}) + order.update_from_params( params, permitted_params) + end + + it 'runs the callbacks' do + expect(order).to receive(:run_callbacks).with(:updating_from_params) + order.update_from_params( params, permitted_params) + end + + context "passing a credit card" do + let(:permitted_params) do + Spree::PermittedAttributes.checkout_attributes + + [payments_attributes: Spree::PermittedAttributes.payment_attributes] + end + + let(:credit_card) { create(:credit_card, user_id: order.user_id) } + + let(:params) do + ActionController::Parameters.new( + order: { payments_attributes: [{payment_method_id: 1}], existing_card: credit_card.id }, + cvc_confirm: "737", + payment_source: { + "1" => { name: "Luis Braga", + number: "4111 1111 1111 1111", + expiry: "06 / 2016", + verification_value: "737", + cc_type: "" } + } + ) + end + + before { order.user_id = 3 } + + it "sets confirmation value when its available via :cvc_confirm" do + allow(Spree::CreditCard).to receive_messages find: credit_card + expect(credit_card).to receive(:verification_value=) + order.update_from_params(params, permitted_params) + end + + it "sets existing card as source for new payment" do + expect { + order.update_from_params(params, permitted_params) + }.to change { Spree::Payment.count }.by(1) + + expect(Spree::Payment.last.source).to eq credit_card + end + + it "sets request_env on payment" do + request_env = { "USER_AGENT" => "Firefox" } + + expected_hash = { "payments_attributes" => [hash_including("request_env" => request_env)] } + expect(order).to receive(:update_attributes).with expected_hash + + order.update_from_params(params, permitted_params, request_env) + end + + it "dont let users mess with others users cards" do + credit_card.update_column :user_id, 5 + + expect { + order.update_from_params(params, permitted_params) + }.to raise_error + end + end + + context 'has params' do + let(:permitted_params) { [ :good_param ] } + let(:params) { ActionController::Parameters.new(order: { bad_param: 'okay' } ) } + + it 'does not let through unpermitted attributes' do + expect(order).to receive(:update_attributes).with({}) + order.update_from_params(params, permitted_params) + end + + context 'has allowed params' do + let(:params) { ActionController::Parameters.new(order: { good_param: 'okay' } ) } + + it 'accepts permitted attributes' do + expect(order).to receive(:update_attributes).with({"good_param" => 'okay'}) + order.update_from_params(params, permitted_params) + end + end + + context 'callbacks halt' do + before do + expect(order).to receive(:update_params_payment_source).and_return false + end + it 'does not let through unpermitted attributes' do + expect(order).not_to receive(:update_attributes).with({}) + order.update_from_params(params, permitted_params) + end + end + end + end +end diff --git a/core/spec/models/spree/order/currency_updater_spec.rb b/core/spec/models/spree/order/currency_updater_spec.rb new file mode 100644 index 00000000000..c515aee98ce --- /dev/null +++ b/core/spec/models/spree/order/currency_updater_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Spree::Order, :type => :model do + context 'CurrencyUpdater' do + context "when changing order currency" do + let!(:line_item) { create(:line_item) } + let!(:euro_price) { create(:price, variant: line_item.variant, amount: 8, currency: 'EUR') } + + context "#homogenize_line_item_currencies" do + it "succeeds without error" do + expect { line_item.order.update_attributes!(currency: 'EUR') }.to_not raise_error + end + + it "changes the line_item currencies" do + expect { line_item.order.update_attributes!(currency: 'EUR') }.to change{ line_item.reload.currency }.from('USD').to('EUR') + end + + it "changes the line_item amounts" do + expect { line_item.order.update_attributes!(currency: 'EUR') }.to change{ line_item.reload.amount }.to(8) + end + + it "fails to change the order currency when no prices are available in that currency" do + expect { line_item.order.update_attributes!(currency: 'GBP') }.to raise_error + end + + it "calculates the item total in the order.currency" do + expect { line_item.order.update_attributes!(currency: 'EUR') }.to change{ line_item.order.item_total }.to(8) + end + end + end + end +end diff --git a/core/spec/models/spree/order/finalizing_spec.rb b/core/spec/models/spree/order/finalizing_spec.rb new file mode 100644 index 00000000000..29648a8b0ae --- /dev/null +++ b/core/spec/models/spree/order/finalizing_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +describe Spree::Order, :type => :model do + let(:order) { stub_model("Spree::Order") } + + context "#finalize!" do + let(:order) { Spree::Order.create(email: 'test@example.com') } + + before do + order.update_column :state, 'complete' + end + + it "should set completed_at" do + expect(order).to receive(:touch).with(:completed_at) + order.finalize! + end + + it "should sell inventory units" do + order.shipments.each do |shipment| + expect(shipment).to receive(:update!) + expect(shipment).to receive(:finalize!) + end + order.finalize! + end + + it "should decrease the stock for each variant in the shipment" do + order.shipments.each do |shipment| + expect(shipment.stock_location).to receive(:decrease_stock_for_variant) + end + order.finalize! + end + + it "should change the shipment state to ready if order is paid" do + Spree::Shipment.create(order: order, stock_location: create(:stock_location)) + order.shipments.reload + + allow(order).to receive_messages(paid?: true, complete?: true) + order.finalize! + order.reload # reload so we're sure the changes are persisted + expect(order.shipment_state).to eq('ready') + end + + after { Spree::Config.set track_inventory_levels: true } + it "should not sell inventory units if track_inventory_levels is false" do + Spree::Config.set track_inventory_levels: false + expect(Spree::InventoryUnit).not_to receive(:sell_units) + order.finalize! + end + + it "should send an order confirmation email" do + mail_message = double "Mail::Message" + expect(Spree::OrderMailer).to receive(:confirm_email).with(order.id).and_return mail_message + expect(mail_message).to receive :deliver + order.finalize! + end + + it "sets confirmation delivered when finalizing" do + expect(order.confirmation_delivered?).to be false + order.finalize! + expect(order.confirmation_delivered?).to be true + end + + it "should not send duplicate confirmation emails" do + allow(order).to receive_messages(:confirmation_delivered? => true) + expect(Spree::OrderMailer).not_to receive(:confirm_email) + order.finalize! + end + + it "should freeze all adjustments" do + # Stub this method as it's called due to a callback + # and it's irrelevant to this test + allow(order).to receive :has_available_shipment + allow(Spree::OrderMailer).to receive_message_chain :confirm_email, :deliver + adjustments = [double] + expect(order).to receive(:all_adjustments).and_return(adjustments) + adjustments.each do |adj| + expect(adj).to receive(:close) + end + order.finalize! + end + + context "order is considered risky" do + before do + allow(order).to receive_messages :is_risky? => true + end + + it "should change state to risky" do + expect(order).to receive(:considered_risky!) + order.finalize! + end + + context "and order is approved" do + before do + allow(order).to receive_messages :approved? => true + end + + it "should leave order in complete state" do + order.finalize! + expect(order.state).to eq 'complete' + end + end + end + + context "order is not considered risky" do + before do + allow(order).to receive_messages :is_risky? => false + end + + it "should set completed_at" do + order.finalize! + expect(order.completed_at).to be_present + end + end + end +end diff --git a/core/spec/models/spree/order/helpers_spec.rb b/core/spec/models/spree/order/helpers_spec.rb new file mode 100644 index 00000000000..2a1064b986a --- /dev/null +++ b/core/spec/models/spree/order/helpers_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Spree::Order, :type => :model do + let(:order) { stub_model(Spree::Order) } +end \ No newline at end of file diff --git a/core/spec/models/spree/order/payment_spec.rb b/core/spec/models/spree/order/payment_spec.rb new file mode 100644 index 00000000000..0d9f8084ad8 --- /dev/null +++ b/core/spec/models/spree/order/payment_spec.rb @@ -0,0 +1,212 @@ +require 'spec_helper' + +module Spree + describe Spree::Order, :type => :model do + let(:order) { stub_model(Spree::Order) } + let(:updater) { Spree::OrderUpdater.new(order) } + + context "processing payments" do + before do + # So that Payment#purchase! is called during processing + Spree::Config[:auto_capture] = true + + allow(order).to receive_message_chain(:line_items, :empty?).and_return(false) + allow(order).to receive_messages :total => 100 + end + + it 'processes all checkout payments' do + payment_1 = create(:payment, :amount => 50) + payment_2 = create(:payment, :amount => 50) + allow(order).to receive(:unprocessed_payments).and_return([payment_1, payment_2]) + + order.process_payments! + updater.update_payment_state + expect(order.payment_state).to eq('paid') + + expect(payment_1).to be_completed + expect(payment_2).to be_completed + end + + it 'does not go over total for order' do + payment_1 = create(:payment, :amount => 50) + payment_2 = create(:payment, :amount => 50) + payment_3 = create(:payment, :amount => 50) + allow(order).to receive(:unprocessed_payments).and_return([payment_1, payment_2, payment_3]) + + order.process_payments! + updater.update_payment_state + expect(order.payment_state).to eq('paid') + + expect(payment_1).to be_completed + expect(payment_2).to be_completed + expect(payment_3).to be_checkout + end + + it "does not use failed payments" do + payment_1 = create(:payment, :amount => 50) + payment_2 = create(:payment, :amount => 50, :state => 'failed') + allow(order).to receive(:pending_payments).and_return([payment_1]) + + expect(payment_2).not_to receive(:process!) + + order.process_payments! + end + end + + context "ensure source attributes stick around" do + # For the reason of this test, please see spree/spree_gateway#132 + it "does not have inverse_of defined" do + expect(Spree::Order.reflections[:payments].options[:inverse_of]).to be_nil + end + + it "keeps source attributes after updating" do + persisted_order = Spree::Order.create + credit_card_payment_method = create(:credit_card_payment_method) + attributes = { + :payments_attributes => [ + { + :payment_method_id => credit_card_payment_method.id, + :source_attributes => { + :name => "Ryan Bigg", + :number => "41111111111111111111", + :expiry => "01 / 15", + :verification_value => "123" + } + } + ] + } + + persisted_order.update_attributes(attributes) + expect(persisted_order.unprocessed_payments.last.source.number).to be_present + end + end + + context "checking if order is paid" do + context "payment_state is paid" do + before { allow(order).to receive_messages payment_state: 'paid' } + it { expect(order).to be_paid } + end + + context "payment_state is credit_owned" do + before { allow(order).to receive_messages payment_state: 'credit_owed' } + it { expect(order).to be_paid } + end + end + + context "#process_payments!" do + let(:payment) { stub_model(Spree::Payment) } + before { allow(order).to receive_messages unprocessed_payments: [payment], total: 10 } + + it "should process the payments" do + expect(payment).to receive(:process!) + expect(order.process_payments!).to be_truthy + end + + # Regression spec for https://github.com/spree/spree/issues/5436 + it 'should raise an error if there are no payments to process' do + allow(order).to receive_messages unprocessed_payments: [] + expect(payment).to_not receive(:process!) + expect(order.process_payments!).to be_falsey + end + + context "when a payment raises a GatewayError" do + before { expect(payment).to receive(:process!).and_raise(Spree::Core::GatewayError) } + + it "should return true when configured to allow checkout on gateway failures" do + Spree::Config.set :allow_checkout_on_gateway_error => true + expect(order.process_payments!).to be true + end + + it "should return false when not configured to allow checkout on gateway failures" do + Spree::Config.set :allow_checkout_on_gateway_error => false + expect(order.process_payments!).to be false + end + end + end + + context "#authorize_payments!" do + let(:payment) { stub_model(Spree::Payment) } + before { allow(order).to receive_messages :unprocessed_payments => [payment], :total => 10 } + subject { order.authorize_payments! } + + it "processes payments with attempt_authorization!" do + expect(payment).to receive(:authorize!) + subject + end + + it { is_expected.to be_truthy } + end + + context "#capture_payments!" do + let(:payment) { stub_model(Spree::Payment) } + before { allow(order).to receive_messages :unprocessed_payments => [payment], :total => 10 } + subject { order.capture_payments! } + + it "processes payments with attempt_authorization!" do + expect(payment).to receive(:purchase!) + subject + end + + it { is_expected.to be_truthy } + end + + context "#outstanding_balance" do + it "should return positive amount when payment_total is less than total" do + order.payment_total = 20.20 + order.total = 30.30 + expect(order.outstanding_balance).to eq(10.10) + end + it "should return negative amount when payment_total is greater than total" do + order.total = 8.20 + order.payment_total = 10.20 + expect(order.outstanding_balance).to be_within(0.001).of(-2.00) + end + it 'should incorporate refund reimbursements' do + # Creates an order w/total 10 + reimbursement = create :reimbursement + # Set the payment amount to actually be the order total of 10 + reimbursement.order.payments.first.update_column :amount, 10 + # Creates a refund of 10 + create :refund, amount: 10, + payment: reimbursement.order.payments.first, + reimbursement: reimbursement + order = reimbursement.order.reload + # Update the order totals so payment_total goes to 0 reflecting the refund.. + order.update! + # Order Total - (Payment Total + Reimbursed) + # 10 - (0 + 10) = 0 + expect(order.outstanding_balance).to eq 0 + end + end + + context "#outstanding_balance?" do + it "should be true when total greater than payment_total" do + order.total = 10.10 + order.payment_total = 9.50 + expect(order.outstanding_balance?).to be true + end + it "should be true when total less than payment_total" do + order.total = 8.25 + order.payment_total = 10.44 + expect(order.outstanding_balance?).to be true + end + it "should be false when total equals payment_total" do + order.total = 10.10 + order.payment_total = 10.10 + expect(order.outstanding_balance?).to be false + end + end + + context "payment required?" do + context "total is zero" do + before { allow(order).to receive_messages(total: 0) } + it { expect(order.payment_required?).to be false } + end + + context "total > zero" do + before { allow(order).to receive_messages(total: 1) } + it { expect(order.payment_required?).to be true } + end + end + end +end diff --git a/core/spec/models/spree/order/risk_assessment_spec.rb b/core/spec/models/spree/order/risk_assessment_spec.rb new file mode 100644 index 00000000000..1bc5a8e43fa --- /dev/null +++ b/core/spec/models/spree/order/risk_assessment_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Spree::Order, :type => :model do + let(:order) { stub_model('Spree::Order') } + + describe ".is_risky?" do + context "Not risky order" do + let(:order) { FactoryGirl.create(:order, payments: [payment]) } + context "with avs_response == D" do + let(:payment) { FactoryGirl.create(:payment, avs_response: "D") } + it "is not considered risky" do + expect(order.is_risky?).to eq(false) + end + end + + context "with avs_response == M" do + let(:payment) { FactoryGirl.create(:payment, avs_response: "M") } + it "is not considered risky" do + expect(order.is_risky?).to eq(false) + end + end + + context "with avs_response == ''" do + let(:payment) { FactoryGirl.create(:payment, avs_response: "") } + it "is not considered risky" do + expect(order.is_risky?).to eq(false) + end + end + + context "with cvv_response_code == M" do + let(:payment) { FactoryGirl.create(:payment, cvv_response_code: "M") } + it "is not considered risky" do + expect(order.is_risky?).to eq(false) + end + end + + context "with cvv_response_message == ''" do + let(:payment) { FactoryGirl.create(:payment, cvv_response_message: "") } + it "is not considered risky" do + expect(order.is_risky?).to eq(false) + end + end + end + + context "Risky order" do + context "AVS response message" do + let(:order) { FactoryGirl.create(:order, payments: [FactoryGirl.create(:payment, avs_response: "A")]) } + it "returns true if the order has an avs_response" do + expect(order.is_risky?).to eq(true) + end + end + + context "CVV response code" do + let(:order) { FactoryGirl.create(:order, payments: [FactoryGirl.create(:payment, cvv_response_code: "N")]) } + it "returns true if the order has an cvv_response_code" do + expect(order.is_risky?).to eq(true) + end + end + + context "state == 'failed'" do + let(:order) { FactoryGirl.create(:order, payments: [FactoryGirl.create(:payment, state: 'failed')]) } + it "returns true if the order has state == 'failed'" do + expect(order.is_risky?).to eq(true) + end + end + end + end + + context "is considered risky" do + let(:order) do + order = FactoryGirl.create(:completed_order_with_pending_payment) + order.considered_risky! + order + end + + it "can be approved by a user" do + expect(order).to receive(:approve!) + order.approved_by(stub_model(Spree::LegacyUser, id: 1)) + expect(order.approver_id).to eq(1) + expect(order.approved_at).to be_present + expect(order.approved?).to be true + end + end +end diff --git a/core/spec/models/spree/order/shipments_spec.rb b/core/spec/models/spree/order/shipments_spec.rb new file mode 100644 index 00000000000..f8871668c13 --- /dev/null +++ b/core/spec/models/spree/order/shipments_spec.rb @@ -0,0 +1,43 @@ +describe Spree::Order, type: :model do + let(:order) { create(:order_with_totals) } + + context "ensure shipments will be updated" do + before { Spree::Shipment.create!(order: order, stock_location: create(:stock_location)) } + + it "destroys current shipments" do + order.ensure_updated_shipments + expect(order.shipments).to be_empty + end + + it "puts order back in address state" do + order.ensure_updated_shipments + expect(order.state).to eq 'address' + end + + it "resets shipment_total" do + order.update_column(:shipment_total, 5) + order.ensure_updated_shipments + expect(order.shipment_total).to eq(0) + end + + context "except when order is completed, that's OrderInventory job" do + it "doesn't touch anything" do + allow(order).to receive_messages completed?: true + order.update_column(:shipment_total, 5) + order.shipments.create!(stock_location: create(:stock_location)) + + expect { + order.ensure_updated_shipments + }.not_to change { order.shipment_total } + + expect { + order.ensure_updated_shipments + }.not_to change { order.shipments } + + expect { + order.ensure_updated_shipments + }.not_to change { order.state } + end + end + end +end diff --git a/core/spec/models/spree/order/state_machine_spec.rb b/core/spec/models/spree/order/state_machine_spec.rb new file mode 100644 index 00000000000..e0d924ee334 --- /dev/null +++ b/core/spec/models/spree/order/state_machine_spec.rb @@ -0,0 +1,219 @@ +require 'spec_helper' + +describe Spree::Order, type: :model do + let(:order) { Spree::Order.new } + before do + # Ensure state machine has been re-defined correctly + Spree::Order.define_state_machine! + # We don't care about this validation here + allow(order).to receive(:require_email) + end + + context "#next!" do + context "when current state is confirm" do + before do + order.state = "confirm" + order.run_callbacks(:create) + allow(order).to receive_messages payment_required?: true + allow(order).to receive_messages process_payments!: true + allow(order).to receive :has_available_shipment + end + + context "when payment processing succeeds" do + before do + order.payments << FactoryGirl.create(:payment, state: 'checkout', order: order) + allow(order).to receive_messages process_payments: true + end + + it "should finalize order when transitioning to complete state" do + expect(order).to receive(:finalize!) + order.next! + end + + context "when credit card processing fails" do + before { allow(order).to receive_messages process_payments!: false } + + it "should not complete the order" do + order.next + expect(order.state).to eq("confirm") + end + end + end + + context "when payment processing fails" do + before { allow(order).to receive_messages process_payments!: false } + + it "cannot transition to complete" do + order.next + expect(order.state).to eq("confirm") + end + end + end + + context "when current state is delivery" do + before do + allow(order).to receive_messages payment_required?: true + allow(order).to receive :apply_free_shipping_promotions + order.state = "delivery" + end + + it "adjusts tax rates when transitioning to delivery" do + # Once for the line items + expect(Spree::TaxRate).to receive(:adjust).once + allow(order).to receive :set_shipments_cost + order.next! + end + + it "adjusts tax rates twice if there are any shipments" do + # Once for the line items, once for the shipments + order.shipments.build stock_location: create(:stock_location) + expect(Spree::TaxRate).to receive(:adjust).twice + allow(order).to receive :set_shipments_cost + order.next! + end + end + end + + context "#can_cancel?" do + + %w(pending backorder ready).each do |shipment_state| + it "should be true if shipment_state is #{shipment_state}" do + allow(order).to receive_messages completed?: true + order.shipment_state = shipment_state + expect(order.can_cancel?).to be true + end + end + + (Spree::Shipment.state_machine.states.keys - %w(pending backorder ready)).each do |shipment_state| + it "should be false if shipment_state is #{shipment_state}" do + allow(order).to receive_messages completed?: true + order.shipment_state = shipment_state + expect(order.can_cancel?).to be false + end + end + + end + + context "#cancel" do + let!(:variant) { stub_model(Spree::Variant) } + let!(:inventory_units) { [stub_model(Spree::InventoryUnit, variant: variant), + stub_model(Spree::InventoryUnit, variant: variant)] } + let!(:shipment) do + shipment = stub_model(Spree::Shipment) + allow(shipment).to receive_messages inventory_units: inventory_units, order: order + allow(order).to receive_messages shipments: [shipment] + shipment + end + + before do + 2.times do + create(:line_item, order: order, price: 10) + end + + allow(order.line_items).to receive_messages find_by_variant_id: order.line_items.first + + allow(order).to receive_messages completed?: true + allow(order).to receive_messages allow_cancel?: true + + shipments = [shipment] + allow(order).to receive_messages shipments: shipments + allow(shipments).to receive_messages states: [] + allow(shipments).to receive_messages ready: [] + allow(shipments).to receive_messages pending: [] + allow(shipments).to receive_messages shipped: [] + + allow_any_instance_of(Spree::OrderUpdater).to receive(:update_adjustment_total) { 10 } + end + + it "should send a cancel email" do + + # Stub methods that cause side-effects in this test + allow(shipment).to receive(:cancel!) + allow(order).to receive :has_available_shipment + allow(order).to receive :restock_items! + mail_message = double "Mail::Message" + order_id = nil + expect(Spree::OrderMailer).to receive(:cancel_email) { |*args| + order_id = args[0] + mail_message + } + expect(mail_message).to receive :deliver + order.cancel! + expect(order_id).to eq(order.id) + end + + context "restocking inventory" do + before do + allow(shipment).to receive(:ensure_correct_adjustment) + allow(shipment).to receive(:update_order) + allow(Spree::OrderMailer).to receive(:cancel_email).and_return(mail_message = double) + allow(mail_message).to receive :deliver + + allow(order).to receive :has_available_shipment + end + end + + context "resets payment state" do + let(:payment) { create(:payment, amount: order.total) } + + before do + # TODO: This is ugly :( + # Stubs methods that cause unwanted side effects in this test + allow(Spree::OrderMailer).to receive(:cancel_email).and_return(mail_message = double) + allow(mail_message).to receive :deliver + allow(order).to receive :has_available_shipment + allow(order).to receive :restock_items! + allow(shipment).to receive(:cancel!) + allow(payment).to receive(:cancel!) + allow(order).to receive_message_chain(:payments, :valid, :size).and_return(1) + allow(order).to receive_message_chain(:payments, :completed).and_return([payment]) + allow(order).to receive_message_chain(:payments, :completed, :includes).and_return([payment]) + allow(order).to receive_message_chain(:payments, :last).and_return(payment) + end + + context "without shipped items" do + it "should set payment state to 'void'" do + expect { order.cancel! }.to change{ order.reload.payment_state }.to("void") + end + end + + context "with shipped items" do + before do + allow(order).to receive_messages shipment_state: 'partial' + allow(order).to receive_messages outstanding_balance?: false + allow(order).to receive_messages payment_state: "paid" + end + + it "should not alter the payment state" do + order.cancel! + expect(order.payment_state).to eql "paid" + end + end + + context "with payments" do + let(:payment) { create(:payment) } + + it "should automatically refund all payments" do + allow(order).to receive_message_chain(:payments, :valid, :size).and_return(1) + allow(order).to receive_message_chain(:payments, :completed).and_return([payment]) + allow(order).to receive_message_chain(:payments, :completed, :includes).and_return([payment]) + allow(order).to receive_message_chain(:payments, :last).and_return(payment) + expect(payment).to receive(:cancel!) + order.cancel! + end + end + end + end + + # Another regression test for #729 + context "#resume" do + before do + allow(order).to receive_messages email: "user@spreecommerce.com" + allow(order).to receive_messages state: "canceled" + allow(order).to receive_messages allow_resume?: true + + # Stubs method that cause unwanted side effects in this test + allow(order).to receive :has_available_shipment + end + end +end diff --git a/core/spec/models/spree/order/tax_spec.rb b/core/spec/models/spree/order/tax_spec.rb new file mode 100644 index 00000000000..92ec95caaab --- /dev/null +++ b/core/spec/models/spree/order/tax_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +module Spree + describe Spree::Order, :type => :model do + let(:order) { stub_model(Spree::Order) } + + context "#tax_zone" do + let(:bill_address) { create :address } + let(:ship_address) { create :address } + let(:order) { Spree::Order.create(:ship_address => ship_address, :bill_address => bill_address) } + let(:zone) { create :zone } + + context "when no zones exist" do + it "should return nil" do + expect(order.tax_zone).to be_nil + end + end + + context "when :tax_using_ship_address => true" do + before { Spree::Config.set(:tax_using_ship_address => true) } + + it "should calculate using ship_address" do + expect(Spree::Zone).to receive(:match).at_least(:once).with(ship_address) + expect(Spree::Zone).not_to receive(:match).with(bill_address) + order.tax_zone + end + end + + context "when :tax_using_ship_address => false" do + before { Spree::Config.set(:tax_using_ship_address => false) } + + it "should calculate using bill_address" do + expect(Spree::Zone).to receive(:match).at_least(:once).with(bill_address) + expect(Spree::Zone).not_to receive(:match).with(ship_address) + order.tax_zone + end + end + + context "when there is a default tax zone" do + before do + @default_zone = create(:zone, :name => "foo_zone") + allow(Spree::Zone).to receive_messages :default_tax => @default_zone + end + + context "when there is a matching zone" do + before { allow(Spree::Zone).to receive_messages(:match => zone) } + + it "should return the matching zone" do + expect(order.tax_zone).to eq(zone) + end + end + + context "when there is no matching zone" do + before { allow(Spree::Zone).to receive_messages(:match => nil) } + + it "should return the default tax zone" do + expect(order.tax_zone).to eq(@default_zone) + end + end + end + + context "when no default tax zone" do + before { allow(Spree::Zone).to receive_messages :default_tax => nil } + + context "when there is a matching zone" do + before { allow(Spree::Zone).to receive_messages(:match => zone) } + + it "should return the matching zone" do + expect(order.tax_zone).to eq(zone) + end + end + + context "when there is no matching zone" do + before { allow(Spree::Zone).to receive_messages(:match => nil) } + + it "should return nil" do + expect(order.tax_zone).to be_nil + end + end + end + end + + end +end diff --git a/core/spec/models/spree/order/totals_spec.rb b/core/spec/models/spree/order/totals_spec.rb new file mode 100644 index 00000000000..35db1ff808b --- /dev/null +++ b/core/spec/models/spree/order/totals_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +module Spree + describe Order, :type => :model do + let(:order) { Order.create } + let(:shirt) { create(:variant) } + + context "adds item to cart and activates promo" do + let(:promotion) { Promotion.create name: 'Huhu' } + let(:calculator) { Calculator::FlatPercentItemTotal.new(:preferred_flat_percent => 10) } + let!(:action) { Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) } + + before { order.contents.add(shirt, 1) } + + context "item quantity changes" do + it "recalculates order adjustments" do + expect { + order.contents.add(shirt, 3) + }.to change { order.adjustments.eligible.pluck(:amount) } + end + end + end + end +end diff --git a/core/spec/models/order/updating_spec.rb b/core/spec/models/spree/order/updating_spec.rb similarity index 83% rename from core/spec/models/order/updating_spec.rb rename to core/spec/models/spree/order/updating_spec.rb index 9b50d029867..8a8dd4112a5 100644 --- a/core/spec/models/order/updating_spec.rb +++ b/core/spec/models/spree/order/updating_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Spree::Order do +describe Spree::Order, :type => :model do let(:order) { stub_model(Spree::Order) } context "#update!" do @@ -10,7 +10,7 @@ before { Spree::Order.register_update_hook :foo } after { Spree::Order.update_hooks.clear } it "should call each of the update hooks" do - order.should_receive :foo + expect(order).to receive :foo order.update! end end diff --git a/core/spec/models/spree/order/validations_spec.rb b/core/spec/models/spree/order/validations_spec.rb new file mode 100644 index 00000000000..f57eb95d78a --- /dev/null +++ b/core/spec/models/spree/order/validations_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +module Spree + describe Spree::Order, :type => :model do + context "validations" do + # Regression test for #2214 + it "does not return two error messages when email is blank" do + order = Spree::Order.new + allow(order).to receive_messages(:require_email => true) + order.valid? + expect(order.errors[:email]).to eq(["can't be blank"]) + end + end + end +end diff --git a/core/spec/models/spree/order_contents_spec.rb b/core/spec/models/spree/order_contents_spec.rb new file mode 100644 index 00000000000..a6c5bce5ac0 --- /dev/null +++ b/core/spec/models/spree/order_contents_spec.rb @@ -0,0 +1,227 @@ +require 'spec_helper' + +describe Spree::OrderContents, :type => :model do + let(:order) { Spree::Order.create } + let(:variant) { create(:variant) } + + subject { described_class.new(order) } + + context "#add" do + context 'given quantity is not explicitly provided' do + it 'should add one line item' do + line_item = subject.add(variant) + expect(line_item.quantity).to eq(1) + expect(order.line_items.size).to eq(1) + end + end + + context 'given a shipment' do + it "ensure shipment calls update_amounts instead of order calling ensure_updated_shipments" do + shipment = create(:shipment) + expect(subject.order).to_not receive(:ensure_updated_shipments) + expect(shipment).to receive(:update_amounts) + subject.add(variant, 1, shipment: shipment) + end + end + + context 'not given a shipment' do + it "ensures updated shipments" do + expect(subject.order).to receive(:ensure_updated_shipments) + subject.add(variant) + end + end + + it 'should add line item if one does not exist' do + line_item = subject.add(variant, 1) + expect(line_item.quantity).to eq(1) + expect(order.line_items.size).to eq(1) + end + + it 'should update line item if one exists' do + subject.add(variant, 1) + line_item = subject.add(variant, 1) + expect(line_item.quantity).to eq(2) + expect(order.line_items.size).to eq(1) + end + + it "should update order totals" do + expect(order.item_total.to_f).to eq(0.00) + expect(order.total.to_f).to eq(0.00) + + subject.add(variant, 1) + + expect(order.item_total.to_f).to eq(19.99) + expect(order.total.to_f).to eq(19.99) + end + + context "running promotions" do + let(:promotion) { create(:promotion) } + let(:calculator) { Spree::Calculator::FlatRate.new(:preferred_amount => 10) } + + shared_context "discount changes order total" do + before { subject.add(variant, 1) } + it { expect(subject.order.total).not_to eq variant.price } + end + + context "one active order promotion" do + let!(:action) { Spree::Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) } + + it "creates valid discount on order" do + subject.add(variant, 1) + expect(subject.order.adjustments.to_a.sum(&:amount)).not_to eq 0 + end + + include_context "discount changes order total" + end + + context "one active line item promotion" do + let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) } + + it "creates valid discount on order" do + subject.add(variant, 1) + expect(subject.order.line_item_adjustments.to_a.sum(&:amount)).not_to eq 0 + end + + include_context "discount changes order total" + end + end + end + + context "#remove" do + context "given an invalid variant" do + it "raises an exception" do + expect { + subject.remove(variant, 1) + }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'given quantity is not explicitly provided' do + it 'should remove one line item' do + line_item = subject.add(variant, 3) + subject.remove(variant) + + expect(line_item.quantity).to eq(2) + end + end + + context 'given a shipment' do + it "ensure shipment calls update_amounts instead of order calling ensure_updated_shipments" do + line_item = subject.add(variant, 1) + shipment = create(:shipment) + expect(subject.order).to_not receive(:ensure_updated_shipments) + expect(shipment).to receive(:update_amounts) + subject.remove(variant, 1, shipment: shipment) + end + end + + context 'not given a shipment' do + it "ensures updated shipments" do + line_item = subject.add(variant, 1) + expect(subject.order).to receive(:ensure_updated_shipments) + subject.remove(variant) + end + end + + it 'should reduce line_item quantity if quantity is less the line_item quantity' do + line_item = subject.add(variant, 3) + subject.remove(variant, 1) + + expect(line_item.quantity).to eq(2) + end + + it 'should remove line_item if quantity matches line_item quantity' do + subject.add(variant, 1) + removed_line_item = subject.remove(variant, 1) + + # Should reflect the change already in Order#line_item + expect(order.line_items).to_not include(removed_line_item) + end + + it "should update order totals" do + expect(order.item_total.to_f).to eq(0.00) + expect(order.total.to_f).to eq(0.00) + + subject.add(variant,2) + + expect(order.item_total.to_f).to eq(39.98) + expect(order.total.to_f).to eq(39.98) + + subject.remove(variant,1) + expect(order.item_total.to_f).to eq(19.99) + expect(order.total.to_f).to eq(19.99) + end + end + + context "update cart" do + let!(:shirt) { subject.add variant, 1 } + + let(:params) do + { line_items_attributes: { + "0" => { id: shirt.id, quantity: 3 } + } } + end + + it "changes item quantity" do + subject.update_cart params + expect(shirt.quantity).to eq 3 + end + + it "updates order totals" do + expect { + subject.update_cart params + }.to change { subject.order.total } + end + + context "submits item quantity 0" do + let(:params) do + { line_items_attributes: { + "0" => { id: shirt.id, quantity: 0 }, + "1" => { id: "666", quantity: 0} + } } + end + + it "removes item from order" do + expect { + subject.update_cart params + }.to change { subject.order.line_items.count } + end + + it "doesnt try to update unexistent items" do + filtered_params = { line_items_attributes: { + "0" => { id: shirt.id, quantity: 0 }, + } } + expect(subject.order).to receive(:update_attributes).with(filtered_params) + subject.update_cart params + end + + it "should not filter if there is only one line item" do + single_line_item_params = { line_items_attributes: { id: shirt.id, quantity: 0 } } + expect(subject.order).to receive(:update_attributes).with(single_line_item_params) + subject.update_cart single_line_item_params + end + + end + + it "ensures updated shipments" do + expect(subject.order).to receive(:ensure_updated_shipments) + subject.update_cart params + end + end + + context "completed order" do + let(:order) { Spree::Order.create! state: 'complete', completed_at: Time.now } + + before { order.shipments.create! stock_location_id: variant.stock_location_ids.first } + + it "updates order payment state" do + expect { + subject.add variant + }.to change { order.payment_state } + + expect { + subject.remove variant + }.to change { order.payment_state } + end + end +end diff --git a/core/spec/models/spree/order_inventory_spec.rb b/core/spec/models/spree/order_inventory_spec.rb new file mode 100644 index 00000000000..286d2d4ec47 --- /dev/null +++ b/core/spec/models/spree/order_inventory_spec.rb @@ -0,0 +1,228 @@ +require 'spec_helper' + +describe Spree::OrderInventory, :type => :model do + let(:order) { create :completed_order_with_totals } + let(:line_item) { order.line_items.first } + + subject { described_class.new(order, line_item) } + + context "when order is missing inventory units" do + before { line_item.update_column(:quantity, 2) } + + it 'creates the proper number of inventory units' do + subject.verify + expect(subject.inventory_units.count).to eq 2 + end + end + + context "#add_to_shipment" do + let(:shipment) { order.shipments.first } + + context "order is not completed" do + before { allow(order).to receive_messages completed?: false } + + it "doesn't unstock items" do + expect(shipment.stock_location).not_to receive(:unstock) + expect(subject.send(:add_to_shipment, shipment, 5)).to eq(5) + end + end + + context "inventory units state" do + before { shipment.inventory_units.destroy_all } + + it 'sets inventory_units state as per stock location availability' do + expect(shipment.stock_location).to receive(:fill_status).with(subject.variant, 5).and_return([3, 2]) + + expect(subject.send(:add_to_shipment, shipment, 5)).to eq(5) + + units = shipment.inventory_units_for(subject.variant).group_by(&:state) + expect(units['backordered'].size).to eq(2) + expect(units['on_hand'].size).to eq(3) + end + end + + context "store doesnt track inventory" do + let(:variant) { create(:variant) } + + before { Spree::Config.track_inventory_levels = false } + + it "creates only on hand inventory units" do + variant.stock_items.destroy_all + + # The before_save callback in LineItem would verify inventory + line_item = order.contents.add variant, 1, shipment: shipment + + units = shipment.inventory_units_for(line_item.variant) + expect(units.count).to eq 1 + expect(units.first).to be_on_hand + end + end + + context "variant doesnt track inventory" do + let(:variant) { create(:variant) } + before { variant.track_inventory = false } + + it "creates only on hand inventory units" do + variant.stock_items.destroy_all + + line_item = order.contents.add variant, 1 + subject.verify(shipment) + + units = shipment.inventory_units_for(line_item.variant) + expect(units.count).to eq 1 + expect(units.first).to be_on_hand + end + end + + it 'should create stock_movement' do + expect(subject.send(:add_to_shipment, shipment, 5)).to eq(5) + + stock_item = shipment.stock_location.stock_item(subject.variant) + movement = stock_item.stock_movements.last + # movement.originator.should == shipment + expect(movement.quantity).to eq(-5) + end + end + + context "#determine_target_shipment" do + let(:stock_location) { create :stock_location } + let(:variant) { line_item.variant } + + before do + subject.verify + + order.shipments.create(:stock_location_id => stock_location.id, :cost => 5) + + shipped = order.shipments.create(:stock_location_id => order.shipments.first.stock_location.id, :cost => 10) + shipped.update_column(:state, 'shipped') + end + + it 'should select first non-shipped shipment that already contains given variant' do + shipment = subject.send(:determine_target_shipment) + expect(shipment.shipped?).to be false + expect(shipment.inventory_units_for(variant)).not_to be_empty + + expect(variant.stock_location_ids.include?(shipment.stock_location_id)).to be true + end + + context "when no shipments already contain this varint" do + before do + subject.line_item.reload + subject.inventory_units.destroy_all + end + + it 'selects first non-shipped shipment that leaves from same stock_location' do + shipment = subject.send(:determine_target_shipment) + shipment.reload + expect(shipment.shipped?).to be false + expect(shipment.inventory_units_for(variant)).to be_empty + expect(variant.stock_location_ids.include?(shipment.stock_location_id)).to be true + end + end + end + + context 'when order has too many inventory units' do + before do + line_item.quantity = 3 + line_item.save! + + line_item.update_column(:quantity, 2) + subject.line_item.reload + end + + it 'should be a messed up order' do + expect(order.shipments.first.inventory_units_for(line_item.variant).size).to eq(3) + expect(line_item.quantity).to eq(2) + end + + it 'should decrease the number of inventory units' do + subject.verify + expect(subject.inventory_units.count).to eq 2 + end + + context '#remove_from_shipment' do + let(:shipment) { order.shipments.first } + let(:variant) { subject.variant } + + context "order is not completed" do + before { allow(order).to receive_messages completed?: false } + + it "doesn't restock items" do + expect(shipment.stock_location).not_to receive(:restock) + expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1) + end + end + + it 'should create stock_movement' do + expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1) + + stock_item = shipment.stock_location.stock_item(variant) + movement = stock_item.stock_movements.last + # movement.originator.should == shipment + expect(movement.quantity).to eq(1) + end + + it 'should destroy backordered units first' do + allow(shipment).to receive_messages(inventory_units_for_item: [ + mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'backordered'), + mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'on_hand'), + mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'backordered') + ]) + + expect(shipment.inventory_units_for_item[0]).to receive(:destroy) + expect(shipment.inventory_units_for_item[1]).not_to receive(:destroy) + expect(shipment.inventory_units_for_item[2]).to receive(:destroy) + + expect(subject.send(:remove_from_shipment, shipment, 2)).to eq(2) + end + + it 'should destroy unshipped units first' do + allow(shipment).to receive_messages(inventory_units_for_item: [ + mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'shipped'), + mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'on_hand') + ]) + + expect(shipment.inventory_units_for_item[0]).not_to receive(:destroy) + expect(shipment.inventory_units_for_item[1]).to receive(:destroy) + + expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1) + end + + it 'only attempts to destroy as many units as are eligible, and return amount destroyed' do + allow(shipment).to receive_messages(inventory_units_for_item: [ + mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'shipped'), + mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'on_hand') + ]) + + expect(shipment.inventory_units_for_item[0]).not_to receive(:destroy) + expect(shipment.inventory_units_for_item[1]).to receive(:destroy) + + expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1) + end + + it 'should destroy self if not inventory units remain' do + allow(shipment.inventory_units).to receive_messages(:count => 0) + expect(shipment).to receive(:destroy) + + expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1) + end + + context "inventory unit line item and variant points to different products" do + let(:different_line_item) { create(:line_item) } + + let!(:different_inventory) do + shipment.set_up_inventory("on_hand", variant, order, different_line_item) + end + + context "completed order" do + before { order.touch :completed_at } + + it "removes only units that match both line item and variant" do + subject.send(:remove_from_shipment, shipment, shipment.inventory_units.count) + expect(different_inventory.reload).to be_persisted + end + end + end + end + end +end diff --git a/core/spec/models/spree/order_merger_spec.rb b/core/spec/models/spree/order_merger_spec.rb new file mode 100644 index 00000000000..5226192c63b --- /dev/null +++ b/core/spec/models/spree/order_merger_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +# Regression tests for #2179 +module Spree + describe OrderMerger, type: :model do + let(:variant) { create(:variant) } + let(:order_1) { Spree::Order.create } + let(:order_2) { Spree::Order.create } + let(:user) { stub_model(Spree::LegacyUser, email: "spree@example.com") } + let(:subject) { Spree::OrderMerger.new(order_1) } + + it "destroys the other order" do + subject.merge!(order_2) + expect { order_2.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "persist the merge" do + expect(subject).to receive(:persist_merge) + subject.merge!(order_2) + end + + context "user is provided" do + it "assigns user to new order" do + subject.merge!(order_2, user) + expect(order_1.user).to eq user + end + end + + context "merging together two orders with line items for the same variant" do + before do + order_1.contents.add(variant, 1) + order_2.contents.add(variant, 1) + end + + specify do + subject.merge!(order_2, user) + expect(order_1.line_items.count).to eq(1) + + line_item = order_1.line_items.first + expect(line_item.quantity).to eq(2) + expect(line_item.variant_id).to eq(variant.id) + end + end + + context "merging using extension-specific line_item_comparison_hooks" do + before do + Spree::Order.register_line_item_comparison_hook(:foos_match) + allow(Spree::Variant).to receive(:price_modifier_amount).and_return(0.00) + end + + after do + # reset to avoid test pollution + Spree::Order.line_item_comparison_hooks = Set.new + end + + context "2 equal line items" do + before do + @line_item_1 = order_1.contents.add(variant, 1, foos: {}) + @line_item_2 = order_2.contents.add(variant, 1, foos: {}) + end + + specify do + expect(order_1).to receive(:foos_match).with(@line_item_1, kind_of(Hash)).and_return(true) + subject.merge!(order_2) + expect(order_1.line_items.count).to eq(1) + + line_item = order_1.line_items.first + expect(line_item.quantity).to eq(2) + expect(line_item.variant_id).to eq(variant.id) + end + end + + context "2 different line items" do + before do + allow(order_1).to receive(:foos_match).and_return(false) + + order_1.contents.add(variant, 1, foos: {}) + order_2.contents.add(variant, 1, foos: { bar: :zoo }) + end + + specify do + subject.merge!(order_2) + expect(order_1.line_items.count).to eq(2) + + line_item = order_1.line_items.first + expect(line_item.quantity).to eq(1) + expect(line_item.variant_id).to eq(variant.id) + + line_item = order_1.line_items.last + expect(line_item.quantity).to eq(1) + expect(line_item.variant_id).to eq(variant.id) + end + end + end + + context "merging together two orders with different line items" do + let(:variant_2) { create(:variant) } + + before do + order_1.contents.add(variant, 1) + order_2.contents.add(variant_2, 1) + end + + specify do + subject.merge!(order_2) + line_items = order_1.line_items.reload + expect(line_items.count).to eq(2) + + expect(order_1.item_count).to eq 2 + expect(order_1.item_total).to eq line_items.map(&:amount).sum + + # No guarantee on ordering of line items, so we do this: + expect(line_items.pluck(:quantity)).to match_array([1, 1]) + expect(line_items.pluck(:variant_id)).to match_array([variant.id, variant_2.id]) + end + end + + context "merging together orders with invalid line items" do + let(:variant_2) { create(:variant) } + + before do + order_1.contents.add(variant, 1) + order_2.contents.add(variant_2, 1) + end + + it "should create errors with invalid line items" do + variant_2.destroy + subject.merge!(order_2) + expect(order_1.errors.full_messages).not_to be_empty + end + end + end +end diff --git a/core/spec/models/spree/order_populator_spec.rb b/core/spec/models/spree/order_populator_spec.rb new file mode 100644 index 00000000000..dab0d91b76f --- /dev/null +++ b/core/spec/models/spree/order_populator_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Spree::OrderPopulator, :type => :model do + let(:order) { double('Order') } + subject { Spree::OrderPopulator.new(order, "USD") } + + context "with stubbed out find_variant" do + let(:variant) { double('Variant', name: "T-Shirt", options_text: "Size: M") } + + before do + allow(Spree::Variant).to receive(:find).and_return(variant) + expect(order).to receive(:contents).at_least(:once).and_return(Spree::OrderContents.new(self)) + end + + context "can populate an order" do + it "can take a list of variants with quantites and add them to the order" do + expect(order.contents).to receive(:add).with(variant, 5, currency: subject.currency).and_return(double.as_null_object) + subject.populate(2, 5) + end + end + + context 'with an invalid variant' do + let(:line_item) { build(:line_item) } + + before do + allow(order.contents).to receive(:add).and_raise(ActiveRecord::RecordInvalid, line_item) + end + + it 'has some errors' do + subject.populate(2, 5) + expect(subject.errors).to_not be_empty + end + end + end +end diff --git a/core/spec/models/spree/order_spec.rb b/core/spec/models/spree/order_spec.rb new file mode 100644 index 00000000000..261642605bc --- /dev/null +++ b/core/spec/models/spree/order_spec.rb @@ -0,0 +1,842 @@ +require 'spec_helper' + +class FakeCalculator < Spree::Calculator + def compute(computable) + 5 + end +end + +describe Spree::Order, :type => :model do + let(:user) { stub_model(Spree::LegacyUser, :email => "spree@example.com") } + let(:order) { stub_model(Spree::Order, :user => user) } + + before do + allow(Spree::LegacyUser).to receive_messages(:current => mock_model(Spree::LegacyUser, :id => 123)) + end + + context "#cancel" do + let(:order) { create(:completed_order_with_totals) } + let!(:payment) do + create( + :payment, + order: order, + amount: order.total, + state: "completed" + ) + end + let(:payment_method) { double } + + it "should mark the payments as void" do + allow_any_instance_of(Spree::Shipment).to receive(:refresh_rates).and_return(true) + order.cancel + order.reload + + expect(order.payments.first).to be_void + end + end + + context "#canceled_by" do + let(:admin_user) { create :admin_user } + let(:order) { create :order } + + before do + allow(order).to receive(:cancel!) + end + + subject { order.canceled_by(admin_user) } + + it 'should cancel the order' do + expect(order).to receive(:cancel!) + subject + end + + it 'should save canceler_id' do + subject + expect(order.reload.canceler_id).to eq(admin_user.id) + end + + it 'should save canceled_at' do + subject + expect(order.reload.canceled_at).to_not be_nil + end + + it 'should have canceler' do + subject + expect(order.reload.canceler).to eq(admin_user) + end + end + + context "#create" do + let(:order) { Spree::Order.create } + + it "should assign an order number" do + expect(order.number).not_to be_nil + end + + it 'should create a randomized 22 character token' do + expect(order.guest_token.size).to eq(22) + end + end + + context "creates shipments cost" do + let(:shipment) { double } + + before { allow(order).to receive_messages shipments: [shipment] } + + it "update and persist totals" do + expect(shipment).to receive :update_amounts + expect(order.updater).to receive :update_shipment_total + expect(order.updater).to receive :persist_totals + + order.set_shipments_cost + end + end + + context "#finalize!" do + let(:order) { Spree::Order.create(email: 'test@example.com') } + + before do + order.update_column :state, 'complete' + end + + it "should set completed_at" do + expect(order).to receive(:touch).with(:completed_at) + order.finalize! + end + + it "should sell inventory units" do + order.shipments.each do |shipment| + expect(shipment).to receive(:update!) + expect(shipment).to receive(:finalize!) + end + order.finalize! + end + + it "should decrease the stock for each variant in the shipment" do + order.shipments.each do |shipment| + expect(shipment.stock_location).to receive(:decrease_stock_for_variant) + end + order.finalize! + end + + it "should change the shipment state to ready if order is paid" do + Spree::Shipment.create(order: order, stock_location: create(:stock_location)) + order.shipments.reload + + allow(order).to receive_messages(paid?: true, complete?: true) + order.finalize! + order.reload # reload so we're sure the changes are persisted + expect(order.shipment_state).to eq('ready') + end + + after { Spree::Config.set track_inventory_levels: true } + it "should not sell inventory units if track_inventory_levels is false" do + Spree::Config.set track_inventory_levels: false + expect(Spree::InventoryUnit).not_to receive(:sell_units) + order.finalize! + end + + it "should send an order confirmation email" do + mail_message = double "Mail::Message" + expect(Spree::OrderMailer).to receive(:confirm_email).with(order.id).and_return mail_message + expect(mail_message).to receive :deliver + order.finalize! + end + + it "sets confirmation delivered when finalizing" do + expect(order.confirmation_delivered?).to be false + order.finalize! + expect(order.confirmation_delivered?).to be true + end + + it "should not send duplicate confirmation emails" do + allow(order).to receive_messages(:confirmation_delivered? => true) + expect(Spree::OrderMailer).not_to receive(:confirm_email) + order.finalize! + end + + it "should freeze all adjustments" do + # Stub this method as it's called due to a callback + # and it's irrelevant to this test + allow(order).to receive :has_available_shipment + allow(Spree::OrderMailer).to receive_message_chain :confirm_email, :deliver + adjustments = [double] + expect(order).to receive(:all_adjustments).and_return(adjustments) + adjustments.each do |adj| + expect(adj).to receive(:close) + end + order.finalize! + end + + context "order is considered risky" do + before do + allow(order).to receive_messages :is_risky? => true + end + + it "should change state to risky" do + expect(order).to receive(:considered_risky!) + order.finalize! + end + + context "and order is approved" do + before do + allow(order).to receive_messages :approved? => true + end + + it "should leave order in complete state" do + order.finalize! + expect(order.state).to eq 'complete' + end + end + end + end + + context "insufficient_stock_lines" do + let(:line_item) { mock_model Spree::LineItem, :insufficient_stock? => true } + + before { allow(order).to receive_messages(:line_items => [line_item]) } + + it "should return line_item that has insufficient stock on hand" do + expect(order.insufficient_stock_lines.size).to eq(1) + expect(order.insufficient_stock_lines.include?(line_item)).to be true + end + end + + describe '#ensure_line_item_variants_are_not_deleted' do + subject { order.ensure_line_item_variants_are_not_deleted } + + let(:order) { create :order_with_line_items } + + context 'when variant is destroyed' do + before do + allow(order).to receive(:restart_checkout_flow) + order.line_items.first.variant.destroy + end + + it 'should restart checkout flow' do + expect(order).to receive(:restart_checkout_flow).once + subject + end + + it 'should have error message' do + subject + expect(order.errors[:base]).to include(Spree.t(:deleted_variants_present)) + end + + it 'should be false' do + expect(subject).to be_falsey + end + end + + context 'when no variants are destroyed' do + it 'should not restart checkout' do + expect(order).to receive(:restart_checkout_flow).never + subject + end + + it 'should be true' do + expect(subject).to be_truthy + end + end + end + + describe '#ensure_line_items_are_in_stock' do + subject { order.ensure_line_items_are_in_stock } + + let(:line_item) { mock_model Spree::LineItem, :insufficient_stock? => true } + + before do + allow(order).to receive(:restart_checkout_flow) + allow(order).to receive_messages(:line_items => [line_item]) + end + + it 'should restart checkout flow' do + expect(order).to receive(:restart_checkout_flow).once + subject + end + + it 'should have error message' do + subject + expect(order.errors[:base]).to include(Spree.t(:insufficient_stock_lines_present)) + end + + it 'should be false' do + expect(subject).to be_falsey + end + end + + context "empty!" do + let(:order) { stub_model(Spree::Order, item_count: 2) } + + before do + allow(order).to receive_messages(:line_items => line_items = [1, 2]) + allow(order).to receive_messages(:adjustments => adjustments = []) + end + + it "clears out line items, adjustments and update totals" do + expect(order.line_items).to receive(:destroy_all) + expect(order.adjustments).to receive(:destroy_all) + expect(order.shipments).to receive(:destroy_all) + expect(order.updater).to receive(:update_totals) + expect(order.updater).to receive(:persist_totals) + + order.empty! + expect(order.item_total).to eq 0 + end + end + + context "#display_outstanding_balance" do + it "returns the value as a spree money" do + allow(order).to receive(:outstanding_balance) { 10.55 } + expect(order.display_outstanding_balance).to eq(Spree::Money.new(10.55)) + end + end + + context "#display_item_total" do + it "returns the value as a spree money" do + allow(order).to receive(:item_total) { 10.55 } + expect(order.display_item_total).to eq(Spree::Money.new(10.55)) + end + end + + context "#display_adjustment_total" do + it "returns the value as a spree money" do + order.adjustment_total = 10.55 + expect(order.display_adjustment_total).to eq(Spree::Money.new(10.55)) + end + end + + context "#display_total" do + it "returns the value as a spree money" do + order.total = 10.55 + expect(order.display_total).to eq(Spree::Money.new(10.55)) + end + end + + context "#currency" do + context "when object currency is ABC" do + before { order.currency = "ABC" } + + it "returns the currency from the object" do + expect(order.currency).to eq("ABC") + end + end + + context "when object currency is nil" do + before { order.currency = nil } + + it "returns the globally configured currency" do + expect(order.currency).to eq("USD") + end + end + end + + context "#confirmation_required?" do + + # Regression test for #4117 + it "is required if the state is currently 'confirm'" do + order = Spree::Order.new + assert !order.confirmation_required? + order.state = 'confirm' + assert order.confirmation_required? + end + + context 'Spree::Config[:always_include_confirm_step] == true' do + + before do + Spree::Config[:always_include_confirm_step] = true + end + + it "returns true if payments empty" do + order = Spree::Order.new + assert order.confirmation_required? + end + end + + context 'Spree::Config[:always_include_confirm_step] == false' do + + it "returns false if payments empty and Spree::Config[:always_include_confirm_step] == false" do + order = Spree::Order.new + assert !order.confirmation_required? + end + + it "does not bomb out when an order has an unpersisted payment" do + order = Spree::Order.new + order.payments.build + assert !order.confirmation_required? + end + end + end + + + context "add_update_hook" do + before do + Spree::Order.class_eval do + register_update_hook :add_awesome_sauce + end + end + + after do + Spree::Order.update_hooks = Set.new + end + + it "calls hook during update" do + order = create(:order) + expect(order).to receive(:add_awesome_sauce) + order.update! + end + + it "calls hook during finalize" do + order = create(:order) + expect(order).to receive(:add_awesome_sauce) + order.finalize! + end + end + + describe "#tax_address" do + before { Spree::Config[:tax_using_ship_address] = tax_using_ship_address } + subject { order.tax_address } + + context "when tax_using_ship_address is true" do + let(:tax_using_ship_address) { true } + + it 'returns ship_address' do + expect(subject).to eq(order.ship_address) + end + end + + context "when tax_using_ship_address is not true" do + let(:tax_using_ship_address) { false } + + it "returns bill_address" do + expect(subject).to eq(order.bill_address) + end + end + end + + describe "#restart_checkout_flow" do + it "updates the state column to the first checkout_steps value" do + order = create(:order_with_totals, state: "delivery") + expect(order.checkout_steps).to eql ["address", "delivery", "complete"] + expect{ order.restart_checkout_flow }.to change{order.state}.from("delivery").to("address") + end + + context "without line items" do + it "updates the state column to cart" do + order = create(:order, state: "delivery") + expect{ order.restart_checkout_flow }.to change{order.state}.from("delivery").to("cart") + end + end + end + + # Regression tests for #4072 + context "#state_changed" do + let(:order) { FactoryGirl.create(:order) } + + it "logs state changes" do + order.update_column(:payment_state, 'balance_due') + order.payment_state = 'paid' + expect(order.state_changes).to be_empty + order.state_changed('payment') + state_change = order.state_changes.find_by(:name => 'payment') + expect(state_change.previous_state).to eq('balance_due') + expect(state_change.next_state).to eq('paid') + end + + it "does not do anything if state does not change" do + order.update_column(:payment_state, 'balance_due') + expect(order.state_changes).to be_empty + order.state_changed('payment') + expect(order.state_changes).to be_empty + end + end + + # Regression test for #4199 + context "#available_payment_methods" do + it "includes frontend payment methods" do + payment_method = Spree::PaymentMethod.create!({ + :name => "Fake", + :active => true, + :display_on => "front_end", + :environment => Rails.env + }) + expect(order.available_payment_methods).to include(payment_method) + end + + it "includes 'both' payment methods" do + payment_method = Spree::PaymentMethod.create!({ + :name => "Fake", + :active => true, + :display_on => "both", + :environment => Rails.env + }) + expect(order.available_payment_methods).to include(payment_method) + end + + it "does not include a payment method twice if display_on is blank" do + payment_method = Spree::PaymentMethod.create!({ + :name => "Fake", + :active => true, + :display_on => "both", + :environment => Rails.env + }) + expect(order.available_payment_methods.count).to eq(1) + expect(order.available_payment_methods).to include(payment_method) + end + end + + context "#apply_free_shipping_promotions" do + it "calls out to the FreeShipping promotion handler" do + shipment = double('Shipment') + allow(order).to receive_messages :shipments => [shipment] + expect(Spree::PromotionHandler::FreeShipping).to receive(:new).and_return(handler = double) + expect(handler).to receive(:activate) + + expect(Spree::ItemAdjustments).to receive(:new).with(shipment).and_return(adjuster = double) + expect(adjuster).to receive(:update) + + expect(order.updater).to receive(:update_shipment_total) + expect(order.updater).to receive(:persist_totals) + order.apply_free_shipping_promotions + end + end + + + context "#products" do + before :each do + @variant1 = mock_model(Spree::Variant, :product => "product1") + @variant2 = mock_model(Spree::Variant, :product => "product2") + @line_items = [mock_model(Spree::LineItem, :product => "product1", :variant => @variant1, :variant_id => @variant1.id, :quantity => 1), + mock_model(Spree::LineItem, :product => "product2", :variant => @variant2, :variant_id => @variant2.id, :quantity => 2)] + allow(order).to receive_messages(:line_items => @line_items) + end + + it "contains?" do + expect(order.contains?(@variant1)).to be true + end + + it "gets the quantity of a given variant" do + expect(order.quantity_of(@variant1)).to eq(1) + + @variant3 = mock_model(Spree::Variant, :product => "product3") + expect(order.quantity_of(@variant3)).to eq(0) + end + + it "can find a line item matching a given variant" do + expect(order.find_line_item_by_variant(@variant1)).not_to be_nil + expect(order.find_line_item_by_variant(mock_model(Spree::Variant))).to be_nil + end + + context "match line item with options" do + before do + Spree::Order.register_line_item_comparison_hook(:foos_match) + end + + after do + # reset to avoid test pollution + Spree::Order.line_item_comparison_hooks = Set.new + end + + it "matches line item when options match" do + allow(order).to receive(:foos_match).and_return(true) + expect(order.line_item_options_match(@line_items.first, {foos: {bar: :zoo}})).to be true + end + + it "does not match line item without options" do + allow(order).to receive(:foos_match).and_return(false) + expect(order.line_item_options_match(@line_items.first, {})).to be false + end + end + end + + context "#generate_order_number" do + context "when no configure" do + let(:default_length) { Spree::Order::ORDER_NUMBER_LENGTH + Spree::Order::ORDER_NUMBER_PREFIX.length } + subject(:order_number) { order.generate_order_number } + + describe '#class' do + subject { super().class } + it { is_expected.to eq String } + end + + describe '#length' do + subject { super().length } + it { is_expected.to eq default_length } + end + it { is_expected.to match /^#{Spree::Order::ORDER_NUMBER_PREFIX}/ } + end + + context "when length option is 5" do + let(:option_length) { 5 + Spree::Order::ORDER_NUMBER_PREFIX.length } + it "should be option length for order number" do + expect(order.generate_order_number(length: 5).length).to eq option_length + end + end + + context "when letters option is true" do + it "generates order number include letter" do + expect(order.generate_order_number(length: 100, letters: true)).to match /[A-Z]/ + end + end + + context "when prefix option is 'P'" do + it "generates order number and it prefix is 'P'" do + expect(order.generate_order_number(prefix: 'P')).to match /^P/ + end + end + end + + context "#associate_user!" do + let!(:user) { FactoryGirl.create(:user) } + + it "should associate a user with a persisted order" do + order = FactoryGirl.create(:order_with_line_items, created_by: nil) + order.user = nil + order.email = nil + order.associate_user!(user) + expect(order.user).to eq(user) + expect(order.email).to eq(user.email) + expect(order.created_by).to eq(user) + + # verify that the changes we made were persisted + order.reload + expect(order.user).to eq(user) + expect(order.email).to eq(user.email) + expect(order.created_by).to eq(user) + end + + it "should not overwrite the created_by if it already is set" do + creator = create(:user) + order = FactoryGirl.create(:order_with_line_items, created_by: creator) + + order.user = nil + order.email = nil + order.associate_user!(user) + expect(order.user).to eq(user) + expect(order.email).to eq(user.email) + expect(order.created_by).to eq(creator) + + # verify that the changes we made were persisted + order.reload + expect(order.user).to eq(user) + expect(order.email).to eq(user.email) + expect(order.created_by).to eq(creator) + end + + it "should associate a user with a non-persisted order" do + order = Spree::Order.new + + expect do + order.associate_user!(user) + end.to change { [order.user, order.email] }.from([nil, nil]).to([user, user.email]) + end + + it "should not persist an invalid address" do + address = Spree::Address.new + order.user = nil + order.email = nil + order.ship_address = address + expect do + order.associate_user!(user) + end.not_to change { address.persisted? }.from(false) + end + end + + context "#can_ship?" do + let(:order) { Spree::Order.create } + + it "should be true for order in the 'complete' state" do + allow(order).to receive_messages(:complete? => true) + expect(order.can_ship?).to be true + end + + it "should be true for order in the 'resumed' state" do + allow(order).to receive_messages(:resumed? => true) + expect(order.can_ship?).to be true + end + + it "should be true for an order in the 'awaiting return' state" do + allow(order).to receive_messages(:awaiting_return? => true) + expect(order.can_ship?).to be true + end + + it "should be true for an order in the 'returned' state" do + allow(order).to receive_messages(:returned? => true) + expect(order.can_ship?).to be true + end + + it "should be false if the order is neither in the 'complete' nor 'resumed' state" do + allow(order).to receive_messages(:resumed? => false, :complete? => false) + expect(order.can_ship?).to be false + end + end + + context "#completed?" do + it "should indicate if order is completed" do + order.completed_at = nil + expect(order.completed?).to be false + + order.completed_at = Time.now + expect(order.completed?).to be true + end + end + + context "#allow_checkout?" do + it "should be true if there are line_items in the order" do + allow(order).to receive_message_chain(:line_items, :count => 1) + expect(order.checkout_allowed?).to be true + end + it "should be false if there are no line_items in the order" do + allow(order).to receive_message_chain(:line_items, :count => 0) + expect(order.checkout_allowed?).to be false + end + end + + context "#amount" do + before do + @order = create(:order, :user => user) + @order.line_items = [create(:line_item, :price => 1.0, :quantity => 2), + create(:line_item, :price => 1.0, :quantity => 1)] + end + it "should return the correct lum sum of items" do + expect(@order.amount).to eq(3.0) + end + end + + context "#backordered?" do + it 'is backordered if one of the shipments is backordered' do + allow(order).to receive_messages(:shipments => [mock_model(Spree::Shipment, :backordered? => false), + mock_model(Spree::Shipment, :backordered? => true)]) + expect(order).to be_backordered + end + end + + context "#can_cancel?" do + it "should be false for completed order in the canceled state" do + order.state = 'canceled' + order.shipment_state = 'ready' + order.completed_at = Time.now + expect(order.can_cancel?).to be false + end + + it "should be true for completed order with no shipment" do + order.state = 'complete' + order.shipment_state = nil + order.completed_at = Time.now + expect(order.can_cancel?).to be true + end + end + + context "#tax_total" do + it "adds included tax and additional tax" do + allow(order).to receive_messages(:additional_tax_total => 10, :included_tax_total => 20) + + expect(order.tax_total).to eq 30 + end + end + + # Regression test for #4923 + context "locking" do + let(:order) { Spree::Order.create } # need a persisted in order to test locking + + it 'can lock' do + expect { order.with_lock {} }.to_not raise_error + end + end + + describe "#pre_tax_item_amount" do + it "sums all of the line items' pre tax amounts" do + subject.line_items = [ + Spree::LineItem.new(price: 10, quantity: 2, pre_tax_amount: 5.0), + Spree::LineItem.new(price: 30, quantity: 1, pre_tax_amount: 14.0), + ] + + expect(subject.pre_tax_item_amount).to eq 19.0 + end + end + + describe '#quantity' do + # Uses a persisted record, as the quantity is retrieved via a DB count + let(:order) { create :order_with_line_items, line_items_count: 3 } + + it 'sums the quantity of all line items' do + expect(order.quantity).to eq 3 + end + end + + describe '#has_non_reimbursement_related_refunds?' do + subject do + order.has_non_reimbursement_related_refunds? + end + + context 'no refunds exist' do + it { is_expected.to eq false } + end + + context 'a non-reimbursement related refund exists' do + let(:order) { refund.payment.order } + let(:refund) { create(:refund, reimbursement_id: nil, amount: 5) } + + it { is_expected.to eq true } + end + + context 'an old-style refund exists' do + let(:order) { create(:order_ready_to_ship) } + let(:payment) { order.payments.first.tap { |p| allow(p).to receive_messages(profiles_supported: false) } } + let!(:refund_payment) { + build(:payment, amount: -1, order: order, state: 'completed', source: payment).tap do |p| + allow(p).to receive_messages(profiles_supported?: false) + p.save! + end + } + + it { is_expected.to eq true } + end + + context 'a reimbursement related refund exists' do + let(:order) { refund.payment.order } + let(:refund) { create(:refund, reimbursement_id: 123, amount: 5)} + + it { is_expected.to eq false } + end + end + + describe "#create_proposed_shipments" do + it "assigns the coordinator returned shipments to its shipments" do + shipment = build(:shipment) + allow_any_instance_of(Spree::Stock::Coordinator).to receive(:shipments).and_return([shipment]) + subject.create_proposed_shipments + expect(subject.shipments).to eq [shipment] + end + end + + describe "#all_inventory_units_returned?" do + let(:order) { create(:order_with_line_items, line_items_count: 3) } + + subject { order.all_inventory_units_returned? } + + context "all inventory units are returned" do + before { order.inventory_units.update_all(state: 'returned') } + + it "is true" do + expect(subject).to eq true + end + end + + context "some inventory units are returned" do + before do + order.inventory_units.first.update_attribute(:state, 'returned') + end + + it "is false" do + expect(subject).to eq false + end + end + + context "no inventory units are returned" do + it "is false" do + expect(subject).to eq false + end + end + end +end diff --git a/core/spec/models/spree/order_updater_spec.rb b/core/spec/models/spree/order_updater_spec.rb new file mode 100644 index 00000000000..e59a1f54d04 --- /dev/null +++ b/core/spec/models/spree/order_updater_spec.rb @@ -0,0 +1,283 @@ +require 'spec_helper' + +module Spree + describe OrderUpdater, type: :model do + let(:order) { Spree::Order.create } + let(:updater) { Spree::OrderUpdater.new(order) } + + context "order totals" do + before do + 2.times do + create(:line_item, order: order, price: 10) + end + end + + it "updates payment totals" do + create(:payment_with_refund, order: order) + Spree::OrderUpdater.new(order).update_payment_total + expect(order.payment_total).to eq(40.75) + end + + it "update item total" do + updater.update_item_total + expect(order.item_total).to eq(20) + end + + it "update shipment total" do + create(:shipment, order: order, cost: 10) + updater.update_shipment_total + expect(order.shipment_total).to eq(10) + end + + context 'with order promotion followed by line item addition' do + let(:promotion) { Spree::Promotion.create!(name: "10% off") } + let(:calculator) { Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) } + + let(:promotion_action) do + Promotion::Actions::CreateAdjustment.create!({ + calculator: calculator, + promotion: promotion, + }) + end + + before do + updater.update + create(:adjustment, source: promotion_action, adjustable: order, order: order) + create(:line_item, order: order, price: 10) # in addition to the two already created + updater.update + end + + it "updates promotion total" do + expect(order.promo_total).to eq(-3) + end + end + + it "update order adjustments" do + # A line item will not have both additional and included tax, + # so please just humour me for now. + order.line_items.first.update_columns({ + adjustment_total: 10.05, + additional_tax_total: 0.05, + included_tax_total: 0.05, + }) + updater.update_adjustment_total + expect(order.adjustment_total).to eq(10.05) + expect(order.additional_tax_total).to eq(0.05) + expect(order.included_tax_total).to eq(0.05) + end + end + + context "updating shipment state" do + before do + allow(order).to receive_messages backordered?: false + allow(order).to receive_message_chain(:shipments, :shipped, :count).and_return(0) + allow(order).to receive_message_chain(:shipments, :ready, :count).and_return(0) + allow(order).to receive_message_chain(:shipments, :pending, :count).and_return(0) + end + + it "is backordered" do + allow(order).to receive_messages backordered?: true + updater.update_shipment_state + + expect(order.shipment_state).to eq('backorder') + end + + it "is nil" do + allow(order).to receive_message_chain(:shipments, :states).and_return([]) + allow(order).to receive_message_chain(:shipments, :count).and_return(0) + + updater.update_shipment_state + expect(order.shipment_state).to be_nil + end + + + ["shipped", "ready", "pending"].each do |state| + it "is #{state}" do + allow(order).to receive_message_chain(:shipments, :states).and_return([state]) + updater.update_shipment_state + expect(order.shipment_state).to eq(state.to_s) + end + end + + it "is partial" do + allow(order).to receive_message_chain(:shipments, :states).and_return(["pending", "ready"]) + updater.update_shipment_state + expect(order.shipment_state).to eq('partial') + end + end + + context "updating payment state" do + let(:order) { Order.new } + let(:updater) { order.updater } + + it "is failed if no valid payments" do + allow(order).to receive_message_chain(:payments, :valid, :size).and_return(0) + + updater.update_payment_state + expect(order.payment_state).to eq('failed') + end + + context "payment total is greater than order total" do + it "is credit_owed" do + order.payment_total = 2 + order.total = 1 + + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'credit_owed' + end + end + + context "order total is greater than payment total" do + it "is balance_due" do + order.payment_total = 1 + order.total = 2 + + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'balance_due' + end + end + + context "order total equals payment total" do + it "is paid" do + order.payment_total = 30 + order.total = 30 + + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'paid' + end + end + + context "order is canceled" do + + before do + order.state = 'canceled' + end + + context "and is still unpaid" do + it "is void" do + order.payment_total = 0 + order.total = 30 + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'void' + end + end + + context "and is paid" do + + it "is credit_owed" do + order.payment_total = 30 + order.total = 30 + allow(order).to receive_message_chain(:payments, :valid, :size).and_return(1) + allow(order).to receive_message_chain(:payments, :completed, :size).and_return(1) + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'credit_owed' + end + + end + + context "and payment is refunded" do + it "is void" do + order.payment_total = 0 + order.total = 30 + expect { + updater.update_payment_state + }.to change { order.payment_state }.to 'void' + end + end + end + + end + + it "state change" do + order.shipment_state = 'shipped' + state_changes = double + allow(order).to receive_messages state_changes: state_changes + expect(state_changes).to receive(:create).with( + previous_state: nil, + next_state: 'shipped', + name: 'shipment', + user_id: nil + ) + + order.state_changed('shipment') + end + + context "completed order" do + before { allow(order).to receive_messages completed?: true } + + it "updates payment state" do + expect(updater).to receive(:update_payment_state) + updater.update + end + + it "updates shipment state" do + expect(updater).to receive(:update_shipment_state) + updater.update + end + + it "updates each shipment" do + shipment = stub_model(Spree::Shipment, order: order) + shipments = [shipment] + allow(order).to receive_messages shipments: shipments + allow(shipments).to receive_messages states: [] + allow(shipments).to receive_messages ready: [] + allow(shipments).to receive_messages pending: [] + allow(shipments).to receive_messages shipped: [] + + expect(shipment).to receive(:update!).with(order) + updater.update_shipments + end + + it "refreshes shipment rates" do + shipment = stub_model(Spree::Shipment, order: order) + shipments = [shipment] + allow(order).to receive_messages shipments: shipments + + expect(shipment).to receive(:refresh_rates) + updater.update_shipments + end + + it "updates the shipment amount" do + shipment = stub_model(Spree::Shipment, order: order) + shipments = [shipment] + allow(order).to receive_messages shipments: shipments + + expect(shipment).to receive(:update_amounts) + updater.update_shipments + end + end + + context "incompleted order" do + before { allow(order).to receive_messages completed?: false } + + it "doesnt update payment state" do + expect(updater).not_to receive(:update_payment_state) + updater.update + end + + it "doesnt update shipment state" do + expect(updater).not_to receive(:update_shipment_state) + updater.update + end + + it "doesnt update each shipment" do + shipment = stub_model(Spree::Shipment) + shipments = [shipment] + allow(order).to receive_messages shipments: shipments + allow(shipments).to receive_messages states: [] + allow(shipments).to receive_messages ready: [] + allow(shipments).to receive_messages pending: [] + allow(shipments).to receive_messages shipped: [] + + allow(updater).to receive(:update_totals) # Otherwise this gets called and causes a scene + expect(updater).not_to receive(:update_shipments).with(order) + updater.update + end + end + end +end diff --git a/core/spec/models/spree/payment_method_spec.rb b/core/spec/models/spree/payment_method_spec.rb new file mode 100644 index 00000000000..1e9ece0484f --- /dev/null +++ b/core/spec/models/spree/payment_method_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Spree::PaymentMethod, :type => :model do + describe "#available" do + before do + [nil, 'both', 'front_end', 'back_end'].each do |display_on| + Spree::Gateway::Test.create( + :name => 'Display Both', + :display_on => display_on, + :active => true, + :environment => 'test', + :description => 'foofah' + ) + end + end + + it "should have 4 total methods" do + expect(Spree::PaymentMethod.all.size).to eq(4) + end + + it "should return all methods available to front-end/back-end when no parameter is passed" do + expect(Spree::PaymentMethod.available.size).to eq(2) + end + + it "should return all methods available to front-end/back-end when display_on = :both" do + expect(Spree::PaymentMethod.available(:both).size).to eq(2) + end + + it "should return all methods available to front-end when display_on = :front_end" do + expect(Spree::PaymentMethod.available(:front_end).size).to eq(2) + end + + it "should return all methods available to back-end when display_on = :back_end" do + expect(Spree::PaymentMethod.available(:back_end).size).to eq(2) + end + end + + describe '#auto_capture?' do + class TestGateway < Spree::Gateway + def provider_class + Provider + end + end + + let(:gateway) { TestGateway.new } + + subject { gateway.auto_capture? } + + context 'when auto_capture is nil' do + before(:each) do + expect(Spree::Config).to receive('[]').with(:auto_capture).and_return(auto_capture) + end + + context 'and when Spree::Config[:auto_capture] is false' do + let(:auto_capture) { false } + + it 'should be false' do + expect(gateway.auto_capture).to be_nil + expect(subject).to be false + end + end + + context 'and when Spree::Config[:auto_capture] is true' do + let(:auto_capture) { true } + + it 'should be true' do + expect(gateway.auto_capture).to be_nil + expect(subject).to be true + end + end + end + + context 'when auto_capture is not nil' do + before(:each) do + gateway.auto_capture = auto_capture + end + + context 'and is true' do + let(:auto_capture) { true } + + it 'should be true' do + expect(subject).to be true + end + end + + context 'and is false' do + let(:auto_capture) { false } + + it 'should be true' do + expect(subject).to be false + end + end + end + end + +end diff --git a/core/spec/models/spree/payment_spec.rb b/core/spec/models/spree/payment_spec.rb new file mode 100644 index 00000000000..d9e0311eb73 --- /dev/null +++ b/core/spec/models/spree/payment_spec.rb @@ -0,0 +1,961 @@ +require 'spec_helper' + +describe Spree::Payment, :type => :model do + let(:order) { Spree::Order.create } + let(:refund_reason) { create(:refund_reason) } + + let(:gateway) do + gateway = Spree::Gateway::Bogus.new(:environment => 'test', :active => true) + allow(gateway).to receive_messages :source_required => true + gateway + end + + let(:avs_code) { 'D' } + let(:cvv_code) { 'M' } + + let(:card) { create :credit_card } + + let(:payment) do + payment = Spree::Payment.new + payment.source = card + payment.order = order + payment.payment_method = gateway + payment.amount = 5 + payment + end + + let(:amount_in_cents) { (payment.amount * 100).round } + + let!(:success_response) do + ActiveMerchant::Billing::Response.new(true, '', {}, { + authorization: '123', + cvv_result: cvv_code, + avs_result: { code: avs_code } + }) + end + + let(:failed_response) do + ActiveMerchant::Billing::Response.new(false, '', {}, {}) + end + + before(:each) do + # So it doesn't create log entries every time a processing method is called + allow(payment.log_entries).to receive(:create!) + end + + context '.risky' do + + let!(:payment_1) { create(:payment, avs_response: 'Y', cvv_response_code: 'M', cvv_response_message: 'Match') } + let!(:payment_2) { create(:payment, avs_response: 'Y', cvv_response_code: 'M', cvv_response_message: '') } + let!(:payment_3) { create(:payment, avs_response: 'A', cvv_response_code: 'M', cvv_response_message: 'Match') } + let!(:payment_4) { create(:payment, avs_response: 'Y', cvv_response_code: 'N', cvv_response_message: 'No Match') } + + it 'should not return successful responses' do + expect(subject.class.risky.to_a).to match_array([payment_3, payment_4]) + end + + end + + context "#captured_amount" do + context "calculates based on capture events" do + it "with 0 capture events" do + expect(payment.captured_amount).to eq(0) + end + + it "with some capture events" do + payment.save + payment.capture_events.create!(amount: 2.0) + payment.capture_events.create!(amount: 3.0) + expect(payment.captured_amount).to eq(5) + end + end + end + + context '#uncaptured_amount' do + context "calculates based on capture events" do + it "with 0 capture events" do + expect(payment.uncaptured_amount).to eq(5.0) + end + + it "with some capture events" do + payment.save + payment.capture_events.create!(amount: 2.0) + payment.capture_events.create!(amount: 3.0) + expect(payment.uncaptured_amount).to eq(0) + end + end + end + + context 'validations' do + it "returns useful error messages when source is invalid" do + payment.source = Spree::CreditCard.new + expect(payment).not_to be_valid + cc_errors = payment.errors['Credit Card'] + expect(cc_errors).to include("Number can't be blank") + expect(cc_errors).to include("Month is not a number") + expect(cc_errors).to include("Year is not a number") + expect(cc_errors).to include("Verification Value can't be blank") + end + end + + # Regression test for https://github.com/spree/spree/pull/2224 + context 'failure' do + it 'should transition to failed from pending state' do + payment.state = 'pending' + payment.failure + expect(payment.state).to eql('failed') + end + + it 'should transition to failed from processing state' do + payment.state = 'processing' + payment.failure + expect(payment.state).to eql('failed') + end + + end + + context 'invalidate' do + it 'should transition from checkout to invalid' do + payment.state = 'checkout' + payment.invalidate + expect(payment.state).to eq('invalid') + end + end + + context "processing" do + describe "#process!" do + it "should purchase if with auto_capture" do + expect(payment.payment_method).to receive(:auto_capture?).and_return(true) + payment.process! + expect(payment).to be_completed + end + + it "should authorize without auto_capture" do + expect(payment.payment_method).to receive(:auto_capture?).and_return(false) + payment.process! + expect(payment).to be_pending + end + + it "should make the state 'processing'" do + expect(payment).to receive(:started_processing!) + payment.process! + end + + it "should invalidate if payment method doesnt support source" do + expect(payment.payment_method).to receive(:supports?).with(payment.source).and_return(false) + expect { payment.process!}.to raise_error(Spree::Core::GatewayError) + expect(payment.state).to eq('invalid') + end + + # Regression test for #4598 + it "should allow payments with a gateway_customer_profile_id" do + allow(payment.source).to receive_messages :gateway_customer_profile_id => "customer_1" + expect(payment.payment_method).to receive(:supports?).with(payment.source).and_return(false) + expect(payment).to receive(:started_processing!) + payment.process! + end + + # Another regression test for #4598 + it "should allow payments with a gateway_payment_profile_id" do + allow(payment.source).to receive_messages :gateway_payment_profile_id => "customer_1" + expect(payment.payment_method).to receive(:supports?).with(payment.source).and_return(false) + expect(payment).to receive(:started_processing!) + payment.process! + end + end + + describe "#authorize!" do + it "should call authorize on the gateway with the payment amount" do + expect(payment.payment_method).to receive(:authorize).with(amount_in_cents, + card, + anything).and_return(success_response) + payment.authorize! + end + + it "should call authorize on the gateway with the currency code" do + allow(payment).to receive_messages :currency => 'GBP' + expect(payment.payment_method).to receive(:authorize).with(amount_in_cents, + card, + hash_including({:currency => "GBP"})).and_return(success_response) + payment.authorize! + end + + it "should log the response" do + payment.save! + expect(payment.log_entries).to receive(:create!).with(details: anything) + payment.authorize! + end + + context "when gateway does not match the environment" do + it "should raise an exception" do + allow(gateway).to receive_messages :environment => "foo" + expect { payment.authorize! }.to raise_error(Spree::Core::GatewayError) + end + end + + context "if successful" do + before do + expect(payment.payment_method).to receive(:authorize).with(amount_in_cents, + card, + anything).and_return(success_response) + end + + it "should store the response_code, avs_response and cvv_response fields" do + payment.authorize! + expect(payment.response_code).to eq('123') + expect(payment.avs_response).to eq(avs_code) + expect(payment.cvv_response_code).to eq(cvv_code) + expect(payment.cvv_response_message).to eq(ActiveMerchant::Billing::CVVResult::MESSAGES[cvv_code]) + end + + it "should make payment pending" do + expect(payment).to receive(:pend!) + payment.authorize! + end + end + + context "if unsuccessful" do + it "should mark payment as failed" do + allow(gateway).to receive(:authorize).and_return(failed_response) + expect(payment).to receive(:failure) + expect(payment).not_to receive(:pend) + expect { + payment.authorize! + }.to raise_error(Spree::Core::GatewayError) + end + end + end + + describe "#purchase!" do + it "should call purchase on the gateway with the payment amount" do + expect(gateway).to receive(:purchase).with(amount_in_cents, card, anything).and_return(success_response) + payment.purchase! + end + + it "should log the response" do + payment.save! + expect(payment.log_entries).to receive(:create!).with(details: anything) + payment.purchase! + end + + context "when gateway does not match the environment" do + it "should raise an exception" do + allow(gateway).to receive_messages :environment => "foo" + expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError) + end + end + + context "if successful" do + before do + expect(payment.payment_method).to receive(:purchase).with(amount_in_cents, + card, + anything).and_return(success_response) + end + + it "should store the response_code and avs_response" do + payment.purchase! + expect(payment.response_code).to eq('123') + expect(payment.avs_response).to eq(avs_code) + end + + it "should make payment complete" do + expect(payment).to receive(:complete!) + payment.purchase! + end + + it "should log a capture event" do + payment.purchase! + expect(payment.capture_events.count).to eq(1) + expect(payment.capture_events.first.amount).to eq(payment.amount) + end + + it "should set the uncaptured amount to 0" do + payment.purchase! + expect(payment.uncaptured_amount).to eq(0) + end + end + + context "if unsuccessful" do + before do + allow(gateway).to receive(:purchase).and_return(failed_response) + expect(payment).to receive(:failure) + expect(payment).not_to receive(:pend) + end + + it "should make payment failed" do + expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError) + end + + it "should not log a capture event" do + expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError) + expect(payment.capture_events.count).to eq(0) + end + end + end + + describe "#capture!" do + context "when payment is pending" do + before do + payment.amount = 100 + payment.state = 'pending' + payment.response_code = '12345' + end + + context "if successful" do + context 'for entire amount' do + before do + expect(payment.payment_method).to receive(:capture).with(payment.display_amount.money.cents, payment.response_code, anything).and_return(success_response) + end + + it "should make payment complete" do + expect(payment).to receive(:complete!) + payment.capture! + end + + it "logs capture events" do + payment.capture! + expect(payment.capture_events.count).to eq(1) + expect(payment.capture_events.first.amount).to eq(payment.amount) + end + end + + context 'for partial amount' do + let(:original_amount) { payment.money.money.cents } + let(:capture_amount) { original_amount - 100 } + + before do + expect(payment.payment_method).to receive(:capture).with(capture_amount, payment.response_code, anything).and_return(success_response) + end + + it "should make payment complete & create pending payment for remaining amount" do + expect(payment).to receive(:complete!) + payment.capture!(capture_amount) + order = payment.order + payments = order.payments + + expect(payments.size).to eq 2 + expect(payments.pending.first.amount).to eq 1 + # Payment stays processing for spec because of receive(:complete!) stub. + expect(payments.processing.first.amount).to eq(capture_amount / 100) + expect(payments.processing.first.source).to eq(payments.pending.first.source) + end + + it "logs capture events" do + payment.capture!(capture_amount) + expect(payment.capture_events.count).to eq(1) + expect(payment.capture_events.first.amount).to eq(capture_amount / 100) + end + end + end + + context "if unsuccessful" do + it "should not make payment complete" do + allow(gateway).to receive_messages :capture => failed_response + expect(payment).to receive(:failure) + expect(payment).not_to receive(:complete) + expect { payment.capture! }.to raise_error(Spree::Core::GatewayError) + end + end + end + + # Regression test for #2119 + context "when payment is completed" do + before do + payment.state = 'completed' + end + + it "should do nothing" do + expect(payment).not_to receive(:complete) + expect(payment.payment_method).not_to receive(:capture) + expect(payment.log_entries).not_to receive(:create!) + payment.capture! + end + end + end + + describe "#void_transaction!" do + before do + payment.response_code = '123' + payment.state = 'pending' + end + + context "when profiles are supported" do + it "should call payment_gateway.void with the payment's response_code" do + allow(gateway).to receive_messages :payment_profiles_supported? => true + expect(gateway).to receive(:void).with('123', card, anything).and_return(success_response) + payment.void_transaction! + end + end + + context "when profiles are not supported" do + it "should call payment_gateway.void with the payment's response_code" do + allow(gateway).to receive_messages :payment_profiles_supported? => false + expect(gateway).to receive(:void).with('123', anything).and_return(success_response) + payment.void_transaction! + end + end + + it "should log the response" do + expect(payment.log_entries).to receive(:create!).with(:details => anything) + payment.void_transaction! + end + + context "when gateway does not match the environment" do + it "should raise an exception" do + allow(gateway).to receive_messages :environment => "foo" + expect { payment.void_transaction! }.to raise_error(Spree::Core::GatewayError) + end + end + + context "if successful" do + it "should update the response_code with the authorization from the gateway" do + # Change it to something different + payment.response_code = 'abc' + payment.void_transaction! + expect(payment.response_code).to eq('12345') + end + end + + context "if unsuccessful" do + it "should not void the payment" do + allow(gateway).to receive_messages :void => failed_response + expect(payment).not_to receive(:void) + expect { payment.void_transaction! }.to raise_error(Spree::Core::GatewayError) + end + end + + # Regression test for #2119 + context "if payment is already voided" do + before do + payment.state = 'void' + end + + it "should not void the payment" do + expect(payment.payment_method).not_to receive(:void) + payment.void_transaction! + end + end + end + + end + + context "when already processing" do + it "should return nil without trying to process the source" do + payment.state = 'processing' + + expect(payment.process!).to be_nil + end + end + + context "with source required" do + context "raises an error if no source is specified" do + before do + payment.source = nil + end + + specify do + expect { payment.process! }.to raise_error(Spree::Core::GatewayError, Spree.t(:payment_processing_failed)) + end + end + end + + context "with source optional" do + context "raises no error if source is not specified" do + before do + payment.source = nil + allow(payment.payment_method).to receive_messages(:source_required? => false) + end + + specify do + expect { payment.process! }.not_to raise_error + end + end + end + + describe "#credit_allowed" do + # Regression test for #4403 & #4407 + it "is the difference between offsets total and payment amount" do + payment.amount = 100 + allow(payment).to receive(:offsets_total).and_return(0) + expect(payment.credit_allowed).to eq(100) + allow(payment).to receive(:offsets_total).and_return(-80) + expect(payment.credit_allowed).to eq(20) + end + end + + describe "#can_credit?" do + it "is true if credit_allowed > 0" do + allow(payment).to receive(:credit_allowed).and_return(100) + expect(payment.can_credit?).to be true + end + + it "is false if credit_allowed is 0" do + allow(payment).to receive(:credit_allowed).and_return(0) + expect(payment.can_credit?).to be false + end + end + + describe "#save" do + context "captured payments" do + it "update order payment total" do + payment = create(:payment, order: order, state: 'completed') + expect(order.payment_total).to eq payment.amount + end + end + + context "not completed payments" do + it "doesn't update order payment total" do + expect { + Spree::Payment.create(:amount => 100, :order => order) + }.not_to change { order.payment_total } + end + + it "requires a payment method" do + expect(Spree::Payment.create(amount: 100, order: order)).to have(1).error_on(:payment_method) + end + end + + context 'when the payment was completed but now void' do + let(:payment) do + Spree::Payment.create( + amount: 100, + order: order, + state: 'completed' + ) + end + + it 'updates order payment total' do + payment.void + expect(order.payment_total).to eq 0 + end + end + + context "completed orders" do + before { allow(order).to receive_messages completed?: true } + + it "updates payment_state and shipments" do + expect(order.updater).to receive(:update_payment_state) + expect(order.updater).to receive(:update_shipment_state) + Spree::Payment.create(amount: 100, order: order, payment_method: gateway) + end + end + + context "when profiles are supported" do + before do + allow(gateway).to receive_messages :payment_profiles_supported? => true + allow(payment.source).to receive_messages :has_payment_profile? => false + end + + context "when there is an error connecting to the gateway" do + it "should call gateway_error " do + expect(gateway).to receive(:create_profile).and_raise(ActiveMerchant::ConnectionError) + expect do + Spree::Payment.create( + :amount => 100, + :order => order, + :source => card, + :payment_method => gateway + ) + end.to raise_error(Spree::Core::GatewayError) + end + end + + context "with multiple payment attempts" do + let(:attributes) { attributes_for(:credit_card) } + it "should not try to create profiles on old failed payment attempts" do + allow_any_instance_of(Spree::Payment).to receive(:payment_method) { gateway } + + order.payments.create!( + source_attributes: attributes, + payment_method: gateway, + amount: 100 + ) + expect(gateway).to receive(:create_profile).exactly :once + expect(order.payments.count).to eq(1) + order.payments.create!( + source_attributes: attributes, + payment_method: gateway, + amount: 100 + ) + end + + end + + context "when successfully connecting to the gateway" do + it "should create a payment profile" do + expect(payment.payment_method).to receive :create_profile + payment = Spree::Payment.create( + :amount => 100, + :order => order, + :source => card, + :payment_method => gateway + ) + end + end + end + + context "when profiles are not supported" do + before { allow(gateway).to receive_messages :payment_profiles_supported? => false } + + it "should not create a payment profile" do + expect(gateway).not_to receive :create_profile + payment = Spree::Payment.create( + :amount => 100, + :order => order, + :source => card, + :payment_method => gateway + ) + end + end + end + + describe '#invalidate_old_payments' do + before { + Spree::Payment.skip_callback(:rollback, :after, :persist_invalid) + } + after { + Spree::Payment.set_callback(:rollback, :after, :persist_invalid) + } + + it 'should not invalidate other payments if not valid' do + payment.save + invalid_payment = Spree::Payment.new(:amount => 100, :order => order, :state => 'invalid', :payment_method => gateway) + invalid_payment.save + expect(payment.reload.state).to eq('checkout') + end + end + + describe "#build_source" do + let(:params) do + { + :amount => 100, + :payment_method => gateway, + :source_attributes => { + :expiry =>"01 / 99", + :number => '1234567890123', + :verification_value => '123', + :name => 'Spree Commerce' + } + } + end + + it "should build the payment's source" do + payment = Spree::Payment.new(params) + expect(payment).to be_valid + expect(payment.source).not_to be_nil + end + + it "assigns user and gateway to payment source" do + order = create(:order) + source = order.payments.new(params).source + + expect(source.user_id).to eq order.user_id + expect(source.payment_method_id).to eq gateway.id + end + + it "errors when payment source not valid" do + params = { :amount => 100, :payment_method => gateway, + :source_attributes => {:expiry => "1 / 12" }} + + payment = Spree::Payment.new(params) + expect(payment).not_to be_valid + expect(payment.source).not_to be_nil + expect(payment.source.error_on(:number).size).to eq(1) + expect(payment.source.error_on(:verification_value).size).to eq(1) + end + + it "does not build a new source when duplicating the model with source_attributes set" do + payment = create(:payment) + payment.source_attributes = params[:source_attributes] + expect { payment.dup }.to_not change { payment.source } + end + end + + describe "#currency" do + before { allow(order).to receive(:currency) { "ABC" } } + it "returns the order currency" do + expect(payment.currency).to eq("ABC") + end + end + + describe "#display_amount" do + it "returns a Spree::Money for this amount" do + expect(payment.display_amount).to eq(Spree::Money.new(payment.amount)) + end + end + + # Regression test for #2216 + describe "#gateway_options" do + before { allow(order).to receive_messages(:last_ip_address => "192.168.1.1") } + + it "contains an IP" do + expect(payment.gateway_options[:ip]).to eq(order.last_ip_address) + end + + it "contains the email address from a persisted order" do + # Sets the payment's order to a different Ruby object entirely + payment.order = Spree::Order.find(payment.order_id) + email = 'foo@example.com' + order.update_attributes(:email => email) + expect(payment.gateway_options[:email]).to eq(email) + end + end + + describe "#set_unique_identifier" do + # Regression test for #1998 + it "sets a unique identifier on create" do + payment.run_callbacks(:create) + expect(payment.identifier).not_to be_blank + expect(payment.identifier.size).to eq(8) + expect(payment.identifier).to be_a(String) + end + + # Regression test for #3733 + it "does not regenerate the identifier on re-save" do + payment.save + old_identifier = payment.identifier + payment.save + expect(payment.identifier).to eq(old_identifier) + end + + context "other payment exists" do + let(:other_payment) { + payment = Spree::Payment.new + payment.source = card + payment.order = order + payment.payment_method = gateway + payment + } + + before { other_payment.save! } + + it "doesn't set duplicate identifier" do + expect(payment).to receive(:generate_identifier).and_return(other_payment.identifier) + expect(payment).to receive(:generate_identifier).and_call_original + + payment.run_callbacks(:create) + + expect(payment.identifier).not_to be_blank + expect(payment.identifier).not_to eq(other_payment.identifier) + end + end + end + + describe "#amount=" do + before do + subject.amount = amount + end + + context "when the amount is a string" do + context "amount is a decimal" do + let(:amount) { '2.99' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2.99')) + end + end + + context "amount is an integer" do + let(:amount) { '2' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2.0')) + end + end + + context "amount contains a dollar sign" do + let(:amount) { '$2.99' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2.99')) + end + end + + context "amount contains a comma" do + let(:amount) { '$2,999.99' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2999.99')) + end + end + + context "amount contains a negative sign" do + let(:amount) { '-2.99' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('-2.99')) + end + end + + context "amount is invalid" do + let(:amount) { 'invalid' } + + # this is a strange default for ActiveRecord + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('0')) + end + end + + context "amount is an empty string" do + let(:amount) { '' } + + it '#amount' do + expect(subject.amount).to be_nil + end + end + end + + context "when the amount is a number" do + let(:amount) { 1.55 } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('1.55')) + end + end + + context "when the locale uses a coma as a decimal separator" do + before(:each) do + I18n.backend.store_translations(:fr, { :number => { :currency => { :format => { :delimiter => ' ', :separator => ',' } } } }) + I18n.locale = :fr + subject.amount = amount + end + + after do + I18n.locale = I18n.default_locale + end + + context "amount is a decimal" do + let(:amount) { '2,99' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2.99')) + end + end + + context "amount contains a $ sign" do + let(:amount) { '2,99 $' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2.99')) + end + end + + context "amount is a number" do + let(:amount) { 2.99 } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2.99')) + end + end + + context "amount contains a negative sign" do + let(:amount) { '-2,99 $' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('-2.99')) + end + end + + context "amount uses a dot as a decimal separator" do + let(:amount) { '2.99' } + + it '#amount' do + expect(subject.amount).to eql(BigDecimal('2.99')) + end + end + end + end + + describe "is_avs_risky?" do + it "returns false if avs_response included in NON_RISKY_AVS_CODES" do + ('A'..'Z').reject{ |x| subject.class::RISKY_AVS_CODES.include?(x) }.to_a.each do |char| + payment.update_attribute(:avs_response, char) + expect(payment.is_avs_risky?).to eq false + end + end + + it "returns false if avs_response.blank?" do + payment.update_attribute(:avs_response, nil) + expect(payment.is_avs_risky?).to eq false + payment.update_attribute(:avs_response, '') + expect(payment.is_avs_risky?).to eq false + end + + it "returns true if avs_response in RISKY_AVS_CODES" do + # should use avs_response_code helper + ('A'..'Z').reject{ |x| subject.class::NON_RISKY_AVS_CODES.include?(x) }.to_a.each do |char| + payment.update_attribute(:avs_response, char) + expect(payment.is_avs_risky?).to eq true + end + end + end + + describe "is_cvv_risky?" do + it "returns false if cvv_response_code == 'M'" do + payment.update_attribute(:cvv_response_code, "M") + expect(payment.is_cvv_risky?).to eq(false) + end + + it "returns false if cvv_response_code == nil" do + payment.update_attribute(:cvv_response_code, nil) + expect(payment.is_cvv_risky?).to eq(false) + end + + it "returns false if cvv_response_message == ''" do + payment.update_attribute(:cvv_response_message, '') + expect(payment.is_cvv_risky?).to eq(false) + end + + it "returns true if cvv_response_code == [A-Z], omitting D" do + # should use cvv_response_code helper + (%w{N P S U} << "").each do |char| + payment.update_attribute(:cvv_response_code, char) + expect(payment.is_cvv_risky?).to eq(true) + end + end + end + + describe "#editable?" do + subject { payment } + + before do + subject.state = state + end + + context "when the state is 'checkout'" do + let(:state) { 'checkout' } + + its(:editable?) { should be(true) } + end + + context "when the state is 'pending'" do + let(:state) { 'pending' } + + its(:editable?) { should be(true) } + end + + %w[processing completed failed void invalid].each do |state| + context "when the state is '#{state}'" do + let(:state) { state } + + its(:editable?) { should be(false) } + end + end + end + + # Regression test for #4072 (kinda) + # The need for this was discovered in the research for #4072 + context "state changes" do + it "are logged to the database" do + expect(payment.state_changes).to be_empty + expect(payment.process!).to be true + expect(payment.state_changes.count).to eq(2) + changes = payment.state_changes.map { |change| { change.previous_state => change.next_state} } + expect(changes).to match_array([ + {"checkout" => "processing"}, + { "processing" => "pending"} + ]) + end + end +end diff --git a/core/spec/models/spree/preference_spec.rb b/core/spec/models/spree/preference_spec.rb new file mode 100644 index 00000000000..2d9b9c1159f --- /dev/null +++ b/core/spec/models/spree/preference_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Spree::Preference, :type => :model do + + it "should require a key" do + @preference = Spree::Preference.new + @preference.key = :test + @preference.value = true + expect(@preference).to be_valid + end + + describe "type coversion for values" do + def round_trip_preference(key, value) + p = Spree::Preference.new + p.value = value + p.key = key + p.save + + Spree::Preference.find_by_key(key) + end + + it ":boolean" do + value = true + key = "boolean_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + it "false :boolean" do + value = false + key = "boolean_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + it ":integer" do + value = 10 + key = "integer_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + it ":decimal" do + value = 1.5 + key = "decimal_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + it ":string" do + value = "This is a string" + key = "string_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + it ":text" do + value = "This is a string stored as text" + key = "text_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + it ":password" do + value = "This is a password" + key = "password_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + it ":any" do + value = [1, 2] + key = "any_key" + pref = round_trip_preference(key, value) + expect(pref.value).to eq value + end + + end + +end diff --git a/core/spec/models/spree/preferences/configuration_spec.rb b/core/spec/models/spree/preferences/configuration_spec.rb new file mode 100644 index 00000000000..3a95cc14ea5 --- /dev/null +++ b/core/spec/models/spree/preferences/configuration_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Spree::Preferences::Configuration, :type => :model do + + before :all do + class AppConfig < Spree::Preferences::Configuration + preference :color, :string, :default => :blue + end + @config = AppConfig.new + end + + it "has named methods to access preferences" do + @config.color = 'orange' + expect(@config.color).to eq 'orange' + end + + it "uses [ ] to access preferences" do + @config[:color] = 'red' + expect(@config[:color]).to eq 'red' + end + + it "uses set/get to access preferences" do + @config.set :color, 'green' + expect(@config.get(:color)).to eq 'green' + end + +end + + + diff --git a/core/spec/models/spree/preferences/preferable_spec.rb b/core/spec/models/spree/preferences/preferable_spec.rb new file mode 100644 index 00000000000..cd02e0069a4 --- /dev/null +++ b/core/spec/models/spree/preferences/preferable_spec.rb @@ -0,0 +1,348 @@ +require 'spec_helper' + +describe Spree::Preferences::Preferable, :type => :model do + + before :all do + class A + include Spree::Preferences::Preferable + attr_reader :id + + def initialize + @id = rand(999) + end + + def preferences + @preferences ||= default_preferences + end + + preference :color, :string, :default => 'green' + end + + class B < A + preference :flavor, :string + end + end + + before :each do + @a = A.new + allow(@a).to receive_messages(:persisted? => true) + @b = B.new + allow(@b).to receive_messages(:persisted? => true) + + # ensure we're persisting as that is the default + # + store = Spree::Preferences::Store.instance + store.persistence = true + end + + describe "preference definitions" do + it "parent should not see child definitions" do + expect(@a.has_preference?(:color)).to be true + expect(@a.has_preference?(:flavor)).not_to be true + end + + it "child should have parent and own definitions" do + expect(@b.has_preference?(:color)).to be true + expect(@b.has_preference?(:flavor)).to be true + end + + it "instances have defaults" do + expect(@a.preferred_color).to eq 'green' + expect(@b.preferred_color).to eq 'green' + expect(@b.preferred_flavor).to be_nil + end + + it "can be asked if it has a preference definition" do + expect(@a.has_preference?(:color)).to be true + expect(@a.has_preference?(:bad)).to be false + end + + it "can be asked and raises" do + expect { + @a.has_preference! :flavor + }.to raise_error(NoMethodError, "flavor preference not defined") + end + + it "has a type" do + expect(@a.preferred_color_type).to eq :string + expect(@a.preference_type(:color)).to eq :string + end + + it "has a default" do + expect(@a.preferred_color_default).to eq 'green' + expect(@a.preference_default(:color)).to eq 'green' + end + + it "raises if not defined" do + expect { + @a.get_preference :flavor + }.to raise_error(NoMethodError, "flavor preference not defined") + end + + end + + describe "preference access" do + it "handles ghost methods for preferences" do + @a.preferred_color = 'blue' + expect(@a.preferred_color).to eq 'blue' + end + + it "parent and child instances have their own prefs" do + @a.preferred_color = 'red' + @b.preferred_color = 'blue' + + expect(@a.preferred_color).to eq 'red' + expect(@b.preferred_color).to eq 'blue' + end + + it "raises when preference not defined" do + expect { + @a.set_preference(:bad, :bone) + }.to raise_exception(NoMethodError, "bad preference not defined") + end + + it "builds a hash of preferences" do + @b.preferred_flavor = :strawberry + expect(@b.preferences[:flavor]).to eq 'strawberry' + expect(@b.preferences[:color]).to eq 'green' #default from A + end + + it "builds a hash of preference defaults" do + expect(@b.default_preferences).to eq({ + flavor: nil, + color: 'green' + }) + end + + context "converts integer preferences to integer values" do + before do + A.preference :is_integer, :integer + end + + it "with strings" do + @a.set_preference(:is_integer, '3') + expect(@a.preferences[:is_integer]).to eq(3) + + @a.set_preference(:is_integer, '') + expect(@a.preferences[:is_integer]).to eq(0) + end + + end + + context "converts decimal preferences to BigDecimal values" do + before do + A.preference :if_decimal, :decimal + end + + it "returns a BigDecimal" do + @a.set_preference(:if_decimal, 3.3) + expect(@a.preferences[:if_decimal].class).to eq(BigDecimal) + end + + it "with strings" do + @a.set_preference(:if_decimal, '3.3') + expect(@a.preferences[:if_decimal]).to eq(3.3) + + @a.set_preference(:if_decimal, '') + expect(@a.preferences[:if_decimal]).to eq(0.0) + end + end + + context "converts boolean preferences to boolean values" do + before do + A.preference :is_boolean, :boolean, :default => true + end + + it "with strings" do + @a.set_preference(:is_boolean, '0') + expect(@a.preferences[:is_boolean]).to be false + @a.set_preference(:is_boolean, 'f') + expect(@a.preferences[:is_boolean]).to be false + @a.set_preference(:is_boolean, 't') + expect(@a.preferences[:is_boolean]).to be true + end + + it "with integers" do + @a.set_preference(:is_boolean, 0) + expect(@a.preferences[:is_boolean]).to be false + @a.set_preference(:is_boolean, 1) + expect(@a.preferences[:is_boolean]).to be true + end + + it "with an empty string" do + @a.set_preference(:is_boolean, '') + expect(@a.preferences[:is_boolean]).to be false + end + + it "with an empty hash" do + @a.set_preference(:is_boolean, []) + expect(@a.preferences[:is_boolean]).to be false + end + end + + context "converts array preferences to array values" do + before do + A.preference :is_array, :array, default: [] + end + + it "with arrays" do + @a.set_preference(:is_array, []) + expect(@a.preferences[:is_array]).to be_is_a(Array) + end + + it "with string" do + @a.set_preference(:is_array, "string") + expect(@a.preferences[:is_array]).to be_is_a(Array) + end + + it "with hash" do + @a.set_preference(:is_array, {}) + expect(@a.preferences[:is_array]).to be_is_a(Array) + end + end + + context "converts hash preferences to hash values" do + before do + A.preference :is_hash, :hash, default: {} + end + + it "with hash" do + @a.set_preference(:is_hash, {}) + expect(@a.preferences[:is_hash]).to be_is_a(Hash) + end + + it "with hash and keys are integers" do + @a.set_preference(:is_hash, {1 => 2, 3 => 4}) + expect(@a.preferences[:is_hash]).to eql({1 => 2, 3 => 4}) + end + + it "with ancestor of a hash" do + ancestor_of_hash = ActionController::Parameters.new({ key: :value }) + @a.set_preference(:is_hash, ancestor_of_hash) + expect(@a.preferences[:is_hash]).to eql({"key" => :value}) + end + + it "with string" do + @a.set_preference(:is_hash, "{\"0\"=>{\"answer\"=>\"1\", \"value\"=>\"No\"}}") + expect(@a.preferences[:is_hash]).to be_is_a(Hash) + end + + it "with boolean" do + @a.set_preference(:is_hash, false) + expect(@a.preferences[:is_hash]).to be_is_a(Hash) + @a.set_preference(:is_hash, true) + expect(@a.preferences[:is_hash]).to be_is_a(Hash) + end + + it "with simple array" do + @a.set_preference(:is_hash, ["key", "value", "another key", "another value"]) + expect(@a.preferences[:is_hash]).to be_is_a(Hash) + expect(@a.preferences[:is_hash]["key"]).to eq("value") + expect(@a.preferences[:is_hash]["another key"]).to eq("another value") + end + + it "with a nested array" do + @a.set_preference(:is_hash, [["key", "value"], ["another key", "another value"]]) + expect(@a.preferences[:is_hash]).to be_is_a(Hash) + expect(@a.preferences[:is_hash]["key"]).to eq("value") + expect(@a.preferences[:is_hash]["another key"]).to eq("another value") + end + + it "with single array" do + expect { @a.set_preference(:is_hash, ["key"]) }.to raise_error(ArgumentError) + end + end + + context "converts any preferences to any values" do + before do + A.preference :product_ids, :any, :default => [] + A.preference :product_attributes, :any, :default => {} + end + + it "with array" do + expect(@a.preferences[:product_ids]).to eq([]) + @a.set_preference(:product_ids, [1, 2]) + expect(@a.preferences[:product_ids]).to eq([1, 2]) + end + + it "with hash" do + expect(@a.preferences[:product_attributes]).to eq({}) + @a.set_preference(:product_attributes, {:id => 1, :name => 2}) + expect(@a.preferences[:product_attributes]).to eq({:id => 1, :name => 2}) + end + end + + end + + describe "persisted preferables" do + before(:all) do + class CreatePrefTest < ActiveRecord::Migration + def self.up + create_table :pref_tests do |t| + t.string :col + t.text :preferences + end + end + + def self.down + drop_table :pref_tests + end + end + + @migration_verbosity = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + CreatePrefTest.migrate(:up) + + class PrefTest < Spree::Base + preference :pref_test_pref, :string, :default => 'abc' + preference :pref_test_any, :any, :default => [] + end + end + + after(:all) do + CreatePrefTest.migrate(:down) + ActiveRecord::Migration.verbose = @migration_verbosity + end + + before(:each) do + @pt = PrefTest.create + end + + describe "pending preferences for new activerecord objects" do + it "saves preferences after record is saved" do + pr = PrefTest.new + pr.set_preference(:pref_test_pref, 'XXX') + expect(pr.get_preference(:pref_test_pref)).to eq('XXX') + pr.save! + expect(pr.get_preference(:pref_test_pref)).to eq('XXX') + end + + it "saves preferences for serialized object" do + pr = PrefTest.new + pr.set_preference(:pref_test_any, [1, 2]) + expect(pr.get_preference(:pref_test_any)).to eq([1, 2]) + pr.save! + expect(pr.get_preference(:pref_test_any)).to eq([1, 2]) + end + end + + it "clear preferences" do + @pt.set_preference(:pref_test_pref, 'xyz') + expect(@pt.preferred_pref_test_pref).to eq('xyz') + @pt.clear_preferences + expect(@pt.preferred_pref_test_pref).to eq('abc') + end + + it "clear preferences when record is deleted" do + @pt.save! + @pt.preferred_pref_test_pref = 'lmn' + @pt.save! + @pt.destroy + @pt1 = PrefTest.new(:col => 'aaaa') + @pt1.id = @pt.id + @pt1.save! + expect(@pt1.get_preference(:pref_test_pref)).to eq('abc') + end + end + +end diff --git a/core/spec/models/spree/preferences/scoped_store_spec.rb b/core/spec/models/spree/preferences/scoped_store_spec.rb new file mode 100644 index 00000000000..df5d48eef57 --- /dev/null +++ b/core/spec/models/spree/preferences/scoped_store_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Spree::Preferences::ScopedStore, :type => :model do + let(:scoped_store){ described_class.new(prefix, suffix) } + subject{ scoped_store } + let(:prefix){ nil } + let(:suffix){ nil } + + describe '#store' do + subject{ scoped_store.store } + it{ is_expected.to be Spree::Preferences::Store.instance } + end + + context 'stubbed store' do + let(:store){ double(:store) } + before do + allow(scoped_store).to receive(:store).and_return(store) + end + + context "with a prefix" do + let(:prefix){ 'my_class' } + + it "can fetch" do + expect(store).to receive(:fetch).with('my_class/attr') + scoped_store.fetch('attr'){ 'default' } + end + + it "can assign" do + expect(store).to receive(:[]=).with('my_class/attr', 'val') + scoped_store['attr'] = 'val' + end + + it "can delete" do + expect(store).to receive(:delete).with('my_class/attr') + scoped_store.delete('attr') + end + + context "and suffix" do + let(:suffix){ 123 } + + it "can fetch" do + expect(store).to receive(:fetch).with('my_class/attr/123') + scoped_store.fetch('attr'){ 'default' } + end + + it "can assign" do + expect(store).to receive(:[]=).with('my_class/attr/123', 'val') + scoped_store['attr'] = 'val' + end + + it "can delete" do + expect(store).to receive(:delete).with('my_class/attr/123') + scoped_store.delete('attr') + end + end + end + end +end diff --git a/core/spec/models/spree/preferences/store_spec.rb b/core/spec/models/spree/preferences/store_spec.rb new file mode 100644 index 00000000000..237e6add063 --- /dev/null +++ b/core/spec/models/spree/preferences/store_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Spree::Preferences::Store, :type => :model do + before :each do + @store = Spree::Preferences::StoreInstance.new + end + + it "sets and gets a key" do + @store.set :test, 1 + expect(@store.exist?(:test)).to be true + expect(@store.get(:test)).to eq 1 + end + + it "can set and get false values when cache return nil" do + @store.set :test, false + expect(@store.get(:test)).to be false + end + + it "will return db value when cache is emtpy and cache the db value" do + preference = Spree::Preference.where(:key => 'test').first_or_initialize + preference.value = '123' + preference.save + + Rails.cache.clear + expect(@store.get(:test)).to eq '123' + expect(Rails.cache.read(:test)).to eq '123' + end + + it "should return and cache fallback value when supplied" do + Rails.cache.clear + expect(@store.get(:test){ false }).to be false + expect(Rails.cache.read(:test)).to be false + end + + it "should return but not cache fallback value when persistence is disabled" do + Rails.cache.clear + allow(@store).to receive_messages(:should_persist? => false) + expect(@store.get(:test){ true }).to be true + expect(Rails.cache.exist?(:test)).to be false + end + + it "should return nil when key can't be found and fallback value is not supplied" do + expect(@store.get(:random_key){ nil }).to be_nil + end + +end diff --git a/core/spec/models/spree/price_spec.rb b/core/spec/models/spree/price_spec.rb new file mode 100644 index 00000000000..a3632f80fa9 --- /dev/null +++ b/core/spec/models/spree/price_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Spree::Price, :type => :model do + describe 'validations' do + let(:variant) { stub_model Spree::Variant } + subject { Spree::Price.new variant: variant, amount: amount } + + context 'when the amount is nil' do + let(:amount) { nil } + it { is_expected.to be_valid } + end + + context 'when the amount is less than 0' do + let(:amount) { -1 } + + it 'has 1 error_on' do + expect(subject.error_on(:amount).size).to eq(1) + end + it 'populates errors' do + subject.valid? + expect(subject.errors.messages[:amount].first).to eq 'must be greater than or equal to 0' + end + end + + context 'when the amount is greater than 999,999.99' do + let(:amount) { 1_000_000 } + + it 'has 1 error_on' do + expect(subject.error_on(:amount).size).to eq(1) + end + it 'populates errors' do + subject.valid? + expect(subject.errors.messages[:amount].first).to eq 'must be less than or equal to 999999.99' + end + end + + context 'when the amount is between 0 and 999,999.99' do + let(:amount) { 100 } + it { is_expected.to be_valid } + end + end +end diff --git a/core/spec/models/spree/product/scopes_spec.rb b/core/spec/models/spree/product/scopes_spec.rb new file mode 100644 index 00000000000..a35e1ef8b69 --- /dev/null +++ b/core/spec/models/spree/product/scopes_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +describe "Product scopes", :type => :model do + let!(:product) { create(:product) } + + context "A product assigned to parent and child taxons" do + before do + @taxonomy = create(:taxonomy) + @root_taxon = @taxonomy.root + + @parent_taxon = create(:taxon, :name => 'Parent', :taxonomy_id => @taxonomy.id, :parent => @root_taxon) + @child_taxon = create(:taxon, :name =>'Child 1', :taxonomy_id => @taxonomy.id, :parent => @parent_taxon) + @parent_taxon.reload # Need to reload for descendents to show up + + product.taxons << @parent_taxon + product.taxons << @child_taxon + end + + it "calling Product.in_taxon returns products in child taxons" do + product.taxons -= [@child_taxon] + expect(product.taxons.count).to eq(1) + + expect(Spree::Product.in_taxon(@parent_taxon)).to include(product) + end + + it "calling Product.in_taxon should not return duplicate records" do + expect(Spree::Product.in_taxon(@parent_taxon).to_a.count).to eq(1) + end + + context 'orders products based on their ordering within the classifications' do + let(:other_taxon) { create(:taxon, products: [product]) } + let!(:product_2) { create(:product, taxons: [@child_taxon, other_taxon]) } + + it 'by initial ordering' do + expect(Spree::Product.in_taxon(@child_taxon)).to eq([product, product_2]) + expect(Spree::Product.in_taxon(other_taxon)).to eq([product, product_2]) + end + + it 'after ordering changed' do + [@child_taxon, other_taxon].each do |taxon| + Spree::Classification.find_by(:taxon => taxon, :product => product).insert_at(2) + expect(Spree::Product.in_taxon(taxon)).to eq([product_2, product]) + end + end + end + end + + context "property scopes" do + let(:name) { "A proper tee" } + let(:value) { "A proper value"} + let!(:property) { create(:property, name: name)} + + before do + product.properties << property + product.product_properties.find_by(property: property).update_column(:value, value) + end + + context "with_property" do + let(:with_property) { Spree::Product.method(:with_property) } + it "finds by a property's name" do + expect(with_property.(name).count).to eq(1) + end + + it "doesn't find any properties with an unknown name" do + expect(with_property.("fake").count).to eq(0) + end + + it "finds by a property" do + expect(with_property.(property).count).to eq(1) + end + + it "finds by an id" do + expect(with_property.(property.id).count).to eq(1) + end + + it "cannot find a property with an unknown id" do + expect(with_property.(0).count).to eq(0) + end + end + + context "with_property_value" do + let(:with_property_value) { Spree::Product.method(:with_property_value) } + it "finds by a property's name" do + expect(with_property_value.(name, value).count).to eq(1) + end + + it "cannot find by an unknown property's name" do + expect(with_property_value.("fake", value).count).to eq(0) + end + + it "cannot find with a name by an incorrect value" do + expect(with_property_value.(name, "fake").count).to eq(0) + end + + it "finds by a property" do + expect(with_property_value.(property, value).count).to eq(1) + end + + it "cannot find with a property by an incorrect value" do + expect(with_property_value.(property, "fake").count).to eq(0) + end + + it "finds by an id with a value" do + expect(with_property_value.(property.id, value).count).to eq(1) + end + + it "cannot find with an invalid id" do + expect(with_property_value.(0, value).count).to eq(0) + end + + it "cannot find with an invalid value" do + expect(with_property_value.(property.id, "fake").count).to eq(0) + end + end + end + + context '#add_simple_scopes' do + let(:simple_scopes) { [:ascend_by_updated_at, :descend_by_name] } + + before do + Spree::Product.add_simple_scopes(simple_scopes) + end + + context 'define scope' do + context 'ascend_by_updated_at' do + context 'on class' do + it { expect(Spree::Product.ascend_by_updated_at.to_sql).to eq Spree::Product.order("#{Spree::Product.quoted_table_name}.updated_at ASC").to_sql } + end + + context 'on ActiveRecord::Relation' do + it { expect(Spree::Product.limit(2).ascend_by_updated_at.to_sql).to eq Spree::Product.limit(2).order("#{Spree::Product.quoted_table_name}.updated_at ASC").to_sql } + it { expect(Spree::Product.limit(2).ascend_by_updated_at.to_sql).to eq Spree::Product.ascend_by_updated_at.limit(2).to_sql } + end + end + + context 'descend_by_name' do + context 'on class' do + it { expect(Spree::Product.descend_by_name.to_sql).to eq Spree::Product.order("#{Spree::Product.quoted_table_name}.name DESC").to_sql } + end + + context 'on ActiveRecord::Relation' do + it { expect(Spree::Product.limit(2).descend_by_name.to_sql).to eq Spree::Product.limit(2).order("#{Spree::Product.quoted_table_name}.name DESC").to_sql } + it { expect(Spree::Product.limit(2).descend_by_name.to_sql).to eq Spree::Product.descend_by_name.limit(2).to_sql } + end + end + end + end +end diff --git a/core/spec/models/spree/product_duplicator_spec.rb b/core/spec/models/spree/product_duplicator_spec.rb new file mode 100644 index 00000000000..7557049272c --- /dev/null +++ b/core/spec/models/spree/product_duplicator_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +module Spree + + describe Spree::ProductDuplicator, :type => :model do + + let(:product) { create(:product, properties: [create(:property, name: "MyProperty")])} + let!(:duplicator) { Spree::ProductDuplicator.new(product)} + + let(:image) { File.open(File.expand_path('../../../fixtures/thinking-cat.jpg', __FILE__)) } + let(:params) { {:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :attachment => image, :alt => "position 1", :position => 1} } + + before do + Spree::Image.create(params) + end + + it "will duplicate the product" do + expect{duplicator.duplicate}.to change{Spree::Product.count}.by(1) + end + + context 'when image duplication enabled' do + + it "will duplicate the product images" do + expect{duplicator.duplicate}.to change{Spree::Image.count}.by(1) + end + + end + + context 'when image duplication disabled' do + + let!(:duplicator) { Spree::ProductDuplicator.new(product, false) } + + it "will not duplicate the product images" do + expect{duplicator.duplicate}.to change{Spree::Image.count}.by(0) + end + + end + + context 'image duplication default' do + + context 'when default is set to true' do + + it 'clones images if no flag passed to initializer' do + expect{duplicator.duplicate}.to change{Spree::Image.count}.by(1) + end + + end + + context 'when default is set to false' do + + before do + ProductDuplicator.clone_images_default = false + end + + after do + ProductDuplicator.clone_images_default = true + end + + it 'does not clone images if no flag passed to initializer' do + expect{ProductDuplicator.new(product).duplicate}.to change{Spree::Image.count}.by(0) + end + + end + + end + + context "product attributes" do + let!(:new_product) {duplicator.duplicate} + + it "will set an unique name" do + expect(new_product.name).to eql "COPY OF #{product.name}" + end + + it "will set an unique sku" do + expect(new_product.sku).to include "COPY OF SKU" + end + + it "copied the properties" do + expect(new_product.product_properties.count).to be 1 + expect(new_product.product_properties.first.property.name).to eql "MyProperty" + end + end + + context "with variants" do + let(:option_type) { create(:option_type, name: "MyOptionType")} + let(:option_value1) { create(:option_value, name: "OptionValue1", option_type: option_type)} + let(:option_value2) { create(:option_value, name: "OptionValue2", option_type: option_type)} + + let!(:variant1) { create(:variant, product: product, option_values: [option_value1]) } + let!(:variant2) { create(:variant, product: product, option_values: [option_value2]) } + + it "will duplciate the variants" do + # will change the count by 3, since there will be a master variant as well + expect{duplicator.duplicate}.to change{Spree::Variant.count}.by(3) + end + + it "will not duplicate the option values" do + expect{duplicator.duplicate}.to change{Spree::OptionValue.count}.by(0) + end + + end + end +end diff --git a/core/spec/models/spree/product_filter_spec.rb b/core/spec/models/spree/product_filter_spec.rb new file mode 100644 index 00000000000..c61c9ceb2fa --- /dev/null +++ b/core/spec/models/spree/product_filter_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'spree/core/product_filters' + +describe 'product filters', :type => :model do + # Regression test for #1709 + context 'finds products filtered by brand' do + let(:product) { create(:product) } + before do + property = Spree::Property.create!(:name => "brand", :presentation => "brand") + product.set_property("brand", "Nike") + end + + it "does not attempt to call value method on Arel::Table" do + expect { Spree::Core::ProductFilters.brand_filter }.not_to raise_error + end + + it "can find products in the 'Nike' brand" do + expect(Spree::Product.brand_any("Nike")).to include(product) + end + it "sorts products without brand specified" do + product.set_property("brand", "Nike") + create(:product).set_property("brand", nil) + expect { Spree::Core::ProductFilters.brand_filter[:labels] }.not_to raise_error + end + end +end diff --git a/core/spec/models/spree/product_option_type_spec.rb b/core/spec/models/spree/product_option_type_spec.rb new file mode 100644 index 00000000000..e1c7e498757 --- /dev/null +++ b/core/spec/models/spree/product_option_type_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Spree::ProductOptionType, :type => :model do + +end diff --git a/core/spec/models/spree/product_property_spec.rb b/core/spec/models/spree/product_property_spec.rb new file mode 100644 index 00000000000..34b4f3fc3f5 --- /dev/null +++ b/core/spec/models/spree/product_property_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Spree::ProductProperty, :type => :model do + + context "validations" do + it "should validate length of value" do + pp = create(:product_property) + pp.value = "x" * 256 + expect(pp).not_to be_valid + end + end + + context "touching" do + it "should update product" do + pp = create(:product_property) + expect(pp.product).to receive(:touch) + pp.touch + end + end +end diff --git a/core/spec/models/spree/product_spec.rb b/core/spec/models/spree/product_spec.rb new file mode 100644 index 00000000000..d1b390fd23e --- /dev/null +++ b/core/spec/models/spree/product_spec.rb @@ -0,0 +1,486 @@ +# coding: UTF-8 + +require 'spec_helper' + +module ThirdParty + class Extension < Spree::Base + # nasty hack so we don't have to create a table to back this fake model + self.table_name = 'spree_products' + end +end + +describe Spree::Product, :type => :model do + + context 'product instance' do + let(:product) { create(:product) } + let(:variant) { create(:variant, :product => product) } + + context '#duplicate' do + before do + allow(product).to receive_messages :taxons => [create(:taxon)] + end + + it 'duplicates product' do + clone = product.duplicate + expect(clone.name).to eq('COPY OF ' + product.name) + expect(clone.master.sku).to eq('COPY OF ' + product.master.sku) + expect(clone.taxons).to eq(product.taxons) + expect(clone.images.size).to eq(product.images.size) + end + + it 'calls #duplicate_extra' do + Spree::Product.class_eval do + def duplicate_extra(old_product) + self.name = old_product.name.reverse + end + end + + clone = product.duplicate + expect(clone.name).to eq(product.name.reverse) + end + end + + context "master variant" do + + context "when master variant changed" do + before do + product.master.sku = "Something changed" + end + + it "saves the master" do + expect(product.master).to receive(:save!) + product.save + end + end + + context "when master default price changed" do + before do + master = product.master + master.default_price.price = 11 + master.save! + product.master.default_price.price = 12 + end + + it "saves the master" do + expect(product.master).to receive(:save!) + product.save + end + + it "saves the default price" do + expect(product.master.default_price).to receive(:save) + product.save + end + end + + context "when master variant and price haven't changed" do + it "does not save the master" do + expect(product.master).not_to receive(:save!) + product.save + end + end + end + + context "product has no variants" do + context "#destroy" do + it "should set deleted_at value" do + product.destroy + expect(product.deleted_at).not_to be_nil + expect(product.master.reload.deleted_at).not_to be_nil + end + end + end + + context "product has variants" do + before do + create(:variant, :product => product) + end + + context "#destroy" do + it "should set deleted_at value" do + product.destroy + expect(product.deleted_at).not_to be_nil + expect(product.variants_including_master.all? { |v| !v.deleted_at.nil? }).to be true + end + end + end + + context "#price" do + # Regression test for #1173 + it 'strips non-price characters' do + product.price = "$10" + expect(product.price).to eq(10.0) + end + end + + context "#display_price" do + before { product.price = 10.55 } + + context "with display_currency set to true" do + before { Spree::Config[:display_currency] = true } + + it "shows the currency" do + expect(product.display_price.to_s).to eq("$10.55 USD") + end + end + + context "with display_currency set to false" do + before { Spree::Config[:display_currency] = false } + + it "does not include the currency" do + expect(product.display_price.to_s).to eq("$10.55") + end + end + + context "with currency set to JPY" do + before do + product.master.default_price.currency = 'JPY' + product.master.default_price.save! + Spree::Config[:currency] = 'JPY' + end + + it "displays the currency in yen" do + expect(product.display_price.to_s).to eq("¥11") + end + end + end + + context "#available?" do + it "should be available if date is in the past" do + product.available_on = 1.day.ago + expect(product).to be_available + end + + it "should not be available if date is nil or in the future" do + product.available_on = nil + expect(product).not_to be_available + + product.available_on = 1.day.from_now + expect(product).not_to be_available + end + + it "should not be available if destroyed" do + product.destroy + expect(product).not_to be_available + end + end + + context "variants_and_option_values" do + let!(:high) { create(:variant, product: product) } + let!(:low) { create(:variant, product: product) } + + before { high.option_values.destroy_all } + + it "returns only variants with option values" do + expect(product.variants_and_option_values).to eq([low]) + end + end + + describe 'Variants sorting' do + context 'without master variant' do + it 'sorts variants by position' do + expect(product.variants.to_sql).to match(/ORDER BY (\`|\")spree_variants(\`|\").position ASC/) + end + end + + context 'with master variant' do + it 'sorts variants by position' do + expect(product.variants_including_master.to_sql).to match(/ORDER BY (\`|\")spree_variants(\`|\").position ASC/) + end + end + end + + context "has stock movements" do + let(:product) { create(:product) } + let(:variant) { product.master } + let(:stock_item) { variant.stock_items.first } + + it "doesnt raise ReadOnlyRecord error" do + Spree::StockMovement.create!(stock_item: stock_item, quantity: 1) + expect { product.destroy }.not_to raise_error + end + end + + # Regression test for #3737 + context "has stock items" do + let(:product) { create(:product) } + it "can retrieve stock items" do + expect(product.master.stock_items.first).not_to be_nil + expect(product.stock_items.first).not_to be_nil + end + end + + context "slugs" do + + it "normalizes slug on update validation" do + product.slug = "hey//joe" + product.valid? + expect(product.slug).not_to match "/" + end + + context "when product destroyed" do + + it "renames slug" do + expect { product.destroy }.to change { product.slug } + end + + context "when slug is already at or near max length" do + + before do + product.slug = "x" * 255 + product.save! + end + + it "truncates renamed slug to ensure it remains within length limit" do + product.destroy + expect(product.slug.length).to eq 255 + end + + end + + end + + it "validates slug uniqueness" do + existing_product = product + new_product = create(:product) + new_product.slug = existing_product.slug + + expect(new_product.valid?).to eq false + end + + it "falls back to 'name-sku' for slug if regular name-based slug already in use" do + product1 = build(:product) + product1.name = "test" + product1.sku = "123" + product1.save! + + product2 = build(:product) + product2.name = "test" + product2.sku = "456" + product2.save! + + expect(product2.slug).to eq 'test-456' + end + end + + context 'history' do + before(:each) do + @product = create(:product) + end + + it 'should keep the history when the product is destroyed' do + @product.destroy + + expect(@product.slugs.with_deleted).to_not be_empty + end + + it 'should update the history when the product is restored' do + @product.destroy + + @product.restore(recursive: true) + + latest_slug = @product.slugs.find_by slug: @product.slug + expect(latest_slug).to_not be_nil + end + end + end + + context "properties" do + let(:product) { create(:product) } + + it "should properly assign properties" do + product.set_property('the_prop', 'value1') + expect(product.property('the_prop')).to eq('value1') + + product.set_property('the_prop', 'value2') + expect(product.property('the_prop')).to eq('value2') + end + + it "should not create duplicate properties when set_property is called" do + expect { + product.set_property('the_prop', 'value2') + product.save + product.reload + }.not_to change(product.properties, :length) + + expect { + product.set_property('the_prop_new', 'value') + product.save + product.reload + expect(product.property('the_prop_new')).to eq('value') + }.to change { product.properties.length }.by(1) + end + + # Regression test for #2455 + it "should not overwrite properties' presentation names" do + Spree::Property.where(:name => 'foo').first_or_create!(:presentation => "Foo's Presentation Name") + product.set_property('foo', 'value1') + product.set_property('bar', 'value2') + expect(Spree::Property.where(:name => 'foo').first.presentation).to eq("Foo's Presentation Name") + expect(Spree::Property.where(:name => 'bar').first.presentation).to eq("bar") + end + + # Regression test for #4416 + context "#possible_promotions" do + let!(:promotion) do + create(:promotion, advertise: true, starts_at: 1.day.ago) + end + let!(:rule) do + Spree::Promotion::Rules::Product.create( + promotion: promotion, + products: [product] + ) + end + + it "lists the promotion as a possible promotion" do + expect(product.possible_promotions).to include(promotion) + end + end + end + + context '#create' do + let!(:prototype) { create(:prototype) } + let!(:product) { Spree::Product.new(name: "Foo", price: 1.99, shipping_category_id: create(:shipping_category).id) } + + before { product.prototype_id = prototype.id } + + context "when prototype is supplied" do + it "should create properties based on the prototype" do + product.save + expect(product.properties.count).to eq(1) + end + end + + context "when prototype with option types is supplied" do + def build_option_type_with_values(name, values) + ot = create(:option_type, :name => name) + values.each do |val| + ot.option_values.create(:name => val.downcase, :presentation => val) + end + ot + end + + let(:prototype) do + size = build_option_type_with_values("size", %w(Small Medium Large)) + create(:prototype, :name => "Size", :option_types => [ size ]) + end + + let(:option_values_hash) do + hash = {} + prototype.option_types.each do |i| + hash[i.id.to_s] = i.option_value_ids + end + hash + end + + it "should create option types based on the prototype" do + product.save + expect(product.option_type_ids.length).to eq(1) + expect(product.option_type_ids).to eq(prototype.option_type_ids) + end + + it "should create product option types based on the prototype" do + product.save + expect(product.product_option_types.pluck(:option_type_id)).to eq(prototype.option_type_ids) + end + + it "should create variants from an option values hash with one option type" do + product.option_values_hash = option_values_hash + product.save + expect(product.variants.length).to eq(3) + end + + it "should still create variants when option_values_hash is given but prototype id is nil" do + product.option_values_hash = option_values_hash + product.prototype_id = nil + product.save + expect(product.option_type_ids.length).to eq(1) + expect(product.option_type_ids).to eq(prototype.option_type_ids) + expect(product.variants.length).to eq(3) + end + + it "should create variants from an option values hash with multiple option types" do + color = build_option_type_with_values("color", %w(Red Green Blue)) + logo = build_option_type_with_values("logo", %w(Ruby Rails Nginx)) + option_values_hash[color.id.to_s] = color.option_value_ids + option_values_hash[logo.id.to_s] = logo.option_value_ids + product.option_values_hash = option_values_hash + product.save + product.reload + expect(product.option_type_ids.length).to eq(3) + expect(product.variants.length).to eq(27) + end + end + end + + context "#images" do + let(:product) { create(:product) } + let(:image) { File.open(File.expand_path('../../../fixtures/thinking-cat.jpg', __FILE__)) } + let(:params) { {:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :attachment => image, :alt => "position 2", :position => 2} } + + before do + Spree::Image.create(params) + Spree::Image.create(params.merge({:alt => "position 1", :position => 1})) + Spree::Image.create(params.merge({:viewable_type => 'ThirdParty::Extension', :alt => "position 1", :position => 2})) + end + + it "only looks for variant images" do + expect(product.images.size).to eq(2) + end + + it "should be sorted by position" do + expect(product.images.pluck(:alt)).to eq(["position 1", "position 2"]) + end + end + + # Regression tests for #2352 + context "classifications and taxons" do + it "is joined through classifications" do + reflection = Spree::Product.reflect_on_association(:taxons) + expect(reflection.options[:through]).to eq(:classifications) + end + + it "will delete all classifications" do + reflection = Spree::Product.reflect_on_association(:classifications) + expect(reflection.options[:dependent]).to eq(:delete_all) + end + end + + context '#total_on_hand' do + it 'should be infinite if track_inventory_levels is false' do + Spree::Config[:track_inventory_levels] = false + expect(build(:product, :variants_including_master => [build(:master_variant)]).total_on_hand).to eql(Float::INFINITY) + end + + it 'should be infinite if variant is on demand' do + Spree::Config[:track_inventory_levels] = true + expect(build(:product, :variants_including_master => [build(:on_demand_master_variant)]).total_on_hand).to eql(Float::INFINITY) + end + + it 'should return sum of stock items count_on_hand' do + product = create(:product) + product.stock_items.first.set_count_on_hand 5 + product.variants_including_master(true) # force load association + expect(product.total_on_hand).to eql(5) + end + + it 'should return sum of stock items count_on_hand when variants_including_master is not loaded' do + product = create(:product) + product.stock_items.first.set_count_on_hand 5 + expect(product.reload.total_on_hand).to eql(5) + end + end + + # Regression spec for https://github.com/spree/spree/issues/5588 + context '#validate_master when duplicate SKUs entered' do + let!(:first_product) { create(:product, sku: 'a-sku') } + let(:second_product) { build(:product, sku: 'a-sku') } + + subject { second_product } + it { is_expected.to be_invalid } + end + + it "initializes a master variant when building a product" do + product = Spree::Product.new + expect(product.master.is_master).to be true + end +end diff --git a/core/spec/models/spree/promotion/actions/create_adjustment_spec.rb b/core/spec/models/spree/promotion/actions/create_adjustment_spec.rb new file mode 100644 index 00000000000..c5016416b34 --- /dev/null +++ b/core/spec/models/spree/promotion/actions/create_adjustment_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Spree::Promotion::Actions::CreateAdjustment, type: :model do + let(:order) { create(:order_with_line_items, line_items_count: 1) } + let(:promotion) { create(:promotion) } + let(:action) { Spree::Promotion::Actions::CreateAdjustment.new } + let(:payload) { { order: order } } + + # From promotion spec: + context "#perform" do + before do + action.calculator = Spree::Calculator::FlatRate.new(preferred_amount: 10) + promotion.promotion_actions = [action] + allow(action).to receive_messages(promotion: promotion) + end + + # Regression test for #3966 + it "does not apply an adjustment if the amount is 0" do + action.calculator.preferred_amount = 0 + action.perform(payload) + expect(promotion.credits_count).to eq(0) + expect(order.adjustments.count).to eq(0) + end + + it "should create a discount with correct negative amount" do + order.shipments.create!(cost: 10, stock_location: create(:stock_location)) + + action.perform(payload) + expect(promotion.credits_count).to eq(1) + expect(order.adjustments.count).to eq(1) + expect(order.adjustments.first.amount.to_i).to eq(-10) + end + + it "should create a discount accessible through both order_id and adjustable_id" do + action.perform(payload) + expect(order.adjustments.count).to eq(1) + expect(order.all_adjustments.count).to eq(1) + end + + it "should not create a discount when order already has one from this promotion" do + order.shipments.create!(cost: 10, stock_location: create(:stock_location)) + + action.perform(payload) + action.perform(payload) + expect(promotion.credits_count).to eq(1) + end + end + + context "#destroy" do + before(:each) do + action.calculator = Spree::Calculator::FlatRate.new(preferred_amount: 10) + promotion.promotion_actions = [action] + end + + context "when order is not complete" do + it "should not keep the adjustment" do + action.perform(payload) + action.destroy + expect(order.adjustments.count).to eq(0) + end + end + + context "when order is complete" do + let(:order) do + create(:completed_order_with_totals, line_items_count: 1) + end + + before(:each) do + action.perform(payload) + action.destroy + end + + it "should keep the adjustment" do + expect(order.adjustments.count).to eq(1) + end + + it "should nullify the adjustment source" do + expect(order.adjustments.reload.first.source).to be_nil + end + end + end +end diff --git a/core/spec/models/spree/promotion/actions/create_item_adjustments_spec.rb b/core/spec/models/spree/promotion/actions/create_item_adjustments_spec.rb new file mode 100644 index 00000000000..b8e6da0f3d1 --- /dev/null +++ b/core/spec/models/spree/promotion/actions/create_item_adjustments_spec.rb @@ -0,0 +1,136 @@ +require 'spec_helper' + +module Spree + class Promotion + module Actions + describe CreateItemAdjustments, :type => :model do + let(:order) { create(:order) } + let(:promotion) { create(:promotion) } + let(:action) { CreateItemAdjustments.new } + let!(:line_item) { create(:line_item, :order => order) } + let(:payload) { { order: order, promotion: promotion } } + + before do + allow(action).to receive(:promotion).and_return(promotion) + promotion.promotion_actions = [action] + end + + context "#perform" do + # Regression test for #3966 + context "when calculator computes 0" do + before do + allow(action).to receive_messages :compute_amount => 0 + end + + it "does not create an adjustment when calculator returns 0" do + action.perform(payload) + expect(action.adjustments).to be_empty + end + end + + context "when calculator returns a non-zero value" do + before do + promotion.promotion_actions = [action] + allow(action).to receive_messages :compute_amount => 10 + end + + it "creates adjustment with item as adjustable" do + action.perform(payload) + expect(action.adjustments.count).to eq(1) + expect(line_item.reload.adjustments).to eq(action.adjustments) + end + + it "creates adjustment with self as source" do + action.perform(payload) + expect(line_item.reload.adjustments.first.source).to eq action + end + + it "does not perform twice on the same item" do + 2.times { action.perform(payload) } + expect(action.adjustments.count).to eq(1) + end + + context "with products rules" do + let!(:second_line_item) { create(:line_item, :order => order) } + let(:rule) { double Spree::Promotion::Rules::Product } + + before do + allow(promotion).to receive(:eligible_rules) { [rule] } + allow(rule).to receive(:actionable?).and_return(true, false) + end + + it "does not create adjustments for line_items not in product rule" do + action.perform(payload) + expect(action.adjustments.count).to eql 1 + expect(line_item.reload.adjustments).to match_array action.adjustments + expect(second_line_item.reload.adjustments).to be_empty + end + end + end + end + + context "#compute_amount" do + before { promotion.promotion_actions = [action] } + + context "when the adjustable is actionable" do + it "calls compute on the calculator" do + expect(action.calculator).to receive(:compute).with(line_item) + action.compute_amount(line_item) + end + + context "calculator returns amount greater than item total" do + before do + expect(action.calculator).to receive(:compute).with(line_item).and_return(300) + allow(line_item).to receive_messages(amount: 100) + end + + it "does not exceed it" do + expect(action.compute_amount(line_item)).to eql(-100) + end + end + end + + context "when the adjustable is not actionable" do + before { allow(promotion).to receive(:line_item_actionable?) { false } } + + it 'returns 0' do + expect(action.compute_amount(line_item)).to eql(0) + end + end + end + + context "#destroy" do + let!(:action) { CreateItemAdjustments.create! } + let(:other_action) { CreateItemAdjustments.create! } + before { promotion.promotion_actions = [other_action] } + + it "destroys adjustments for incompleted orders" do + order = Order.create + action.adjustments.create!(label: "Check", amount: 0, order: order, adjustable: order) + + expect { + action.destroy + }.to change { Adjustment.count }.by(-1) + end + + it "nullifies adjustments for completed orders" do + order = Order.create(completed_at: Time.now) + adjustment = action.adjustments.create!(label: "Check", amount: 0, order: order, adjustable: order) + + expect { + action.destroy + }.to change { adjustment.reload.source_id }.from(action.id).to nil + end + + it "doesnt mess with unrelated adjustments" do + other_action.adjustments.create!(label: "Check", amount: 0, order: order, adjustable: order) + + expect { + action.destroy + }.not_to change { other_action.adjustments.count } + end + end + end + end + end +end diff --git a/core/spec/models/spree/promotion/actions/create_line_items_spec.rb b/core/spec/models/spree/promotion/actions/create_line_items_spec.rb new file mode 100644 index 00000000000..7c9f7f5b09a --- /dev/null +++ b/core/spec/models/spree/promotion/actions/create_line_items_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Spree::Promotion::Actions::CreateLineItems, type: :model do + let(:order) { create(:order) } + let(:action) { Spree::Promotion::Actions::CreateLineItems.create } + let(:promotion) { stub_model(Spree::Promotion) } + let(:shirt) { create(:variant) } + let(:mug) { create(:variant) } + let(:payload) { { order: order } } + + def empty_stock(variant) + variant.stock_items.update_all(backorderable: false) + variant.stock_items.each(&:reduce_count_on_hand_to_zero) + end + + context "#perform" do + before do + allow(action).to receive_messages promotion: promotion + action.promotion_action_line_items.create!( + variant: mug, + quantity: 1 + ) + action.promotion_action_line_items.create!( + variant: shirt, + quantity: 2 + ) + end + + context "order is eligible" do + before do + allow(promotion).to receive_messages eligible: true + end + + it "adds line items to order with correct variant and quantity" do + action.perform(payload) + expect(order.line_items.count).to eq(2) + line_item = order.line_items.find_by_variant_id(mug.id) + expect(line_item).not_to be_nil + expect(line_item.quantity).to eq(1) + end + + it "only adds the delta of quantity to an order" do + order.contents.add(shirt, 1) + action.perform(payload) + line_item = order.line_items.find_by_variant_id(shirt.id) + expect(line_item).not_to be_nil + expect(line_item.quantity).to eq(2) + end + + it "doesn't add if the quantity is greater" do + order.contents.add(shirt, 3) + action.perform(payload) + line_item = order.line_items.find_by_variant_id(shirt.id) + expect(line_item).not_to be_nil + expect(line_item.quantity).to eq(3) + end + + it "doesn't try to add an item if it's out of stock" do + empty_stock(mug) + empty_stock(shirt) + + expect(order.contents).to_not receive(:add) + action.perform(order: order) + end + end + end + + describe "#item_available?" do + let(:item_out_of_stock) do + action.promotion_action_line_items.create!(variant: mug, quantity: 1) + end + + let(:item_in_stock) do + action.promotion_action_line_items.create!(variant: shirt, quantity: 1) + end + + it "returns false if the item is out of stock" do + empty_stock(mug) + expect(action.item_available? item_out_of_stock).to be false + end + + it "returns true if the item is in stock" do + expect(action.item_available? item_in_stock).to be true + end + end +end diff --git a/core/spec/models/spree/promotion/actions/free_shipping_spec.rb b/core/spec/models/spree/promotion/actions/free_shipping_spec.rb new file mode 100644 index 00000000000..4234dba2dce --- /dev/null +++ b/core/spec/models/spree/promotion/actions/free_shipping_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Spree::Promotion::Actions::FreeShipping, :type => :model do + let(:order) { create(:completed_order_with_totals) } + let(:promotion) { create(:promotion) } + let(:action) { Spree::Promotion::Actions::FreeShipping.create } + let(:payload) { { order: order } } + + # From promotion spec: + context "#perform" do + before do + order.shipments << create(:shipment) + promotion.promotion_actions << action + end + + it "should create a discount with correct negative amount" do + expect(order.shipments.count).to eq(2) + expect(order.shipments.first.cost).to eq(100) + expect(order.shipments.last.cost).to eq(100) + expect(action.perform(payload)).to be true + expect(promotion.credits_count).to eq(2) + expect(order.shipment_adjustments.count).to eq(2) + expect(order.shipment_adjustments.first.amount.to_i).to eq(-100) + expect(order.shipment_adjustments.last.amount.to_i).to eq(-100) + end + + it "should not create a discount when order already has one from this promotion" do + expect(action.perform(payload)).to be true + expect(action.perform(payload)).to be false + expect(promotion.credits_count).to eq(2) + expect(order.shipment_adjustments.count).to eq(2) + end + end +end diff --git a/core/spec/models/spree/promotion/rules/first_order_spec.rb b/core/spec/models/spree/promotion/rules/first_order_spec.rb new file mode 100644 index 00000000000..e2c58816b06 --- /dev/null +++ b/core/spec/models/spree/promotion/rules/first_order_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Spree::Promotion::Rules::FirstOrder, :type => :model do + let(:rule) { Spree::Promotion::Rules::FirstOrder.new } + let(:order) { mock_model(Spree::Order, :user => nil, :email => nil) } + let(:user) { mock_model(Spree::LegacyUser) } + + context "without a user or email" do + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "You need to login or provide your email before applying this coupon code." + end + end + + context "first order" do + context "for a signed user" do + context "with no completed orders" do + before(:each) do + allow(user).to receive_message_chain(:orders, :complete => []) + end + + specify do + allow(order).to receive_messages(:user => user) + expect(rule).to be_eligible(order) + end + + it "should be eligible when user passed in payload data" do + expect(rule).to be_eligible(order, :user => user) + end + end + + context "with completed orders" do + before(:each) do + allow(order).to receive_messages(:user => user) + end + + it "should be eligible when checked against first completed order" do + allow(user).to receive_message_chain(:orders, :complete => [order]) + expect(rule).to be_eligible(order) + end + + context "with another order" do + before { allow(user).to receive_message_chain(:orders, :complete => [mock_model(Spree::Order)]) } + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can only be applied to your first order." + end + end + end + end + + context "for a guest user" do + let(:email) { 'user@spreecommerce.com' } + before { allow(order).to receive_messages :email => 'user@spreecommerce.com' } + + context "with no other orders" do + it { expect(rule).to be_eligible(order) } + end + + context "with another order" do + before { allow(rule).to receive_messages(:orders_by_email => [mock_model(Spree::Order)]) } + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can only be applied to your first order." + end + end + end + end +end diff --git a/core/spec/models/spree/promotion/rules/item_total_spec.rb b/core/spec/models/spree/promotion/rules/item_total_spec.rb new file mode 100644 index 00000000000..a230452badd --- /dev/null +++ b/core/spec/models/spree/promotion/rules/item_total_spec.rb @@ -0,0 +1,282 @@ +require 'spec_helper' + +describe Spree::Promotion::Rules::ItemTotal, :type => :model do + let(:rule) { Spree::Promotion::Rules::ItemTotal.new } + let(:order) { double(:order) } + + before { rule.preferred_amount_min = 50 } + before { rule.preferred_amount_max = 60 } + + context "preferred operator_min set to gt and preferred operator_max set to lt" do + before do + rule.preferred_operator_min = 'gt' + rule.preferred_operator_max = 'lt' + end + + context "and item total is lower than prefered maximum amount" do + + context "and item total is higher than prefered minimum amount" do + it "should be eligible" do + allow(order).to receive_messages item_total: 51 + expect(rule).to be_eligible(order) + end + end + + context "and item total is equal to the prefered minimum amount" do + + before { allow(order).to receive_messages item_total: 50 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders less than or equal to $50.00." + end + end + + context "and item total is lower to the prefered minimum amount" do + before { allow(order).to receive_messages item_total: 49 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders less than or equal to $50.00." + end + end + end + + context "and item total is equal to the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 60 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders higher than $60.00." + end + end + + context "and item total is higher than the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 61 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders higher than $60.00." + end + end + + end + + context "preferred operator set to gt and preferred operator_max set to lte" do + before do + rule.preferred_operator_min = 'gt' + rule.preferred_operator_max = 'lte' + end + + context "and item total is lower than prefered maximum amount" do + + context "and item total is higher than prefered minimum amount" do + it "should be eligible" do + allow(order).to receive_messages item_total: 51 + expect(rule).to be_eligible(order) + end + end + + context "and item total is equal to the prefered minimum amount" do + + before { allow(order).to receive_messages item_total: 50 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders less than or equal to $50.00." + end + end + + context "and item total is lower to the prefered minimum amount" do + before { allow(order).to receive_messages item_total: 49 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders less than or equal to $50.00." + end + end + end + + context "and item total is equal to the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 60 } + + it "should not be eligible" do + expect(rule).to be_eligible(order) + end + end + + context "and item total is higher than the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 61 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders higher than $60.00." + end + end + end + + context "preferred operator set to gte and preferred operator_max set to lt" do + before do + rule.preferred_operator_min = 'gte' + rule.preferred_operator_max = 'lt' + end + + context "and item total is lower than prefered maximum amount" do + + context "and item total is higher than prefered minimum amount" do + it "should be eligible" do + allow(order).to receive_messages item_total: 51 + expect(rule).to be_eligible(order) + end + end + + context "and item total is equal to the prefered minimum amount" do + + before { allow(order).to receive_messages item_total: 50 } + + it "should not be eligible" do + expect(rule).to be_eligible(order) + end + end + + context "and item total is lower to the prefered minimum amount" do + before { allow(order).to receive_messages item_total: 49 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders less than $50.00." + end + end + end + + context "and item total is equal to the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 60 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders higher than $60.00." + end + end + + context "and item total is higher than the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 61 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders higher than $60.00." + end + end + + end + + context "preferred operator set to gte and preferred operator_max set to lte" do + before do + rule.preferred_operator_min = 'gte' + rule.preferred_operator_max = 'lte' + end + + context "and item total is lower than prefered maximum amount" do + context "and item total is higher than prefered minimum amount" do + it "should be eligible" do + allow(order).to receive_messages item_total: 51 + expect(rule).to be_eligible(order) + end + end + + context "and item total is equal to the prefered minimum amount" do + + before { allow(order).to receive_messages item_total: 50 } + + it "should not be eligible" do + expect(rule).to be_eligible(order) + end + end + + context "and item total is lower to the prefered minimum amount" do + before { allow(order).to receive_messages item_total: 49 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders less than $50.00." + end + end + end + + context "and item total is equal to the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 60 } + + it "should not be eligible" do + expect(rule).to be_eligible(order) + end + end + + context "and item total is higher than the prefered maximum amount" do + before { allow(order).to receive_messages item_total: 61 } + + it "should not be eligible" do + expect(rule).to_not be_eligible(order) + end + + it "set an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied to orders higher than $60.00." + end + end + end +end diff --git a/core/spec/models/spree/promotion/rules/one_use_per_user_spec.rb b/core/spec/models/spree/promotion/rules/one_use_per_user_spec.rb new file mode 100644 index 00000000000..d280919071f --- /dev/null +++ b/core/spec/models/spree/promotion/rules/one_use_per_user_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Spree::Promotion::Rules::OneUsePerUser, :type => :model do + let(:rule) { described_class.new } + + describe '#eligible?(order)' do + subject { rule.eligible?(order) } + let(:order) { double Spree::Order, user: user } + let(:user) { double Spree::LegacyUser } + let(:promotion) { stub_model Spree::Promotion, used_by?: used_by } + let(:used_by) { false } + + before { rule.promotion = promotion } + + context 'when the order is assigned to a user' do + context 'when the user has used this promotion before' do + let(:used_by) { true } + + it { is_expected.to be false } + it "sets an error message" do + subject + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can only be used once per user." + end + end + + context 'when the user has not used this promotion before' do + it { is_expected.to be true } + end + end + + context 'when the order is not assigned to a user' do + let(:user) { nil } + it { is_expected.to be false } + it "sets an error message" do + subject + expect(rule.eligibility_errors.full_messages.first). + to eq "You need to login before applying this coupon code." + end + end + end +end diff --git a/core/spec/models/spree/promotion/rules/product_spec.rb b/core/spec/models/spree/promotion/rules/product_spec.rb new file mode 100644 index 00000000000..e2b4e7324dc --- /dev/null +++ b/core/spec/models/spree/promotion/rules/product_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper' + +describe Spree::Promotion::Rules::Product, :type => :model do + let(:rule) { Spree::Promotion::Rules::Product.new(rule_options) } + let(:rule_options) { {} } + + context "#eligible?(order)" do + let(:order) { Spree::Order.new } + + it "should be eligible if there are no products" do + allow(rule).to receive_messages(:eligible_products => []) + expect(rule).to be_eligible(order) + end + + before do + 3.times { |i| instance_variable_set("@product#{i}", mock_model(Spree::Product)) } + end + + context "with 'any' match policy" do + let(:rule_options) { super().merge(preferred_match_policy: 'any') } + + it "should be eligible if any of the products is in eligible products" do + allow(order).to receive_messages(:products => [@product1, @product2]) + allow(rule).to receive_messages(:eligible_products => [@product2, @product3]) + expect(rule).to be_eligible(order) + end + + context "when none of the products are eligible products" do + before do + allow(order).to receive_messages(products: [@product1]) + allow(rule).to receive_messages(eligible_products: [@product2, @product3]) + end + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "You need to add an applicable product before applying this coupon code." + end + end + end + + context "with 'all' match policy" do + let(:rule_options) { super().merge(preferred_match_policy: 'all') } + + it "should be eligible if all of the eligible products are ordered" do + allow(order).to receive_messages(:products => [@product3, @product2, @product1]) + allow(rule).to receive_messages(:eligible_products => [@product2, @product3]) + expect(rule).to be_eligible(order) + end + + context "when any of the eligible products is not ordered" do + before do + allow(order).to receive_messages(products: [@product1, @product2]) + allow(rule).to receive_messages(eligible_products: [@product1, @product2, @product3]) + end + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "This coupon code can't be applied because you don't have all of the necessary products in your cart." + end + end + end + + context "with 'none' match policy" do + let(:rule_options) { super().merge(preferred_match_policy: 'none') } + + it "should be eligible if none of the order's products are in eligible products" do + allow(order).to receive_messages(:products => [@product1]) + allow(rule).to receive_messages(:eligible_products => [@product2, @product3]) + expect(rule).to be_eligible(order) + end + + context "when any of the order's products are in eligible products" do + before do + allow(order).to receive_messages(products: [@product1, @product2]) + allow(rule).to receive_messages(eligible_products: [@product2, @product3]) + end + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "Your cart contains a product that prevents this coupon code from being applied." + end + end + end + end + + describe '#actionable?' do + subject do + rule.actionable?(line_item) + end + + let(:rule_line_item) { Spree::LineItem.new(product: rule_product) } + let(:other_line_item) { Spree::LineItem.new(product: other_product) } + + let(:rule_options) { super().merge(products: [rule_product]) } + let(:rule_product) { mock_model(Spree::Product) } + let(:other_product) { mock_model(Spree::Product) } + + context "with 'any' match policy" do + let(:rule_options) { super().merge(preferred_match_policy: 'any') } + + context 'for product in rule' do + let(:line_item) { rule_line_item } + it { should be_truthy } + end + + context 'for product not in rule' do + let(:line_item) { other_line_item } + it { should be_falsey } + end + end + + context "with 'all' match policy" do + let(:rule_options) { super().merge(preferred_match_policy: 'all') } + + context 'for product in rule' do + let(:line_item) { rule_line_item } + it { should be_truthy } + end + + context 'for product not in rule' do + let(:line_item) { other_line_item } + it { should be_falsey } + end + end + + context "with 'none' match policy" do + let(:rule_options) { super().merge(preferred_match_policy: 'none') } + + context 'for product in rule' do + let(:line_item) { rule_line_item } + it { should be_falsey } + end + + context 'for product not in rule' do + let(:line_item) { other_line_item } + it { should be_truthy } + end + end + end +end diff --git a/core/spec/models/spree/promotion/rules/taxon_spec.rb b/core/spec/models/spree/promotion/rules/taxon_spec.rb new file mode 100644 index 00000000000..0fce153f12f --- /dev/null +++ b/core/spec/models/spree/promotion/rules/taxon_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe Spree::Promotion::Rules::Taxon, :type => :model do + let(:rule){ subject } + + context '#elegible?(order)' do + let(:taxon){ create :taxon, name: 'first' } + let(:taxon2){ create :taxon, name: 'second'} + let(:order){ create :order_with_line_items } + + before do + rule.save + end + + context 'with any match policy' do + before do + rule.preferred_match_policy = 'any' + end + + it 'is eligible if order does has any prefered taxon' do + order.products.first.taxons << taxon + rule.taxons << taxon + expect(rule).to be_eligible(order) + end + + context 'when order contains items from different taxons' do + before do + order.products.first.taxons << taxon + rule.taxons << taxon + end + + it 'should act on a product within the eligible taxon' do + expect(rule).to be_actionable(order.line_items.last) + end + + it 'should not act on a product in another taxon' do + order.line_items << create(:line_item, product: create(:product, taxons: [taxon2])) + expect(rule).not_to be_actionable(order.line_items.last) + end + end + + context "when order does not have any prefered taxon" do + before { rule.taxons << taxon2 } + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "You need to add a product from an applicable category before applying this coupon code." + end + end + + context 'when a product has a taxon child of a taxon rule' do + before do + taxon.children << taxon2 + order.products.first.taxons << taxon2 + rule.taxons << taxon2 + end + + it{ expect(rule).to be_eligible(order) } + end + end + + context 'with all match policy' do + before do + rule.preferred_match_policy = 'all' + end + + it 'is eligible order has all prefered taxons' do + order.products.first.taxons << taxon2 + order.products.last.taxons << taxon + + rule.taxons = [taxon, taxon2] + + expect(rule).to be_eligible(order) + end + + context "when order does not have all prefered taxons" do + before { rule.taxons << taxon } + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "You need to add a product from all applicable categories before applying this coupon code." + end + end + + context 'when a product has a taxon child of a taxon rule' do + let(:taxon3){ create :taxon } + + before do + taxon.children << taxon2 + order.products.first.taxons << taxon2 + order.products.last.taxons << taxon3 + rule.taxons << taxon2 + rule.taxons << taxon3 + end + + it{ expect(rule).to be_eligible(order) } + end + end + end +end diff --git a/core/spec/models/spree/promotion/rules/user_logged_in_spec.rb b/core/spec/models/spree/promotion/rules/user_logged_in_spec.rb new file mode 100644 index 00000000000..d903eb72784 --- /dev/null +++ b/core/spec/models/spree/promotion/rules/user_logged_in_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Spree::Promotion::Rules::UserLoggedIn, :type => :model do + let(:rule) { Spree::Promotion::Rules::UserLoggedIn.new } + + context "#eligible?(order)" do + let(:order) { Spree::Order.new } + + it "should be eligible if order has an associated user" do + user = double('User') + allow(order).to receive_messages(:user => user) + + expect(rule).to be_eligible(order) + end + + context "when user is not logged in" do + before { allow(order).to receive_messages(:user => nil) } # better to be explicit here + it { expect(rule).not_to be_eligible(order) } + it "sets an error message" do + rule.eligible?(order) + expect(rule.eligibility_errors.full_messages.first). + to eq "You need to login before applying this coupon code." + end + end + end +end + diff --git a/core/spec/models/spree/promotion/rules/user_spec.rb b/core/spec/models/spree/promotion/rules/user_spec.rb new file mode 100644 index 00000000000..8367dcada4d --- /dev/null +++ b/core/spec/models/spree/promotion/rules/user_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Spree::Promotion::Rules::User, :type => :model do + let(:rule) { Spree::Promotion::Rules::User.new } + + context "#eligible?(order)" do + let(:order) { Spree::Order.new } + + it "should not be eligible if users are not provided" do + expect(rule).not_to be_eligible(order) + end + + it "should be eligible if users include user placing the order" do + user = mock_model(Spree::LegacyUser) + users = [user, mock_model(Spree::LegacyUser)] + allow(rule).to receive_messages(:users => users) + allow(order).to receive_messages(:user => user) + + expect(rule).to be_eligible(order) + end + + it "should not be eligible if user placing the order is not listed" do + allow(order).to receive_messages(:user => mock_model(Spree::LegacyUser)) + users = [mock_model(Spree::LegacyUser), mock_model(Spree::LegacyUser)] + allow(rule).to receive_messages(:users => users) + + expect(rule).not_to be_eligible(order) + end + + # Regression test for #3885 + it "can assign to user_ids" do + user1 = Spree::LegacyUser.create!(:email => "test1@example.com") + user2 = Spree::LegacyUser.create!(:email => "test2@example.com") + expect { rule.user_ids = "#{user1.id}, #{user2.id}" }.not_to raise_error + end + end +end diff --git a/core/spec/models/spree/promotion_action_spec.rb b/core/spec/models/spree/promotion_action_spec.rb new file mode 100644 index 00000000000..09982dc89ea --- /dev/null +++ b/core/spec/models/spree/promotion_action_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe Spree::PromotionAction, :type => :model do + + it "should force developer to implement 'perform' method" do + expect { MyAction.new.perform }.to raise_error + end + +end + diff --git a/core/spec/models/spree/promotion_category_spec.rb b/core/spec/models/spree/promotion_category_spec.rb new file mode 100644 index 00000000000..e5efefbc762 --- /dev/null +++ b/core/spec/models/spree/promotion_category_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Spree::PromotionCategory, :type => :model do + describe 'validation' do + let(:name) { 'Nom' } + subject { Spree::PromotionCategory.new name: name } + + context 'when all required attributes are specified' do + it { is_expected.to be_valid } + end + + context 'when name is missing' do + let(:name) { nil } + it { is_expected.not_to be_valid } + end + end +end diff --git a/core/spec/models/spree/promotion_handler/cart_spec.rb b/core/spec/models/spree/promotion_handler/cart_spec.rb new file mode 100644 index 00000000000..57b0f850178 --- /dev/null +++ b/core/spec/models/spree/promotion_handler/cart_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +module Spree + module PromotionHandler + describe Cart, :type => :model do + let(:line_item) { create(:line_item) } + let(:order) { line_item.order } + + let(:promotion) { Promotion.create(name: "At line items") } + let(:calculator) { Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) } + + subject { Cart.new(order, line_item) } + + context "activates in LineItem level" do + let!(:action) { Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) } + let(:adjustable) { line_item } + + shared_context "creates the adjustment" do + it "creates the adjustment" do + expect { + subject.activate + }.to change { adjustable.adjustments.count }.by(1) + end + end + + context "promotion with no rules" do + include_context "creates the adjustment" + end + + context "promotion includes item involved" do + let!(:rule) { Promotion::Rules::Product.create(products: [line_item.product], promotion: promotion) } + + include_context "creates the adjustment" + end + + context "promotion has item total rule" do + let(:shirt) { create(:product) } + let!(:rule) { Promotion::Rules::ItemTotal.create(preferred_operator_min: 'gt', preferred_amount_min: 50, preferred_operator_max: 'lt', preferred_amount_max: 150, promotion: promotion) } + + before do + # Makes the order eligible for this promotion + order.item_total = 100 + order.save + end + + include_context "creates the adjustment" + end + end + + context "activates in Order level" do + let!(:action) { Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) } + let(:adjustable) { order } + + shared_context "creates the adjustment" do + it "creates the adjustment" do + expect { + subject.activate + }.to change { adjustable.adjustments.count }.by(1) + end + end + + context "promotion with no rules" do + before do + # Gives the calculator something to discount + order.item_total = 10 + order.save + end + + include_context "creates the adjustment" + end + + context "promotion has item total rule" do + let(:shirt) { create(:product) } + let!(:rule) { Promotion::Rules::ItemTotal.create(preferred_operator_min: 'gt', preferred_amount_min: 50, preferred_operator_max: 'lt', preferred_amount_max: 150, promotion: promotion) } + + before do + # Makes the order eligible for this promotion + order.item_total = 100 + order.save + end + + include_context "creates the adjustment" + end + end + + context "activates promotions associated with the order" do + let(:promo) { create :promotion_with_item_adjustment, adjustment_rate: 5, code: 'promo' } + let(:adjustable) { line_item } + + before do + order.promotions << promo + end + + it "creates the adjustment" do + expect { + subject.activate + }.to change { adjustable.adjustments.count }.by(1) + end + end + end + end +end diff --git a/core/spec/models/spree/promotion_handler/coupon_spec.rb b/core/spec/models/spree/promotion_handler/coupon_spec.rb new file mode 100644 index 00000000000..eb2e87d7bf4 --- /dev/null +++ b/core/spec/models/spree/promotion_handler/coupon_spec.rb @@ -0,0 +1,343 @@ +require 'spec_helper' + +module Spree + module PromotionHandler + describe Coupon, :type => :model do + let(:order) { double("Order", coupon_code: "10off").as_null_object } + + subject { Coupon.new(order) } + + it "returns self in apply" do + expect(subject.apply).to be_a Coupon + end + + context 'status messages' do + let(:coupon) { Coupon.new(order) } + + describe "#set_success_code" do + let(:status) { :coupon_code_applied } + subject { coupon.set_success_code status } + + it 'should have status_code' do + subject + expect(coupon.status_code).to eq(status) + end + + it 'should have success message' do + subject + expect(coupon.success).to eq(Spree.t(status)) + end + end + + describe "#set_error_code" do + let(:status) { :coupon_code_not_found } + + subject { coupon.set_error_code status } + + it 'should have status_code' do + subject + expect(coupon.status_code).to eq(status) + end + + it 'should have error message' do + subject + expect(coupon.error).to eq(Spree.t(status)) + end + end + end + + context "coupon code promotion doesnt exist" do + before { Promotion.create name: "promo", :code => nil } + + it "doesnt fetch any promotion" do + expect(subject.promotion).to be_blank + end + + context "with no actions defined" do + before { Promotion.create name: "promo", :code => "10off" } + + it "populates error message" do + subject.apply + expect(subject.error).to eq Spree.t(:coupon_code_not_found) + end + end + end + + context "existing coupon code promotion" do + let!(:promotion) { Promotion.create name: "promo", :code => "10off" } + let!(:action) { Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) } + let(:calculator) { Calculator::FlatRate.new(preferred_amount: 10) } + + it "fetches with given code" do + expect(subject.promotion).to eq promotion + end + + context "with a per-item adjustment action" do + let(:order) { create(:order_with_line_items, :line_items_count => 3) } + + context "right coupon given" do + context "with correct coupon code casing" do + before { allow(order).to receive_messages :coupon_code => "10off" } + + it "successfully activates promo" do + expect(order.total).to eq(130) + subject.apply + expect(subject.success).to be_present + order.line_items.each do |line_item| + expect(line_item.adjustments.count).to eq(1) + end + # Ensure that applying the adjustment actually affects the order's total! + expect(order.reload.total).to eq(100) + end + + context "and first line item is not promotionable" do + before(:each) do + order.line_items.first.variant.product.update_attributes!( + promotionable: false + ) + order.reload + end + + it "successfully activates promo" do + expect(order.total).to eq(130) + subject.apply + expect(subject.success).to be_present + order.line_items.each do |line_item| + expect(line_item.adjustments.count).to eq(1) + end + + expect(order.reload.total).to eq(110) # only 2 items + end + end + + it "coupon already applied to the order" do + subject.apply + expect(subject.success).to be_present + subject.apply + expect(subject.error).to eq Spree.t(:coupon_code_already_applied) + end + end + + # Regression test for #4211 + context "with incorrect coupon code casing" do + before { allow(order).to receive_messages :coupon_code => "10OFF" } + it "successfully activates promo" do + expect(order.total).to eq(130) + subject.apply + expect(subject.success).to be_present + order.line_items.each do |line_item| + expect(line_item.adjustments.count).to eq(1) + end + # Ensure that applying the adjustment actually affects the order's total! + expect(order.reload.total).to eq(100) + end + end + end + + context "coexists with a non coupon code promo" do + let!(:order) { Order.create } + + before do + allow(order).to receive_messages :coupon_code => "10off" + calculator = Calculator::FlatRate.new(preferred_amount: 10) + general_promo = Promotion.create name: "General Promo" + general_action = Promotion::Actions::CreateItemAdjustments.create(promotion: general_promo, calculator: calculator) + + order.contents.add create(:variant) + end + + # regression spec for #4515 + it "successfully activates promo" do + subject.apply + expect(subject).to be_successful + end + end + end + + context "with a free-shipping adjustment action" do + let!(:action) { Promotion::Actions::FreeShipping.create(promotion: promotion) } + context "right coupon code given" do + let(:order) { create(:order_with_line_items, :line_items_count => 3) } + + before { allow(order).to receive_messages :coupon_code => "10off" } + + it "successfully activates promo" do + expect(order.total).to eq(130) + subject.apply + expect(subject.success).to be_present + + expect(order.shipment_adjustments.count).to eq(1) + end + + it "coupon already applied to the order" do + subject.apply + expect(subject.success).to be_present + subject.apply + expect(subject.error).to eq Spree.t(:coupon_code_already_applied) + end + end + end + + context "with a whole-order adjustment action" do + let!(:action) { Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) } + context "right coupon given" do + let(:order) { create(:order) } + let(:calculator) { Calculator::FlatRate.new(preferred_amount: 10) } + + before do + allow(order).to receive_messages({ + :coupon_code => "10off", + # These need to be here so that promotion adjustment "wins" + :item_total => 50, + :ship_total => 10 + }) + end + + it "successfully activates promo" do + subject.apply + expect(subject.success).to be_present + expect(order.adjustments.count).to eq(1) + end + + it "coupon already applied to the order" do + subject.apply + expect(subject.success).to be_present + subject.apply + expect(subject.error).to eq Spree.t(:coupon_code_already_applied) + end + + it "coupon fails to activate" do + allow_any_instance_of(Spree::Promotion).to receive(:activate).and_return false + subject.apply + expect(subject.error).to eq Spree.t(:coupon_code_unknown_error) + end + + + it "coupon code hit max usage" do + promotion.update_column(:usage_limit, 1) + coupon = Coupon.new(order) + coupon.apply + expect(coupon.successful?).to be true + + order_2 = create(:order) + allow(order_2).to receive_messages :coupon_code => "10off" + coupon = Coupon.new(order_2) + coupon.apply + expect(coupon.successful?).to be false + expect(coupon.error).to eq Spree.t(:coupon_code_max_usage) + end + + context "when the a new coupon is less good" do + let!(:action_5) { Promotion::Actions::CreateAdjustment.create(promotion: promotion_5, calculator: calculator_5) } + let(:calculator_5) { Calculator::FlatRate.new(preferred_amount: 5) } + let!(:promotion_5) { Promotion.create name: "promo", :code => "5off" } + + it 'notifies of better deal' do + subject.apply + allow(order).to receive_messages( { coupon_code: '5off' } ) + coupon = Coupon.new(order).apply + expect(coupon.error).to eq Spree.t(:coupon_code_better_exists) + end + end + end + end + + context "for an order with taxable line items" do + before(:each) do + @country = create(:country) + @zone = create(:zone, :name => "Country Zone", :default_tax => true, :zone_members => []) + @zone.zone_members.create(:zoneable => @country) + @category = Spree::TaxCategory.create :name => "Taxable Foo" + @rate1 = Spree::TaxRate.create( + :amount => 0.10, + :calculator => Spree::Calculator::DefaultTax.create, + :tax_category => @category, + :zone => @zone + ) + + @order = Spree::Order.create! + allow(@order).to receive_messages :coupon_code => "10off" + end + context "and the product price is less than promo discount" do + before(:each) do + 3.times do |i| + taxable = create(:product, :tax_category => @category, :price => 9.0) + @order.contents.add(taxable.master, 1) + end + end + it "successfully applies the promo" do + # 3 * (9 + 0.9) + expect(@order.total).to eq(29.7) + coupon = Coupon.new(@order) + coupon.apply + expect(coupon.success).to be_present + # 3 * ((9 - [9,10].min) + 0) + expect(@order.reload.total).to eq(0) + expect(@order.additional_tax_total).to eq(0) + end + end + context "and the product price is greater than promo discount" do + before(:each) do + 3.times do |i| + taxable = create(:product, :tax_category => @category, :price => 11.0) + @order.contents.add(taxable.master, 2) + end + end + it "successfully applies the promo" do + # 3 * (22 + 2.2) + expect(@order.total.to_f).to eq(72.6) + coupon = Coupon.new(@order) + coupon.apply + expect(coupon.success).to be_present + # 3 * ( (22 - 10) + 1.2) + expect(@order.reload.total).to eq(39.6) + expect(@order.additional_tax_total).to eq(3.6) + end + end + context "and multiple quantity per line item" do + before(:each) do + twnty_off = Promotion.create name: "promo", :code => "20off" + twnty_off_calc = Calculator::FlatRate.new(preferred_amount: 20) + Promotion::Actions::CreateItemAdjustments.create(promotion: twnty_off, + calculator: twnty_off_calc) + + allow(@order).to receive(:coupon_code).and_call_original + allow(@order).to receive_messages :coupon_code => "20off" + 3.times do |i| + taxable = create(:product, :tax_category => @category, :price => 10.0) + @order.contents.add(taxable.master, 2) + end + end + it "successfully applies the promo" do + # 3 * ((2 * 10) + 2.0) + expect(@order.total.to_f).to eq(66) + coupon = Coupon.new(@order) + coupon.apply + expect(coupon.success).to be_present + # 0 + expect(@order.reload.total).to eq(0) + expect(@order.additional_tax_total).to eq(0) + end + end + end + + context "with a CreateLineItems action" do + let!(:variant) { create(:variant) } + let!(:action) { Promotion::Actions::CreateLineItems.create(promotion: promotion, promotion_action_line_items_attributes: { :'0' => { variant_id: variant.id }}) } + let(:order) { create(:order) } + + before do + allow(order).to receive_messages(coupon_code: "10off") + end + + it "successfully activates promo" do + subject.apply + expect(subject.success).to be_present + expect(order.line_items.pluck(:variant_id)).to include(variant.id) + end + end + + end + end + end +end diff --git a/core/spec/models/spree/promotion_handler/free_shipping_spec.rb b/core/spec/models/spree/promotion_handler/free_shipping_spec.rb new file mode 100644 index 00000000000..f3f6eb86e0f --- /dev/null +++ b/core/spec/models/spree/promotion_handler/free_shipping_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +module Spree + module PromotionHandler + describe FreeShipping, type: :model do + let(:order) { create(:order) } + let(:shipment) { create(:shipment, order: order ) } + + let(:promotion) { Promotion.create(name: "Free Shipping") } + let(:calculator) { Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) } + let!(:action) { Promotion::Actions::FreeShipping.create(promotion: promotion) } + + subject { Spree::PromotionHandler::FreeShipping.new(order) } + + context "activates in Shipment level" do + it "creates the adjustment" do + expect { subject.activate }.to change { shipment.adjustments.count }.by(1) + end + end + + context "if promo has a code" do + before do + promotion.update_column(:code, "code") + end + + it "does adjust the shipment when applied to order" do + order.promotions << promotion + + expect { subject.activate }.to change { shipment.adjustments.count } + end + + it "does not adjust the shipment when not applied to order" do + expect { subject.activate }.to_not change { shipment.adjustments.count } + end + end + + context "if promo has a path" do + before do + promotion.update_column(:path, "path") + end + + it "does not adjust the shipment" do + expect { subject.activate }.to_not change { shipment.adjustments.count } + end + end + end + end +end diff --git a/core/spec/models/spree/promotion_handler/page_spec.rb b/core/spec/models/spree/promotion_handler/page_spec.rb new file mode 100644 index 00000000000..0ddc40a0f25 --- /dev/null +++ b/core/spec/models/spree/promotion_handler/page_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +module Spree + module PromotionHandler + describe Page, :type => :model do + let(:order) { create(:order_with_line_items, :line_items_count => 1) } + + let(:promotion) { Promotion.create(name: "10% off", :path => '10off') } + before do + calculator = Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) + action = Promotion::Actions::CreateItemAdjustments.create(:calculator => calculator) + promotion.actions << action + end + + it "activates at the right path" do + expect(order.line_item_adjustments.count).to eq(0) + Spree::PromotionHandler::Page.new(order, '10off').activate + expect(order.line_item_adjustments.count).to eq(1) + end + + context "when promotion is expired" do + before do + promotion.update_columns( + :starts_at => 1.week.ago, + :expires_at => 1.day.ago + ) + end + + it "is not activated" do + expect(order.line_item_adjustments.count).to eq(0) + Spree::PromotionHandler::Page.new(order, '10off').activate + expect(order.line_item_adjustments.count).to eq(0) + end + end + + it "does not activate at the wrong path" do + expect(order.line_item_adjustments.count).to eq(0) + Spree::PromotionHandler::Page.new(order, 'wrongpath').activate + expect(order.line_item_adjustments.count).to eq(0) + end + end + end +end + diff --git a/core/spec/models/spree/promotion_rule_spec.rb b/core/spec/models/spree/promotion_rule_spec.rb new file mode 100644 index 00000000000..1fdd60d6942 --- /dev/null +++ b/core/spec/models/spree/promotion_rule_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +module Spree + describe Spree::PromotionRule, :type => :model do + + class BadTestRule < Spree::PromotionRule; end + + class TestRule < Spree::PromotionRule + def eligible? + true + end + end + + it "should force developer to implement eligible? method" do + expect { BadTestRule.new.eligible? }.to raise_error + end + + it "validates unique rules for a promotion" do + p1 = TestRule.new + p1.promotion_id = 1 + p1.save + + p2 = TestRule.new + p2.promotion_id = 1 + expect(p2).not_to be_valid + end + + end +end diff --git a/core/spec/models/spree/promotion_spec.rb b/core/spec/models/spree/promotion_spec.rb new file mode 100644 index 00000000000..d856c88bdcd --- /dev/null +++ b/core/spec/models/spree/promotion_spec.rb @@ -0,0 +1,603 @@ +require 'spec_helper' + +describe Spree::Promotion, :type => :model do + let(:promotion) { Spree::Promotion.new } + + describe "validations" do + before :each do + @valid_promotion = Spree::Promotion.new :name => "A promotion" + end + + it "valid_promotion is valid" do + expect(@valid_promotion).to be_valid + end + + it "validates usage limit" do + @valid_promotion.usage_limit = -1 + expect(@valid_promotion).not_to be_valid + + @valid_promotion.usage_limit = 100 + expect(@valid_promotion).to be_valid + end + + it "validates name" do + @valid_promotion.name = nil + expect(@valid_promotion).not_to be_valid + end + end + + describe ".coupons" do + it "scopes promotions with coupon code present only" do + promotion = Spree::Promotion.create! name: "test", code: '' + expect(Spree::Promotion.coupons).to be_empty + + promotion.update_column :code, "check" + expect(Spree::Promotion.coupons.first).to eq promotion + end + end + + describe ".applied" do + it "scopes promotions that have been applied to an order only" do + promotion = Spree::Promotion.create! name: "test", code: '' + expect(Spree::Promotion.applied).to be_empty + + promotion.orders << create(:order) + expect(Spree::Promotion.applied.first).to eq promotion + end + end + + describe ".advertised" do + let(:promotion) { create(:promotion) } + let(:advertised_promotion) { create(:promotion, :advertise => true) } + + it "only shows advertised promotions" do + advertised = Spree::Promotion.advertised + expect(advertised).to include(advertised_promotion) + expect(advertised).not_to include(promotion) + end + end + + describe "#destroy" do + let(:promotion) { Spree::Promotion.create(:name => "delete me") } + + before(:each) do + promotion.actions << Spree::Promotion::Actions::CreateAdjustment.new + promotion.rules << Spree::Promotion::Rules::FirstOrder.new + promotion.save! + promotion.destroy + end + + it "should delete actions" do + expect(Spree::PromotionAction.count).to eq(0) + end + + it "should delete rules" do + expect(Spree::PromotionRule.count).to eq(0) + end + end + + describe "#save" do + let(:promotion) { Spree::Promotion.create(:name => "delete me") } + + before(:each) do + promotion.actions << Spree::Promotion::Actions::CreateAdjustment.new + promotion.rules << Spree::Promotion::Rules::FirstOrder.new + promotion.save! + end + + it "should deeply autosave records and preferences" do + promotion.actions[0].calculator.preferred_flat_percent = 10 + promotion.save! + expect(Spree::Calculator.first.preferred_flat_percent).to eq(10) + end + end + + describe "#activate" do + before do + @action1 = Spree::Promotion::Actions::CreateAdjustment.create! + @action2 = Spree::Promotion::Actions::CreateAdjustment.create! + allow(@action1).to receive_messages perform: true + allow(@action2).to receive_messages perform: true + + promotion.promotion_actions = [@action1, @action2] + promotion.created_at = 2.days.ago + + @user = stub_model(Spree::LegacyUser, :email => "spree@example.com") + @order = Spree::Order.create user: @user + @payload = { :order => @order, :user => @user } + end + + it "should check path if present" do + promotion.path = 'content/cvv' + @payload[:path] = 'content/cvv' + expect(@action1).to receive(:perform).with(@payload) + expect(@action2).to receive(:perform).with(@payload) + promotion.activate(@payload) + end + + it "does not perform actions against an order in a finalized state" do + expect(@action1).not_to receive(:perform).with(@payload) + + @order.state = 'complete' + promotion.activate(@payload) + + @order.state = 'awaiting_return' + promotion.activate(@payload) + + @order.state = 'returned' + promotion.activate(@payload) + end + + it "does activate if newer then order" do + expect(@action1).to receive(:perform).with(@payload) + promotion.created_at = DateTime.now + 2 + expect(promotion.activate(@payload)).to be true + end + + context "keeps track of the orders" do + context "when activated" do + it "assigns the order" do + expect(promotion.orders).to be_empty + expect(promotion.activate(@payload)).to be true + expect(promotion.orders.first).to eql @order + end + end + context "when not activated" do + it "will not assign the order" do + @order.state = 'complete' + expect(promotion.orders).to be_empty + expect(promotion.activate(@payload)).to be_falsey + expect(promotion.orders).to be_empty + end + end + + end + + end + + context "#usage_limit_exceeded" do + let(:promotable) { double('Promotable') } + it "should not have its usage limit exceeded with no usage limit" do + promotion.usage_limit = 0 + expect(promotion.usage_limit_exceeded?(promotable)).to be false + end + + it "should have its usage limit exceeded" do + promotion.usage_limit = 2 + allow(promotion).to receive_messages(:adjusted_credits_count => 2) + expect(promotion.usage_limit_exceeded?(promotable)).to be true + + allow(promotion).to receive_messages(:adjusted_credits_count => 3) + expect(promotion.usage_limit_exceeded?(promotable)).to be true + end + end + + context "#expired" do + it "should not be exipired" do + expect(promotion).not_to be_expired + end + + it "should be expired if it hasn't started yet" do + promotion.starts_at = Time.now + 1.day + expect(promotion).to be_expired + end + + it "should be expired if it has already ended" do + promotion.expires_at = Time.now - 1.day + expect(promotion).to be_expired + end + + it "should not be expired if it has started already" do + promotion.starts_at = Time.now - 1.day + expect(promotion).not_to be_expired + end + + it "should not be expired if it has not ended yet" do + promotion.expires_at = Time.now + 1.day + expect(promotion).not_to be_expired + end + + it "should not be expired if current time is within starts_at and expires_at range" do + promotion.starts_at = Time.now - 1.day + promotion.expires_at = Time.now + 1.day + expect(promotion).not_to be_expired + end + + it "should not be expired if usage limit is not exceeded" do + promotion.usage_limit = 2 + allow(promotion).to receive_messages(:credits_count => 1) + expect(promotion).not_to be_expired + end + end + + context "#credits_count" do + let!(:promotion) do + promotion = Spree::Promotion.new + promotion.name = "Foo" + promotion.code = "XXX" + calculator = Spree::Calculator::FlatRate.new + promotion.tap(&:save) + end + + let!(:action) do + calculator = Spree::Calculator::FlatRate.new + action_params = { :promotion => promotion, :calculator => calculator } + action = Spree::Promotion::Actions::CreateAdjustment.create(action_params) + promotion.actions << action + action + end + + let!(:adjustment) do + order = create(:order) + Spree::Adjustment.create!( + order: order, + adjustable: order, + source: action, + amount: 10, + label: 'Promotional adjustment' + ) + end + + it "counts eligible adjustments" do + adjustment.update_column(:eligible, true) + expect(promotion.credits_count).to eq(1) + end + + # Regression test for #4112 + it "does not count ineligible adjustments" do + adjustment.update_column(:eligible, false) + expect(promotion.credits_count).to eq(0) + end + end + + context "#adjusted_credits_count" do + let(:order) { create :order } + let(:line_item) { create :line_item, order: order } + let(:promotion) { Spree::Promotion.create name: "promo", :code => "10off" } + let(:order_action) { + action = Spree::Promotion::Actions::CreateAdjustment.create(calculator: Spree::Calculator::FlatPercentItemTotal.new) + promotion.actions << action + action + } + let(:item_action) { + action = Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: Spree::Calculator::FlatPercentItemTotal.new) + promotion.actions << action + action + } + let(:order_adjustment) do + Spree::Adjustment.create!( + :source => order_action, + :amount => 10, + :adjustable => order, + :order => order, + :label => "Promotional adjustment" + ) + end + let(:item_adjustment) do + Spree::Adjustment.create!( + :source => item_action, + :amount => 10, + :adjustable => line_item, + :order => order, + :label => "Promotional adjustment" + ) + end + + it "counts order level adjustments" do + expect(order_adjustment.adjustable).to eq(order) + expect(promotion.credits_count).to eq(1) + expect(promotion.adjusted_credits_count(order)).to eq(0) + end + + it "counts item level adjustments" do + expect(item_adjustment.adjustable).to eq(line_item) + expect(promotion.credits_count).to eq(1) + expect(promotion.adjusted_credits_count(order)).to eq(0) + end + end + + context "#products" do + let(:promotion) { create(:promotion) } + + context "when it has product rules with products associated" do + let(:promotion_rule) { Spree::Promotion::Rules::Product.new } + + before do + promotion_rule.promotion = promotion + promotion_rule.products << create(:product) + promotion_rule.save + end + + it "should have products" do + expect(promotion.reload.products.size).to eq(1) + end + end + + context "when there's no product rule associated" do + it "should not have products but still return an empty array" do + expect(promotion.products).to be_blank + end + end + end + + context "#eligible?" do + let(:promotable) { create :order } + subject { promotion.eligible?(promotable) } + context "when promotion is expired" do + before { promotion.expires_at = Time.now - 10.days } + it { is_expected.to be false } + end + context "when promotable is a Spree::LineItem" do + let(:promotable) { create :line_item } + let(:product) { promotable.product } + before do + product.promotionable = promotionable + end + context "and product is promotionable" do + let(:promotionable) { true } + it { is_expected.to be true } + end + context "and product is not promotionable" do + let(:promotionable) { false } + it { is_expected.to be false } + end + end + context "when promotable is a Spree::Order" do + let(:promotable) { create :order } + context "and it is empty" do + it { is_expected.to be true } + end + context "and it contains items" do + let!(:line_item) { create(:line_item, order: promotable) } + context "and the items are all non-promotionable" do + before do + line_item.product.update_column(:promotionable, false) + end + it { is_expected.to be false } + end + context "and at least one item is promotionable" do + it { is_expected.to be true } + end + end + end + end + + context "#eligible_rules" do + let(:promotable) { double('Promotable') } + it "true if there are no rules" do + expect(promotion.eligible_rules(promotable)).to eq [] + end + + it "true if there are no applicable rules" do + promotion.promotion_rules = [stub_model(Spree::PromotionRule, :eligible? => true, :applicable? => false)] + allow(promotion.promotion_rules).to receive(:for).and_return([]) + expect(promotion.eligible_rules(promotable)).to eq [] + end + + context "with 'all' match policy" do + let(:promo1) { Spree::PromotionRule.create! } + let(:promo2) { Spree::PromotionRule.create! } + + before { promotion.match_policy = 'all' } + + context "when all rules are eligible" do + before do + allow(promo1).to receive_messages(eligible?: true, applicable?: true) + allow(promo2).to receive_messages(eligible?: true, applicable?: true) + + promotion.promotion_rules = [promo1, promo2] + allow(promotion.promotion_rules).to receive(:for).and_return(promotion.promotion_rules) + end + it "returns the eligible rules" do + expect(promotion.eligible_rules(promotable)).to eq [promo1, promo2] + end + it "does set anything to eligiblity errors" do + promotion.eligible_rules(promotable) + expect(promotion.eligibility_errors).to be_nil + end + end + + context "when any of the rules is not eligible" do + let(:errors) { double ActiveModel::Errors, empty?: false } + before do + allow(promo1).to receive_messages(eligible?: true, applicable?: true, eligibility_errors: nil) + allow(promo2).to receive_messages(eligible?: false, applicable?: true, eligibility_errors: errors) + + promotion.promotion_rules = [promo1, promo2] + allow(promotion.promotion_rules).to receive(:for).and_return(promotion.promotion_rules) + end + it "returns nil" do + expect(promotion.eligible_rules(promotable)).to be_nil + end + it "sets eligibility errors to the first non-nil one" do + promotion.eligible_rules(promotable) + expect(promotion.eligibility_errors).to eq errors + end + end + end + + context "with 'any' match policy" do + let(:promotion) { Spree::Promotion.create(:name => "Promo", :match_policy => 'any') } + let(:promotable) { double('Promotable') } + + it "should have eligible rules if any of the rules are eligible" do + allow_any_instance_of(Spree::PromotionRule).to receive_messages(:applicable? => true) + true_rule = Spree::PromotionRule.create(:promotion => promotion) + allow(true_rule).to receive_messages(:eligible? => true) + allow(promotion).to receive_messages(:rules => [true_rule]) + allow(promotion).to receive_message_chain(:rules, :for).and_return([true_rule]) + expect(promotion.eligible_rules(promotable)).to eq [true_rule] + end + + context "when none of the rules are eligible" do + let(:promo) { Spree::PromotionRule.create! } + let(:errors) { double ActiveModel::Errors, empty?: false } + before do + allow(promo).to receive_messages(eligible?: false, applicable?: true, eligibility_errors: errors) + + promotion.promotion_rules = [promo] + allow(promotion.promotion_rules).to receive(:for).and_return(promotion.promotion_rules) + end + it "returns nil" do + expect(promotion.eligible_rules(promotable)).to be_nil + end + it "sets eligibility errors to the first non-nil one" do + promotion.eligible_rules(promotable) + expect(promotion.eligibility_errors).to eq errors + end + end + end + end + + describe '#line_item_actionable?' do + let(:order) { double Spree::Order } + let(:line_item) { double Spree::LineItem} + let(:true_rule) { double Spree::PromotionRule, eligible?: true, applicable?: true, actionable?: true } + let(:false_rule) { double Spree::PromotionRule, eligible?: true, applicable?: true, actionable?: false } + let(:rules) { [] } + + before do + allow(promotion).to receive(:rules) { rules } + allow(rules).to receive(:for) { rules } + end + + subject { promotion.line_item_actionable? order, line_item } + + context 'when the order is eligible for promotion' do + context 'when there are no rules' do + it { is_expected.to be } + end + + context 'when there are rules' do + context 'when the match policy is all' do + before { promotion.match_policy = 'all' } + + context 'when all rules allow action on the line item' do + let(:rules) { [true_rule] } + it { is_expected.to be} + end + + context 'when at least one rule does not allow action on the line item' do + let(:rules) { [true_rule, false_rule] } + it { is_expected.not_to be} + end + end + + context 'when the match policy is any' do + before { promotion.match_policy = 'any' } + + context 'when at least one rule allows action on the line item' do + let(:rules) { [true_rule, false_rule] } + it { is_expected.to be } + end + + context 'when no rules allow action on the line item' do + let(:rules) { [false_rule] } + it { is_expected.not_to be} + end + end + end + end + + context 'when the order is not eligible for the promotion' do + before { promotion.starts_at = Time.current + 2.days } + it { is_expected.not_to be } + end + end + + # regression for #4059 + # admin form posts the code and path as empty string + describe "normalize blank values for code & path" do + it "will save blank value as nil value instead" do + promotion = Spree::Promotion.create(:name => "A promotion", :code => "", :path => "") + expect(promotion.code).to be_nil + expect(promotion.path).to be_nil + end + end + + # Regression test for #4081 + describe "#with_coupon_code" do + context "and code stored in uppercase" do + let!(:promotion) { create(:promotion, :code => "MY-COUPON-123") } + it "finds the code with lowercase" do + expect(Spree::Promotion.with_coupon_code("my-coupon-123")).to eql promotion + end + end + end + + describe '#used_by?' do + subject { promotion.used_by? user, [excluded_order] } + + let(:promotion) { create :promotion, :with_order_adjustment } + let(:user) { create :user } + let(:order) { create :order_with_line_items, user: user } + let(:excluded_order) { create :order_with_line_items, user: user } + + before do + order.user_id = user.id + order.save! + end + + context 'when the user has used this promo' do + before do + promotion.activate(order: order) + order.update! + order.completed_at = Time.now + order.save! + end + + context 'when the order is complete' do + it { is_expected.to be true } + + context 'when the promotion was not eligible' do + let(:adjustment) { order.adjustments.first } + + before do + adjustment.eligible = false + adjustment.save! + end + + it { is_expected.to be false } + end + + context 'when the only matching order is the excluded order' do + let(:excluded_order) { order } + it { is_expected.to be false } + end + end + + context 'when the order is not complete' do + let(:order) { create :order, user: user } + it { is_expected.to be false } + end + end + + context 'when the user has not used this promo' do + it { is_expected.to be false } + end + end + + describe "adding items to the cart" do + let(:order) { create :order } + let(:line_item) { create :line_item, order: order } + let(:promo) { create :promotion_with_item_adjustment, adjustment_rate: 5, code: 'promo' } + let(:variant) { create :variant } + + it "updates the promotions for new line items" do + expect(line_item.adjustments).to be_empty + expect(order.adjustment_total).to eq 0 + + promo.activate order: order + order.update! + + expect(line_item.adjustments.size).to eq(1) + expect(order.adjustment_total).to eq -5 + + other_line_item = order.contents.add(variant, 1, currency: order.currency) + + expect(other_line_item).not_to eq line_item + expect(other_line_item.adjustments.size).to eq(1) + expect(order.adjustment_total).to eq -10 + end + end +end diff --git a/core/spec/models/spree/property_spec.rb b/core/spec/models/spree/property_spec.rb new file mode 100644 index 00000000000..999afaea9d2 --- /dev/null +++ b/core/spec/models/spree/property_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Spree::Property, :type => :model do + +end diff --git a/core/spec/models/spree/prototype_spec.rb b/core/spec/models/spree/prototype_spec.rb new file mode 100644 index 00000000000..16fff45391d --- /dev/null +++ b/core/spec/models/spree/prototype_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Spree::Prototype, :type => :model do + +end diff --git a/core/spec/models/spree/refund_spec.rb b/core/spec/models/spree/refund_spec.rb new file mode 100644 index 00000000000..30a118ac504 --- /dev/null +++ b/core/spec/models/spree/refund_spec.rb @@ -0,0 +1,204 @@ +require 'spec_helper' + +describe Spree::Refund, :type => :model do + + describe 'create' do + let(:amount) { 100.0 } + let(:amount_in_cents) { amount * 100 } + + let(:authorization) { generate(:refund_transaction_id) } + + let(:payment) { create(:payment, amount: payment_amount, payment_method: payment_method) } + let(:payment_amount) { amount*2 } + let(:payment_method) { create(:credit_card_payment_method, environment: payment_method_environment) } + let(:payment_method_environment) { 'test' } + + let(:refund_reason) { create(:refund_reason) } + + let(:gateway_response) { + ActiveMerchant::Billing::Response.new( + gateway_response_success, + gateway_response_message, + gateway_response_params, + gateway_response_options + ) + } + let(:gateway_response_success) { true } + let(:gateway_response_message) { "" } + let(:gateway_response_params) { {} } + let(:gateway_response_options) { {} } + + subject { create(:refund, payment: payment, amount: amount, reason: refund_reason, transaction_id: nil) } + + before do + allow(payment.payment_method) + .to receive(:credit) + .with(amount_in_cents, payment.source, payment.transaction_id, {originator: an_instance_of(Spree::Refund)}) + .and_return(gateway_response) + end + + context "transaction id exists on creation" do + let(:transaction_id) { "12kfjas0" } + subject { create(:refund, payment: payment, amount: amount, reason: refund_reason, transaction_id: transaction_id) } + + it "creates a refund record" do + expect{ subject }.to change { Spree::Refund.count }.by(1) + end + + it "maintains the transaction id" do + expect(subject.reload.transaction_id).to eq transaction_id + end + + it "saves the amount" do + expect(subject.reload.amount).to eq amount + end + + it "creates a log entry" do + expect(subject.log_entries).to be_present + end + + it "does not attempt to process a transaction" do + expect(payment.payment_method).not_to receive(:credit) + subject + end + + end + + context "processing is successful" do + let(:gateway_response_options) { { authorization: authorization } } + + it 'should create a refund' do + expect{ subject }.to change{ Spree::Refund.count }.by(1) + end + + it 'return the newly created refund' do + expect(subject).to be_a(Spree::Refund) + end + + it 'should save the returned authorization value' do + expect(subject.reload.transaction_id).to eq authorization + end + + it 'should save the passed amount as the refund amount' do + expect(subject.amount).to eq amount + end + + it 'should create a log entry' do + expect(subject.log_entries).to be_present + end + + it "attempts to process a transaction" do + expect(payment.payment_method).to receive(:credit).once + subject + end + + it 'should update the payment total' do + expect(payment.order.updater).to receive(:update) + subject + end + + end + + context "processing fails" do + let(:gateway_response_success) { false } + let(:gateway_response_message) { "failure message" } + + it 'should raise error and not create a refund' do + expect do + expect { subject }.to raise_error(Spree::Core::GatewayError, gateway_response_message) + end.to_not change{ Spree::Refund.count } + end + end + + context 'without payment profiles supported' do + before do + allow(payment.payment_method).to receive(:payment_profiles_supported?) { false } + end + + it 'should not supply the payment source' do + expect(payment.payment_method) + .to receive(:credit) + .with(amount * 100, payment.transaction_id, {originator: an_instance_of(Spree::Refund)}) + .and_return(gateway_response) + + subject + end + end + + context 'with payment profiles supported' do + before do + allow(payment.payment_method).to receive(:payment_profiles_supported?) { true } + end + + it 'should supply the payment source' do + expect(payment.payment_method) + .to receive(:credit) + .with(amount_in_cents, payment.source, payment.transaction_id, {originator: an_instance_of(Spree::Refund)}) + .and_return(gateway_response) + + subject + end + end + + context 'with an activemerchant gateway connection error' do + before do + expect(payment.payment_method) + .to receive(:credit) + .with(amount_in_cents, payment.source, payment.transaction_id, {originator: an_instance_of(Spree::Refund)}) + .and_raise(ActiveMerchant::ConnectionError) + end + + it 'raises Spree::Core::GatewayError' do + expect { subject }.to raise_error(Spree::Core::GatewayError, Spree.t(:unable_to_connect_to_gateway)) + end + end + + context 'with the incorrect payment method environment' do + let(:payment_method_environment) { 'development' } + + it 'raises a Spree::Core::GatewayError' do + expect { subject }.to raise_error { |error| + expect(error).to be_a(ActiveRecord::RecordInvalid) + expect(error.record.errors.full_messages).to eq [Spree.t(:gateway_config_unavailable) + " - test"] + } + end + end + + context 'with amount too large' do + let(:payment_amount) { 10 } + let(:amount) { payment_amount*2 } + + it 'is invalid' do + expect { subject }.to raise_error { |error| + expect(error).to be_a(ActiveRecord::RecordInvalid) + expect(error.record.errors.full_messages).to eq ["Amount #{I18n.t('activerecord.errors.models.spree/refund.attributes.amount.greater_than_allowed')}"] + } + end + end + end + + describe 'total_amount_reimbursed_for' do + let(:customer_return) { reimbursement.customer_return} + let(:reimbursement) { create(:reimbursement) } + let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) } + + subject { Spree::Refund.total_amount_reimbursed_for(reimbursement) } + + context 'with reimbursements performed' do + before { reimbursement.perform! } + + it 'returns the total amount' do + amount = Spree::Refund.total_amount_reimbursed_for(reimbursement) + expect(amount).to be > 0 + expect(amount).to eq reimbursement.total + end + end + + context 'without reimbursements performed' do + it 'returns zero' do + amount = Spree::Refund.total_amount_reimbursed_for(reimbursement) + expect(amount).to eq 0 + end + end + end +end diff --git a/core/spec/models/spree/reimbursement/credit_spec.rb b/core/spec/models/spree/reimbursement/credit_spec.rb new file mode 100644 index 00000000000..34873999f3b --- /dev/null +++ b/core/spec/models/spree/reimbursement/credit_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +module Spree + describe Reimbursement::Credit, :type => :model do + context 'class methods' do + describe '.total_amount_reimbursed_for' do + subject { Spree::Reimbursement::Credit.total_amount_reimbursed_for(reimbursement) } + + let(:reimbursement) { create(:reimbursement) } + let(:credit_double) { double(amount: 99.99) } + + before { allow(reimbursement).to receive(:credits).and_return([credit_double, credit_double])} + + it 'should sum the amounts of all of the reimbursements credits' do + expect(subject).to eq BigDecimal.new('199.98') + end + end + end + + describe '#description' do + let(:credit) { Spree::Reimbursement::Credit.new(amount: 100, creditable: mock_model(Spree::PaymentMethod::Check)) } + + it "should be the creditable's class name" do + expect(credit.description).to eq 'Check' + end + end + + describe '#display_amount' do + let(:credit) { Spree::Reimbursement::Credit.new(amount: 100) } + + it 'should be a money object' do + expect(credit.display_amount).to eq Spree::Money.new(100, currency: "USD") + end + end + end +end diff --git a/core/spec/models/spree/reimbursement/reimbursement_type_engine_spec.rb b/core/spec/models/spree/reimbursement/reimbursement_type_engine_spec.rb new file mode 100644 index 00000000000..5844c83b971 --- /dev/null +++ b/core/spec/models/spree/reimbursement/reimbursement_type_engine_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +module Spree + describe Reimbursement::ReimbursementTypeEngine, :type => :model do + describe '#calculate_reimbursement_types' do + let(:return_item) { create(:return_item) } + let(:return_items) { [ return_item ] } + let(:reimbursement_type_engine) { Spree::Reimbursement::ReimbursementTypeEngine.new(return_items) } + let(:expired_reimbursement_type) { Spree::ReimbursementType::OriginalPayment } + let(:override_reimbursement_type) { Spree::ReimbursementType::OriginalPayment.new } + let(:preferred_reimbursement_type) { Spree::ReimbursementType::OriginalPayment.new } + let(:calculated_reimbursement_types) { subject } + let(:all_reimbursement_types) {[ + reimbursement_type_engine.default_reimbursement_type, + reimbursement_type_engine.exchange_reimbursement_type, + expired_reimbursement_type, + override_reimbursement_type, + preferred_reimbursement_type + ]} + + subject { reimbursement_type_engine.calculate_reimbursement_types } + + shared_examples_for "reimbursement type hash" do + it "contain all keys that respond to reimburse" do + calculated_reimbursement_types.keys.each do |r_type| + expect(r_type).to respond_to :reimburse + end + end + end + + before do + reimbursement_type_engine.expired_reimbursement_type = expired_reimbursement_type + allow(return_item.inventory_unit.shipment).to receive(:shipped_at).and_return(Date.yesterday) + allow(return_item).to receive(:exchange_required?).and_return(false) + end + + context 'the return item requires exchange' do + before { allow(return_item).to receive(:exchange_required?).and_return(true) } + + it 'returns a hash with the exchange reimbursement type associated to the return items' do + expect(calculated_reimbursement_types[reimbursement_type_engine.exchange_reimbursement_type]).to eq(return_items) + end + + it 'the return items are not included in any of the other reimbursement types' do + (all_reimbursement_types - [reimbursement_type_engine.exchange_reimbursement_type]).each do |r_type| + expect(calculated_reimbursement_types[r_type]).to eq([]) + end + end + + it_should_behave_like 'reimbursement type hash' + end + + context 'the return item does not require exchange' do + context 'the return item has an override reimbursement type' do + before { allow(return_item).to receive(:override_reimbursement_type).and_return(override_reimbursement_type) } + + it 'returns a hash with the override reimbursement type associated to the return items' do + expect(calculated_reimbursement_types[override_reimbursement_type.class]).to eq(return_items) + end + + it 'the return items are not included in any of the other reimbursement types' do + (all_reimbursement_types - [override_reimbursement_type.class]).each do |r_type| + expect(calculated_reimbursement_types[r_type]).to eq([]) + end + end + + it_should_behave_like 'reimbursement type hash' + end + + context 'the return item does not have an override reimbursement type' do + context 'the return item has a preferred reimbursement type' do + before { allow(return_item).to receive(:preferred_reimbursement_type).and_return(preferred_reimbursement_type) } + + context 'the reimbursement type is not valid for the return item' do + before { expect(reimbursement_type_engine).to receive(:valid_preferred_reimbursement_type?).and_return(false) } + + it 'returns a hash with no return items associated to the preferred reimbursement type' do + expect(calculated_reimbursement_types[preferred_reimbursement_type]).to eq([]) + end + + it 'the return items are not included in any of the other reimbursement types' do + (all_reimbursement_types - [preferred_reimbursement_type]).each do |r_type| + expect(calculated_reimbursement_types[r_type]).to eq([]) + end + end + + it_should_behave_like 'reimbursement type hash' + end + + context 'the reimbursement type is valid for the return item' do + it 'returns a hash with the expired reimbursement type associated to the return items' do + expect(calculated_reimbursement_types[preferred_reimbursement_type.class]).to eq(return_items) + end + + it 'the return items are not included in any of the other reimbursement types' do + (all_reimbursement_types - [preferred_reimbursement_type.class]).each do |r_type| + expect(calculated_reimbursement_types[r_type]).to eq([]) + end + end + + it_should_behave_like 'reimbursement type hash' + end + end + + context 'the return item does not have a preferred reimbursement type' do + context 'the return item is past the time constraint' do + before { allow(reimbursement_type_engine).to receive(:past_reimbursable_time_period?).and_return(true) } + + it 'returns a hash with the expired reimbursement type associated to the return items' do + expect(calculated_reimbursement_types[expired_reimbursement_type]).to eq(return_items) + end + + it 'the return items are not included in any of the other reimbursement types' do + (all_reimbursement_types - [expired_reimbursement_type]).each do |r_type| + expect(calculated_reimbursement_types[r_type]).to eq([]) + end + end + + it_should_behave_like 'reimbursement type hash' + end + + context 'the return item is within the time constraint' do + it 'returns a hash with the default reimbursement type associated to the return items' do + expect(calculated_reimbursement_types[reimbursement_type_engine.default_reimbursement_type]).to eq(return_items) + end + + it 'the return items are not included in any of the other reimbursement types' do + (all_reimbursement_types - [reimbursement_type_engine.default_reimbursement_type]).each do |r_type| + expect(calculated_reimbursement_types[r_type]).to eq([]) + end + end + + it_should_behave_like 'reimbursement type hash' + end + end + end + end + end + end +end diff --git a/core/spec/models/spree/reimbursement/reimbursement_type_validator_spec.rb b/core/spec/models/spree/reimbursement/reimbursement_type_validator_spec.rb new file mode 100644 index 00000000000..b86b1029432 --- /dev/null +++ b/core/spec/models/spree/reimbursement/reimbursement_type_validator_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +module Spree + describe Reimbursement::ReimbursementTypeValidator, :type => :model do + class DummyClass + include Spree::Reimbursement::ReimbursementTypeValidator + + class_attribute :expired_reimbursement_type + self.expired_reimbursement_type = Spree::ReimbursementType::Credit + + class_attribute :refund_time_constraint + self.refund_time_constraint = 90.days + end + + let(:return_item) do + create( + :return_item, + preferred_reimbursement_type: preferred_reimbursement_type + ) + end + let(:dummy) { DummyClass.new } + let(:preferred_reimbursement_type) { Spree::ReimbursementType::Credit.new } + + describe '#valid_preferred_reimbursement_type?' do + before do + allow(dummy).to receive(:past_reimbursable_time_period?).and_return(true) + end + + subject { dummy.valid_preferred_reimbursement_type?(return_item) } + + context 'is valid' do + it 'if it is not past the reimbursable time period' do + allow(dummy).to receive(:past_reimbursable_time_period?).and_return(false) + expect(subject).to be true + end + + it 'if the return items preferred method of reimbursement is the expired method of reimbursement' do + expect(subject).to be true + end + end + + context 'is invalid' do + it 'if the return item is past the eligible time period and the preferred method of reimbursement is not the expired method of reimbursement' do + return_item.preferred_reimbursement_type = + Spree::ReimbursementType::OriginalPayment.new + expect(subject).to be false + end + end + end + + describe '#past_reimbursable_time_period?' do + subject { dummy.past_reimbursable_time_period?(return_item) } + + before do + allow(return_item).to receive_message_chain(:inventory_unit, :shipment, :shipped_at).and_return(shipped_at) + end + + context 'it has not shipped' do + let(:shipped_at) { nil } + + it 'is not past the reimbursable time period' do + expect(subject).to be_falsey + end + end + + context 'it has shipped and it is more recent than the time constraint' do + let(:shipped_at) { (dummy.refund_time_constraint - 1.day).ago } + + it 'is not past the reimbursable time period' do + expect(subject).to be false + end + end + + context 'it has shipped and it is further in the past than the time constraint' do + let(:shipped_at) { (dummy.refund_time_constraint + 1.day).ago } + + it 'is past the reimbursable time period' do + expect(subject).to be true + end + end + end + end +end diff --git a/core/spec/models/spree/reimbursement_performer_spec.rb b/core/spec/models/spree/reimbursement_performer_spec.rb new file mode 100644 index 00000000000..c237a3e68a0 --- /dev/null +++ b/core/spec/models/spree/reimbursement_performer_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Spree::ReimbursementPerformer, :type => :model do + let(:reimbursement) { create(:reimbursement, return_items_count: 1) } + let(:return_item) { reimbursement.return_items.first } + let(:reimbursement_type) { double("ReimbursementType") } + let(:reimbursement_type_hash) { { reimbursement_type => [return_item]} } + + before do + expect(Spree::ReimbursementPerformer).to receive(:calculate_reimbursement_types).and_return(reimbursement_type_hash) + end + + describe ".simulate" do + subject { Spree::ReimbursementPerformer.simulate(reimbursement) } + + it "reimburses each calculated reimbursement types with the correct return items as a simulation" do + expect(reimbursement_type).to receive(:reimburse).with(reimbursement, [return_item], true) + subject + end + end + + describe '.perform' do + subject { Spree::ReimbursementPerformer.perform(reimbursement) } + + it "reimburses each calculated reimbursement types with the correct return items as a simulation" do + expect(reimbursement_type).to receive(:reimburse).with(reimbursement, [return_item], false) + subject + end + end +end diff --git a/core/spec/models/spree/reimbursement_spec.rb b/core/spec/models/spree/reimbursement_spec.rb new file mode 100644 index 00000000000..a8b19b053e5 --- /dev/null +++ b/core/spec/models/spree/reimbursement_spec.rb @@ -0,0 +1,216 @@ +require 'spec_helper' + +describe Spree::Reimbursement, type: :model do + + describe ".before_create" do + describe "#generate_number" do + context "number is assigned" do + let(:number) { '123' } + let(:reimbursement) { Spree::Reimbursement.new(number: number) } + + it "should return the assigned number" do + reimbursement.save + expect(reimbursement.number).to eq number + end + end + + context "number is not assigned" do + let(:reimbursement) { Spree::Reimbursement.new(number: nil) } + + before do + allow(reimbursement).to receive_messages(valid?: true) + end + + it "should assign number with random RI number" do + reimbursement.save + expect(reimbursement.number).to be =~ /RI\d{9}/ + end + end + end + end + + describe "#display_total" do + let(:total) { 100.50 } + let(:currency) { "USD" } + let(:order) { Spree::Order.new(currency: currency) } + let(:reimbursement) { Spree::Reimbursement.new(total: total, order: order) } + + subject { reimbursement.display_total } + + it "returns the value as a Spree::Money instance" do + expect(subject).to eq Spree::Money.new(total) + end + + it "uses the order's currency" do + expect(subject.money.currency.to_s).to eq currency + end + end + + describe "#perform!" do + let!(:adjustments) { [] } # placeholder to ensure it gets run prior the "before" at this level + + let!(:tax_rate) { nil } + let!(:tax_zone) { create(:zone, default_tax: true) } + + let(:order) { create(:order_with_line_items, state: 'payment', line_items_count: 1, line_items_price: line_items_price, shipment_cost: 0) } + let(:line_items_price) { BigDecimal.new(10) } + let(:line_item) { order.line_items.first } + let(:inventory_unit) { line_item.inventory_units.first } + let(:payment) { build(:payment, amount: payment_amount, order: order, state: 'completed') } + let(:payment_amount) { order.total } + let(:customer_return) { build(:customer_return, return_items: [return_item]) } + let(:return_item) { build(:return_item, inventory_unit: inventory_unit) } + + let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) } + + let(:reimbursement) { create(:reimbursement, customer_return: customer_return, order: order, return_items: [return_item]) } + + subject { reimbursement.perform! } + + before do + order.shipments.each do |shipment| + shipment.inventory_units.update_all state: 'shipped' + shipment.update_column('state', 'shipped') + end + order.reload + order.update! + if payment + payment.save! + order.next! # confirm + end + order.next! # completed + customer_return.save! + return_item.accept! + end + + it "refunds the total amount" do + subject + expect(reimbursement.unpaid_amount).to eq 0 + end + + it "creates a refund" do + expect { + subject + }.to change{ Spree::Refund.count }.by(1) + expect(Spree::Refund.last.amount).to eq order.total + end + + context 'with additional tax' do + let!(:tax_rate) { create(:tax_rate, name: "Sales Tax", amount: 0.10, included_in_price: false, zone: tax_zone) } + + it 'saves the additional tax and refunds the total' do + expect { + subject + }.to change { Spree::Refund.count }.by(1) + return_item.reload + expect(return_item.additional_tax_total).to be > 0 + expect(return_item.additional_tax_total).to eq line_item.additional_tax_total + expect(reimbursement.total).to eq line_item.pre_tax_amount + line_item.additional_tax_total + expect(Spree::Refund.last.amount).to eq line_item.pre_tax_amount + line_item.additional_tax_total + end + end + + context 'with included tax' do + let!(:tax_rate) { create(:tax_rate, name: "VAT Tax", amount: 0.1, included_in_price: true, zone: tax_zone) } + + it 'saves the included tax and refunds the total' do + expect { + subject + }.to change { Spree::Refund.count }.by(1) + return_item.reload + expect(return_item.included_tax_total).to be < 0 + expect(return_item.included_tax_total).to eq line_item.included_tax_total + expect(reimbursement.total).to eq (line_item.pre_tax_amount + line_item.included_tax_total).round(2) + expect(Spree::Refund.last.amount).to eq (line_item.pre_tax_amount + line_item.included_tax_total).round(2) + end + end + + context 'when reimbursement cannot be fully performed' do + let!(:non_return_refund) { create(:refund, amount: 1, payment: payment) } + + it 'raises IncompleteReimbursement error' do + expect { subject }.to raise_error(Spree::Reimbursement::IncompleteReimbursementError) + end + end + + context "when exchange is required" do + let(:exchange_variant) { build(:variant) } + before { return_item.exchange_variant = exchange_variant } + it "generates an exchange shipment for the order for the exchange items" do + expect { subject }.to change { order.reload.shipments.count }.by 1 + expect(order.shipments.last.inventory_units.first.variant).to eq exchange_variant + end + end + + it "triggers the reimbursement mailer to be sent" do + expect(Spree::ReimbursementMailer).to receive(:reimbursement_email).with(reimbursement.id) { double(deliver: true) } + subject + end + + end + + describe "#return_items_requiring_exchange" do + it "returns only the return items that require an exchange" do + return_items = [double(exchange_required?: true), double(exchange_required?: true),double(exchange_required?: false)] + allow(subject).to receive(:return_items) { return_items } + expect(subject.return_items_requiring_exchange).to eq return_items.take(2) + end + end + + describe "#calculated_total" do + context 'with return item amounts that would round up if added' do + let(:reimbursement) { Spree::Reimbursement.new } + + subject { reimbursement.calculated_total } + + before do + reimbursement.return_items << Spree::ReturnItem.new(pre_tax_amount: 10.003) + reimbursement.return_items << Spree::ReturnItem.new(pre_tax_amount: 10.003) + end + + it 'rounds down' do + expect(subject).to eq 20 + end + end + + context 'with a return item amount that should round up' do + let(:reimbursement) { Spree::Reimbursement.new } + + subject { reimbursement.calculated_total } + + before do + reimbursement.return_items << Spree::ReturnItem.new(pre_tax_amount: 19.998) + end + + it 'rounds up' do + expect(subject).to eq 20 + end + end + end + + describe '.build_from_customer_return' do + let(:customer_return) { create(:customer_return, line_items_count: 5) } + + let!(:pending_return_item) { customer_return.return_items.first.tap { |ri| ri.update!(acceptance_status: 'pending') } } + let!(:accepted_return_item) { customer_return.return_items.second.tap(&:accept!) } + let!(:rejected_return_item) { customer_return.return_items.third.tap(&:reject!) } + let!(:manual_intervention_return_item) { customer_return.return_items.fourth.tap(&:require_manual_intervention!) } + let!(:already_reimbursed_return_item) { customer_return.return_items.fifth } + + let!(:previous_reimbursement) { create(:reimbursement, order: customer_return.order, return_items: [already_reimbursed_return_item]) } + + subject { Spree::Reimbursement.build_from_customer_return(customer_return) } + + it 'connects to the accepted return items' do + expect(subject.return_items.to_a).to eq [accepted_return_item] + end + + it 'connects to the order' do + expect(subject.order).to eq customer_return.order + end + + it 'connects to the customer_return' do + expect(subject.customer_return).to eq customer_return + end + end +end diff --git a/core/spec/models/spree/reimbursement_tax_calculator_spec.rb b/core/spec/models/spree/reimbursement_tax_calculator_spec.rb new file mode 100644 index 00000000000..86f4a177c1a --- /dev/null +++ b/core/spec/models/spree/reimbursement_tax_calculator_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Spree::ReimbursementTaxCalculator, :type => :model do + + let!(:tax_rate) { nil } + + let(:reimbursement) { create(:reimbursement, return_items_count: 1) } + let(:return_item) { reimbursement.return_items.first } + let(:line_item) { return_item.inventory_unit.line_item } + + subject do + Spree::ReimbursementTaxCalculator.call(reimbursement) + end + + context 'without taxes' do + let!(:tax_rate) { nil } + + it 'leaves the return items additional_tax_total and included_tax_total at zero' do + subject + + expect(return_item.additional_tax_total).to eq 0 + expect(return_item.included_tax_total).to eq 0 + end + end + + context 'with additional tax' do + let!(:tax_rate) { create(:tax_rate, name: "Sales Tax", amount: 0.10, included_in_price: false, zone: tax_zone) } + let(:tax_zone) { create(:zone, default_tax: true) } + + it 'sets additional_tax_total on the return items' do + subject + return_item.reload + + expect(return_item.additional_tax_total).to be > 0 + expect(return_item.additional_tax_total).to eq line_item.additional_tax_total + end + end + + context 'with included tax' do + let!(:tax_rate) { create(:tax_rate, name: "VAT Tax", amount: 0.1, included_in_price: true, zone: tax_zone) } + let(:tax_zone) { create(:zone, default_tax: true) } + + it 'sets included_tax_total on the return items' do + subject + return_item.reload + + expect(return_item.included_tax_total).to be < 0 + expect(return_item.included_tax_total).to eq line_item.included_tax_total + end + end +end diff --git a/core/spec/models/spree/reimbursement_type/credit_spec.rb b/core/spec/models/spree/reimbursement_type/credit_spec.rb new file mode 100644 index 00000000000..4b46c28abbc --- /dev/null +++ b/core/spec/models/spree/reimbursement_type/credit_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +module Spree + describe ReimbursementType::Credit, :type => :model do + let(:reimbursement) { create(:reimbursement, return_items_count: 1) } + let(:return_item) { reimbursement.return_items.first } + let(:payment) { reimbursement.order.payments.first } + let(:simulate) { false } + let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) } + let(:creditable) { DummyCreditable.new(amount: 99.99) } + + class DummyCreditable < Spree::Base + attr_accessor :amount + self.table_name = 'spree_payments' # Your creditable class should not use this table + end + + subject { Spree::ReimbursementType::Credit.reimburse(reimbursement, [return_item], simulate)} + + before do + reimbursement.update!(total: reimbursement.calculated_total) + allow(Spree::ReimbursementType::Credit).to receive(:create_creditable).and_return(creditable) + end + + describe '.reimburse' do + context 'simulate is true' do + let(:simulate) { true } + + it 'creates one readonly lump credit for all outstanding balance payable to the customer' do + expect(subject.map(&:class)).to eq [Spree::Reimbursement::Credit] + expect(subject.map(&:readonly?)).to eq [true] + expect(subject.sum(&:amount)).to eq reimbursement.return_items.to_a.sum(&:total) + end + + it 'does not save to the database' do + expect { subject }.to_not change { Spree::Reimbursement::Credit.count } + end + end + + context 'simulate is false' do + let(:simulate) { false } + + before do + expect(creditable).to receive(:save).and_return(true) + end + + it 'creates one lump credit for all outstanding balance payable to the customer' do + expect { subject }.to change { Spree::Reimbursement::Credit.count }.by(1) + expect(subject.sum(&:amount)).to eq reimbursement.return_items.to_a.sum(&:total) + end + end + end + end +end diff --git a/core/spec/models/spree/reimbursement_type/exchange_spec.rb b/core/spec/models/spree/reimbursement_type/exchange_spec.rb new file mode 100644 index 00000000000..ed3cd90381f --- /dev/null +++ b/core/spec/models/spree/reimbursement_type/exchange_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +module Spree + describe ReimbursementType::Exchange, :type => :model do + describe '.reimburse' do + let(:reimbursement) { create(:reimbursement, return_items_count: 1) } + let(:return_items) { reimbursement.return_items } + let(:new_exchange) { double("Exchange") } + let(:simulate) { true } + + subject { Spree::ReimbursementType::Exchange.reimburse(reimbursement, return_items, simulate)} + + context 'return items are supplied' do + before do + expect(Spree::Exchange).to receive(:new).with(reimbursement.order, return_items).and_return(new_exchange) + end + + context "simulate is true" do + + it 'does not perform an exchange and returns the exchange object' do + expect(new_exchange).not_to receive(:perform!) + expect(subject).to eq [new_exchange] + end + end + + context "simulate is false" do + let(:simulate) { false } + + it 'performs an exchange and returns the exchange object' do + expect(new_exchange).to receive(:perform!) + expect(subject).to eq [new_exchange] + end + end + end + + context "no return items are supplied" do + let(:return_items) { [] } + + it 'does not perform an exchange and returns an empty array' do + expect(new_exchange).not_to receive(:perform!) + expect(subject).to eq [] + end + end + end + end +end diff --git a/core/spec/models/spree/reimbursement_type/original_payment_spec.rb b/core/spec/models/spree/reimbursement_type/original_payment_spec.rb new file mode 100644 index 00000000000..08384bef4ca --- /dev/null +++ b/core/spec/models/spree/reimbursement_type/original_payment_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +module Spree + describe ReimbursementType::OriginalPayment, :type => :model do + let(:reimbursement) { create(:reimbursement, return_items_count: 1) } + let(:return_item) { reimbursement.return_items.first } + let(:payment) { reimbursement.order.payments.first } + let(:simulate) { false } + let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) } + + subject { Spree::ReimbursementType::OriginalPayment.reimburse(reimbursement, [return_item], simulate)} + + before { reimbursement.update!(total: reimbursement.calculated_total) } + + describe ".reimburse" do + context "simulate is true" do + let(:simulate) { true } + + it "returns an array of readonly refunds" do + expect(subject.map(&:class)).to eq [Spree::Refund] + expect(subject.map(&:readonly?)).to eq [true] + end + end + + context "simulate is false" do + it 'performs the refund' do + expect { + subject + }.to change { payment.refunds.count }.by(1) + expect(payment.refunds.sum(:amount)).to eq reimbursement.return_items.to_a.sum(&:total) + end + end + + context 'when no credit is allowed on the payment' do + before do + expect_any_instance_of(Spree::Payment).to receive(:credit_allowed).and_return 0 + end + + it 'returns an empty array' do + expect(subject).to eq [] + end + end + + context 'when a payment is negative' do + before do + expect_any_instance_of(Spree::Payment).to receive(:amount).and_return -100 + end + + it 'returns an empty array' do + expect(subject).to eq [] + end + end + end + end +end diff --git a/core/spec/models/spree/return_authorization_spec.rb b/core/spec/models/spree/return_authorization_spec.rb new file mode 100644 index 00000000000..466e8e48b94 --- /dev/null +++ b/core/spec/models/spree/return_authorization_spec.rb @@ -0,0 +1,250 @@ +require 'spec_helper' + +describe Spree::ReturnAuthorization, :type => :model do + let(:order) { create(:shipped_order) } + let(:stock_location) { create(:stock_location) } + let(:rma_reason) { create(:return_authorization_reason) } + let(:inventory_unit_1) { order.inventory_units.first } + + let(:variant) { order.variants.first } + let(:return_authorization) do + Spree::ReturnAuthorization.new(order: order, + stock_location_id: stock_location.id, + return_authorization_reason_id: rma_reason.id) + end + + context "save" do + let(:order) { Spree::Order.create } + + it "should be invalid when order has no inventory units" do + return_authorization.save + expect(return_authorization.errors[:order]).to eq(["has no shipped units"]) + end + + context "expedited exchanges are configured" do + let(:order) { create(:shipped_order, line_items_count: 2) } + let(:exchange_return_item) { create(:exchange_return_item, inventory_unit: order.inventory_units.first) } + let(:return_item) { create(:return_item, inventory_unit: order.inventory_units.last) } + subject { create(:return_authorization, order: order, return_items: [exchange_return_item, return_item]) } + + before do + @expediteted_exchanges_config = Spree::Config[:expedited_exchanges] + Spree::Config[:expedited_exchanges] = true + @pre_exchange_hooks = subject.class.pre_expedited_exchange_hooks + end + + after do + Spree::Config[:expedited_exchanges] = @expediteted_exchanges_config + subject.class.pre_expedited_exchange_hooks = @pre_exchange_hooks + end + + context "no items to exchange" do + subject { create(:return_authorization, order: order) } + + it "does not create a reimbursement" do + expect{subject.save}.to_not change { Spree::Reimbursement.count } + end + end + + context "items to exchange" do + it "calls pre_expedited_exchange hooks with the return items to exchange" do + hook = double(:as_null_object) + expect(hook).to receive(:call).with [exchange_return_item] + subject.class.pre_expedited_exchange_hooks = [hook] + subject.save + end + + it "attempts to accept all return items requiring exchange" do + expect(exchange_return_item).to receive :attempt_accept + expect(return_item).not_to receive :attempt_accept + subject.save + end + + it "performs an exchange reimbursement for the exchange return items" do + subject.save + reimbursement = Spree::Reimbursement.last + expect(reimbursement.order).to eq subject.order + expect(reimbursement.return_items).to eq [exchange_return_item] + expect(exchange_return_item.reload.exchange_shipment).to be_present + end + + context "the reimbursement fails" do + before do + allow_any_instance_of(Spree::Reimbursement).to receive(:save) { false } + allow_any_instance_of(Spree::Reimbursement).to receive(:errors) { double(full_messages: "foo") } + end + + it "puts errors on the return authorization" do + subject.save + expect(subject.errors[:base]).to include "foo" + end + end + end + + end + end + + describe ".before_create" do + describe "#generate_number" do + context "number is assigned" do + let(:return_authorization) { Spree::ReturnAuthorization.new(number: '123') } + + it "should return the assigned number" do + return_authorization.save + expect(return_authorization.number).to eq('123') + end + end + + context "number is not assigned" do + let(:return_authorization) { Spree::ReturnAuthorization.new(number: nil) } + + before { allow(return_authorization).to receive_messages valid?: true } + + it "should assign number with random RA number" do + return_authorization.save + expect(return_authorization.number).to match(/RA\d{9}/) + end + end + end + end + + context "#currency" do + before { allow(order).to receive(:currency) { "ABC" } } + it "returns the order currency" do + expect(return_authorization.currency).to eq("ABC") + end + end + + describe "#pre_tax_total" do + let(:pre_tax_amount_1) { 15.0 } + let!(:return_item_1) { create(:return_item, return_authorization: return_authorization, pre_tax_amount: pre_tax_amount_1) } + + let(:pre_tax_amount_2) { 50.0 } + let!(:return_item_2) { create(:return_item, return_authorization: return_authorization, pre_tax_amount: pre_tax_amount_2) } + + let(:pre_tax_amount_3) { 5.0 } + let!(:return_item_3) { create(:return_item, return_authorization: return_authorization, pre_tax_amount: pre_tax_amount_3) } + + subject { return_authorization.pre_tax_total } + + it "sums it's associated return_item's pre-tax amounts" do + expect(subject).to eq (pre_tax_amount_1 + pre_tax_amount_2 + pre_tax_amount_3) + end + end + + describe "#display_pre_tax_total" do + it "returns a Spree::Money" do + allow(return_authorization).to receive_messages(pre_tax_total: 21.22) + expect(return_authorization.display_pre_tax_total).to eq(Spree::Money.new(21.22)) + end + end + + describe "#refundable_amount" do + let(:weighted_line_item_pre_tax_amount) { 5.0 } + let(:line_item_count) { return_authorization.order.line_items.count } + + subject { return_authorization.refundable_amount } + + before do + return_authorization.order.line_items.update_all(pre_tax_amount: weighted_line_item_pre_tax_amount) + return_authorization.order.update_attribute(:promo_total, promo_total) + end + + context "no promotions" do + let(:promo_total) { 0.0 } + it "returns the pre-tax line item total" do + expect(subject).to eq (weighted_line_item_pre_tax_amount * line_item_count) + end + end + + context "promotions" do + let(:promo_total) { -10.0 } + it "returns the pre-tax line item total minus the order level promotion value" do + expect(subject).to eq (weighted_line_item_pre_tax_amount * line_item_count) + promo_total + end + end + end + + describe "#customer_returned_items?" do + before do + allow_any_instance_of(Spree::Order).to receive_messages(return!: true) + end + + subject { return_authorization.customer_returned_items? } + + context "has associated customer returns" do + let(:customer_return) { create(:customer_return) } + let(:return_authorization) { customer_return.return_authorizations.first } + + it "returns true" do + expect(subject).to eq true + end + end + + context "does not have associated customer returns" do + let(:return_authorization) { create(:return_authorization) } + + it "returns false" do + expect(subject).to eq false + end + end + end + + describe 'cancel_return_items' do + let(:return_authorization) { create(:return_authorization, return_items: return_items) } + let(:return_items) { [return_item] } + let(:return_item) { create(:return_item) } + + subject { + return_authorization.cancel! + } + + it 'cancels the associated return items' do + subject + expect(return_item.reception_status).to eq 'cancelled' + end + + context 'some return items cannot be cancelled' do + let(:return_items) { [return_item, return_item_2] } + let(:return_item_2) { create(:return_item, reception_status: 'received') } + + it 'cancels those that can be cancelled' do + subject + expect(return_item.reception_status).to eq 'cancelled' + expect(return_item_2.reception_status).to eq 'received' + end + end + end + + describe '#can_cancel?' do + subject { create(:return_authorization, return_items: return_items).can_cancel? } + let(:return_items) { [return_item_1, return_item_2] } + let(:return_item_1) { create(:return_item) } + let(:return_item_2) { create(:return_item) } + + context 'all items can be cancelled' do + it 'returns true' do + expect(subject).to eq true + end + end + + context 'at least one return item can be cancelled' do + let(:return_item_2) { create(:return_item, reception_status: 'received') } + + it { should eq true } + end + + context 'no items can be cancelled' do + let(:return_item_1) { create(:return_item, reception_status: 'received') } + let(:return_item_2) { create(:return_item, reception_status: 'received') } + + it { should eq false } + end + + context 'when return_authorization has no return_items' do + let(:return_items) { [] } + + it { should eq true } + end + end +end diff --git a/core/spec/models/spree/return_item/eligibility_validator/default_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/default_spec.rb new file mode 100644 index 00000000000..cf463319f17 --- /dev/null +++ b/core/spec/models/spree/return_item/eligibility_validator/default_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Spree::ReturnItem::EligibilityValidator::Default, :type => :model do + let(:return_item) { create(:return_item) } + let(:validator) { Spree::ReturnItem::EligibilityValidator::Default.new(return_item) } + + let(:time_eligibility_class) { double("TimeEligibilityValidatorClass") } + let(:rma_eligibility_class) { double("RMAEligibilityValidatorClass") } + + let(:time_eligibility_instance) { double(errors: time_error) } + let(:rma_eligibility_instance) { double(errors: rma_error) } + + let(:time_error) {{}} + let(:rma_error) {{}} + + before do + validator.permitted_eligibility_validators = [ time_eligibility_class, rma_eligibility_class ] + + expect(time_eligibility_class).to receive(:new).and_return(time_eligibility_instance) + expect(rma_eligibility_class).to receive(:new).and_return(rma_eligibility_instance) + end + + describe "#eligible_for_return?" do + subject { validator.eligible_for_return? } + + it "checks that all permitted eligibility validators are eligible for return" do + expect(time_eligibility_instance).to receive(:eligible_for_return?).and_return(true) + expect(rma_eligibility_instance).to receive(:eligible_for_return?).and_return(true) + + expect(subject).to be true + end + end + + describe "#requires_manual_intervention?" do + subject { validator.requires_manual_intervention? } + + context "any of the permitted eligibility validators require manual intervention" do + it "returns true" do + expect(time_eligibility_instance).to receive(:requires_manual_intervention?).and_return(false) + expect(rma_eligibility_instance).to receive(:requires_manual_intervention?).and_return(true) + + expect(subject).to be true + end + end + + context "no permitted eligibility validators require manual intervention" do + it "returns false" do + expect(time_eligibility_instance).to receive(:requires_manual_intervention?).and_return(false) + expect(rma_eligibility_instance).to receive(:requires_manual_intervention?).and_return(false) + + expect(subject).to be false + end + end + end + + describe "#errors" do + subject { validator.errors } + + context "the validator errors are empty" do + it "returns an empty hash" do + expect(subject).to eq({}) + end + end + + context "the validators have errors" do + let(:time_error) { { time: time_error_text }} + let(:rma_error) { { rma: rma_error_text }} + + let(:time_error_text) { "Time eligibility error" } + let(:rma_error_text) { "RMA eligibility error" } + + it "gathers all errors from permitted eligibility validators into a single errors hash" do + expect(subject).to eq({time: time_error_text, rma: rma_error_text}) + end + end + end +end diff --git a/core/spec/models/spree/return_item/eligibility_validator/order_completed_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/order_completed_spec.rb new file mode 100644 index 00000000000..10c18b79fc7 --- /dev/null +++ b/core/spec/models/spree/return_item/eligibility_validator/order_completed_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Spree::ReturnItem::EligibilityValidator::OrderCompleted do + let(:inventory_unit) { create(:inventory_unit, order: order) } + let(:return_item) { create(:return_item, inventory_unit: inventory_unit) } + let(:validator) { Spree::ReturnItem::EligibilityValidator::OrderCompleted.new(return_item) } + + describe "#eligible_for_return?" do + subject { validator.eligible_for_return? } + + context "the order was completed" do + let(:order) { create(:completed_order_with_totals) } + + it "returns true" do + expect(subject).to be true + end + end + + context "the order is not completed" do + let(:order) { create(:order) } + + it "returns false" do + expect(subject).to be false + end + + it "sets an error" do + subject + expect(validator.errors[:order_not_completed]).to eq Spree.t('return_item_order_not_completed') + end + end + end +end diff --git a/core/spec/models/spree/return_item/eligibility_validator/rma_required_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/rma_required_spec.rb new file mode 100644 index 00000000000..bee343e3e79 --- /dev/null +++ b/core/spec/models/spree/return_item/eligibility_validator/rma_required_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Spree::ReturnItem::EligibilityValidator::RMARequired, :type => :model do + let(:return_item) { create(:return_item) } + let(:validator) { Spree::ReturnItem::EligibilityValidator::RMARequired.new(return_item) } + + describe "#eligible_for_return?" do + subject { validator.eligible_for_return? } + + context "there is an rma on the return item" do + it "returns true" do + expect(subject).to be true + end + end + + context "there is no rma on the return item" do + before { allow(return_item).to receive(:return_authorization).and_return(nil) } + + it "returns false" do + expect(subject).to be false + end + + it "sets an error" do + subject + expect(validator.errors[:rma_required]).to eq Spree.t('return_item_rma_ineligible') + end + end + end +end diff --git a/core/spec/models/spree/return_item/eligibility_validator/time_since_purchase_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/time_since_purchase_spec.rb new file mode 100644 index 00000000000..e8341b04029 --- /dev/null +++ b/core/spec/models/spree/return_item/eligibility_validator/time_since_purchase_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Spree::ReturnItem::EligibilityValidator::TimeSincePurchase, :type => :model do + let(:inventory_unit) { create(:inventory_unit, order: create(:shipped_order)) } + let(:return_item) { create(:return_item, inventory_unit: inventory_unit) } + let(:validator) { Spree::ReturnItem::EligibilityValidator::TimeSincePurchase.new(return_item) } + + describe "#eligible_for_return?" do + subject { validator.eligible_for_return? } + + context "it is within the return timeframe" do + it "returns true" do + completed_at = return_item.inventory_unit.order.completed_at - (Spree::Config[:return_eligibility_number_of_days].days / 2) + return_item.inventory_unit.order.update_attributes(completed_at: completed_at) + expect(subject).to be true + end + end + + context "it is past the return timeframe" do + before do + completed_at = return_item.inventory_unit.order.completed_at - Spree::Config[:return_eligibility_number_of_days].days - 1.day + return_item.inventory_unit.order.update_attributes(completed_at: completed_at) + end + + it "returns false" do + expect(subject).to be false + end + + it "sets an error" do + subject + expect(validator.errors[:number_of_days]).to eq Spree.t('return_item_time_period_ineligible') + end + end + end +end diff --git a/core/spec/models/spree/return_item/exchange_variant_eligibility/same_option_value_spec.rb b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_option_value_spec.rb new file mode 100644 index 00000000000..70f368298ad --- /dev/null +++ b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_option_value_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +module Spree + module ReturnItem::ExchangeVariantEligibility + describe SameOptionValue, :type => :model do + describe ".eligible_variants" do + let(:color_option_type) { create(:option_type, name: "color") } + let(:waist_option_type) { create(:option_type, name: "waist") } + let(:inseam_option_type) { create(:option_type, name: "inseam") } + + let(:blue_option_value) { create(:option_value, name: "blue", option_type: color_option_type) } + let(:red_option_value) { create(:option_value, name: "red", option_type: color_option_type) } + + let(:three_two_waist_option_value) { create(:option_value, name: 32, option_type: waist_option_type) } + let(:three_four_waist_option_value) { create(:option_value, name: 34, option_type: waist_option_type) } + + let(:three_zero_inseam_option_value) { create(:option_value, name: 30, option_type: inseam_option_type) } + let(:three_one_inseam_option_value) { create(:option_value, name: 31, option_type: inseam_option_type) } + + let(:product) { create(:product, option_types: [color_option_type, waist_option_type, inseam_option_type]) } + + let!(:variant) { create(:variant, product: product, option_values: [blue_option_value, three_two_waist_option_value, three_zero_inseam_option_value]) } + let!(:same_option_values_variant) { create(:variant, product: product, option_values: [blue_option_value, three_two_waist_option_value, three_one_inseam_option_value]) } + let!(:different_color_option_value_variant) { create(:variant, product: product, option_values: [red_option_value, three_two_waist_option_value, three_one_inseam_option_value]) } + let!(:different_waist_option_value_variant) { create(:variant, product: product, option_values: [blue_option_value, three_four_waist_option_value, three_one_inseam_option_value]) } + + before do + @original_option_type_restrictions = SameOptionValue.option_type_restrictions + SameOptionValue.option_type_restrictions = ["color", "waist"] + end + + after { SameOptionValue.option_type_restrictions = @original_option_type_restrictions } + + subject { SameOptionValue.eligible_variants(variant.reload) } + + it "returns all other variants for the same product with the same option value for the specified option type" do + Spree::StockItem.update_all(count_on_hand: 10) + + expect(subject.sort).to eq [variant, same_option_values_variant].sort + end + + it "does not return variants for another product" do + other_product_variant = create(:variant) + expect(subject).not_to include other_product_variant + end + + context "no option value restrictions are specified" do + before do + @original_option_type_restrictions = SameOptionValue.option_type_restrictions + SameOptionValue.option_type_restrictions = [] + end + + after { SameOptionValue.option_type_restrictions = @original_option_type_restrictions } + + it "returns all variants for the product" do + Spree::StockItem.update_all(count_on_hand: 10) + + expect(subject.sort).to eq [variant, same_option_values_variant, different_waist_option_value_variant, different_color_option_value_variant].sort + end + end + end + end + end +end + diff --git a/core/spec/models/spree/return_item/exchange_variant_eligibility/same_product_spec.rb b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_product_spec.rb new file mode 100644 index 00000000000..d5d06454e3d --- /dev/null +++ b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_product_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +module Spree + module ReturnItem::ExchangeVariantEligibility + describe SameProduct, :type => :model do + describe ".eligible_variants" do + + context "product has no variants" do + it "returns the master variant for the same product" do + product = create(:product) + product.master.stock_items.first.update_column(:count_on_hand, 10) + + expect(SameProduct.eligible_variants(product.master)).to eq [product.master] + end + end + + context "product has variants" do + it "returns all variants for the same product" do + product = create(:product, variants: 3.times.map { create(:variant) }) + product.variants.map { |v| v.stock_items.first.update_column(:count_on_hand, 10) } + + expect(SameProduct.eligible_variants(product.variants.first).sort).to eq product.variants.sort + end + end + + it "does not return variants for another product" do + variant = create(:variant) + other_product_variant = create(:variant) + expect(SameProduct.eligible_variants(variant)).not_to include other_product_variant + end + + it "only returns variants that are on hand" do + product = create(:product, variants: 2.times.map { create(:variant) }) + in_stock_variant = product.variants.first + + in_stock_variant.stock_items.first.update_column(:count_on_hand, 10) + expect(SameProduct.eligible_variants(in_stock_variant)).to eq [in_stock_variant] + end + end + + end + end +end diff --git a/core/spec/models/spree/return_item_spec.rb b/core/spec/models/spree/return_item_spec.rb new file mode 100644 index 00000000000..c072165162c --- /dev/null +++ b/core/spec/models/spree/return_item_spec.rb @@ -0,0 +1,599 @@ +require 'spec_helper' + +shared_examples "an invalid state transition" do |status, expected_status| + let(:status) { status } + + it "cannot transition to #{expected_status}" do + expect { subject }.to raise_error(StateMachine::InvalidTransition) + end +end + +describe Spree::ReturnItem, :type => :model do + + all_reception_statuses = Spree::ReturnItem.state_machines[:reception_status].states.map(&:name).map(&:to_s) + all_acceptance_statuses = Spree::ReturnItem.state_machines[:acceptance_status].states.map(&:name).map(&:to_s) + + before do + allow_any_instance_of(Spree::Order).to receive_messages(return!: true) + end + + describe '#receive!' do + let(:now) { Time.now } + let(:inventory_unit) { create(:inventory_unit, state: 'shipped') } + let(:return_item) { create(:return_item, inventory_unit: inventory_unit) } + + before do + inventory_unit.update_attributes!(state: 'shipped') + return_item.update_attributes!(reception_status: 'awaiting') + allow(return_item).to receive(:eligible_for_return?).and_return(true) + end + + subject { return_item.receive! } + + + it 'returns the inventory unit' do + subject + expect(inventory_unit.reload.state).to eq 'returned' + end + + it 'attempts to accept the return item' do + expect(return_item).to receive(:attempt_accept) + subject + end + + context 'with a stock location' do + let(:stock_item) { inventory_unit.find_stock_item } + let!(:customer_return) { create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id) } + + before do + inventory_unit.update_attributes!(state: 'shipped') + return_item.update_attributes!(reception_status: 'awaiting') + end + + it 'increases the count on hand' do + expect { subject }.to change { stock_item.reload.count_on_hand }.by(1) + end + + context 'when variant does not track inventory' do + before do + inventory_unit.update_attributes!(state: 'shipped') + inventory_unit.variant.update_attributes!(track_inventory: false) + return_item.update_attributes!(reception_status: 'awaiting') + end + + it 'does not increase the count on hand' do + expect { subject }.to_not change { stock_item.reload.count_on_hand } + end + end + + context 'when the restock_inventory preference is false' do + before do + Spree::Config[:restock_inventory] = false + end + + it 'does not increase the count on hand' do + expect { subject }.to_not change { stock_item.reload.count_on_hand } + end + end + end + end + + describe "#display_pre_tax_amount" do + let(:pre_tax_amount) { 21.22 } + let(:return_item) { build(:return_item, pre_tax_amount: pre_tax_amount) } + + it "returns a Spree::Money" do + expect(return_item.display_pre_tax_amount).to eq(Spree::Money.new(pre_tax_amount)) + end + end + + describe ".default_refund_amount_calculator" do + it "defaults to the default refund amount calculator" do + expect(Spree::ReturnItem.refund_amount_calculator).to eq Spree::Calculator::Returns::DefaultRefundAmount + end + end + + describe "pre_tax_amount calculations on create" do + let(:inventory_unit) { build(:inventory_unit) } + before { subject.save! } + + context "pre tax amount is not specified" do + subject { build(:return_item, inventory_unit: inventory_unit) } + + context "not an exchange" do + it { expect(subject.pre_tax_amount).to eq Spree::Calculator::Returns::DefaultRefundAmount.new.compute(subject) } + end + + context "an exchange" do + subject { build(:exchange_return_item) } + + it { expect(subject.pre_tax_amount).to eq 0.0 } + end + end + + context "pre tax amount is specified" do + subject { build(:return_item, inventory_unit: inventory_unit, pre_tax_amount: 100) } + + it { expect(subject.pre_tax_amount).to eq 100 } + end + end + + describe ".from_inventory_unit" do + let(:inventory_unit) { build(:inventory_unit) } + + subject { Spree::ReturnItem.from_inventory_unit(inventory_unit) } + + context "with a cancelled return item" do + let!(:return_item) { create(:return_item, inventory_unit: inventory_unit, reception_status: 'cancelled') } + + it { is_expected.not_to be_persisted } + end + + context "with a non-cancelled return item" do + let!(:return_item) { create(:return_item, inventory_unit: inventory_unit) } + + it { is_expected.to be_persisted } + end + end + + describe "reception_status state_machine" do + subject(:return_item) { create(:return_item) } + + it "starts off in the awaiting state" do + expect(return_item).to be_awaiting + end + end + + describe "acceptance_status state_machine" do + subject(:return_item) { create(:return_item) } + + it "starts off in the pending state" do + expect(return_item).to be_pending + end + end + + describe "#receive" do + let(:inventory_unit) { create(:inventory_unit, order: create(:shipped_order)) } + let(:return_item) { create(:return_item, reception_status: status, inventory_unit: inventory_unit) } + + subject { return_item.receive! } + + context "awaiting status" do + let(:status) { 'awaiting' } + + before do + expect(return_item.inventory_unit).to receive(:return!) + end + + before { subject } + + it "transitions successfully" do + expect(return_item).to be_received + end + end + + (all_reception_statuses - ['awaiting']).each do |invalid_transition_status| + context "return_item has a reception status of #{invalid_transition_status}" do + it_behaves_like "an invalid state transition", invalid_transition_status, 'received' + end + end + end + + describe "#cancel" do + let(:return_item) { create(:return_item, reception_status: status) } + + subject { return_item.cancel! } + + context "awaiting status" do + let(:status) { 'awaiting' } + + before { subject } + + it "transitions successfully" do + expect(return_item).to be_cancelled + end + end + + (all_reception_statuses - ['awaiting']).each do |invalid_transition_status| + context "return_item has a reception status of #{invalid_transition_status}" do + it_behaves_like "an invalid state transition", invalid_transition_status, 'cancelled' + end + end + end + + describe "#give" do + let(:return_item) { create(:return_item, reception_status: status) } + + subject { return_item.give! } + + context "awaiting status" do + let(:status) { 'awaiting' } + + before { subject } + + it "transitions successfully" do + expect(return_item).to be_given_to_customer + end + end + + (all_reception_statuses - ['awaiting']).each do |invalid_transition_status| + context "return_item has a reception status of #{invalid_transition_status}" do + it_behaves_like "an invalid state transition", invalid_transition_status, 'give_to_customer' + end + end + end + + describe "#attempt_accept" do + let(:return_item) { create(:return_item, acceptance_status: status) } + let(:validator_errors) { {} } + let(:validator_double) { double(errors: validator_errors) } + + subject { return_item.attempt_accept! } + + before do + allow(return_item).to receive(:validator).and_return(validator_double) + end + + context "pending status" do + let(:status) { 'pending' } + + before do + allow(return_item).to receive(:eligible_for_return?).and_return(true) + subject + end + + it "transitions successfully" do + expect(return_item).to be_accepted + end + + it "has no acceptance status errors" do + expect(return_item.acceptance_status_errors).to be_empty + end + end + + (all_acceptance_statuses - ['accepted', 'pending']).each do |invalid_transition_status| + context "return_item has an acceptance status of #{invalid_transition_status}" do + it_behaves_like "an invalid state transition", invalid_transition_status, 'accepted' + end + end + + context "not eligible for return" do + let(:status) { 'pending' } + let(:validator_errors) { { number_of_days: "Return Item is outside the eligible time period" } } + + before do + allow(return_item).to receive(:eligible_for_return?).and_return(false) + end + + context "manual intervention required" do + before do + allow(return_item).to receive(:requires_manual_intervention?).and_return(true) + subject + end + + it "transitions to manual intervention required" do + expect(return_item).to be_manual_intervention_required + end + + it "sets the acceptance status errors" do + expect(return_item.acceptance_status_errors).to eq validator_errors + end + end + + context "manual intervention not required" do + before do + allow(return_item).to receive(:requires_manual_intervention?).and_return(false) + subject + end + + it "transitions to rejected" do + expect(return_item).to be_rejected + end + + it "sets the acceptance status errors" do + expect(return_item.acceptance_status_errors).to eq validator_errors + end + end + end + end + + describe "#reject" do + let(:return_item) { create(:return_item, acceptance_status: status) } + + subject { return_item.reject! } + + context "pending status" do + let(:status) { 'pending' } + + before { subject } + + it "transitions successfully" do + expect(return_item).to be_rejected + end + + it "has no acceptance status errors" do + expect(return_item.acceptance_status_errors).to be_empty + end + end + + (all_acceptance_statuses - ['accepted', 'pending', 'manual_intervention_required']).each do |invalid_transition_status| + context "return_item has an acceptance status of #{invalid_transition_status}" do + it_behaves_like "an invalid state transition", invalid_transition_status, 'rejected' + end + end + end + + describe "#accept" do + let(:return_item) { create(:return_item, acceptance_status: status) } + + subject { return_item.accept! } + + context "pending status" do + let(:status) { 'pending' } + + before { subject } + + it "transitions successfully" do + expect(return_item).to be_accepted + end + + it "has no acceptance status errors" do + expect(return_item.acceptance_status_errors).to be_empty + end + end + + (all_acceptance_statuses - ['accepted', 'pending', 'manual_intervention_required']).each do |invalid_transition_status| + context "return_item has an acceptance status of #{invalid_transition_status}" do + it_behaves_like "an invalid state transition", invalid_transition_status, 'accepted' + end + end + end + + describe "#require_manual_intervention" do + let(:return_item) { create(:return_item, acceptance_status: status) } + + subject { return_item.require_manual_intervention! } + + context "pending status" do + let(:status) { 'pending' } + + before { subject } + + it "transitions successfully" do + expect(return_item).to be_manual_intervention_required + end + + it "has no acceptance status errors" do + expect(return_item.acceptance_status_errors).to be_empty + end + end + + (all_acceptance_statuses - ['accepted', 'pending', 'manual_intervention_required']).each do |invalid_transition_status| + context "return_item has an acceptance status of #{invalid_transition_status}" do + it_behaves_like "an invalid state transition", invalid_transition_status, 'manual_intervention_required' + end + end + end + + describe 'validity for reimbursements' do + let(:return_item) { create(:return_item, acceptance_status: acceptance_status) } + let(:acceptance_status) { 'pending' } + + before { return_item.reimbursement = build(:reimbursement) } + + subject { return_item } + + context 'when acceptance_status is accepted' do + let(:acceptance_status) { 'accepted' } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when acceptance_status is accepted' do + let(:acceptance_status) { 'pending' } + + it 'is valid' do + expect(subject).to_not be_valid + expect(subject.errors.messages).to eq({reimbursement: [I18n.t(:cannot_be_associated_unless_accepted, scope: 'activerecord.errors.models.spree/return_item.attributes.reimbursement')]}) + end + end + end + + describe "#exchange_requested?" do + context "exchange variant exists" do + before { allow(subject).to receive(:exchange_variant) { mock_model(Spree::Variant) } } + it { expect(subject.exchange_requested?).to eq true } + end + context "exchange variant does not exist" do + before { allow(subject).to receive(:exchange_variant) { nil } } + it { expect(subject.exchange_requested?).to eq false } + end + end + + describe "#exchange_processed?" do + context "exchange inventory unit exists" do + before { allow(subject).to receive(:exchange_inventory_unit) { mock_model(Spree::InventoryUnit) } } + it { expect(subject.exchange_processed?).to eq true } + end + context "exchange inventory unit does not exist" do + before { allow(subject).to receive(:exchange_inventory_unit) { nil } } + it { expect(subject.exchange_processed?).to eq false } + end + end + + describe "#exchange_required?" do + context "exchange has been requested and not yet processed" do + before do + allow(subject).to receive(:exchange_requested?) { true } + allow(subject).to receive(:exchange_processed?) { false } + end + + it { expect(subject.exchange_required?).to be true } + end + + context "exchange has not been requested" do + before { allow(subject).to receive(:exchange_requested?) { false } } + it { expect(subject.exchange_required?).to be false } + end + + context "exchange has been requested and processed" do + before do + allow(subject).to receive(:exchange_requested?) { true } + allow(subject).to receive(:exchange_processed?) { true } + end + it { expect(subject.exchange_required?).to be false } + end + end + + describe "#eligible_exchange_variants" do + it "uses the exchange variant calculator to compute possible variants to exchange for" do + return_item = build(:return_item) + expect(Spree::ReturnItem.exchange_variant_engine).to receive(:eligible_variants).with(return_item.variant) + return_item.eligible_exchange_variants + end + end + + describe ".exchange_variant_engine" do + it "defaults to the same product calculator" do + expect(Spree::ReturnItem.exchange_variant_engine).to eq Spree::ReturnItem::ExchangeVariantEligibility::SameProduct + end + end + + describe "exchange pre_tax_amount" do + let(:return_item) { build(:return_item) } + + context "the return item is intended to be exchanged" do + before { return_item.exchange_variant = build(:variant) } + it do + return_item.pre_tax_amount = 5.0 + return_item.save! + expect(return_item.reload.pre_tax_amount).to eq 0.0 + end + end + + context "the return item is not intended to be exchanged" do + it do + return_item.pre_tax_amount = 5.0 + return_item.save! + expect(return_item.reload.pre_tax_amount).to eq 5.0 + end + end + end + + describe "#build_exchange_inventory_unit" do + let(:return_item) { build(:return_item) } + subject { return_item.build_exchange_inventory_unit } + + context "the return item is intended to be exchanged" do + before { allow(return_item).to receive(:exchange_variant).and_return(mock_model(Spree::Variant)) } + + context "an exchange inventory unit already exists" do + before { allow(return_item).to receive(:exchange_inventory_unit).and_return(mock_model(Spree::InventoryUnit)) } + it { expect(subject).to be_nil } + end + + context "no exchange inventory unit exists" do + it "builds a pending inventory unit with references to the return item, variant, and previous inventory unit" do + expect(subject.variant).to eq return_item.exchange_variant + expect(subject.pending).to eq true + expect(subject).not_to be_persisted + expect(subject.original_return_item).to eq return_item + expect(subject.line_item).to eq return_item.inventory_unit.line_item + expect(subject.order).to eq return_item.inventory_unit.order + end + end + end + + context "the return item is not intended to be exchanged" do + it { expect(subject).to be_nil } + end + end + + describe "#exchange_shipment" do + it "returns the exchange inventory unit's shipment" do + inventory_unit = build(:inventory_unit) + subject.exchange_inventory_unit = inventory_unit + expect(subject.exchange_shipment).to eq inventory_unit.shipment + end + end + + describe "#shipment" do + it "returns the inventory unit's shipment" do + inventory_unit = build(:inventory_unit) + subject.inventory_unit = inventory_unit + expect(subject.shipment).to eq inventory_unit.shipment + end + end + + describe 'inventory_unit uniqueness' do + let!(:old_return_item) { create(:return_item, reception_status: old_reception_status) } + let(:old_reception_status) { 'awaiting' } + + subject do + build(:return_item, { + return_authorization: old_return_item.return_authorization, + inventory_unit: old_return_item.inventory_unit, + }) + end + + context 'with other awaiting return items exist for the same inventory unit' do + let(:old_reception_status) { 'awaiting' } + + it 'cancels the others' do + expect { + subject.save! + }.to change { old_return_item.reload.reception_status }.from('awaiting').to('cancelled') + end + + it 'does not cancel itself' do + subject.save! + expect(subject).to be_awaiting + end + end + + context 'with other cancelled return items exist for the same inventory unit' do + let(:old_reception_status) { 'cancelled' } + + it 'succeeds' do + expect { subject.save! }.to_not raise_error + end + end + + context 'with other received return items exist for the same inventory unit' do + let(:old_reception_status) { 'received' } + + it 'is invalid' do + expect(subject).to_not be_valid + expect(subject.errors.to_a).to eq ["Inventory unit #{subject.inventory_unit_id} has already been taken by return item #{old_return_item.id}"] + end + end + + context 'with other given_to_customer return items exist for the same inventory unit' do + let(:old_reception_status) { 'given_to_customer' } + + it 'is invalid' do + expect(subject).to_not be_valid + expect(subject.errors.to_a).to eq ["Inventory unit #{subject.inventory_unit_id} has already been taken by return item #{old_return_item.id}"] + end + end + end + + describe "included tax in total" do + let(:inventory_unit) { create(:inventory_unit, state: 'shipped') } + let(:return_item) do + create( + :return_item, + inventory_unit: inventory_unit, + included_tax_total: 10 + ) + end + + it 'includes included tax total' do + expect(return_item.pre_tax_amount).to eq 10 + expect(return_item.included_tax_total).to eq 10 + expect(return_item.total).to eq 20 + end + end +end diff --git a/core/spec/models/spree/returns_calculator_spec.rb b/core/spec/models/spree/returns_calculator_spec.rb new file mode 100644 index 00000000000..7bed34d2263 --- /dev/null +++ b/core/spec/models/spree/returns_calculator_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +module Spree + describe ReturnsCalculator, :type => :model do + let(:return_item) { build(:return_item) } + subject { ReturnsCalculator.new } + + it 'compute_shipment must be overridden' do + expect { + subject.compute(return_item) + }.to raise_error + end + end +end diff --git a/core/spec/models/spree/shipment_spec.rb b/core/spec/models/spree/shipment_spec.rb new file mode 100644 index 00000000000..2bc3b0ee8fd --- /dev/null +++ b/core/spec/models/spree/shipment_spec.rb @@ -0,0 +1,732 @@ +require 'spec_helper' +require 'benchmark' + +describe Spree::Shipment, :type => :model do + let(:order) { mock_model Spree::Order, backordered?: false, + canceled?: false, + can_ship?: true, + currency: 'USD', + number: 'S12345', + paid?: false, + touch: true } + let(:shipping_method) { create(:shipping_method, name: "UPS") } + let(:shipment) do + shipment = Spree::Shipment.new(cost: 1, state: 'pending', stock_location: create(:stock_location)) + allow(shipment).to receive_messages order: order + allow(shipment).to receive_messages shipping_method: shipping_method + shipment.save + shipment + end + + let(:variant) { mock_model(Spree::Variant) } + let(:line_item) { mock_model(Spree::LineItem, variant: variant) } + + def create_shipment(order, stock_location) + order.shipments.create({ stock_location_id: stock_location.id }).inventory_units.create( + order_id: order.id, + variant_id: order.line_items.first.variant_id, + line_item_id: order.line_items.first.id + ) + end + + # Regression test for #4063 + context "number generation" do + before do + allow(order).to receive :update! + end + + it "generates a number containing a letter + 11 numbers" do + shipment.save + expect(shipment.number[0]).to eq("H") + expect(/\d{11}/.match(shipment.number)).not_to be_nil + expect(shipment.number.length).to eq(12) + end + end + + it 'is backordered if one if its inventory_units is backordered' do + allow(shipment).to receive_messages(inventory_units: [ + mock_model(Spree::InventoryUnit, backordered?: false), + mock_model(Spree::InventoryUnit, backordered?: true) + ]) + expect(shipment).to be_backordered + end + + context '#determine_state' do + it 'returns canceled if order is canceled?' do + allow(order).to receive_messages canceled?: true + expect(shipment.determine_state(order)).to eq 'canceled' + end + + it 'returns pending unless order.can_ship?' do + allow(order).to receive_messages can_ship?: false + expect(shipment.determine_state(order)).to eq 'pending' + end + + it 'returns pending if backordered' do + allow(shipment).to receive_messages inventory_units: [mock_model(Spree::InventoryUnit, backordered?: true)] + expect(shipment.determine_state(order)).to eq 'pending' + end + + it 'returns shipped when already shipped' do + allow(shipment).to receive_messages state: 'shipped' + expect(shipment.determine_state(order)).to eq 'shipped' + end + + it 'returns pending when unpaid' do + expect(shipment.determine_state(order)).to eq 'pending' + end + + it 'returns ready when paid' do + allow(order).to receive_messages paid?: true + expect(shipment.determine_state(order)).to eq 'ready' + end + + it 'returns ready when Config.auto_capture_on_dispatch' do + Spree::Config.auto_capture_on_dispatch = true + expect(shipment.determine_state(order)).to eq 'ready' + end + end + + context "display_amount" do + it "retuns a Spree::Money" do + allow(shipment).to receive(:cost) { 21.22 } + expect(shipment.display_amount).to eq(Spree::Money.new(21.22)) + end + end + + context "display_final_price" do + it "retuns a Spree::Money" do + allow(shipment).to receive(:final_price) { 21.22 } + expect(shipment.display_final_price).to eq(Spree::Money.new(21.22)) + end + end + + context "display_item_cost" do + it "retuns a Spree::Money" do + allow(shipment).to receive(:item_cost) { 21.22 } + expect(shipment.display_item_cost).to eq(Spree::Money.new(21.22)) + end + end + + context "#item_cost" do + it 'should equal shipment line items amount with tax' do + order = create(:order_with_line_item_quantity, line_items_quantity: 2) + + stock_location = create(:stock_location) + + create_shipment(order, stock_location) + create_shipment(order, stock_location) + + create :tax_adjustment, adjustable: order.line_items.first, order: order + + expect(order.shipments.first.item_cost).to eql(11.0) + expect(order.shipments.last.item_cost).to eql(11.0) + end + + it 'should equal line items final amount with tax' do + shipment = create(:shipment, order: create(:order_with_line_item_quantity, line_items_quantity: 2)) + create :tax_adjustment, adjustable: shipment.order.line_items.first, order: shipment.order + expect(shipment.item_cost).to eql(22.0) + end + end + + it "#discounted_cost" do + shipment = create(:shipment) + shipment.cost = 10 + shipment.promo_total = -1 + expect(shipment.discounted_cost).to eq(9) + end + + it "#tax_total with included taxes" do + shipment = Spree::Shipment.new + expect(shipment.tax_total).to eq(0) + shipment.included_tax_total = 10 + expect(shipment.tax_total).to eq(10) + end + + it "#tax_total with additional taxes" do + shipment = Spree::Shipment.new + expect(shipment.tax_total).to eq(0) + shipment.additional_tax_total = 10 + expect(shipment.tax_total).to eq(10) + end + + it "#final_price" do + shipment = Spree::Shipment.new + shipment.cost = 10 + shipment.adjustment_total = -2 + shipment.included_tax_total = 1 + expect(shipment.final_price).to eq(8) + end + + context "manifest" do + let(:order) { Spree::Order.create } + let(:variant) { create(:variant) } + let!(:line_item) { order.contents.add variant } + let!(:shipment) { order.create_proposed_shipments.first } + + it "returns variant expected" do + expect(shipment.manifest.first.variant).to eq variant + end + + context "variant was removed" do + before { variant.destroy } + + it "still returns variant expected" do + expect(shipment.manifest.first.variant).to eq variant + end + end + end + + context 'shipping_rates' do + let(:shipment) { create(:shipment) } + let(:shipping_method1) { create(:shipping_method) } + let(:shipping_method2) { create(:shipping_method) } + let(:shipping_rates) { [ + Spree::ShippingRate.new(shipping_method: shipping_method1, cost: 10.00, selected: true), + Spree::ShippingRate.new(shipping_method: shipping_method2, cost: 20.00) + ] } + + it 'returns shipping_method from selected shipping_rate' do + shipment.shipping_rates.delete_all + shipment.shipping_rates.create shipping_method: shipping_method1, cost: 10.00, selected: true + expect(shipment.shipping_method).to eq shipping_method1 + end + + context 'refresh_rates' do + let(:mock_estimator) { double('estimator', shipping_rates: shipping_rates) } + before { allow(shipment).to receive(:can_get_rates?){ true } } + + it 'should request new rates, and maintain shipping_method selection' do + expect(Spree::Stock::Estimator).to receive(:new).with(shipment.order).and_return(mock_estimator) + allow(shipment).to receive_messages(shipping_method: shipping_method2) + + expect(shipment.refresh_rates).to eq(shipping_rates) + expect(shipment.reload.selected_shipping_rate.shipping_method_id).to eq(shipping_method2.id) + end + + it 'should handle no shipping_method selection' do + expect(Spree::Stock::Estimator).to receive(:new).with(shipment.order).and_return(mock_estimator) + allow(shipment).to receive_messages(shipping_method: nil) + expect(shipment.refresh_rates).to eq(shipping_rates) + expect(shipment.reload.selected_shipping_rate).not_to be_nil + end + + it 'should not refresh if shipment is shipped' do + expect(Spree::Stock::Estimator).not_to receive(:new) + shipment.shipping_rates.delete_all + allow(shipment).to receive_messages(shipped?: true) + expect(shipment.refresh_rates).to eq([]) + end + + it "can't get rates without a shipping address" do + shipment.order(ship_address: nil) + expect(shipment.refresh_rates).to eq([]) + end + + context 'to_package' do + let(:inventory_units) do + [build(:inventory_unit, line_item: line_item, variant: variant, state: 'on_hand'), + build(:inventory_unit, line_item: line_item, variant: variant, state: 'backordered')] + end + + before do + allow(shipment).to receive(:inventory_units) { inventory_units } + allow(inventory_units).to receive_message_chain(:includes, :joins).and_return inventory_units + end + + it 'should use symbols for states when adding contents to package' do + package = shipment.to_package + expect(package.on_hand.count).to eq 1 + expect(package.backordered.count).to eq 1 + end + end + end + end + + context "#update!" do + shared_examples_for "immutable once shipped" do + it "should remain in shipped state once shipped" do + shipment.state = 'shipped' + expect(shipment).to receive(:update_columns).with(state: 'shipped', updated_at: kind_of(Time)) + shipment.update!(order) + end + end + + shared_examples_for "pending if backordered" do + it "should have a state of pending if backordered" do + allow(shipment).to receive_messages(inventory_units: [mock_model(Spree::InventoryUnit, backordered?: true)]) + expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time)) + shipment.update!(order) + end + end + + context "when order cannot ship" do + before { allow(order).to receive_messages can_ship?: false } + it "should result in a 'pending' state" do + expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time)) + shipment.update!(order) + end + end + + context "when order is paid" do + before { allow(order).to receive_messages paid?: true } + it "should result in a 'ready' state" do + expect(shipment).to receive(:update_columns).with(state: 'ready', updated_at: kind_of(Time)) + shipment.update!(order) + end + it_should_behave_like 'immutable once shipped' + it_should_behave_like 'pending if backordered' + end + + context "when order has balance due" do + before { allow(order).to receive_messages paid?: false } + it "should result in a 'pending' state" do + shipment.state = 'ready' + expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time)) + shipment.update!(order) + end + it_should_behave_like 'immutable once shipped' + it_should_behave_like 'pending if backordered' + end + + context "when order has a credit owed" do + before { allow(order).to receive_messages payment_state: 'credit_owed', paid?: true } + it "should result in a 'ready' state" do + shipment.state = 'pending' + expect(shipment).to receive(:update_columns).with(state: 'ready', updated_at: kind_of(Time)) + shipment.update!(order) + end + it_should_behave_like 'immutable once shipped' + it_should_behave_like 'pending if backordered' + end + + context "when shipment state changes to shipped" do + before do + allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) + allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) + end + + it "should call after_ship" do + shipment.state = 'pending' + expect(shipment).to receive :after_ship + allow(shipment).to receive_messages determine_state: 'shipped' + expect(shipment).to receive(:update_columns).with(state: 'shipped', updated_at: kind_of(Time)) + shipment.update!(order) + end + + context "when using the default shipment handler" do + it "should call the 'perform' method" do + shipment.state = 'pending' + allow(shipment).to receive_messages determine_state: 'shipped' + expect_any_instance_of(Spree::ShipmentHandler).to receive(:perform) + shipment.update!(order) + end + end + + context "when using a custom shipment handler" do + before do + Spree::ShipmentHandler::UPS = Class.new { + def initialize(shipment) true end + def perform() true end + } + end + + it "should call the custom handler's 'perform' method" do + shipment.state = 'pending' + allow(shipment).to receive_messages determine_state: 'shipped' + expect_any_instance_of(Spree::ShipmentHandler::UPS).to receive(:perform) + shipment.update!(order) + end + + after do + Spree::ShipmentHandler.send(:remove_const, :UPS) + end + end + + # Regression test for #4347 + context "with adjustments" do + before do + shipment.adjustments << Spree::Adjustment.create(order: order, label: "Label", amount: 5) + end + + it "transitions to shipped" do + shipment.update_column(:state, "ready") + expect { shipment.ship! }.not_to raise_error + end + end + end + end + + context "when order is completed" do + after { Spree::Config.set track_inventory_levels: true } + + before do + allow(order).to receive_messages completed?: true + allow(order).to receive_messages canceled?: false + end + + context "with inventory tracking" do + before { Spree::Config.set track_inventory_levels: true } + + it "should validate with inventory" do + shipment.inventory_units = [create(:inventory_unit)] + expect(shipment.valid?).to be true + end + end + + context "without inventory tracking" do + before { Spree::Config.set track_inventory_levels: false } + + it "should validate with no inventory" do + expect(shipment.valid?).to be true + end + end + end + + context "#cancel" do + it 'cancels the shipment' do + allow(shipment.order).to receive(:update!) + + shipment.state = 'pending' + expect(shipment).to receive(:after_cancel) + shipment.cancel! + expect(shipment.state).to eq 'canceled' + end + + it 'restocks the items' do + allow(shipment).to receive_message_chain(inventory_units: [mock_model(Spree::InventoryUnit, state: "on_hand", line_item: line_item, variant: variant)]) + shipment.stock_location = mock_model(Spree::StockLocation) + expect(shipment.stock_location).to receive(:restock).with(variant, 1, shipment) + shipment.after_cancel + end + + context "with backordered inventory units" do + let(:order) { create(:order) } + let(:variant) { create(:variant) } + let(:other_order) { create(:order) } + + before do + order.contents.add variant + order.create_proposed_shipments + + other_order.contents.add variant + other_order.create_proposed_shipments + end + + it "doesn't fill backorders when restocking inventory units" do + shipment = order.shipments.first + expect(shipment.inventory_units.count).to eq 1 + expect(shipment.inventory_units.first).to be_backordered + + other_shipment = other_order.shipments.first + expect(other_shipment.inventory_units.count).to eq 1 + expect(other_shipment.inventory_units.first).to be_backordered + + expect { + shipment.cancel! + }.not_to change { other_shipment.inventory_units.first.state } + end + end + end + + context "#resume" do + it 'will determine new state based on order' do + allow(shipment.order).to receive(:update!) + + shipment.state = 'canceled' + expect(shipment).to receive(:determine_state).and_return(:ready) + expect(shipment).to receive(:after_resume) + shipment.resume! + expect(shipment.state).to eq 'ready' + end + + it 'unstocks them items' do + allow(shipment).to receive_message_chain(inventory_units: [mock_model(Spree::InventoryUnit, line_item: line_item, variant: variant)]) + shipment.stock_location = mock_model(Spree::StockLocation) + expect(shipment.stock_location).to receive(:unstock).with(variant, 1, shipment) + shipment.after_resume + end + + it 'will determine new state based on order' do + allow(shipment.order).to receive(:update!) + + shipment.state = 'canceled' + expect(shipment).to receive(:determine_state).twice.and_return('ready') + expect(shipment).to receive(:after_resume) + shipment.resume! + # Shipment is pending because order is already paid + expect(shipment.state).to eq 'pending' + end + end + + context "#ship" do + context "when the shipment is canceled" do + let(:shipment_with_inventory_units) { create(:shipment, order: create(:order_with_line_items), state: 'canceled') } + let(:subject) { shipment_with_inventory_units.ship! } + before do + allow(order).to receive(:update!) + allow(shipment_with_inventory_units).to receive_messages(require_inventory: false, update_order: true) + end + + it 'unstocks them items' do + allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) + allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) + + expect(shipment_with_inventory_units.stock_location).to receive(:unstock) + subject + end + end + + ['ready', 'canceled'].each do |state| + context "from #{state}" do + before do + allow(order).to receive(:update!) + allow(shipment).to receive_messages(require_inventory: false, update_order: true, state: state) + end + + it "should update shipped_at timestamp" do + allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) + allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) + + shipment.ship! + expect(shipment.shipped_at).not_to be_nil + # Ensure value is persisted + shipment.reload + expect(shipment.shipped_at).not_to be_nil + end + + it "should send a shipment email" do + mail_message = double 'Mail::Message' + shipment_id = nil + expect(Spree::ShipmentMailer).to receive(:shipped_email) { |*args| + shipment_id = args[0] + mail_message + } + expect(mail_message).to receive :deliver + allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) + + shipment.ship! + expect(shipment_id).to eq(shipment.id) + end + + it "finalizes adjustments" do + allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state) + allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) + + shipment.adjustments.each do |adjustment| + expect(adjustment).to receive(:finalize!) + end + shipment.ship! + end + end + end + end + + context "#ready" do + context 'with Config.auto_capture_on_dispatch == false' do + # Regression test for #2040 + it "cannot ready a shipment for an order if the order is unpaid" do + allow(order).to receive_messages(paid?: false) + assert !shipment.can_ready? + end + end + + context 'with Config.auto_capture_on_dispatch == true' do + before do + Spree::Config[:auto_capture_on_dispatch] = true + @order = create :completed_order_with_pending_payment + @shipment = @order.shipments.first + @shipment.cost = @order.ship_total + end + + it "shipments ready for an order if the order is unpaid" do + expect(@shipment.ready?).to be true + end + + it "tells the order to process payment in #after_ship" do + expect(@shipment).to receive(:process_order_payments) + @shipment.ship! + end + + context "order has pending payments" do + let(:payment) do + payment = @order.payments.first + payment.update_attribute :state, 'pending' + payment + end + + it "can fully capture an authorized payment" do + payment.update_attribute(:amount, @order.total) + + expect(payment.amount).to eq payment.uncaptured_amount + @shipment.ship! + expect(payment.reload.uncaptured_amount.to_f).to eq 0 + end + + it "can partially capture an authorized payment" do + payment.update_attribute(:amount, @order.total + 50) + + expect(payment.amount).to eq payment.uncaptured_amount + @shipment.ship! + expect(payment.captured_amount).to eq @order.total + expect(payment.captured_amount).to eq payment.amount + expect(payment.order.payments.pending.first.amount).to eq 50 + end + end + end + end + + context "updates cost when selected shipping rate is present" do + let(:shipment) { create(:shipment) } + + before { allow(shipment).to receive_message_chain :selected_shipping_rate, cost: 5 } + + it "updates shipment totals" do + shipment.update_amounts + expect(shipment.reload.cost).to eq(5) + end + + it "factors in additional adjustments to adjustment total" do + shipment.adjustments.create!( + order: order, + label: "Additional", + amount: 5, + included: false, + state: "closed" + ) + shipment.update_amounts + expect(shipment.reload.adjustment_total).to eq(5) + end + + it "does not factor in included adjustments to adjustment total" do + shipment.adjustments.create!( + order: order, + label: "Included", + amount: 5, + included: true, + state: "closed" + ) + shipment.update_amounts + expect(shipment.reload.adjustment_total).to eq(0) + end + end + + context "changes shipping rate via general update" do + let(:order) do + Spree::Order.create( + payment_total: 100, payment_state: 'paid', total: 100, item_total: 100 + ) + end + + let(:shipment) { Spree::Shipment.create order_id: order.id, stock_location: create(:stock_location) } + + let(:shipping_rate) do + Spree::ShippingRate.create shipment_id: shipment.id, cost: 10 + end + + before do + shipment.update_attributes_and_order selected_shipping_rate_id: shipping_rate.id + end + + it "updates everything around order shipment total and state" do + expect(shipment.cost.to_f).to eq 10 + expect(shipment.state).to eq 'pending' + expect(shipment.order.total.to_f).to eq 110 + expect(shipment.order.payment_state).to eq 'balance_due' + end + end + + context "after_save" do + context "line item changes" do + before do + shipment.cost = shipment.cost + 10 + end + + it "triggers adjustment total recalculation" do + expect(shipment).to receive(:recalculate_adjustments) + shipment.save + end + + it "does not trigger adjustment recalculation if shipment has shipped" do + shipment.state = 'shipped' + expect(shipment).not_to receive(:recalculate_adjustments) + shipment.save + end + end + + context "line item does not change" do + it "does not trigger adjustment total recalculation" do + expect(shipment).not_to receive(:recalculate_adjustments) + shipment.save + end + end + end + + context "currency" do + it "returns the order currency" do + expect(shipment.currency).to eq(order.currency) + end + end + + context "nil costs" do + it "sets cost to 0" do + shipment = Spree::Shipment.new + shipment.valid? + expect(shipment.cost).to eq 0 + end + end + + context "#tracking_url" do + it "uses shipping method to determine url" do + expect(shipping_method).to receive(:build_tracking_url).with('1Z12345').and_return(:some_url) + shipment.tracking = '1Z12345' + + expect(shipment.tracking_url).to eq(:some_url) + end + end + + context "set up new inventory units" do + # let(:line_item) { double( + let(:variant) { double("Variant", id: 9) } + + let(:inventory_units) { double } + + let(:params) do + { variant_id: variant.id, state: 'on_hand', order_id: order.id, line_item_id: line_item.id } + end + + before { allow(shipment).to receive_messages inventory_units: inventory_units } + + it "associates variant and order" do + expect(inventory_units).to receive(:create).with(params) + unit = shipment.set_up_inventory('on_hand', variant, order, line_item) + end + end + + # Regression test for #3349 + context "#destroy" do + it "destroys linked shipping_rates" do + reflection = Spree::Shipment.reflect_on_association(:shipping_rates) + expect(reflection.options[:dependent]).to be(:delete_all) + end + end + + # Regression test for #4072 (kinda) + # The need for this was discovered in the research for #4702 + context "state changes" do + before do + # Must be stubbed so transition can succeed + allow(order).to receive_messages :paid? => true + end + + it "are logged to the database" do + expect(shipment.state_changes).to be_empty + expect(shipment.ready!).to be true + expect(shipment.state_changes.count).to eq(1) + state_change = shipment.state_changes.first + expect(state_change.previous_state).to eq('pending') + expect(state_change.next_state).to eq('ready') + end + end +end diff --git a/core/spec/models/spree/shipping_calculator_spec.rb b/core/spec/models/spree/shipping_calculator_spec.rb new file mode 100644 index 00000000000..3469a686c69 --- /dev/null +++ b/core/spec/models/spree/shipping_calculator_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +module Spree + describe ShippingCalculator, :type => :model do + let(:variant1) { build(:variant, :price => 10) } + let(:variant2) { build(:variant, :price => 20) } + + let(:package) do + build(:stock_package, variants_contents: { variant1 => 2, variant2 => 1 }) + end + + subject { ShippingCalculator.new } + + it 'computes with a shipment' do + shipment = mock_model(Spree::Shipment) + expect(subject).to receive(:compute_shipment).with(shipment) + subject.compute(shipment) + end + + it 'computes with a package' do + expect(subject).to receive(:compute_package).with(package) + subject.compute(package) + end + + it 'compute_shipment must be overridden' do + expect { + subject.compute_shipment(shipment) + }.to raise_error + end + + it 'compute_package must be overridden' do + expect { + subject.compute_package(package) + }.to raise_error + end + + it 'checks availability for a package' do + expect(subject.available?(package)).to be true + end + + it 'calculates totals for content_items' do + expect(subject.send(:total, package.contents)).to eq 40.00 + end + end +end diff --git a/core/spec/models/spree/shipping_category_spec.rb b/core/spec/models/spree/shipping_category_spec.rb new file mode 100644 index 00000000000..4db54c8dffb --- /dev/null +++ b/core/spec/models/spree/shipping_category_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Spree::ShippingCategory, :type => :model do + +end diff --git a/core/spec/models/spree/shipping_method_spec.rb b/core/spec/models/spree/shipping_method_spec.rb new file mode 100644 index 00000000000..e07fee08722 --- /dev/null +++ b/core/spec/models/spree/shipping_method_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +class DummyShippingCalculator < Spree::ShippingCalculator +end + +describe Spree::ShippingMethod, :type => :model do + let(:shipping_method){ create(:shipping_method) } + + context 'calculators' do + it "Should reject calculators that don't inherit from Spree::ShippingCalculator" do + allow(Spree::ShippingMethod).to receive_message_chain(:spree_calculators, :shipping_methods).and_return([ + Spree::Calculator::Shipping::FlatPercentItemTotal, + Spree::Calculator::Shipping::PriceSack, + Spree::Calculator::DefaultTax, + DummyShippingCalculator # included as regression test for https://github.com/spree/spree/issues/3109 + ]) + + expect(Spree::ShippingMethod.calculators).to eq([Spree::Calculator::Shipping::FlatPercentItemTotal, Spree::Calculator::Shipping::PriceSack, DummyShippingCalculator ]) + expect(Spree::ShippingMethod.calculators).not_to eq([Spree::Calculator::DefaultTax]) + end + end + + # Regression test for #4492 + context "#shipments" do + let!(:shipping_method) { create(:shipping_method) } + let!(:shipment) do + shipment = create(:shipment) + shipment.shipping_rates.create!(:shipping_method => shipping_method) + shipment + end + + it "can gather all the related shipments" do + expect(shipping_method.shipments).to include(shipment) + end + end + + context "validations" do + before { subject.valid? } + + it "validates presence of name" do + expect(subject.error_on(:name).size).to eq(1) + end + + context "shipping category" do + it "validates presence of at least one" do + expect(subject.error_on(:base).size).to eq(1) + end + + context "one associated" do + before { subject.shipping_categories.push create(:shipping_category) } + it { expect(subject.error_on(:base).size).to eq(0) } + end + end + end + + context 'factory' do + it "should set calculable correctly" do + expect(shipping_method.calculator.calculable).to eq(shipping_method) + end + end + + context "generating tracking URLs" do + context "shipping method has a tracking URL mask on file" do + let(:tracking_url) { "https://track-o-matic.com/:tracking" } + before { allow(subject).to receive(:tracking_url) { tracking_url } } + + context 'tracking number has spaces' do + let(:tracking_numbers) { ["1234 5678 9012 3456", "a bcdef"] } + let(:expectations) { %w[https://track-o-matic.com/1234%205678%209012%203456 https://track-o-matic.com/a%20bcdef] } + + it "should return a single URL with '%20' in lieu of spaces" do + tracking_numbers.each_with_index do |num, i| + expect(subject.build_tracking_url(num)).to eq(expectations[i]) + end + end + end + end + end + + # Regression test for #4320 + context "soft deletion" do + let(:shipping_method) { create(:shipping_method) } + it "soft-deletes when destroy is called" do + shipping_method.destroy + expect(shipping_method.deleted_at).not_to be_blank + end + end +end diff --git a/core/spec/models/spree/shipping_rate_spec.rb b/core/spec/models/spree/shipping_rate_spec.rb new file mode 100644 index 00000000000..0cf319e2fdd --- /dev/null +++ b/core/spec/models/spree/shipping_rate_spec.rb @@ -0,0 +1,141 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe Spree::ShippingRate, :type => :model do + let(:shipment) { create(:shipment) } + let(:shipping_method) { create(:shipping_method) } + let(:shipping_rate) { Spree::ShippingRate.new(:shipment => shipment, + :shipping_method => shipping_method, + :cost => 10) } + + context "#display_price" do + context "when tax included in price" do + context "when the tax rate is from the default zone" do + let!(:zone) { create(:zone, :default_tax => true) } + let(:tax_rate) do + create(:tax_rate, + :name => "VAT", + :amount => 0.1, + :included_in_price => true, + :zone => zone) + end + + before { shipping_rate.tax_rate = tax_rate } + + it "shows correct tax amount" do + expect(shipping_rate.display_price.to_s).to eq("$10.00 (incl. $0.91 #{tax_rate.name})") + end + + context "when cost is zero" do + before do + shipping_rate.cost = 0 + end + + it "shows no tax amount" do + expect(shipping_rate.display_price.to_s).to eq("$0.00") + end + end + end + + context "when the tax rate is from a non-default zone" do + let!(:default_zone) { create(:zone, :default_tax => true) } + let!(:non_default_zone) { create(:zone, :default_tax => false) } + let(:tax_rate) do + create(:tax_rate, + :name => "VAT", + :amount => 0.1, + :included_in_price => true, + :zone => non_default_zone) + end + before { shipping_rate.tax_rate = tax_rate } + + it "shows correct tax amount" do + expect(shipping_rate.display_price.to_s).to eq("$10.00 (excl. $0.91 #{tax_rate.name})") + end + + context "when cost is zero" do + before do + shipping_rate.cost = 0 + end + + it "shows no tax amount" do + expect(shipping_rate.display_price.to_s).to eq("$0.00") + end + end + end + end + + context "when tax is additional to price" do + let(:tax_rate) { create(:tax_rate, :name => "Sales Tax", :amount => 0.1) } + before { shipping_rate.tax_rate = tax_rate } + + it "shows correct tax amount" do + expect(shipping_rate.display_price.to_s).to eq("$10.00 (+ $1.00 #{tax_rate.name})") + end + + context "when cost is zero" do + before do + shipping_rate.cost = 0 + end + + it "shows no tax amount" do + expect(shipping_rate.display_price.to_s).to eq("$0.00") + end + end + end + + context "when the currency is JPY" do + let(:shipping_rate) { shipping_rate = Spree::ShippingRate.new(:cost => 205) + allow(shipping_rate).to receive_messages(:currency => "JPY") + shipping_rate } + + it "displays the price in yen" do + expect(shipping_rate.display_price.to_s).to eq("¥205") + end + end + end + + # Regression test for #3829 + context "#shipping_method" do + it "can be retrieved" do + expect(shipping_rate.shipping_method.reload).to eq(shipping_method) + end + + it "can be retrieved even when deleted" do + shipping_method.update_column(:deleted_at, Time.now) + shipping_rate.save + shipping_rate.reload + expect(shipping_rate.shipping_method).to eq(shipping_method) + end + end + + context "#tax_rate" do + let!(:tax_rate) { create(:tax_rate) } + + before do + shipping_rate.tax_rate = tax_rate + end + + it "can be retrieved" do + expect(shipping_rate.tax_rate.reload).to eq(tax_rate) + end + + it "can be retrieved even when deleted" do + tax_rate.update_column(:deleted_at, Time.now) + shipping_rate.save + shipping_rate.reload + expect(shipping_rate.tax_rate).to eq(tax_rate) + end + end + + context "#shipping_method_code" do + before do + shipping_method.code = "THE_CODE" + end + + it 'should be shipping_method.code' do + expect(shipping_rate.shipping_method_code).to eq("THE_CODE") + end + end +end diff --git a/core/spec/models/spree/state_spec.rb b/core/spec/models/spree/state_spec.rb new file mode 100644 index 00000000000..bf8db223ffd --- /dev/null +++ b/core/spec/models/spree/state_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Spree::State, :type => :model do + it "can find a state by name or abbr" do + state = create(:state, :name => "California", :abbr => "CA") + expect(Spree::State.find_all_by_name_or_abbr("California")).to include(state) + expect(Spree::State.find_all_by_name_or_abbr("CA")).to include(state) + end + + it "can find all states group by country id" do + state = create(:state) + expect(Spree::State.states_group_by_country_id).to eq({ state.country_id.to_s => [[state.id, state.name]] }) + end +end diff --git a/core/spec/models/spree/stock/availability_validator_spec.rb b/core/spec/models/spree/stock/availability_validator_spec.rb new file mode 100644 index 00000000000..6e89871a0de --- /dev/null +++ b/core/spec/models/spree/stock/availability_validator_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +module Spree + module Stock + describe AvailabilityValidator, :type => :model do + let!(:line_item) { double(quantity: 5, variant_id: 1, variant: double.as_null_object, errors: double('errors'), inventory_units: []) } + + subject { described_class.new } + + it 'should be valid when supply is sufficient' do + allow_any_instance_of(Stock::Quantifier).to receive_messages(can_supply?: true) + expect(line_item).not_to receive(:errors) + subject.validate(line_item) + end + + it 'should be invalid when supply is insufficent' do + allow_any_instance_of(Stock::Quantifier).to receive_messages(can_supply?: false) + expect(line_item.errors).to receive(:[]).with(:quantity).and_return [] + subject.validate(line_item) + end + + it 'should consider existing inventory_units sufficient' do + allow_any_instance_of(Stock::Quantifier).to receive_messages(can_supply?: false) + expect(line_item).not_to receive(:errors) + allow(line_item).to receive_messages(inventory_units: [double] * 5) + subject.validate(line_item) + end + + it 'should be valid when the quantity is zero' do + expect(line_item).to receive(:quantity).and_return(0) + expect(line_item.errors).to_not receive(:[]).with(:quantity) + subject.validate(line_item) + end + end + end +end diff --git a/core/spec/models/spree/stock/coordinator_spec.rb b/core/spec/models/spree/stock/coordinator_spec.rb new file mode 100644 index 00000000000..6d159c792a2 --- /dev/null +++ b/core/spec/models/spree/stock/coordinator_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +module Spree + module Stock + describe Coordinator, :type => :model do + let!(:order) { create(:order_with_line_items) } + + subject { Coordinator.new(order) } + + context "packages" do + it "builds, prioritizes and estimates" do + expect(subject).to receive(:build_packages).ordered + expect(subject).to receive(:prioritize_packages).ordered + expect(subject).to receive(:estimate_packages).ordered + subject.packages + end + end + + describe "#shipments" do + let(:packages) { [build(:stock_package_fulfilled), build(:stock_package_fulfilled)] } + + before { allow(subject).to receive(:packages).and_return(packages) } + + it "turns packages into shipments" do + shipments = subject.shipments + expect(shipments.count).to eq packages.count + shipments.each { |shipment| expect(shipment).to be_a Shipment } + end + + it "puts the order's ship address on the shipments" do + shipments = subject.shipments + shipments.each { |shipment| expect(shipment.address).to eq order.ship_address } + end + end + + context "build packages" do + it "builds a package for every stock location" do + subject.packages.count == StockLocation.count + end + + context "missing stock items in stock location" do + let!(:another_location) { create(:stock_location, propagate_all_variants: false) } + + it "builds packages only for valid stock locations" do + expect(subject.build_packages.count).to eq(StockLocation.count - 1) + end + end + end + end + end +end diff --git a/core/spec/models/spree/stock/differentiator_spec.rb b/core/spec/models/spree/stock/differentiator_spec.rb new file mode 100644 index 00000000000..643ca133bc3 --- /dev/null +++ b/core/spec/models/spree/stock/differentiator_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +module Spree + module Stock + describe Differentiator, :type => :model do + let(:variant1) { mock_model(Variant) } + let(:variant2) { mock_model(Variant) } + + let(:line_item1) { build(:line_item, variant: variant1, quantity: 2) } + let(:line_item2) { build(:line_item, variant: variant2, quantity: 2) } + + let(:stock_location) { mock_model(StockLocation) } + + let(:inventory_unit1) { build(:inventory_unit, variant: variant1, line_item: line_item1) } + let(:inventory_unit2) { build(:inventory_unit, variant: variant2, line_item: line_item2) } + + let(:order) { mock_model(Order, line_items: [line_item1, line_item2]) } + + let(:package1) do + Package.new(stock_location).tap { |p| p.add(inventory_unit1) } + end + + let(:package2) do + Package.new(stock_location).tap { |p| p.add(inventory_unit2) } + end + + let(:packages) { [package1, package2] } + + subject { Differentiator.new(order, packages) } + + it { is_expected.to be_missing } + + it 'calculates the missing items' do + expect(subject.missing[variant1]).to eq 1 + expect(subject.missing[variant2]).to eq 1 + end + end + end +end diff --git a/core/spec/models/spree/stock/estimator_spec.rb b/core/spec/models/spree/stock/estimator_spec.rb new file mode 100644 index 00000000000..ed6e9f5e7d9 --- /dev/null +++ b/core/spec/models/spree/stock/estimator_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +module Spree + module Stock + describe Estimator, :type => :model do + let!(:shipping_method) { create(:shipping_method) } + let(:package) { build(:stock_package, contents: inventory_units.map { |i| ContentItem.new(inventory_unit) }) } + let(:order) { build(:order_with_line_items) } + let(:inventory_units) { order.inventory_units } + + subject { Estimator.new(order) } + + context "#shipping rates" do + before(:each) do + shipping_method.zones.first.members.create(:zoneable => order.ship_address.country) + allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :available?).and_return(true) + allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :compute).and_return(4.00) + allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :preferences).and_return({:currency => currency}) + allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :marked_for_destruction?) + + allow(package).to receive_messages(:shipping_methods => [shipping_method]) + end + + let(:currency) { "USD" } + + shared_examples_for "shipping rate matches" do + it "returns shipping rates" do + shipping_rates = subject.shipping_rates(package) + expect(shipping_rates.first.cost).to eq 4.00 + end + end + + shared_examples_for "shipping rate doesn't match" do + it "does not return shipping rates" do + shipping_rates = subject.shipping_rates(package) + expect(shipping_rates).to eq([]) + end + end + + context "when the order's ship address is in the same zone" do + it_should_behave_like "shipping rate matches" + end + + context "when the order's ship address is in a different zone" do + before { shipping_method.zones.each{|z| z.members.delete_all} } + it_should_behave_like "shipping rate doesn't match" + end + + context "when the calculator is not available for that order" do + before { allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :available?).and_return(false) } + it_should_behave_like "shipping rate doesn't match" + end + + context "when the currency is nil" do + let(:currency) { nil } + it_should_behave_like "shipping rate matches" + end + + context "when the currency is an empty string" do + let(:currency) { "" } + it_should_behave_like "shipping rate matches" + end + + context "when the current matches the order's currency" do + it_should_behave_like "shipping rate matches" + end + + context "if the currency is different than the order's currency" do + let(:currency) { "GBP" } + it_should_behave_like "shipping rate doesn't match" + end + + context "when the shipping method's calculator raises an exception" do + before do + allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :available?).and_raise(Exception, "Something went wrong!") + expect(subject).to receive(:log_calculator_exception) + end + it_should_behave_like "shipping rate doesn't match" + end + + it "sorts shipping rates by cost" do + shipping_methods = 3.times.map { create(:shipping_method) } + allow(shipping_methods[0]).to receive_message_chain(:calculator, :compute).and_return(5.00) + allow(shipping_methods[1]).to receive_message_chain(:calculator, :compute).and_return(3.00) + allow(shipping_methods[2]).to receive_message_chain(:calculator, :compute).and_return(4.00) + + allow(subject).to receive(:shipping_methods).and_return(shipping_methods) + + expect(subject.shipping_rates(package).map(&:cost)).to eq %w[3.00 4.00 5.00].map(&BigDecimal.method(:new)) + end + + context "general shipping methods" do + let(:shipping_methods) { 2.times.map { create(:shipping_method) } } + + it "selects the most affordable shipping rate" do + allow(shipping_methods[0]).to receive_message_chain(:calculator, :compute).and_return(5.00) + allow(shipping_methods[1]).to receive_message_chain(:calculator, :compute).and_return(3.00) + + allow(subject).to receive(:shipping_methods).and_return(shipping_methods) + + expect(subject.shipping_rates(package).sort_by(&:cost).map(&:selected)).to eq [true, false] + end + + it "selects the most affordable shipping rate and doesn't raise exception over nil cost" do + allow(shipping_methods[0]).to receive_message_chain(:calculator, :compute).and_return(1.00) + allow(shipping_methods[1]).to receive_message_chain(:calculator, :compute).and_return(nil) + + allow(subject).to receive(:shipping_methods).and_return(shipping_methods) + + subject.shipping_rates(package) + end + end + + context "involves backend only shipping methods" do + let(:backend_method) { create(:shipping_method, display_on: "back_end") } + let(:generic_method) { create(:shipping_method) } + + before do + allow(backend_method).to receive_message_chain(:calculator, :compute).and_return(0.00) + allow(generic_method).to receive_message_chain(:calculator, :compute).and_return(5.00) + allow(subject).to receive(:shipping_methods).and_return([backend_method, generic_method]) + end + + it "does not return backend rates at all" do + expect(subject.shipping_rates(package).map(&:shipping_method_id)).to eq([generic_method.id]) + end + + # regression for #3287 + it "doesn't select backend rates even if they're more affordable" do + expect(subject.shipping_rates(package).map(&:selected)).to eq [true] + end + end + + context "includes tax adjustments if applicable" do + let!(:tax_rate) { create(:tax_rate, zone: order.tax_zone) } + + before do + Spree::ShippingMethod.all.each do |sm| + sm.tax_category_id = tax_rate.tax_category_id + sm.save + end + package.shipping_methods.map(&:reload) + end + + + it "links the shipping rate and the tax rate" do + shipping_rates = subject.shipping_rates(package) + expect(shipping_rates.first.tax_rate).to eq(tax_rate) + end + end + end + end + end +end diff --git a/core/spec/models/spree/stock/inventory_unit_builder_spec.rb b/core/spec/models/spree/stock/inventory_unit_builder_spec.rb new file mode 100644 index 00000000000..d37587879d0 --- /dev/null +++ b/core/spec/models/spree/stock/inventory_unit_builder_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +module Spree + module Stock + describe InventoryUnitBuilder, :type => :model do + let(:line_item_1) { build(:line_item) } + let(:line_item_2) { build(:line_item, quantity: 2) } + let(:order) { build(:order, line_items: [line_item_1, line_item_2]) } + + subject { InventoryUnitBuilder.new(order) } + + describe "#units" do + it "returns an inventory unit for each quantity for the order's line items" do + units = subject.units + expect(units.count).to eq 3 + expect(units.first.line_item).to eq line_item_1 + expect(units.first.variant).to eq line_item_1.variant + + expect(units[1].line_item).to eq line_item_2 + expect(units[1].variant).to eq line_item_2.variant + + expect(units[2].line_item).to eq line_item_2 + expect(units[2].variant).to eq line_item_2.variant + end + + it "builds the inventory units as pending" do + expect(subject.units.map(&:pending).uniq).to eq [true] + end + + it "associates the inventory units to the order" do + expect(subject.units.map(&:order).uniq).to eq [order] + end + + end + + end + end +end diff --git a/core/spec/models/spree/stock/package_spec.rb b/core/spec/models/spree/stock/package_spec.rb new file mode 100644 index 00000000000..7d4b03bd0d7 --- /dev/null +++ b/core/spec/models/spree/stock/package_spec.rb @@ -0,0 +1,163 @@ +require 'spec_helper' + +module Spree + module Stock + describe Package, :type => :model do + let(:variant) { build(:variant, weight: 25.0) } + let(:stock_location) { build(:stock_location) } + let(:order) { build(:order) } + + subject { Package.new(stock_location) } + + def build_inventory_unit + build(:inventory_unit, variant: variant) + end + + it 'calculates the weight of all the contents' do + 4.times { subject.add build_inventory_unit } + expect(subject.weight).to eq(100.0) + end + + it 'filters by on_hand and backordered' do + 4.times { subject.add build_inventory_unit } + 3.times { subject.add build_inventory_unit, :backordered } + expect(subject.on_hand.count).to eq 4 + expect(subject.backordered.count).to eq 3 + end + + it 'calculates the quantity by state' do + 4.times { subject.add build_inventory_unit } + 3.times { subject.add build_inventory_unit, :backordered } + + expect(subject.quantity).to eq 7 + expect(subject.quantity(:on_hand)).to eq 4 + expect(subject.quantity(:backordered)).to eq 3 + end + + it 'returns nil for content item not found' do + unit = build_inventory_unit + item = subject.find_item(unit, :on_hand) + expect(item).to be_nil + end + + it 'finds content item for an inventory unit' do + unit = build_inventory_unit + subject.add unit + item = subject.find_item(unit, :on_hand) + expect(item.quantity).to eq 1 + end + + # Contains regression test for #2804 + it 'builds a list of shipping methods common to all categories' do + category1 = create(:shipping_category) + category2 = create(:shipping_category) + method1 = create(:shipping_method) + method2 = create(:shipping_method) + method1.shipping_categories = [category1, category2] + method2.shipping_categories = [category1] + variant1 = mock_model(Variant, shipping_category: category1) + variant2 = mock_model(Variant, shipping_category: category2) + variant3 = mock_model(Variant, shipping_category: nil) + contents = [ContentItem.new(build(:inventory_unit, variant: variant1)), + ContentItem.new(build(:inventory_unit, variant: variant1)), + ContentItem.new(build(:inventory_unit, variant: variant2)), + ContentItem.new(build(:inventory_unit, variant: variant3))] + + package = Package.new(stock_location, contents) + expect(package.shipping_methods).to eq([method1]) + end + + it 'builds an empty list of shipping methods when no categories' do + variant = mock_model(Variant, shipping_category: nil) + contents = [ContentItem.new(build(:inventory_unit, variant: variant))] + package = Package.new(stock_location, contents) + expect(package.shipping_methods).to be_empty + end + + it "can convert to a shipment" do + 2.times { subject.add build_inventory_unit } + subject.add build_inventory_unit, :backordered + + shipping_method = build(:shipping_method) + subject.shipping_rates = [ Spree::ShippingRate.new(shipping_method: shipping_method, cost: 10.00, selected: true) ] + + shipment = subject.to_shipment + expect(shipment.stock_location).to eq subject.stock_location + expect(shipment.inventory_units.size).to eq 3 + + first_unit = shipment.inventory_units.first + expect(first_unit.variant).to eq variant + expect(first_unit.state).to eq 'on_hand' + expect(first_unit).to be_pending + + last_unit = shipment.inventory_units.last + expect(last_unit.variant).to eq variant + expect(last_unit.state).to eq 'backordered' + + expect(shipment.shipping_method).to eq shipping_method + end + + it 'does not add an inventory unit to a package twice' do + # since inventory units currently don't have a quantity + unit = build_inventory_unit + subject.add unit + subject.add unit + expect(subject.quantity).to eq 1 + expect(subject.contents.first.inventory_unit).to eq unit + expect(subject.contents.first.quantity).to eq 1 + end + + describe "#add_multiple" do + it "adds multiple inventory units" do + expect { subject.add_multiple [build_inventory_unit, build_inventory_unit] }.to change { subject.quantity }.by(2) + end + + it "allows adding with a state" do + expect { subject.add_multiple [build_inventory_unit, build_inventory_unit], :backordered }.to change { subject.backordered.count }.by(2) + end + + it "defaults to adding with the on hand state" do + expect { subject.add_multiple [build_inventory_unit, build_inventory_unit] }.to change { subject.on_hand.count }.by(2) + end + end + + describe "#remove" do + let(:unit) { build_inventory_unit } + context "there is a content item for the inventory unit" do + + before { subject.add unit } + + it "removes that content item" do + expect { subject.remove(unit) }.to change { subject.quantity }.by(-1) + expect(subject.contents.map(&:inventory_unit)).not_to include unit + end + end + + context "there is no content item for the inventory unit" do + it "doesn't change the set of content items" do + expect { subject.remove(unit) }.not_to change { subject.quantity } + end + end + end + + describe "#order" do + let(:unit) { build_inventory_unit } + context "there is an inventory unit" do + + before { subject.add unit } + + it "returns an order" do + expect(subject.order).to be_a_kind_of Spree::Order + expect(subject.order).to eq unit.order + end + end + + context "there is no inventory unit" do + it "returns nil" do + expect(subject.order).to eq nil + end + end + end + end + end +end diff --git a/core/spec/models/spree/stock/packer_spec.rb b/core/spec/models/spree/stock/packer_spec.rb new file mode 100644 index 00000000000..fdc9ecc0ba1 --- /dev/null +++ b/core/spec/models/spree/stock/packer_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +module Spree + module Stock + describe Packer, :type => :model do + let!(:inventory_units) { 5.times.map { build(:inventory_unit) } } + let(:stock_location) { create(:stock_location) } + + subject { Packer.new(stock_location, inventory_units) } + + context 'packages' do + it 'builds an array of packages' do + packages = subject.packages + expect(packages.size).to eq 1 + expect(packages.first.contents.size).to eq 5 + end + + it 'allows users to set splitters to an empty array' do + packages = Packer.new(stock_location, inventory_units, []).packages + expect(packages.size).to eq 1 + end + end + + context 'default_package' do + it 'contains all the items' do + package = subject.default_package + expect(package.contents.size).to eq 5 + end + + it 'variants are added as backordered without enough on_hand' do + expect(stock_location).to receive(:fill_status).exactly(5).times.and_return( + *(Array.new(3, [1,0]) + Array.new(2, [0,1])) + ) + + package = subject.default_package + expect(package.on_hand.size).to eq 3 + expect(package.backordered.size).to eq 2 + end + + context "location doesn't have order items in stock" do + let(:stock_location) { create(:stock_location, propagate_all_variants: false) } + let(:packer) { Packer.new(stock_location, inventory_units) } + + it "builds an empty package" do + expect(packer.default_package.contents).to be_empty + end + end + + context "doesn't track inventory levels" do + let(:variant) { build(:variant) } + let(:inventory_units) { 30.times.map { build(:inventory_unit, variant: variant) } } + + before { Config.track_inventory_levels = false } + + it "doesn't bother stock items status in stock location" do + expect(subject.stock_location).not_to receive(:fill_status) + subject.default_package + end + + it "still creates package with proper quantity" do + expect(subject.default_package.quantity).to eql 30 + end + end + end + end + end +end diff --git a/core/spec/models/spree/stock/prioritizer_spec.rb b/core/spec/models/spree/stock/prioritizer_spec.rb new file mode 100644 index 00000000000..f7fd858e19b --- /dev/null +++ b/core/spec/models/spree/stock/prioritizer_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +module Spree + module Stock + describe Prioritizer, :type => :model do + let(:order) { mock_model(Order) } + let(:stock_location) { build(:stock_location) } + let(:variant) { build(:variant) } + + def inventory_units + @inventory_units ||= [] + end + + def build_inventory_unit + mock_model(InventoryUnit, variant: variant).tap do |unit| + inventory_units << unit + end + end + + def pack + package = Package.new(order) + yield(package) if block_given? + package + end + + it 'keeps a single package' do + package1 = pack do |package| + package.add build_inventory_unit + package.add build_inventory_unit + end + + packages = [package1] + prioritizer = Prioritizer.new(inventory_units, packages) + packages = prioritizer.prioritized_packages + expect(packages.size).to eq 1 + end + + it 'removes duplicate packages' do + package1 = pack do |package| + package.add build_inventory_unit + package.add build_inventory_unit + end + + package2 = pack do |package| + package.add inventory_units.first + package.add inventory_units.last + end + + packages = [package1, package2] + prioritizer = Prioritizer.new(inventory_units, packages) + packages = prioritizer.prioritized_packages + expect(packages.size).to eq 1 + end + + it 'split over 2 packages' do + package1 = pack do |package| + package.add build_inventory_unit + end + package2 = pack do |package| + package.add build_inventory_unit + end + + packages = [package1, package2] + prioritizer = Prioritizer.new(inventory_units, packages) + packages = prioritizer.prioritized_packages + expect(packages.size).to eq 2 + end + + it '1st has some, 2nd has remaining' do + 5.times { build_inventory_unit } + + package1 = pack do |package| + 2.times { |i| package.add inventory_units[i] } + end + package2 = pack do |package| + 5.times { |i| package.add inventory_units[i] } + end + + packages = [package1, package2] + prioritizer = Prioritizer.new(inventory_units, packages) + packages = prioritizer.prioritized_packages + expect(packages.count).to eq 2 + expect(packages[0].quantity).to eq 2 + expect(packages[1].quantity).to eq 3 + end + + it '1st has backorder, 2nd has some' do + 5.times { build_inventory_unit } + + package1 = pack do |package| + 5.times { |i| package.add inventory_units[i], :backordered } + end + package2 = pack do |package| + 2.times { |i| package.add inventory_units[i] } + end + + packages = [package1, package2] + prioritizer = Prioritizer.new(inventory_units, packages) + packages = prioritizer.prioritized_packages + + expect(packages[0].quantity(:backordered)).to eq 3 + expect(packages[1].quantity(:on_hand)).to eq 2 + end + + it '1st has backorder, 2nd has all' do + 5.times { build_inventory_unit } + + package1 = pack do |package| + 3.times { |i| package.add inventory_units[i], :backordered } + end + package2 = pack do |package| + 5.times { |i| package.add inventory_units[i] } + end + + packages = [package1, package2] + prioritizer = Prioritizer.new(inventory_units, packages) + packages = prioritizer.prioritized_packages + expect(packages[0]).to eq package2 + expect(packages[1]).to be_nil + expect(packages[0].quantity(:backordered)).to eq 0 + expect(packages[0].quantity(:on_hand)).to eq 5 + end + end + end +end diff --git a/core/spec/models/spree/stock/quantifier_spec.rb b/core/spec/models/spree/stock/quantifier_spec.rb new file mode 100644 index 00000000000..f06e890c769 --- /dev/null +++ b/core/spec/models/spree/stock/quantifier_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +shared_examples_for 'unlimited supply' do + it 'can_supply? any amount' do + expect(subject.can_supply?(1)).to be true + expect(subject.can_supply?(101)).to be true + expect(subject.can_supply?(100_001)).to be true + end +end + +module Spree + module Stock + describe Quantifier, :type => :model do + + let!(:stock_location) { create :stock_location_with_items } + let!(:stock_item) { stock_location.stock_items.order(:id).first } + + subject { described_class.new(stock_item.variant) } + + specify { expect(subject.stock_items).to eq([stock_item]) } + + + context 'with a single stock location/item' do + it 'total_on_hand should match stock_item' do + expect(subject.total_on_hand).to eq(stock_item.count_on_hand) + end + + context 'when track_inventory_levels is false' do + before { configure_spree_preferences { |config| config.track_inventory_levels = false } } + + specify { expect(subject.total_on_hand).to eq(Float::INFINITY) } + + it_should_behave_like 'unlimited supply' + end + + context 'when variant inventory tracking is off' do + before { stock_item.variant.track_inventory = false } + + specify { expect(subject.total_on_hand).to eq(Float::INFINITY) } + + it_should_behave_like 'unlimited supply' + end + + context 'when stock item allows backordering' do + + specify { expect(subject.backorderable?).to be true } + + it_should_behave_like 'unlimited supply' + end + + context 'when stock item prevents backordering' do + before { stock_item.update_attributes(backorderable: false) } + + specify { expect(subject.backorderable?).to be false } + + it 'can_supply? only upto total_on_hand' do + expect(subject.can_supply?(1)).to be true + expect(subject.can_supply?(10)).to be true + expect(subject.can_supply?(11)).to be false + end + end + + end + + context 'with multiple stock locations/items' do + let!(:stock_location_2) { create :stock_location } + let!(:stock_location_3) { create :stock_location, active: false } + + before do + stock_location_2.stock_items.where(variant_id: stock_item.variant).update_all(count_on_hand: 5, backorderable: false) + stock_location_3.stock_items.where(variant_id: stock_item.variant).update_all(count_on_hand: 5, backorderable: false) + end + + it 'total_on_hand should total all active stock_items' do + expect(subject.total_on_hand).to eq(15) + end + + context 'when any stock item allows backordering' do + specify { expect(subject.backorderable?).to be true } + + it_should_behave_like 'unlimited supply' + end + + context 'when all stock items prevent backordering' do + before { stock_item.update_attributes(backorderable: false) } + + specify { expect(subject.backorderable?).to be false } + + it 'can_supply? upto total_on_hand' do + expect(subject.can_supply?(1)).to be true + expect(subject.can_supply?(15)).to be true + expect(subject.can_supply?(16)).to be false + end + end + + end + + end + end +end diff --git a/core/spec/models/spree/stock/splitter/backordered_spec.rb b/core/spec/models/spree/stock/splitter/backordered_spec.rb new file mode 100644 index 00000000000..97d9bde96a9 --- /dev/null +++ b/core/spec/models/spree/stock/splitter/backordered_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +module Spree + module Stock + module Splitter + describe Backordered, :type => :model do + let(:variant) { build(:variant) } + + let(:packer) { build(:stock_packer) } + + subject { Backordered.new(packer) } + + it 'splits packages by status' do + package = Package.new(packer.stock_location) + 4.times { package.add build(:inventory_unit, variant: variant) } + 5.times { package.add build(:inventory_unit, variant: variant), :backordered } + + packages = subject.split([package]) + expect(packages.count).to eq 2 + expect(packages.first.quantity).to eq 4 + expect(packages.first.on_hand.count).to eq 4 + expect(packages.first.backordered.count).to eq 0 + + expect(packages[1].contents.count).to eq 5 + end + end + end + end +end diff --git a/core/spec/models/spree/stock/splitter/base_spec.rb b/core/spec/models/spree/stock/splitter/base_spec.rb new file mode 100644 index 00000000000..ad64d10c709 --- /dev/null +++ b/core/spec/models/spree/stock/splitter/base_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +module Spree + module Stock + module Splitter + describe Base, :type => :model do + let(:packer) { build(:stock_packer) } + + it 'continues to splitter chain' do + splitter1 = Base.new(packer) + splitter2 = Base.new(packer, splitter1) + packages = [] + + expect(splitter1).to receive(:split).with(packages) + splitter2.split(packages) + end + + end + end + end +end diff --git a/core/spec/models/spree/stock/splitter/shipping_category_spec.rb b/core/spec/models/spree/stock/splitter/shipping_category_spec.rb new file mode 100644 index 00000000000..554dc0862ea --- /dev/null +++ b/core/spec/models/spree/stock/splitter/shipping_category_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +module Spree + module Stock + module Splitter + describe ShippingCategory, :type => :model do + + let(:variant1) { build(:variant) } + let(:variant2) { build(:variant) } + let(:shipping_category_1) { create(:shipping_category, name: 'A') } + let(:shipping_category_2) { create(:shipping_category, name: 'B') } + + def inventory_unit1 + build(:inventory_unit, variant: variant1).tap do |inventory_unit| + inventory_unit.variant.product.shipping_category = shipping_category_1 + end + end + + def inventory_unit2 + build(:inventory_unit, variant: variant2).tap do |inventory_unit| + inventory_unit.variant.product.shipping_category = shipping_category_2 + end + end + + let(:packer) { build(:stock_packer) } + + subject { ShippingCategory.new(packer) } + + it 'splits each package by shipping category' do + package1 = Package.new(packer.stock_location) + 4.times { package1.add inventory_unit1 } + 8.times { package1.add inventory_unit2 } + + package2 = Package.new(packer.stock_location) + 6.times { package2.add inventory_unit1 } + 9.times { package2.add inventory_unit2, :backordered } + + packages = subject.split([package1, package2]) + expect(packages[0].quantity).to eq 4 + expect(packages[1].quantity).to eq 8 + expect(packages[2].quantity).to eq 6 + expect(packages[3].quantity).to eq 9 + end + + end + end + end +end diff --git a/core/spec/models/spree/stock/splitter/weight_spec.rb b/core/spec/models/spree/stock/splitter/weight_spec.rb new file mode 100644 index 00000000000..4d788794625 --- /dev/null +++ b/core/spec/models/spree/stock/splitter/weight_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +module Spree + module Stock + module Splitter + describe Weight, :type => :model do + let(:packer) { build(:stock_packer) } + let(:variant) { build(:base_variant, :weight => 100) } + + subject { Weight.new(packer) } + + it 'splits and keeps splitting until all packages are underweight' do + package = Package.new(packer.stock_location) + 4.times { package.add build(:inventory_unit, variant: variant) } + packages = subject.split([package]) + expect(packages.size).to eq 4 + end + + it 'handles packages that can not be reduced' do + package = Package.new(packer.stock_location) + allow(variant).to receive_messages(:weight => 200) + 2.times { package.add build(:inventory_unit, variant: variant) } + packages = subject.split([package]) + expect(packages.size).to eq 2 + end + end + end + end +end diff --git a/core/spec/models/spree/stock_item_spec.rb b/core/spec/models/spree/stock_item_spec.rb new file mode 100644 index 00000000000..1ae2d04ad32 --- /dev/null +++ b/core/spec/models/spree/stock_item_spec.rb @@ -0,0 +1,410 @@ +require 'spec_helper' + +describe Spree::StockItem, :type => :model do + let(:stock_location) { create(:stock_location_with_items) } + + subject { stock_location.stock_items.order(:id).first } + + it 'maintains the count on hand for a variant' do + expect(subject.count_on_hand).to eq 10 + end + + it "can return the stock item's variant's name" do + expect(subject.variant_name).to eq(subject.variant.name) + end + + context "available to be included in shipment" do + context "has stock" do + it { expect(subject).to be_available } + end + + context "backorderable" do + before { subject.backorderable = true } + it { expect(subject).to be_available } + end + + context "no stock and not backorderable" do + before do + subject.backorderable = false + allow(subject).to receive_messages(count_on_hand: 0) + end + + it { expect(subject).not_to be_available } + end + end + + describe 'reduce_count_on_hand_to_zero' do + context 'when count_on_hand > 0' do + before(:each) do + subject.update_column('count_on_hand', 4) + subject.reduce_count_on_hand_to_zero + end + + it { expect(subject.count_on_hand).to eq(0) } + end + + context 'when count_on_hand > 0' do + before(:each) do + subject.update_column('count_on_hand', -4) + @count_on_hand = subject.count_on_hand + subject.reduce_count_on_hand_to_zero + end + + it { expect(subject.count_on_hand).to eq(@count_on_hand) } + end + end + + context "adjust count_on_hand" do + let!(:current_on_hand) { subject.count_on_hand } + + it 'is updated pessimistically' do + copy = Spree::StockItem.find(subject.id) + + subject.adjust_count_on_hand(5) + expect(subject.count_on_hand).to eq(current_on_hand + 5) + + expect(copy.count_on_hand).to eq(current_on_hand) + copy.adjust_count_on_hand(5) + expect(copy.count_on_hand).to eq(current_on_hand + 10) + end + + context "item out of stock (by two items)" do + let(:inventory_unit) { double('InventoryUnit') } + let(:inventory_unit_2) { double('InventoryUnit2') } + + before do + allow(subject).to receive_messages(:backordered_inventory_units => [inventory_unit, inventory_unit_2]) + subject.update_column(:count_on_hand, -2) + end + + # Regression test for #3755 + it "processes existing backorders, even with negative stock" do + expect(inventory_unit).to receive(:fill_backorder) + expect(inventory_unit_2).not_to receive(:fill_backorder) + subject.adjust_count_on_hand(1) + expect(subject.count_on_hand).to eq(-1) + end + + # Test for #3755 + it "does not process backorders when stock is adjusted negatively" do + expect(inventory_unit).not_to receive(:fill_backorder) + expect(inventory_unit_2).not_to receive(:fill_backorder) + subject.adjust_count_on_hand(-1) + expect(subject.count_on_hand).to eq(-3) + end + + context "adds new items" do + before { allow(subject).to receive_messages(:backordered_inventory_units => [inventory_unit, inventory_unit_2]) } + + it "fills existing backorders" do + expect(inventory_unit).to receive(:fill_backorder) + expect(inventory_unit_2).to receive(:fill_backorder) + + subject.adjust_count_on_hand(3) + expect(subject.count_on_hand).to eq(1) + end + end + end + end + + context "set count_on_hand" do + let!(:current_on_hand) { subject.count_on_hand } + + it 'is updated pessimistically' do + copy = Spree::StockItem.find(subject.id) + + subject.set_count_on_hand(5) + expect(subject.count_on_hand).to eq(5) + + expect(copy.count_on_hand).to eq(current_on_hand) + copy.set_count_on_hand(10) + expect(copy.count_on_hand).to eq(current_on_hand) + end + + context "item out of stock (by two items)" do + let(:inventory_unit) { double('InventoryUnit') } + let(:inventory_unit_2) { double('InventoryUnit2') } + + before { subject.set_count_on_hand(-2) } + + it "doesn't process backorders" do + expect(subject).not_to receive(:backordered_inventory_units) + end + + context "adds new items" do + before { allow(subject).to receive_messages(:backordered_inventory_units => [inventory_unit, inventory_unit_2]) } + + it "fills existing backorders" do + expect(inventory_unit).to receive(:fill_backorder) + expect(inventory_unit_2).to receive(:fill_backorder) + + subject.set_count_on_hand(1) + expect(subject.count_on_hand).to eq(1) + end + end + end + end + + context "with stock movements" do + before { Spree::StockMovement.create(stock_item: subject, quantity: 1) } + + it "doesnt raise ReadOnlyRecord error" do + expect { subject.destroy }.not_to raise_error + end + end + + context "destroyed" do + before { subject.destroy } + + it "recreates stock item just fine" do + expect { + stock_location.stock_items.create!(variant: subject.variant) + }.not_to raise_error + end + + it "doesnt allow recreating more than one stock item at once" do + stock_location.stock_items.create!(variant: subject.variant) + + expect { + stock_location.stock_items.create!(variant: subject.variant) + }.to raise_error + end + end + + describe "#after_save" do + before do + subject.variant.update_column(:updated_at, 1.day.ago) + end + + context "binary_inventory_cache is set to false (default)" do + context "in_stock? changes" do + it "touches its variant" do + expect do + subject.adjust_count_on_hand(subject.count_on_hand * -1) + end.to change { subject.variant.reload.updated_at } + end + end + + context "in_stock? does not change" do + it "touches its variant" do + expect do + subject.adjust_count_on_hand((subject.count_on_hand * -1) + 1) + end.to change { subject.variant.reload.updated_at } + end + end + end + + context "binary_inventory_cache is set to true" do + before { Spree::Config.binary_inventory_cache = true } + context "in_stock? changes" do + it "touches its variant" do + expect do + subject.adjust_count_on_hand(subject.count_on_hand * -1) + end.to change { subject.variant.reload.updated_at } + end + end + + context "in_stock? does not change" do + it "does not touch its variant" do + expect do + subject.adjust_count_on_hand((subject.count_on_hand * -1) + 1) + end.not_to change { subject.variant.reload.updated_at } + end + end + + context "when a new stock location is added" do + it "touches its variant" do + expect do + create(:stock_location) + end.to change { subject.variant.reload.updated_at } + end + end + end + end + + describe "#after_touch" do + it "touches its variant" do + expect do + subject.touch + end.to change { subject.variant.updated_at } + end + end + + # Regression test for #4651 + context "variant" do + it "can be found even if the variant is deleted" do + subject.variant.destroy + expect(subject.reload.variant).not_to be_nil + end + end + + describe 'validations' do + describe 'count_on_hand' do + shared_examples_for 'valid count_on_hand' do + before(:each) do + subject.save + end + + it 'has :no errors_on' do + expect(subject.errors_on(:count_on_hand).size).to eq(0) + end + end + + shared_examples_for 'not valid count_on_hand' do + before(:each) do + subject.save + end + + it 'has 1 error_on' do + expect(subject.error_on(:count_on_hand).size).to eq(1) + end + it { expect(subject.errors[:count_on_hand]).to include 'must be greater than or equal to 0' } + end + + context 'when count_on_hand not changed' do + context 'when not backorderable' do + before(:each) do + subject.backorderable = false + end + it_should_behave_like 'valid count_on_hand' + end + + context 'when backorderable' do + before(:each) do + subject.backorderable = true + end + it_should_behave_like 'valid count_on_hand' + end + end + + context 'when count_on_hand changed' do + context 'when backorderable' do + before(:each) do + subject.backorderable = true + end + context 'when both count_on_hand and count_on_hand_was are positive' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, 3) + subject.send(:count_on_hand=, subject.count_on_hand + 3) + end + it_should_behave_like 'valid count_on_hand' + end + + context 'when count_on_hand is smaller than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, 3) + subject.send(:count_on_hand=, subject.count_on_hand - 2) + end + + it_should_behave_like 'valid count_on_hand' + end + end + + context 'when both count_on_hand and count_on_hand_was are negative' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, -3) + subject.send(:count_on_hand=, subject.count_on_hand + 2) + end + it_should_behave_like 'valid count_on_hand' + end + + context 'when count_on_hand is smaller than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, 3) + subject.send(:count_on_hand=, subject.count_on_hand - 3) + end + + it_should_behave_like 'valid count_on_hand' + end + end + + context 'when both count_on_hand is positive and count_on_hand_was is negative' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, -3) + subject.send(:count_on_hand=, subject.count_on_hand + 6) + end + it_should_behave_like 'valid count_on_hand' + end + end + + context 'when both count_on_hand is negative and count_on_hand_was is positive' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, 3) + subject.send(:count_on_hand=, subject.count_on_hand - 6) + end + it_should_behave_like 'valid count_on_hand' + end + end + end + + context 'when not backorderable' do + before(:each) do + subject.backorderable = false + end + + context 'when both count_on_hand and count_on_hand_was are positive' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, 3) + subject.send(:count_on_hand=, subject.count_on_hand + 3) + end + it_should_behave_like 'valid count_on_hand' + end + + context 'when count_on_hand is smaller than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, 3) + subject.send(:count_on_hand=, subject.count_on_hand - 2) + end + + it_should_behave_like 'valid count_on_hand' + end + end + + context 'when both count_on_hand and count_on_hand_was are negative' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, -3) + subject.send(:count_on_hand=, subject.count_on_hand + 2) + end + it_should_behave_like 'valid count_on_hand' + end + + context 'when count_on_hand is smaller than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, -3) + subject.send(:count_on_hand=, subject.count_on_hand - 3) + end + + it_should_behave_like 'not valid count_on_hand' + end + end + + context 'when both count_on_hand is positive and count_on_hand_was is negative' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, -3) + subject.send(:count_on_hand=, subject.count_on_hand + 6) + end + it_should_behave_like 'valid count_on_hand' + end + end + + context 'when both count_on_hand is negative and count_on_hand_was is positive' do + context 'when count_on_hand is greater than count_on_hand_was' do + before(:each) do + subject.update_column(:count_on_hand, 3) + subject.send(:count_on_hand=, subject.count_on_hand - 6) + end + it_should_behave_like 'not valid count_on_hand' + end + end + end + end + end + end +end diff --git a/core/spec/models/spree/stock_location_spec.rb b/core/spec/models/spree/stock_location_spec.rb new file mode 100644 index 00000000000..9388e4b20f4 --- /dev/null +++ b/core/spec/models/spree/stock_location_spec.rb @@ -0,0 +1,248 @@ +require 'spec_helper' + +module Spree + describe StockLocation, :type => :model do + subject { create(:stock_location_with_items, backorderable_default: true) } + let(:stock_item) { subject.stock_items.order(:id).first } + let(:variant) { stock_item.variant } + + it 'creates stock_items for all variants' do + expect(subject.stock_items.count).to eq Variant.count + end + + context "handling stock items" do + let!(:variant) { create(:variant) } + + context "given a variant" do + subject { StockLocation.create(name: "testing", propagate_all_variants: false) } + + context "set up" do + it "creates stock item" do + expect(subject).to receive(:propagate_variant) + subject.set_up_stock_item(variant) + end + + context "stock item exists" do + let!(:stock_item) { subject.propagate_variant(variant) } + + it "returns existing stock item" do + expect(subject.set_up_stock_item(variant)).to eq(stock_item) + end + end + end + + context "propagate variants" do + let(:stock_item) { subject.propagate_variant(variant) } + + it "creates a new stock item" do + expect { + subject.propagate_variant(variant) + }.to change{ StockItem.count }.by(1) + end + + context "passes backorderable default config" do + context "true" do + before { subject.backorderable_default = true } + it { expect(stock_item.backorderable).to be true } + end + + context "false" do + before { subject.backorderable_default = false } + it { expect(stock_item.backorderable).to be false } + end + end + end + + context "propagate all variants" do + subject { StockLocation.new(name: "testing") } + + context "true" do + before { subject.propagate_all_variants = true } + + specify do + expect(subject).to receive(:propagate_variant).at_least(:once) + subject.save! + end + end + + context "false" do + before { subject.propagate_all_variants = false } + + specify do + expect(subject).not_to receive(:propagate_variant) + subject.save! + end + end + end + end + end + + it 'finds a stock_item for a variant' do + stock_item = subject.stock_item(variant) + expect(stock_item.count_on_hand).to eq 10 + end + + it 'finds a stock_item for a variant by id' do + stock_item = subject.stock_item(variant.id) + expect(stock_item.variant).to eq variant + end + + it 'returns nil when stock_item is not found for variant' do + stock_item = subject.stock_item(100) + expect(stock_item).to be_nil + end + + describe '#stock_item_or_create' do + before do + variant = create(:variant) + variant.stock_items.destroy_all + variant.save + end + + it 'creates a stock_item if not found for a variant' do + stock_item = subject.stock_item_or_create(variant) + expect(stock_item.variant).to eq variant + end + + it 'creates a stock_item if not found for a variant_id' do + stock_item = subject.stock_item_or_create(variant.id) + expect(stock_item.variant).to eq variant + end + end + + it 'finds a count_on_hand for a variant' do + expect(subject.count_on_hand(variant)).to eq 10 + end + + it 'finds determines if you a variant is backorderable' do + expect(subject.backorderable?(variant)).to be true + end + + it 'restocks a variant with a positive stock movement' do + originator = double + expect(subject).to receive(:move).with(variant, 5, originator) + subject.restock(variant, 5, originator) + end + + it 'unstocks a variant with a negative stock movement' do + originator = double + expect(subject).to receive(:move).with(variant, -5, originator) + subject.unstock(variant, 5, originator) + end + + it 'it creates a stock_movement' do + expect { + subject.move variant, 5 + }.to change { subject.stock_movements.where(stock_item_id: stock_item).count }.by(1) + end + + it 'can be deactivated' do + create(:stock_location, :active => true) + create(:stock_location, :active => false) + expect(Spree::StockLocation.active.count).to eq 1 + end + + it 'ensures only one stock location is default at a time' do + first = create(:stock_location, :active => true, :default => true) + second = create(:stock_location, :active => true, :default => true) + + expect(first.reload.default).to eq false + expect(second.reload.default).to eq true + + first.default = true + first.save! + + expect(first.reload.default).to eq true + expect(second.reload.default).to eq false + end + + context 'fill_status' do + it 'all on_hand with no backordered' do + on_hand, backordered = subject.fill_status(variant, 5) + expect(on_hand).to eq 5 + expect(backordered).to eq 0 + end + + it 'some on_hand with some backordered' do + on_hand, backordered = subject.fill_status(variant, 20) + expect(on_hand).to eq 10 + expect(backordered).to eq 10 + end + + it 'zero on_hand with all backordered' do + zero_stock_item = mock_model(StockItem, + count_on_hand: 0, + backorderable?: true) + expect(subject).to receive(:stock_item).with(variant).and_return(zero_stock_item) + + on_hand, backordered = subject.fill_status(variant, 20) + expect(on_hand).to eq 0 + expect(backordered).to eq 20 + end + + context 'when backordering is not allowed' do + before do + @stock_item = mock_model(StockItem, backorderable?: false) + expect(subject).to receive(:stock_item).with(variant).and_return(@stock_item) + end + + it 'all on_hand' do + allow(@stock_item).to receive_messages(count_on_hand: 10) + + on_hand, backordered = subject.fill_status(variant, 5) + expect(on_hand).to eq 5 + expect(backordered).to eq 0 + end + + it 'some on_hand' do + allow(@stock_item).to receive_messages(count_on_hand: 10) + + on_hand, backordered = subject.fill_status(variant, 20) + expect(on_hand).to eq 10 + expect(backordered).to eq 0 + end + + it 'zero on_hand' do + allow(@stock_item).to receive_messages(count_on_hand: 0) + + on_hand, backordered = subject.fill_status(variant, 20) + expect(on_hand).to eq 0 + expect(backordered).to eq 0 + end + end + + context 'without stock_items' do + subject { create(:stock_location) } + let(:variant) { create(:base_variant) } + + it 'zero on_hand and backordered' do + subject + variant.stock_items.destroy_all + on_hand, backordered = subject.fill_status(variant, 1) + expect(on_hand).to eq 0 + expect(backordered).to eq 0 + end + end + end + + context '#state_text' do + context 'state is blank' do + subject { StockLocation.create(name: "testing", state: nil, state_name: 'virginia') } + specify { expect(subject.state_text).to eq('virginia') } + end + + context 'both name and abbr is present' do + let(:state) { stub_model(Spree::State, name: 'virginia', abbr: 'va') } + subject { StockLocation.create(name: "testing", state: state, state_name: nil) } + specify { expect(subject.state_text).to eq('va') } + end + + context 'only name is present' do + let(:state) { stub_model(Spree::State, name: 'virginia', abbr: nil) } + subject { StockLocation.create(name: "testing", state: state, state_name: nil) } + specify { expect(subject.state_text).to eq('virginia') } + end + end + + end +end diff --git a/core/spec/models/spree/stock_movement_spec.rb b/core/spec/models/spree/stock_movement_spec.rb new file mode 100644 index 00000000000..33a178651b3 --- /dev/null +++ b/core/spec/models/spree/stock_movement_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Spree::StockMovement, :type => :model do + let(:stock_location) { create(:stock_location_with_items) } + let(:stock_item) { stock_location.stock_items.order(:id).first } + subject { build(:stock_movement, stock_item: stock_item) } + + it 'should belong to a stock item' do + expect(subject).to respond_to(:stock_item) + end + + it 'is readonly unless new' do + subject.save + expect { + subject.save + }.to raise_error(ActiveRecord::ReadOnlyRecord) + end + + it 'does not update count on hand when track inventory levels is false' do + Spree::Config[:track_inventory_levels] = false + subject.quantity = 1 + subject.save + stock_item.reload + expect(stock_item.count_on_hand).to eq(10) + end + + it 'does not update count on hand when variant inventory tracking is off' do + stock_item.variant.track_inventory = false + subject.quantity = 1 + subject.save + stock_item.reload + expect(stock_item.count_on_hand).to eq(10) + end + + context "when quantity is negative" do + context "after save" do + it "should decrement the stock item count on hand" do + subject.quantity = -1 + subject.save + stock_item.reload + expect(stock_item.count_on_hand).to eq(9) + end + end + end + + context "when quantity is positive" do + context "after save" do + it "should increment the stock item count on hand" do + subject.quantity = 1 + subject.save + stock_item.reload + expect(stock_item.count_on_hand).to eq(11) + end + end + end +end diff --git a/core/spec/models/spree/stock_transfer_spec.rb b/core/spec/models/spree/stock_transfer_spec.rb new file mode 100644 index 00000000000..23f94b942dd --- /dev/null +++ b/core/spec/models/spree/stock_transfer_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +module Spree + describe StockTransfer, :type => :model do + let(:destination_location) { create(:stock_location_with_items) } + let(:source_location) { create(:stock_location_with_items) } + let(:stock_item) { source_location.stock_items.order(:id).first } + let(:variant) { stock_item.variant } + + subject { StockTransfer.create(reference: 'PO123') } + + describe '#reference' do + subject { super().reference } + it { is_expected.to eq 'PO123' } + end + + describe '#to_param' do + subject { super().to_param } + it { is_expected.to match /T\d+/ } + end + + it 'transfers variants between 2 locations' do + variants = { variant => 5 } + + subject.transfer(source_location, + destination_location, + variants) + + expect(source_location.count_on_hand(variant)).to eq 5 + expect(destination_location.count_on_hand(variant)).to eq 5 + + expect(subject.source_location).to eq source_location + expect(subject.destination_location).to eq destination_location + + expect(subject.source_movements.first.quantity).to eq -5 + expect(subject.destination_movements.first.quantity).to eq 5 + end + + it 'receive new inventory (from a vendor)' do + variants = { variant => 5 } + + subject.receive(destination_location, variants) + + expect(destination_location.count_on_hand(variant)).to eq 5 + + expect(subject.source_location).to be_nil + expect(subject.destination_location).to eq destination_location + end + end +end diff --git a/core/spec/models/spree/store_spec.rb b/core/spec/models/spree/store_spec.rb new file mode 100644 index 00000000000..60edbf1f38c --- /dev/null +++ b/core/spec/models/spree/store_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Spree::Store, :type => :model do + + describe ".by_url" do + let!(:store) { create(:store, url: "website1.com\nwww.subdomain.com") } + let!(:store_2) { create(:store, url: 'freethewhales.com') } + + it "should find stores by url" do + by_domain = Spree::Store.by_url('www.subdomain.com') + + expect(by_domain).to include(store) + expect(by_domain).not_to include(store_2) + end + end + + describe '.current' do + # there is a default store created with the test_app rake task. + let!(:store_1) { Spree::Store.first || create(:store) } + + let!(:store_2) { create(:store, default: false, url: 'www.subdomain.com') } + + it 'should return default when no domain' do + expect(subject.class.current).to eql(store_1) + end + + it 'should return store for domain' do + expect(subject.class.current('spreecommerce.com')).to eql(store_1) + expect(subject.class.current('www.subdomain.com')).to eql(store_2) + end + end + + describe ".default" do + let!(:store) { create(:store) } + let!(:store_2) { create(:store, default: true) } + + it "should ensure there is a default if one doesn't exist yet" do + expect(store_2.default).to be true + end + + it "should ensure there is only one default" do + [store, store_2].each(&:reload) + + expect(Spree::Store.where(default: true).count).to eq(1) + expect(store_2.default).to be true + expect(store.default).not_to be true + end + end + +end diff --git a/core/spec/models/spree/tax_category_spec.rb b/core/spec/models/spree/tax_category_spec.rb new file mode 100644 index 00000000000..236e6ee752d --- /dev/null +++ b/core/spec/models/spree/tax_category_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Spree::TaxCategory, :type => :model do + context 'default tax category' do + let(:tax_category) { create(:tax_category) } + let(:new_tax_category) { create(:tax_category) } + + before do + tax_category.update_column(:is_default, true) + end + + it "should undefault the previous default tax category" do + new_tax_category.update_attributes({:is_default => true}) + expect(new_tax_category.is_default).to be true + + tax_category.reload + expect(tax_category.is_default).to be false + end + + it "should undefault the previous default tax category except when updating the existing default tax category" do + tax_category.update_column(:description, "Updated description") + + tax_category.reload + expect(tax_category.is_default).to be true + end + end +end diff --git a/core/spec/models/spree/tax_rate_spec.rb b/core/spec/models/spree/tax_rate_spec.rb new file mode 100644 index 00000000000..74980468433 --- /dev/null +++ b/core/spec/models/spree/tax_rate_spec.rb @@ -0,0 +1,379 @@ +require 'spec_helper' + +describe Spree::TaxRate, :type => :model do + context "match" do + let(:order) { create(:order) } + let(:country) { create(:country) } + let(:tax_category) { create(:tax_category) } + let(:calculator) { Spree::Calculator::FlatRate.new } + + it "should return an empty array when tax_zone is nil" do + allow(order).to receive_messages :tax_zone => nil + expect(Spree::TaxRate.match(order.tax_zone)).to eq([]) + end + + context "when no rate zones match the tax zone" do + before do + Spree::TaxRate.create(:amount => 1, :zone => create(:zone)) + end + + context "when there is no default tax zone" do + before do + @zone = create(:zone, :name => "Country Zone", :default_tax => false, :zone_members => []) + @zone.zone_members.create(:zoneable => country) + end + + it "should return an empty array" do + allow(order).to receive_messages :tax_zone => @zone + expect(Spree::TaxRate.match(order.tax_zone)).to eq([]) + end + + it "should return the rate that matches the rate zone" do + rate = Spree::TaxRate.create( + :amount => 1, + :zone => @zone, + :tax_category => tax_category, + :calculator => calculator + ) + + allow(order).to receive_messages :tax_zone => @zone + expect(Spree::TaxRate.match(order.tax_zone)).to eq([rate]) + end + + it "should return all rates that match the rate zone" do + rate1 = Spree::TaxRate.create( + :amount => 1, + :zone => @zone, + :tax_category => tax_category, + :calculator => calculator + ) + + rate2 = Spree::TaxRate.create( + :amount => 2, + :zone => @zone, + :tax_category => tax_category, + :calculator => Spree::Calculator::FlatRate.new + ) + + allow(order).to receive_messages :tax_zone => @zone + expect(Spree::TaxRate.match(order.tax_zone)).to match_array([rate1, rate2]) + end + + context "when the tax_zone is contained within a rate zone" do + before do + sub_zone = create(:zone, :name => "State Zone", :zone_members => []) + sub_zone.zone_members.create(:zoneable => create(:state, :country => country)) + allow(order).to receive_messages :tax_zone => sub_zone + @rate = Spree::TaxRate.create( + :amount => 1, + :zone => @zone, + :tax_category => tax_category, + :calculator => calculator + ) + end + + it "should return the rate zone" do + expect(Spree::TaxRate.match(order.tax_zone)).to eq([@rate]) + end + end + end + + context "when there is a default tax zone" do + before do + @zone = create(:zone, :name => "Country Zone", :default_tax => true, :zone_members => []) + @zone.zone_members.create(:zoneable => country) + end + + let(:included_in_price) { false } + let!(:rate) do + Spree::TaxRate.create(:amount => 1, + :zone => @zone, + :tax_category => tax_category, + :calculator => calculator, + :included_in_price => included_in_price) + end + + subject { Spree::TaxRate.match(order.tax_zone) } + + context "when the order has the same tax zone" do + before do + allow(order).to receive_messages :tax_zone => @zone + allow(order).to receive_messages :tax_address => tax_address + end + + let(:tax_address) { stub_model(Spree::Address) } + + context "when the tax is not a VAT" do + it { is_expected.to eq([rate]) } + end + + context "when the tax is a VAT" do + let(:included_in_price) { true } + it { is_expected.to eq([rate]) } + end + end + + context "when the order has a different tax zone" do + before do + allow(order).to receive_messages :tax_zone => create(:zone, :name => "Other Zone") + allow(order).to receive_messages :tax_address => tax_address + end + + context "when the order has a tax_address" do + let(:tax_address) { stub_model(Spree::Address) } + + context "when the tax is a VAT" do + let(:included_in_price) { true } + # The rate should match in this instance because: + # 1) It's the default rate (and as such, a negative adjustment should apply) + it { is_expected.to eq([rate]) } + end + + context "when the tax is not VAT" do + it "returns no tax rate" do + expect(subject).to be_empty + end + end + end + + context "when the order does not have a tax_address" do + let(:tax_address) { nil} + + context "when the tax is a VAT" do + let(:included_in_price) { true } + # The rate should match in this instance because: + # 1) The order has no tax address by this stage + # 2) With no tax address, it has no tax zone + # 3) Therefore, we assume the default tax zone + # 4) This default zone has a default tax rate. + it { is_expected.to eq([rate]) } + end + + context "when the tax is not a VAT" do + it { is_expected.to be_empty } + end + end + end + end + end + end + + context ".adjust" do + let(:order) { stub_model(Spree::Order) } + let(:tax_category_1) { stub_model(Spree::TaxCategory) } + let(:tax_category_2) { stub_model(Spree::TaxCategory) } + let(:rate_1) { stub_model(Spree::TaxRate, :tax_category => tax_category_1) } + let(:rate_2) { stub_model(Spree::TaxRate, :tax_category => tax_category_2) } + + context "with line items" do + let(:line_item) do + stub_model(Spree::LineItem, + :price => 10.0, + :quantity => 1, + :tax_category => tax_category_1, + :variant => stub_model(Spree::Variant) + ) + end + + let(:line_items) { [line_item] } + + before do + allow(Spree::TaxRate).to receive_messages :match => [rate_1, rate_2] + end + + it "should apply adjustments for two tax rates to the order" do + expect(rate_1).to receive(:adjust) + expect(rate_2).not_to receive(:adjust) + Spree::TaxRate.adjust(order.tax_zone, line_items) + end + end + + context "with shipments" do + let(:shipments) { [stub_model(Spree::Shipment, :cost => 10.0, :tax_category => tax_category_1)] } + + before do + allow(Spree::TaxRate).to receive_messages :match => [rate_1, rate_2] + end + + it "should apply adjustments for two tax rates to the order" do + expect(rate_1).to receive(:adjust) + expect(rate_2).not_to receive(:adjust) + Spree::TaxRate.adjust(order.tax_zone, shipments) + end + end + end + + context "#adjust" do + before do + @country = create(:country) + @zone = create(:zone, :name => "Country Zone", :default_tax => true, :zone_members => []) + @zone.zone_members.create(:zoneable => @country) + @category = Spree::TaxCategory.create :name => "Taxable Foo" + @category2 = Spree::TaxCategory.create(:name => "Non Taxable") + @rate1 = Spree::TaxRate.create( + :amount => 0.10, + :calculator => Spree::Calculator::DefaultTax.create, + :tax_category => @category, + :zone => @zone + ) + @rate2 = Spree::TaxRate.create( + :amount => 0.05, + :calculator => Spree::Calculator::DefaultTax.create, + :tax_category => @category, + :zone => @zone + ) + @order = Spree::Order.create! + @taxable = create(:product, :tax_category => @category) + @nontaxable = create(:product, :tax_category => @category2) + end + + context "not taxable line item " do + let!(:line_item) { @order.contents.add(@nontaxable.master, 1) } + + it "should not create a tax adjustment" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.tax.charge.count).to eq(0) + end + + it "should not create a refund" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.credit.count).to eq(0) + end + end + + context "taxable line item" do + let!(:line_item) { @order.contents.add(@taxable.master, 1) } + + context "when price includes tax" do + before do + @rate1.update_column(:included_in_price, true) + @rate2.update_column(:included_in_price, true) + Spree::TaxRate.store_pre_tax_amount(line_item, [@rate1, @rate2]) + end + + context "when zone is contained by default tax zone" do + it "should create two adjustments, one for each tax rate" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.count).to eq(1) + end + + it "should not create a tax refund" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.credit.count).to eq(0) + end + end + + context "when order's zone is neither the default zone, or included in the default zone, but matches the rate's zone" do + before do + # With no zone members, this zone will not contain anything + # Previously: + # Zone.stub_chain :default_tax, :contains? => false + @zone.zone_members.delete_all + end + it "should create an adjustment" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.charge.count).to eq(1) + end + + it "should not create a tax refund for each tax rate" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.credit.count).to eq(0) + end + end + + context "when order's zone does not match default zone, is not included in the default zone, AND does not match the rate's zone" do + before do + @new_zone = create(:zone, :name => "New Zone", :default_tax => false) + @new_country = create(:country, :name => "New Country") + @new_zone.zone_members.create(:zoneable => @new_country) + @order.ship_address = create(:address, :country => @new_country) + @order.save + @order.reload + end + + it "should not create positive adjustments" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.charge.count).to eq(0) + end + + it "should create a tax refund for each tax rate" do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.credit.count).to eq(1) + end + end + + context "when price does not include tax" do + before do + allow(@order).to receive_messages :tax_zone => @zone + [@rate1, @rate2].each do |rate| + rate.included_in_price = false + rate.zone = @zone + rate.save + end + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + end + + it "should delete adjustments for open order when taxrate is deleted" do + @rate1.destroy! + @rate2.destroy! + expect(line_item.adjustments.count).to eq(0) + end + + it "should not delete adjustments for complete order when taxrate is deleted" do + @order.update_column :completed_at, Time.now + @rate1.destroy! + @rate2.destroy! + expect(line_item.adjustments.count).to eq(2) + end + + it "should create an adjustment" do + expect(line_item.adjustments.count).to eq(2) + end + + it "should not create a tax refund" do + expect(line_item.adjustments.credit.count).to eq(0) + end + + describe 'tax adjustments' do + before { Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) } + + it "should apply adjustments when a tax zone is present" do + expect(line_item.adjustments.count).to eq(2) + end + + describe 'when the tax zone is removed' do + before { allow(@order).to receive_messages :tax_zone => nil } + + it 'does not apply any adjustments' do + Spree::TaxRate.adjust(@order.tax_zone, @order.line_items) + expect(line_item.adjustments.count).to eq(0) + end + end + end + end + + context "when two rates apply" do + before do + @price_before_taxes = line_item.price / (1 + @rate1.amount + @rate2.amount) + # Use the same rounding method as in DefaultTax calculator + @price_before_taxes = BigDecimal.new(@price_before_taxes).round(2, BigDecimal::ROUND_HALF_UP) + line_item.update_column(:pre_tax_amount, @price_before_taxes) + # Clear out any previously automatically-applied adjustments + @order.all_adjustments.delete_all + @rate1.adjust(@order.tax_zone, line_item) + @rate2.adjust(@order.tax_zone, line_item) + end + + it "should create two price adjustments" do + expect(@order.line_item_adjustments.count).to eq(2) + end + + it "price adjustments should be accurate" do + included_tax = @order.line_item_adjustments.sum(:amount) + expect(@price_before_taxes + included_tax).to eq(line_item.price) + end + end + end + end + end +end diff --git a/core/spec/models/spree/taxon_spec.rb b/core/spec/models/spree/taxon_spec.rb new file mode 100644 index 00000000000..0a9ee6f1bb1 --- /dev/null +++ b/core/spec/models/spree/taxon_spec.rb @@ -0,0 +1,74 @@ +# coding: UTF-8 + +require 'spec_helper' + +describe Spree::Taxon, :type => :model do + let(:taxon) { FactoryGirl.build(:taxon, :name => "Ruby on Rails") } + + describe '#to_param' do + subject { super().to_param } + it { is_expected.to eql taxon.permalink } + end + + context "set_permalink" do + + it "should set permalink correctly when no parent present" do + taxon.set_permalink + expect(taxon.permalink).to eql "ruby-on-rails" + end + + it "should support Chinese characters" do + taxon.name = "你好" + taxon.set_permalink + expect(taxon.permalink).to eql 'ni-hao' + end + + context "with parent taxon" do + let(:parent) { FactoryGirl.build(:taxon, :permalink => "brands") } + before { allow(taxon).to receive_messages parent: parent } + + it "should set permalink correctly when taxon has parent" do + taxon.set_permalink + expect(taxon.permalink).to eql "brands/ruby-on-rails" + end + + it "should set permalink correctly with existing permalink present" do + taxon.permalink = "b/rubyonrails" + taxon.set_permalink + expect(taxon.permalink).to eql "brands/rubyonrails" + end + + it "should support Chinese characters" do + taxon.name = "我" + taxon.set_permalink + expect(taxon.permalink).to eql "brands/wo" + end + + # Regression test for #3390 + context "setting a new node sibling position via :child_index=" do + let(:idx) { rand(0..100) } + before { allow(parent).to receive(:move_to_child_with_index) } + + context "taxon is not new" do + before { allow(taxon).to receive(:new_record?).and_return(false) } + + it "passes the desired index move_to_child_with_index of :parent " do + expect(taxon).to receive(:move_to_child_with_index).with(parent, idx) + + taxon.child_index = idx + end + end + end + + end + end + + # Regression test for #2620 + context "creating a child node using first_or_create" do + let(:taxonomy) { create(:taxonomy) } + + it "does not error out" do + expect { taxonomy.root.children.unscoped.where(:name => "Some name").first_or_create }.not_to raise_error + end + end +end diff --git a/core/spec/models/taxonomy_spec.rb b/core/spec/models/spree/taxonomy_spec.rb similarity index 91% rename from core/spec/models/taxonomy_spec.rb rename to core/spec/models/spree/taxonomy_spec.rb index f825f49764d..1aae5a0d871 100644 --- a/core/spec/models/taxonomy_spec.rb +++ b/core/spec/models/spree/taxonomy_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Spree::Taxonomy do +describe Spree::Taxonomy, :type => :model do context "#destroy" do before do @taxonomy = create(:taxonomy) diff --git a/core/spec/models/spree/tracker_spec.rb b/core/spec/models/spree/tracker_spec.rb new file mode 100644 index 00000000000..c7790910436 --- /dev/null +++ b/core/spec/models/spree/tracker_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Spree::Tracker, :type => :model do + describe "current" do + before(:each) { @tracker = create(:tracker) } + + it "returns the first active tracker for the environment" do + expect(Spree::Tracker.current).to eq(@tracker) + end + + it "does not return a tracker with a blank analytics_id" do + @tracker.update_attribute(:analytics_id, '') + expect(Spree::Tracker.current).to be_nil + end + + it "does not return an inactive tracker" do + @tracker.update_attribute(:active, false) + expect(Spree::Tracker.current).to be_nil + end + end +end diff --git a/core/spec/models/spree/user_spec.rb b/core/spec/models/spree/user_spec.rb new file mode 100644 index 00000000000..36bb9d40036 --- /dev/null +++ b/core/spec/models/spree/user_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe Spree::LegacyUser, :type => :model do + # Regression test for #2844 + #3346 + context "#last_incomplete_order" do + let!(:user) { create(:user) } + let!(:order) { create(:order, bill_address: create(:address), ship_address: create(:address)) } + + let!(:order_1) { create(:order, :created_at => 1.day.ago, :user => user, :created_by => user) } + let!(:order_2) { create(:order, :user => user, :created_by => user) } + let!(:order_3) { create(:order, :user => user, :created_by => create(:user)) } + + it "returns correct order" do + expect(user.last_incomplete_spree_order).to eq order_3 + end + + context "persists order address" do + it "copies over order addresses" do + expect { + user.persist_order_address(order) + }.to change { Spree::Address.count }.by(2) + + expect(user.bill_address).to eq order.bill_address + expect(user.ship_address).to eq order.ship_address + end + + it "doesnt create new addresses if user has already" do + user.update_column(:bill_address_id, create(:address)) + user.update_column(:ship_address_id, create(:address)) + user.reload + + expect { + user.persist_order_address(order) + }.not_to change { Spree::Address.count } + end + + it "set both bill and ship address id on subject" do + user.persist_order_address(order) + + expect(user.bill_address_id).not_to be_blank + expect(user.ship_address_id).not_to be_blank + end + end + + context "payment source" do + let(:payment_method) { create(:credit_card_payment_method) } + let!(:cc) do + create(:credit_card, user_id: user.id, payment_method: payment_method, gateway_customer_profile_id: "2342343") + end + + it "has payment sources" do + expect(user.payment_sources.first.gateway_customer_profile_id).not_to be_empty + end + + it "drops payment source" do + user.drop_payment_source cc + expect(cc.gateway_customer_profile_id).to be_nil + end + end + end +end + +describe Spree.user_class, :type => :model do + context "reporting" do + let(:order_value) { BigDecimal.new("80.94") } + let(:order_count) { 4 } + let(:orders) { Array.new(order_count, double(total: order_value)) } + + before do + allow(orders).to receive(:pluck).with(:total).and_return(orders.map(&:total)) + allow(orders).to receive(:count).and_return(orders.length) + end + + def load_orders + allow(subject).to receive(:spree_orders).and_return(double(complete: orders)) + end + + describe "#lifetime_value" do + context "with orders" do + before { load_orders } + it "returns the total of completed orders for the user" do + expect(subject.lifetime_value).to eq (order_count * order_value) + end + end + context "without orders" do + it "returns 0.00" do + expect(subject.lifetime_value).to eq BigDecimal("0.00") + end + end + end + + describe "#display_lifetime_value" do + it "returns a Spree::Money version of lifetime_value" do + value = BigDecimal("500.05") + allow(subject).to receive(:lifetime_value).and_return(value) + expect(subject.display_lifetime_value).to eq Spree::Money.new(value) + end + end + + describe "#order_count" do + before { load_orders } + it "returns the count of completed orders for the user" do + expect(subject.order_count).to eq BigDecimal(order_count) + end + end + + describe "#average_order_value" do + context "with orders" do + before { load_orders } + it "returns the average completed order price for the user" do + expect(subject.average_order_value).to eq order_value + end + end + context "without orders" do + it "returns 0.00" do + expect(subject.average_order_value).to eq BigDecimal("0.00") + end + end + end + + describe "#display_average_order_value" do + before { load_orders } + it "returns a Spree::Money version of average_order_value" do + value = BigDecimal("500.05") + allow(subject).to receive(:average_order_value).and_return(value) + expect(subject.display_average_order_value).to eq Spree::Money.new(value) + end + end + end +end diff --git a/core/spec/models/spree/validations/db_maximum_length_validator_spec.rb b/core/spec/models/spree/validations/db_maximum_length_validator_spec.rb new file mode 100644 index 00000000000..8c9f416843e --- /dev/null +++ b/core/spec/models/spree/validations/db_maximum_length_validator_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Spree::Validations::DbMaximumLengthValidator, :type => :model do + context 'when Spree::Product' do + Spree::Product.class_eval do + # Slug currently has no validation for maximum length + validates_with Spree::Validations::DbMaximumLengthValidator, field: :slug + end + let(:limit) { 255 } # The default limit of db.string. + let(:product) { create :product } + let(:slug) { "x" * (limit + 1)} + + before do + product.slug = slug + end + + subject { product.valid? } + + it 'should maximum validate slug' do + subject + expect(product.errors[:slug]).to include(I18n.t("errors.messages.too_long", count: limit)) + end + end +end diff --git a/core/spec/models/spree/variant/scopes_spec.rb b/core/spec/models/spree/variant/scopes_spec.rb new file mode 100644 index 00000000000..0f4288ffbfb --- /dev/null +++ b/core/spec/models/spree/variant/scopes_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe "Variant scopes", :type => :model do + let!(:product) { create(:product) } + let!(:variant_1) { create(:variant, :product => product) } + let!(:variant_2) { create(:variant, :product => product) } + + it ".descend_by_popularity" do + # Requires a product with at least two variants, where one has a higher number of + # orders than the other + Spree::LineItem.delete_all # FIXME leaky database - too many line_items + create(:line_item, :variant => variant_1) + expect(Spree::Variant.descend_by_popularity.first).to eq(variant_1) + end + + context "finding by option values" do + let!(:option_type) { create(:option_type, :name => "bar") } + let!(:option_value_1) do + option_value = create(:option_value, :name => "foo", :option_type => option_type) + variant_1.option_values << option_value + option_value + end + + let!(:option_value_2) do + option_value = create(:option_value, :name => "fizz", :option_type => option_type) + variant_1.option_values << option_value + option_value + end + + let!(:product_variants) { product.variants_including_master } + + it "by objects" do + variants = product_variants.has_option(option_type, option_value_1) + expect(variants).to include(variant_1) + expect(variants).not_to include(variant_2) + end + + it "by names" do + variants = product_variants.has_option("bar", "foo") + expect(variants).to include(variant_1) + expect(variants).not_to include(variant_2) + end + + it "by ids" do + variants = product_variants.has_option(option_type.id, option_value_1.id) + expect(variants).to include(variant_1) + expect(variants).not_to include(variant_2) + end + + it "by mixed conditions" do + variants = product_variants.has_option(option_type.id, "foo", option_value_2) + end + end +end diff --git a/core/spec/models/spree/variant_spec.rb b/core/spec/models/spree/variant_spec.rb new file mode 100644 index 00000000000..d7e42b5f3b2 --- /dev/null +++ b/core/spec/models/spree/variant_spec.rb @@ -0,0 +1,500 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe Spree::Variant, :type => :model do + let!(:variant) { create(:variant) } + + it_behaves_like 'default_price' + + context 'sorting' do + it 'responds to set_list_position' do + expect(variant.respond_to?(:set_list_position)).to eq(true) + end + end + + context "validations" do + it "should validate price is greater than 0" do + variant.price = -1 + expect(variant).to be_invalid + end + + it "should validate price is 0" do + variant.price = 0 + expect(variant).to be_valid + end + end + + context "after create" do + let!(:product) { create(:product) } + + it "propagate to stock items" do + expect_any_instance_of(Spree::StockLocation).to receive(:propagate_variant) + product.variants.create(:name => "Foobar") + end + + context "stock location has disable propagate all variants" do + before { Spree::StockLocation.update_all propagate_all_variants: false } + + it "propagate to stock items" do + expect_any_instance_of(Spree::StockLocation).not_to receive(:propagate_variant) + product.variants.create(:name => "Foobar") + end + end + + describe 'mark_master_out_of_stock' do + before do + product.master.stock_items.first.set_count_on_hand(5) + end + context 'when product is created without variants but with stock' do + it { expect(product.master).to be_in_stock } + end + + context 'when a variant is created' do + before(:each) do + product.variants.create!(:name => 'any-name') + end + + it { expect(product.master).to_not be_in_stock } + end + end + end + + context "product has other variants" do + describe "option value accessors" do + before { + @multi_variant = FactoryGirl.create :variant, :product => variant.product + variant.product.reload + } + + let(:multi_variant) { @multi_variant } + + it "should set option value" do + expect(multi_variant.option_value('media_type')).to be_nil + + multi_variant.set_option_value('media_type', 'DVD') + expect(multi_variant.option_value('media_type')).to eql 'DVD' + + multi_variant.set_option_value('media_type', 'CD') + expect(multi_variant.option_value('media_type')).to eql 'CD' + end + + it "should not duplicate associated option values when set multiple times" do + multi_variant.set_option_value('media_type', 'CD') + + expect { + multi_variant.set_option_value('media_type', 'DVD') + }.to_not change(multi_variant.option_values, :count) + + expect { + multi_variant.set_option_value('coolness_type', 'awesome') + }.to change(multi_variant.option_values, :count).by(1) + end + end + + context "product has other variants" do + describe "option value accessors" do + before { + @multi_variant = create(:variant, :product => variant.product) + variant.product.reload + } + + let(:multi_variant) { @multi_variant } + + it "should set option value" do + expect(multi_variant.option_value('media_type')).to be_nil + + multi_variant.set_option_value('media_type', 'DVD') + expect(multi_variant.option_value('media_type')).to eql 'DVD' + + multi_variant.set_option_value('media_type', 'CD') + expect(multi_variant.option_value('media_type')).to eql 'CD' + end + + it "should not duplicate associated option values when set multiple times" do + multi_variant.set_option_value('media_type', 'CD') + + expect { + multi_variant.set_option_value('media_type', 'DVD') + }.to_not change(multi_variant.option_values, :count) + + expect { + multi_variant.set_option_value('coolness_type', 'awesome') + }.to change(multi_variant.option_values, :count).by(1) + end + end + end + end + + context "#cost_price=" do + it "should use LocalizedNumber.parse" do + expect(Spree::LocalizedNumber).to receive(:parse).with('1,599.99') + subject.cost_price = '1,599.99' + end + end + + context "#price=" do + it "should use LocalizedNumber.parse" do + expect(Spree::LocalizedNumber).to receive(:parse).with('1,599.99') + subject.price = '1,599.99' + end + end + + context "#weight=" do + it "should use LocalizedNumber.parse" do + expect(Spree::LocalizedNumber).to receive(:parse).with('1,599.99') + subject.weight = '1,599.99' + end + end + + context "#currency" do + it "returns the globally configured currency" do + expect(variant.currency).to eql "USD" + end + end + + context "#display_amount" do + it "returns a Spree::Money" do + variant.price = 21.22 + expect(variant.display_amount.to_s).to eql "$21.22" + end + end + + context "#cost_currency" do + context "when cost currency is nil" do + before { variant.cost_currency = nil } + it "populates cost currency with the default value on save" do + variant.save! + expect(variant.cost_currency).to eql "USD" + end + end + end + + describe '.price_in' do + before do + variant.prices << create(:price, :variant => variant, :currency => "EUR", :amount => 33.33) + end + subject { variant.price_in(currency).display_amount } + + context "when currency is not specified" do + let(:currency) { nil } + + it "returns 0" do + expect(subject.to_s).to eql "$0.00" + end + end + + context "when currency is EUR" do + let(:currency) { 'EUR' } + + it "returns the value in the EUR" do + expect(subject.to_s).to eql "€33.33" + end + end + + context "when currency is USD" do + let(:currency) { 'USD' } + + it "returns the value in the USD" do + expect(subject.to_s).to eql "$19.99" + end + end + end + + describe '.amount_in' do + before do + variant.prices << create(:price, :variant => variant, :currency => "EUR", :amount => 33.33) + end + + subject { variant.amount_in(currency) } + + context "when currency is not specified" do + let(:currency) { nil } + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when currency is EUR" do + let(:currency) { 'EUR' } + + it "returns the value in the EUR" do + expect(subject).to eql 33.33 + end + end + + context "when currency is USD" do + let(:currency) { 'USD' } + + it "returns the value in the USD" do + expect(subject).to eql 19.99 + end + end + end + + # Regression test for #2432 + describe 'options_text' do + let!(:variant) { create(:variant, option_values: []) } + let!(:master) { create(:master_variant) } + + before do + # Order bar than foo + variant.option_values << create(:option_value, {name: 'Foo', presentation: 'Foo', option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type')}) + variant.option_values << create(:option_value, {name: 'Bar', presentation: 'Bar', option_type: create(:option_type, position: 1, name: 'Bar Type', presentation: 'Bar Type')}) + end + + it 'should order by bar than foo' do + expect(variant.options_text).to eql 'Bar Type: Bar, Foo Type: Foo' + end + + end + + describe 'exchange_name' do + let!(:variant) { create(:variant, option_values: []) } + let!(:master) { create(:master_variant) } + + before do + variant.option_values << create(:option_value, { + name: 'Foo', + presentation: 'Foo', + option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type') + }) + end + + context 'master variant' do + it 'should return name' do + expect(master.exchange_name).to eql master.name + end + end + + context 'variant' do + it 'should return options text' do + expect(variant.exchange_name).to eql 'Foo Type: Foo' + end + end + + end + + describe 'exchange_name' do + let!(:variant) { create(:variant, option_values: []) } + let!(:master) { create(:master_variant) } + + before do + variant.option_values << create(:option_value, { + name: 'Foo', + presentation: 'Foo', + option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type') + }) + end + + context 'master variant' do + it 'should return name' do + expect(master.exchange_name).to eql master.name + end + end + + context 'variant' do + it 'should return options text' do + expect(variant.exchange_name).to eql 'Foo Type: Foo' + end + end + + end + + describe 'descriptive_name' do + let!(:variant) { create(:variant, option_values: []) } + let!(:master) { create(:master_variant) } + + before do + variant.option_values << create(:option_value, { + name: 'Foo', + presentation: 'Foo', + option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type') + }) + end + + context 'master variant' do + it 'should return name with Master identifier' do + expect(master.descriptive_name).to eql master.name + ' - Master' + end + end + + context 'variant' do + it 'should return options text with name' do + expect(variant.descriptive_name).to eql variant.name + ' - Foo Type: Foo' + end + end + + end + + # Regression test for #2744 + describe "set_position" do + it "sets variant position after creation" do + variant = create(:variant) + expect(variant.position).to_not be_nil + end + end + + describe '#in_stock?' do + before do + Spree::Config.track_inventory_levels = true + end + + context 'when stock_items are not backorderable' do + before do + allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: false) + end + + context 'when stock_items in stock' do + before do + variant.stock_items.first.update_column(:count_on_hand, 10) + end + + it 'returns true if stock_items in stock' do + expect(variant.in_stock?).to be true + end + end + + context 'when stock_items out of stock' do + before do + allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: false) + allow_any_instance_of(Spree::StockItem).to receive_messages(count_on_hand: 0) + end + + it 'return false if stock_items out of stock' do + expect(variant.in_stock?).to be false + end + end + end + + describe "#can_supply?" do + it "calls out to quantifier" do + expect(Spree::Stock::Quantifier).to receive(:new).and_return(quantifier = double) + expect(quantifier).to receive(:can_supply?).with(10) + variant.can_supply?(10) + end + end + + context 'when stock_items are backorderable' do + before do + allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: true) + end + + context 'when stock_items out of stock' do + before do + allow_any_instance_of(Spree::StockItem).to receive_messages(count_on_hand: 0) + end + + it 'in_stock? returns false' do + expect(variant.in_stock?).to be false + end + + it 'can_supply? return true' do + expect(variant.can_supply?).to be true + end + end + end + end + + describe '#is_backorderable' do + let(:variant) { build(:variant) } + subject { variant.is_backorderable? } + + it 'should invoke Spree::Stock::Quantifier' do + expect_any_instance_of(Spree::Stock::Quantifier).to receive(:backorderable?) { true } + subject + end + end + + describe '#total_on_hand' do + it 'should be infinite if track_inventory_levels is false' do + Spree::Config[:track_inventory_levels] = false + expect(build(:variant).total_on_hand).to eql(Float::INFINITY) + end + + it 'should match quantifier total_on_hand' do + variant = build(:variant) + expect(variant.total_on_hand).to eq(Spree::Stock::Quantifier.new(variant).total_on_hand) + end + end + + describe '#tax_category' do + context 'when tax_category is nil' do + let(:product) { build(:product) } + let(:variant) { build(:variant, product: product, tax_category_id: nil) } + it 'returns the parent products tax_category' do + expect(variant.tax_category).to eq(product.tax_category) + end + end + + context 'when tax_category is set' do + let(:tax_category) { create(:tax_category) } + let(:variant) { build(:variant, tax_category: tax_category) } + it 'returns the tax_category set on itself' do + expect(variant.tax_category).to eq(tax_category) + end + end + end + + describe "touching" do + it "updates a product" do + variant.product.update_column(:updated_at, 1.day.ago) + variant.touch + expect(variant.product.reload.updated_at).to be_within(3.seconds).of(Time.now) + end + + it "clears the in_stock cache key" do + expect(Rails.cache).to receive(:delete).with(variant.send(:in_stock_cache_key)) + variant.touch + end + end + + describe "#should_track_inventory?" do + + it 'should not track inventory when global setting is off' do + Spree::Config[:track_inventory_levels] = false + + expect(build(:variant).should_track_inventory?).to eq(false) + end + + it 'should not track inventory when variant is turned off' do + Spree::Config[:track_inventory_levels] = true + + expect(build(:on_demand_variant).should_track_inventory?).to eq(false) + end + + it 'should track inventory when global and variant are on' do + Spree::Config[:track_inventory_levels] = true + + expect(build(:variant).should_track_inventory?).to eq(true) + end + end + + describe "deleted_at scope" do + before { variant.destroy && variant.reload } + it "should have a price if deleted" do + variant.price = 10 + expect(variant.price).to eq(10) + end + end + + describe "stock movements" do + let!(:movement) { create(:stock_movement, stock_item: variant.stock_items.first) } + + it "builds out collection just fine through stock items" do + expect(variant.stock_movements.to_a).not_to be_empty + end + end + + describe "in_stock scope" do + it "returns all in stock variants" do + in_stock_variant = create(:variant) + out_of_stock_variant = create(:variant) + + in_stock_variant.stock_items.first.update_column(:count_on_hand, 10) + + expect(Spree::Variant.in_stock).to eq [in_stock_variant] + end + end +end diff --git a/core/spec/models/spree/zone_spec.rb b/core/spec/models/spree/zone_spec.rb new file mode 100644 index 00000000000..f9a76afcc81 --- /dev/null +++ b/core/spec/models/spree/zone_spec.rb @@ -0,0 +1,305 @@ +require 'spec_helper' + +describe Spree::Zone, :type => :model do + context "#match" do + let(:country_zone) { create(:zone, name: 'CountryZone') } + let(:country) do + country = create(:country) + # Create at least one state for this country + state = create(:state, country: country) + country + end + + before { country_zone.members.create(zoneable: country) } + + context "when there is only one qualifying zone" do + let(:address) { create(:address, country: country, state: country.states.first) } + + it "should return the qualifying zone" do + expect(Spree::Zone.match(address)).to eq(country_zone) + end + end + + context "when there are two qualified zones with same member type" do + let(:address) { create(:address, country: country, state: country.states.first) } + let(:second_zone) { create(:zone, name: 'SecondZone') } + + before { second_zone.members.create(zoneable: country) } + + context "when both zones have the same number of members" do + it "should return the zone that was created first" do + expect(Spree::Zone.match(address)).to eq(country_zone) + end + end + + context "when one of the zones has fewer members" do + let(:country2) { create(:country) } + + before { country_zone.members.create(zoneable: country2) } + + it "should return the zone with fewer members" do + expect(Spree::Zone.match(address)).to eq(second_zone) + end + end + end + + context "when there are two qualified zones with different member types" do + let(:state_zone) { create(:zone, name: 'StateZone') } + let(:address) { create(:address, country: country, state: country.states.first) } + + before { state_zone.members.create(zoneable: country.states.first) } + + it "should return the zone with the more specific member type" do + expect(Spree::Zone.match(address)).to eq(state_zone) + end + end + + context "when there are no qualifying zones" do + it "should return nil" do + expect(Spree::Zone.match(Spree::Address.new)).to be_nil + end + end + end + + context "#country_list" do + let(:state) { create(:state) } + let(:country) { state.country } + + context "when zone consists of countries" do + let(:country_zone) { create(:zone, name: 'CountryZone') } + + before { country_zone.members.create(zoneable: country) } + + it 'should return a list of countries' do + expect(country_zone.country_list).to eq([country]) + end + end + + context "when zone consists of states" do + let(:state_zone) { create(:zone, name: 'StateZone') } + + before { state_zone.members.create(zoneable: state) } + + it 'should return a list of countries' do + expect(state_zone.country_list).to eq([state.country]) + end + end + end + + context "#include?" do + let(:state) { create(:state) } + let(:country) { state.country } + let(:address) { create(:address, state: state) } + + context "when zone is country type" do + let(:country_zone) { create(:zone, name: 'CountryZone') } + before { country_zone.members.create(zoneable: country) } + + it "should be true" do + expect(country_zone.include?(address)).to be true + end + end + + context "when zone is state type" do + let(:state_zone) { create(:zone, name: 'StateZone') } + before { state_zone.members.create(zoneable: state) } + + it "should be true" do + expect(state_zone.include?(address)).to be true + end + end + end + + context ".default_tax" do + context "when there is a default tax zone specified" do + before { @foo_zone = create(:zone, name: 'whatever', default_tax: true) } + + it "should be the correct zone" do + foo_zone = create(:zone, name: 'foo') + expect(Spree::Zone.default_tax).to eq(@foo_zone) + end + end + + context "when there is no default tax zone specified" do + it "should be nil" do + expect(Spree::Zone.default_tax).to be_nil + end + end + end + + context "#contains?" do + let(:country1) { create(:country) } + let(:country2) { create(:country) } + let(:country3) { create(:country) } + + before do + @source = create(:zone, name: 'source', zone_members: []) + @target = create(:zone, name: 'target', zone_members: []) + end + + context "when the target has no members" do + before { @source.members.create(zoneable: country1) } + + it "should be false" do + expect(@source.contains?(@target)).to be false + end + end + + context "when the source has no members" do + before { @target.members.create(zoneable: country1) } + + it "should be false" do + expect(@source.contains?(@target)).to be false + end + end + + context "when both zones are the same zone" do + before do + @source.members.create(zoneable: country1) + @target = @source + end + + it "should be true" do + expect(@source.contains?(@target)).to be true + end + end + + context "when both zones are of the same type" do + before do + @source.members.create(zoneable: country1) + @source.members.create(zoneable: country2) + end + + context "when all members are included in the zone we check against" do + before do + @target.members.create(zoneable: country1) + @target.members.create(zoneable: country2) + end + + it "should be true" do + expect(@source.contains?(@target)).to be true + end + end + + context "when some members are included in the zone we check against" do + before do + @target.members.create(zoneable: country1) + @target.members.create(zoneable: country2) + @target.members.create(zoneable: create(:country)) + end + + it "should be false" do + expect(@source.contains?(@target)).to be false + end + end + + context "when none of the members are included in the zone we check against" do + before do + @target.members.create(zoneable: create(:country)) + @target.members.create(zoneable: create(:country)) + end + + it "should be false" do + expect(@source.contains?(@target)).to be false + end + end + end + + context "when checking country against state" do + before do + @source.members.create(zoneable: create(:state)) + @target.members.create(zoneable: country1) + end + + it "should be false" do + expect(@source.contains?(@target)).to be false + end + end + + context "when checking state against country" do + before { @source.members.create(zoneable: country1) } + + context "when all states contained in one of the countries we check against" do + + before do + state1 = create(:state, country: country1) + @target.members.create(zoneable: state1) + end + + it "should be true" do + expect(@source.contains?(@target)).to be true + end + end + + context "when some states contained in one of the countries we check against" do + + before do + state1 = create(:state, country: country1) + @target.members.create(zoneable: state1) + @target.members.create(zoneable: create(:state, country: country2)) + end + + it "should be false" do + expect(@source.contains?(@target)).to be false + end + end + + context "when none of the states contained in any of the countries we check against" do + + before do + @target.members.create(zoneable: create(:state, country: country2)) + @target.members.create(zoneable: create(:state, country: country2)) + end + + it "should be false" do + expect(@source.contains?(@target)).to be false + end + end + end + + end + + context "#save" do + context "when default_tax is true" do + it "should clear previous default tax zone" do + zone1 = create(:zone, name: 'foo', default_tax: true) + zone = create(:zone, name: 'bar', default_tax: true) + expect(zone1.reload.default_tax).to be false + end + end + + context "when a zone member country is added to an existing zone consisting of state members" do + it "should remove existing state members" do + zone = create(:zone, name: 'foo', zone_members: []) + state = create(:state) + country = create(:country) + zone.members.create(zoneable: state) + country_member = zone.members.create(zoneable: country) + zone.save + expect(zone.reload.members).to eq([country_member]) + end + end + end + + context "#kind" do + context "when the zone consists of country zone members" do + before do + @zone = create(:zone, name: 'country', zone_members: []) + @zone.members.create(zoneable: create(:country)) + end + it "should return the kind of zone member" do + expect(@zone.kind).to eq("country") + end + end + + context "when the zone consists of state zone members" do + before do + @zone = create(:zone, name: 'state', zone_members: []) + @zone.members.create(zoneable: create(:state)) + end + it "should return the kind of zone member" do + expect(@zone.kind).to eq("state") + end + end + end +end diff --git a/core/spec/models/state_spec.rb b/core/spec/models/state_spec.rb deleted file mode 100644 index 4c369d14943..00000000000 --- a/core/spec/models/state_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' - -describe Spree::State do - before(:all) do - Spree::State.destroy_all - end - - it "can find a state by name or abbr" do - state = create(:state, :name => "California", :abbr => "CA") - Spree::State.find_all_by_name_or_abbr("California").should include(state) - Spree::State.find_all_by_name_or_abbr("CA").should include(state) - end - - it "can find all states group by country id" do - state = create(:state) - Spree::State.states_group_by_country_id.should == { state.country_id.to_s => [[state.id, state.name]] } - end -end diff --git a/core/spec/models/tax_category_spec.rb b/core/spec/models/tax_category_spec.rb deleted file mode 100644 index c9b94c1ef70..00000000000 --- a/core/spec/models/tax_category_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -describe Spree::TaxCategory do - context '#mark_deleted!' do - let(:tax_category) { create(:tax_category) } - - it "should set the deleted at column to the current time" do - tax_category.mark_deleted! - tax_category.deleted_at.should_not be_nil - end - end - - context 'default tax category' do - let(:tax_category) { create(:tax_category) } - let(:new_tax_category) { create(:tax_category) } - - before do - tax_category.update_column(:is_default, true) - end - - it "should undefault the previous default tax category" do - new_tax_category.update_attributes({:is_default => true}) - new_tax_category.is_default.should be_true - - tax_category.reload - tax_category.is_default.should be_false - end - - it "should undefault the previous default tax category except when updating the existing default tax category" do - tax_category.update_column(:description, "Updated description") - - tax_category.reload - tax_category.is_default.should be_true - end - end -end diff --git a/core/spec/models/tax_rate_spec.rb b/core/spec/models/tax_rate_spec.rb deleted file mode 100644 index 8b8e31cbb90..00000000000 --- a/core/spec/models/tax_rate_spec.rb +++ /dev/null @@ -1,309 +0,0 @@ -require 'spec_helper' - -describe Spree::TaxRate do - context "match" do - let(:order) { create(:order) } - let(:country) { create(:country) } - let(:tax_category) { create(:tax_category) } - let(:calculator) { Spree::Calculator::FlatRate.new } - - - it "should return an empty array when tax_zone is nil" do - order.stub :tax_zone => nil - Spree::TaxRate.match(order).should == [] - end - - context "when no rate zones match the tax zone" do - before do - Spree::TaxRate.create({:amount => 1, :zone => create(:zone, :name => 'other_zone')}, :without_protection => true) - end - - context "when there is no default tax zone" do - before do - @zone = create(:zone, :name => "Country Zone", :default_tax => false, :zone_members => []) - @zone.zone_members.create(:zoneable => country) - end - - it "should return an empty array" do - order.stub :tax_zone => @zone - Spree::TaxRate.match(order).should == [] - end - - it "should return the rate that matches the rate zone" do - rate = Spree::TaxRate.create({ :amount => 1, :zone => @zone, :tax_category => tax_category, - :calculator => calculator }, :without_protection => true) - - order.stub :tax_zone => @zone - Spree::TaxRate.match(order).should == [rate] - end - - it "should return all rates that match the rate zone" do - rate1 = Spree::TaxRate.create({:amount => 1, :zone => @zone, :tax_category => tax_category, - :calculator => calculator}, :without_protection => true) - rate2 = Spree::TaxRate.create({:amount => 2, :zone => @zone, :tax_category => tax_category, - :calculator => Spree::Calculator::FlatRate.new}, :without_protection => true) - - order.stub :tax_zone => @zone - Spree::TaxRate.match(order).should == [rate1, rate2] - end - - context "when the tax_zone is contained within a rate zone" do - before do - sub_zone = create(:zone, :name => "State Zone", :zone_members => []) - sub_zone.zone_members.create(:zoneable => create(:state, :country => country)) - order.stub :tax_zone => sub_zone - @rate = Spree::TaxRate.create({:amount => 1, :zone => @zone, :tax_category => tax_category, - :calculator => calculator}, :without_protection => true) - end - - it "should return the rate zone" do - Spree::TaxRate.match(order).should == [@rate] - end - end - - end - - context "when there is a default tax zone" do - before do - @zone = create(:zone, :name => "Country Zone", :default_tax => true, :zone_members => []) - @zone.zone_members.create(:zoneable => country) - end - - context "when there order has a different tax zone" do - before { order.stub :tax_zone => create(:zone, :name => "Other Zone") } - - it "should return the rates associated with the default tax zone" do - rate = Spree::TaxRate.create({:amount => 1, :zone => @zone, :tax_category => tax_category, - :calculator => calculator}, :without_protection => true) - - Spree::TaxRate.match(order).should == [rate] - end - end - end - end - end - - context "adjust" do - let(:order) { stub_model(Spree::Order) } - let(:rate_1) { stub_model(Spree::TaxRate) } - let(:rate_2) { stub_model(Spree::TaxRate) } - - before do - Spree::TaxRate.stub :match => [rate_1, rate_2] - end - - it "should apply adjustments for two tax rates to the order" do - rate_1.should_receive(:adjust) - rate_2.should_receive(:adjust) - Spree::TaxRate.adjust(order) - end - end - - context "default" do - let(:tax_category) { create(:tax_category) } - let(:country) { create(:country) } - let(:calculator) { Spree::Calculator::FlatRate.new } - - context "when there is no default tax_category" do - before { tax_category.is_default = false } - - it "should return 0" do - Spree::TaxRate.default.should == 0 - end - end - - context "when there is a default tax_category" do - before { tax_category.update_column :is_default, true } - - context "when the default category has tax rates in the default tax zone" do - before(:each) do - Spree::Config[:default_country_id] = country.id - @zone = create(:zone, :name => "Country Zone", :default_tax => true) - @zone.zone_members.create(:zoneable => country) - rate = Spree::TaxRate.create({:amount => 1, :zone => @zone, :tax_category => tax_category, :calculator => calculator}, :without_protection => true) - end - - it "should return the correct tax_rate" do - Spree::TaxRate.default.to_f.should == 1.0 - end - end - - context "when the default category has no tax rates in the default tax zone" do - it "should return 0" do - Spree::TaxRate.default.should == 0 - end - end - end - end - - context "#adjust" do - before do - @category = Spree::TaxCategory.create :name => "Taxable Foo" - @category2 = Spree::TaxCategory.create(:name => "Non Taxable") - @calculator = Spree::Calculator::DefaultTax.new - @rate = Spree::TaxRate.create({:amount => 0.10, :calculator => @calculator, :tax_category => @category}, :without_protection => true) - @order = Spree::Order.create! - @taxable = create(:product, :tax_category => @category) - @nontaxable = create(:product, :tax_category => @category2) - end - - context "when order has no taxable line items" do - before { @order.add_variant @nontaxable.master } - - it "should not create a tax adjustment" do - @rate.adjust(@order) - @order.adjustments.tax.charge.count.should == 0 - end - - it "should not create a price adjustment" do - @rate.adjust(@order) - @order.price_adjustments.count.should == 0 - end - - it "should not create a refund" do - @rate.adjust(@order) - @order.adjustments.credit.count.should == 0 - end - end - - context "when order has one taxable line item" do - before { @order.add_variant @taxable.master } - - context "when price includes tax" do - before { @rate.included_in_price = true } - - context "when zone is contained by default tax zone" do - before { Spree::Zone.stub_chain :default_tax, :contains? => true } - - it "should create one price adjustment" do - @rate.adjust(@order) - @order.price_adjustments.count.should == 1 - end - - it "should not create a tax refund" do - @rate.adjust(@order) - @order.adjustments.credit.count.should == 0 - end - - it "should not create a tax adjustment" do - @rate.adjust(@order) - @order.adjustments.tax.charge.count.should == 0 - end - end - - context "when zone is not contained by default tax zone" do - before { Spree::Zone.stub_chain :default_tax, :contains? => false } - - it "should not create a price adjustment" do - @rate.adjust(@order) - @order.price_adjustments.count.should == 0 - end - - it "should create a tax refund" do - @rate.adjust(@order) - @order.adjustments.credit.count.should == 1 - end - - it "should not create a tax adjustment" do - @rate.adjust(@order) - @order.adjustments.tax.charge.count.should == 0 - end - end - - end - - context "when price does not include tax" do - before { @rate.included_in_price = false } - - it "should not create price adjustment" do - @rate.adjust(@order) - @order.price_adjustments.count.should == 0 - end - - it "should not create a tax refund" do - @rate.adjust(@order) - @order.adjustments.credit.count.should == 0 - end - - it "should create a tax adjustment" do - @rate.adjust(@order) - @order.adjustments.tax.charge.count.should == 1 - end - end - - end - - context "when order has multiple taxable line items" do - before do - @taxable2 = create(:product, :tax_category => @category) - @order.add_variant @taxable.master - @order.add_variant @taxable2.master - end - - context "when price includes tax" do - before { @rate.included_in_price = true } - - context "when zone is contained by default tax zone" do - before { Spree::Zone.stub_chain :default_tax, :contains? => true } - - it "should create multiple price adjustments" do - @rate.adjust(@order) - @order.price_adjustments.count.should == 2 - end - - it "should not create a tax refund" do - @rate.adjust(@order) - @order.adjustments.credit.count.should == 0 - end - - it "should not create a tax adjustment" do - @rate.adjust(@order) - @order.adjustments.tax.charge.count.should == 0 - end - end - - context "when zone is not contained by default tax zone" do - before { Spree::Zone.stub_chain :default_tax, :contains? => false } - - it "should not create a price adjustment" do - @rate.adjust(@order) - @order.price_adjustments.count.should == 0 - end - - it "should create a single tax refund" do - @rate.adjust(@order) - @order.adjustments.credit.count.should == 1 - end - - it "should not create a tax adjustment" do - @rate.adjust(@order) - @order.adjustments.tax.charge.count.should == 0 - end - end - - end - - context "when price does not include tax" do - before { @rate.included_in_price = false } - - it "should not create a price adjustment" do - @rate.adjust(@order) - @order.price_adjustments.count.should == 0 - end - - it "should not create a tax refund" do - @rate.adjust(@order) - @order.adjustments.credit.count.should == 0 - end - - it "should create a single tax adjustment" do - @rate.adjust(@order) - @order.adjustments.tax.charge.count.should == 1 - end - end - - end - - end - -end diff --git a/core/spec/models/taxon_spec.rb b/core/spec/models/taxon_spec.rb deleted file mode 100644 index 9e79a5526a5..00000000000 --- a/core/spec/models/taxon_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# coding: UTF-8 - -require 'spec_helper' - -describe Spree::Taxon do - let(:taxon) { Spree::Taxon.new(:name => "Ruby on Rails") } - - context "set_permalink" do - - it "should set permalink correctly when no parent present" do - taxon.set_permalink - taxon.permalink.should == "ruby-on-rails" - end - - it "should support Chinese characters" do - taxon.name = "你好" - taxon.set_permalink - taxon.permalink.should == 'ni-hao' - end - - context "with parent taxon" do - before do - taxon.stub(:parent_id => 123) - Spree::Taxon.should_receive(:find).with(123).and_return(mock_model(Spree::Taxon, :permalink => "brands")) - end - - it "should set permalink correctly when taxon has parent" do - taxon.set_permalink - taxon.permalink.should == "brands/ruby-on-rails" - end - - it "should set permalink correctly with existing permalink present" do - taxon.permalink = "b/rubyonrails" - taxon.set_permalink - taxon.permalink.should == "brands/rubyonrails" - end - - it "should support Chinese characters" do - taxon.name = "我" - taxon.set_permalink - taxon.permalink.should == "brands/wo" - end - - end - - end - -end diff --git a/core/spec/models/tracker_spec.rb b/core/spec/models/tracker_spec.rb deleted file mode 100644 index b4b97ad1980..00000000000 --- a/core/spec/models/tracker_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Spree::Tracker do - describe "current" do - before(:each) { @tracker = create(:tracker) } - - it "returns the first active tracker for the environment" do - Spree::Tracker.current.should == @tracker - end - - it "does not return a tracker with a blank analytics_id" do - @tracker.update_attribute(:analytics_id, '') - Spree::Tracker.current.should == nil - end - - it "does not return an inactive tracker" do - @tracker.update_attribute(:active, false) - Spree::Tracker.current.should == nil - end - end -end diff --git a/core/spec/models/variant/scopes_spec.rb b/core/spec/models/variant/scopes_spec.rb deleted file mode 100644 index 9ffdd1c92cf..00000000000 --- a/core/spec/models/variant/scopes_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe "Variant scopes" do - let!(:product) { create(:product) } - let!(:variant_1) { create(:variant, :product => product) } - let!(:variant_2) { create(:variant, :product => product) } - - it ".descend_by_popularity" do - # Requires a product with at least two variants, where one has a higher number of orders than the other - create(:line_item, :variant => variant_1) - Spree::Variant.descend_by_popularity.first.should == variant_1 - end - - context "finding by option values" do - let!(:option_type) { create(:option_type, :name => "bar") } - let!(:option_value_1) do - option_value = create(:option_value, :name => "foo", :option_type => option_type) - variant_1.option_values << option_value - option_value - end - - let!(:option_value_2) do - option_value = create(:option_value, :name => "fizz", :option_type => option_type) - variant_1.option_values << option_value - option_value - end - - let!(:product_variants) { product.variants_including_master } - - it "by objects" do - variants = product_variants.has_option(option_type, option_value_1) - variants.should include(variant_1) - variants.should_not include(variant_2) - end - - it "by names" do - variants = product_variants.has_option("bar", "foo") - variants.should include(variant_1) - variants.should_not include(variant_2) - end - - it "by ids" do - variants = product_variants.has_option(option_type.id, option_value_1.id) - variants.should include(variant_1) - variants.should_not include(variant_2) - end - - it "by mixed conditions" do - variants = product_variants.has_option(option_type.id, "foo", option_value_2) - end - end -end diff --git a/core/spec/models/variant_spec.rb b/core/spec/models/variant_spec.rb deleted file mode 100644 index c59b2a7fd12..00000000000 --- a/core/spec/models/variant_spec.rb +++ /dev/null @@ -1,361 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -describe Spree::Variant do - let!(:variant) { create(:variant, :count_on_hand => 95) } - - before(:each) do - reset_spree_preferences - end - - context "validations" do - it "should validate price is greater than 0" do - variant.price = -1 - variant.should be_invalid - end - - it "should validate price is 0" do - variant.price = 0 - variant.should be_valid - end - end - - # Regression test for #1778 - it "recalculates product's count_on_hand when saved" do - Spree::Config[:track_inventory_levels] = true - variant.stub :is_master? => true - variant.product.should_receive(:on_hand).and_return(3) - variant.product.should_receive(:update_column).with(:count_on_hand, 3) - variant.run_callbacks(:save) - end - - it "lock_version should prevent stale updates" do - copy = Spree::Variant.find(variant.id) - - copy.count_on_hand = 200 - copy.save! - - variant.count_on_hand = 100 - expect { variant.save }.to raise_error ActiveRecord::StaleObjectError - - variant.reload.count_on_hand.should == 200 - variant.count_on_hand = 100 - variant.save - - variant.reload.count_on_hand.should == 100 - end - - context "on_hand=" do - before { variant.stub(:inventory_units => mock('inventory-units')) } - - context "when :track_inventory_levels is true" do - before { Spree::Config.set :track_inventory_levels => true } - - context "and count is increased" do - before { variant.inventory_units.stub(:with_state).and_return([]) } - let(:inventory_unit) { mock_model(Spree::InventoryUnit, :state => "backordered") } - - it "should change count_on_hand to given value" do - variant.on_hand = 100 - variant.count_on_hand.should == 100 - end - - it "should check for backordered units" do - variant.save! - variant.inventory_units.should_receive(:with_state).with("backordered") - variant.on_hand = 100 - variant.save! - end - - it "should fill 1 backorder when count_on_hand is zero" do - variant.count_on_hand = 0 - variant.save! - variant.inventory_units.stub(:with_state).and_return([inventory_unit]) - inventory_unit.should_receive(:fill_backorder) - variant.on_hand = 100 - variant.save! - variant.count_on_hand.should == 99 - end - - it "should fill multiple backorders when count_on_hand is negative" do - variant.count_on_hand = -5 - variant.save! - variant.inventory_units.stub(:with_state).and_return(Array.new(5, inventory_unit)) - inventory_unit.should_receive(:fill_backorder).exactly(5).times - variant.on_hand = 100 - variant.save! - variant.count_on_hand.should == 95 - end - - it "should keep count_on_hand negative when count is not enough to fill backorders" do - variant.count_on_hand = -10 - variant.save! - variant.inventory_units.stub(:with_state).and_return(Array.new(10, inventory_unit)) - inventory_unit.should_receive(:fill_backorder).exactly(5).times - variant.on_hand = 5 - variant.save! - variant.count_on_hand.should == -5 - end - - end - - context "and count is negative" do - before { variant.inventory_units.stub(:with_state).and_return([]) } - - it "should change count_on_hand to given value" do - variant.on_hand = 10 - variant.count_on_hand.should == 10 - end - - it "should not check for backordered units" do - variant.inventory_units.should_not_receive(:with_state) - variant.on_hand = 10 - end - - end - - end - - context "when :track_inventory_levels is false" do - before { Spree::Config.set :track_inventory_levels => false } - - it "should raise an exception" do - lambda { variant.on_hand = 100 }.should raise_error - end - - end - - end - - context "on_hand" do - context "when :track_inventory_levels is true" do - before { Spree::Config.set :track_inventory_levels => true } - - it "should return count_on_hand" do - variant.on_hand.should == variant.count_on_hand - end - end - - context "when :track_inventory_levels is false" do - before { Spree::Config.set :track_inventory_levels => false } - - it "should return nil" do - variant.on_hand.should eql(1.0/0) # Infinity - end - - end - - end - - context "in_stock?" do - context "when :track_inventory_levels is true" do - before { Spree::Config.set :track_inventory_levels => true } - - it "should be true when count_on_hand is positive" do - variant.in_stock?.should be_true - end - - it "should be false when count_on_hand is zero" do - variant.stub(:count_on_hand => 0) - variant.in_stock?.should be_false - end - - it "should be false when count_on_hand is negative" do - variant.stub(:count_on_hand => -10) - variant.in_stock?.should be_false - end - end - - context "when :track_inventory_levels is false" do - before { Spree::Config.set :track_inventory_levels => false } - - it "should be true" do - variant.in_stock?.should be_true - end - - end - - context "product has other variants" do - describe "option value accessors" do - before { - @multi_variant = FactoryGirl.create :variant, :product => variant.product - variant.product.reload - } - - let(:multi_variant) { @multi_variant } - - it "should set option value" do - multi_variant.option_value('media_type').should be_nil - - multi_variant.set_option_value('media_type', 'DVD') - multi_variant.option_value('media_type').should == 'DVD' - - multi_variant.set_option_value('media_type', 'CD') - multi_variant.option_value('media_type').should == 'CD' - end - - it "should not duplicate associated option values when set multiple times" do - multi_variant.set_option_value('media_type', 'CD') - - expect { - multi_variant.set_option_value('media_type', 'DVD') - }.to_not change(multi_variant.option_values, :count) - - expect { - multi_variant.set_option_value('coolness_type', 'awesome') - }.to change(multi_variant.option_values, :count).by(1) - end - end - end - - end - - context "price parsing" do - before(:each) do - I18n.locale = I18n.default_locale - I18n.backend.store_translations(:de, { :number => { :currency => { :format => { :delimiter => '.', :separator => ',' } } } }) - end - - after do - I18n.locale = I18n.default_locale - end - - context "price=" do - context "with decimal point" do - it "captures the proper amount for a formatted price" do - variant.price = '1,599.99' - variant.price.should == 1599.99 - end - end - - context "with decimal comma" do - it "captures the proper amount for a formatted price" do - I18n.locale = :de - variant.price = '1.599,99' - variant.price.should == 1599.99 - end - end - - context "with a numeric price" do - it "uses the price as is" do - I18n.locale = :de - variant.price = 1599.99 - variant.price.should == 1599.99 - end - end - end - - context "cost_price=" do - context "with decimal point" do - it "captures the proper amount for a formatted price" do - variant.cost_price = '1,599.99' - variant.cost_price.should == 1599.99 - end - end - - context "with decimal comma" do - it "captures the proper amount for a formatted price" do - I18n.locale = :de - variant.cost_price = '1.599,99' - variant.cost_price.should == 1599.99 - end - end - - context "with a numeric price" do - it "uses the price as is" do - I18n.locale = :de - variant.cost_price = 1599.99 - variant.cost_price.should == 1599.99 - end - end - end - end - - context "#currency" do - it "returns the globally configured currency" do - variant.currency.should == "USD" - end - end - - context "#display_amount" do - it "retuns a Spree::Money" do - variant.price = 21.22 - variant.display_amount.should == "$21.22" - end - end - - context "#cost_currency" do - context "when cost currency is nil" do - before { variant.cost_currency = nil } - it "populates cost currency with the default value on save" do - variant.save! - variant.cost_currency.should == "USD" - end - end - end - - describe '.price_in' do - before do - variant.prices << create(:price, :variant => variant, :currency => "EUR", :amount => 33.33) - end - - subject { variant.price_in(currency).display_amount } - - context "when currency is not specified" do - let(:currency) { nil } - - it "returns nil" do - subject.should be_nil - end - end - - context "when currency is EUR" do - let(:currency) { 'EUR' } - - it "returns the value in the EUR" do - subject.should == "€33.33" - end - end - - context "when currency is USD" do - let(:currency) { 'USD' } - - it "returns the value in the USD" do - subject.should == "$19.99" - end - end - end - - describe '.amount_in' do - before do - variant.prices << create(:price, :variant => variant, :currency => "EUR", :amount => 33.33) - end - - subject { variant.amount_in(currency) } - - context "when currency is not specified" do - let(:currency) { nil } - - it "returns nil" do - subject.should be_nil - end - end - - context "when currency is EUR" do - let(:currency) { 'EUR' } - - it "returns the value in the EUR" do - subject.should == 33.33 - end - end - - context "when currency is USD" do - let(:currency) { 'USD' } - - it "returns the value in the USD" do - subject.should == 19.99 - end - end - end -end diff --git a/core/spec/models/zone_spec.rb b/core/spec/models/zone_spec.rb deleted file mode 100644 index 098f9402c09..00000000000 --- a/core/spec/models/zone_spec.rb +++ /dev/null @@ -1,305 +0,0 @@ -require 'spec_helper' - -describe Spree::Zone do - context "#match" do - let(:country_zone) { create(:zone, :name => "CountryZone") } - let(:country) do - country = create(:country) - # Create at least one state for this country - state = create(:state, :country => country) - country - end - - before { country_zone.members.create(:zoneable => country) } - - context "when there is only one qualifying zone" do - let(:address) { create(:address, :country => country, :state => country.states.first) } - - it "should return the qualifying zone" do - Spree::Zone.match(address).should == country_zone - end - end - - context "when there are two qualified zones with same member type" do - let(:address) { create(:address, :country => country, :state => country.states.first) } - let(:second_zone) { create(:zone, :name => "SecondZone") } - - before { second_zone.members.create(:zoneable => country) } - - context "when both zones have the same number of members" do - it "should return the zone that was created first" do - Spree::Zone.match(address).should == country_zone - end - end - - context "when one of the zones has fewer members" do - let(:country2) { create(:country) } - - before { country_zone.members.create(:zoneable => country2) } - - it "should return the zone with fewer members" do - Spree::Zone.match(address).should == second_zone - end - end - end - - context "when there are two qualified zones with different member types" do - let(:state_zone) { create(:zone, :name => "StateZone") } - let(:address) { create(:address, :country => country, :state => country.states.first) } - - before { state_zone.members.create(:zoneable => country.states.first) } - - it "should return the zone with the more specific member type" do - Spree::Zone.match(address).should == state_zone - end - end - - context "when there are no qualifying zones" do - it "should return nil" do - Spree::Zone.match(Spree::Address.new).should be_nil - end - end - end - - context "#country_list" do - let(:state) { create(:state) } - let(:country) { state.country } - - context "when zone consists of countries" do - let(:country_zone) { create(:zone, :name => "CountryZone") } - - before { country_zone.members.create(:zoneable => country) } - - it 'should return a list of countries' do - country_zone.country_list.should == [country] - end - end - - context "when zone consists of states" do - let(:state_zone) { create(:zone, :name => "StateZone") } - - before { state_zone.members.create(:zoneable => state) } - - it 'should return a list of countries' do - state_zone.country_list.should == [state.country] - end - end - end - - context "#include?" do - let(:state) { create(:state) } - let(:country) { state.country } - let(:address) { create(:address, :state => state) } - - context "when zone is country type" do - let(:country_zone) { create(:zone, :name => "CountryZone") } - before { country_zone.members.create(:zoneable => country) } - - it "should be true" do - country_zone.include?(address).should be_true - end - end - - context "when zone is state type" do - let(:state_zone) { create(:zone, :name => "StateZone") } - before { state_zone.members.create(:zoneable => state) } - - it "should be true" do - state_zone.include?(address).should be_true - end - end - end - - context ".default_tax" do - context "when there is a default tax zone specified" do - before { @foo_zone = create(:zone, :name => "whatever", :default_tax => true) } - - it "should be the correct zone" do - foo_zone = create(:zone, :name => "foo") - Spree::Zone.default_tax.should == @foo_zone - end - end - - context "when there is no default tax zone specified" do - it "should be nil" do - Spree::Zone.default_tax.should be_nil - end - end - end - - context "#contains?" do - let(:country1) { create(:country) } - let(:country2) { create(:country) } - let(:country3) { create(:country) } - - before do - @source = create(:zone, :name => "source", :zone_members => []) - @target = create(:zone, :name => "target", :zone_members => []) - end - - context "when the target has no members" do - before { @source.members.create(:zoneable => country1) } - - it "should be false" do - @source.contains?(@target).should be_false - end - end - - context "when the source has no members" do - before { @target.members.create(:zoneable => country1) } - - it "should be false" do - @source.contains?(@target).should be_false - end - end - - context "when both zones are the same zone" do - before do - @source.members.create(:zoneable => country1) - @target = @source - end - - it "should be true" do - @source.contains?(@target).should be_true - end - end - - context "when both zones are of the same type" do - before do - @source.members.create(:zoneable => country1) - @source.members.create(:zoneable => country2) - end - - context "when all members are included in the zone we check against" do - before do - @target.members.create(:zoneable => country1) - @target.members.create(:zoneable => country2) - end - - it "should be true" do - @source.contains?(@target).should be_true - end - end - - context "when some members are included in the zone we check against" do - before do - @target.members.create(:zoneable => country1) - @target.members.create(:zoneable => country2) - @target.members.create(:zoneable => create(:country)) - end - - it "should be false" do - @source.contains?(@target).should be_false - end - end - - context "when none of the members are included in the zone we check against" do - before do - @target.members.create(:zoneable => create(:country)) - @target.members.create(:zoneable => create(:country)) - end - - it "should be false" do - @source.contains?(@target).should be_false - end - end - end - - context "when checking country against state" do - before do - @source.members.create(:zoneable => create(:state)) - @target.members.create(:zoneable => country1) - end - - it "should be false" do - @source.contains?(@target).should be_false - end - end - - context "when checking state against country" do - before { @source.members.create(:zoneable => country1) } - - context "when all states contained in one of the countries we check against" do - - before do - state1 = create(:state, :country => country1) - @target.members.create(:zoneable => state1) - end - - it "should be true" do - @source.contains?(@target).should be_true - end - end - - context "when some states contained in one of the countries we check against" do - - before do - state1 = create(:state, :country => country1) - @target.members.create(:zoneable => state1) - @target.members.create(:zoneable => create(:state, :country => country2)) - end - - it "should be false" do - @source.contains?(@target).should be_false - end - end - - context "when none of the states contained in any of the countries we check against" do - - before do - @target.members.create(:zoneable => create(:state, :country => country2)) - @target.members.create(:zoneable => create(:state, :country => country2)) - end - - it "should be false" do - @source.contains?(@target).should be_false - end - end - end - - end - - context "#save" do - context "when default_tax is true" do - it "should clear previous default tax zone" do - zone1 = create(:zone, :name => "foo", :default_tax => true) - zone = create(:zone, :name => "bar", :default_tax => true) - zone1.reload.default_tax.should == false - end - end - - context "when a zone member country is added to an existing zone consisting of state members" do - it "should remove existing state members" do - zone = create(:zone, :name => "foo", :zone_members => []) - state = create(:state) - country = create(:country) - zone.members.create(:zoneable => state) - country_member = zone.members.create(:zoneable => country) - zone.save - zone.reload.members.should == [country_member] - end - end - end - - context "#kind" do - context "when the zone consists of country zone members" do - before do - @zone = create(:zone, :name => "country", :zone_members => []) - @zone.members.create(:zoneable => create(:country)) - end - it "should return the kind of zone member" do - @zone.kind.should == "country" - end - end - - context "when the zone consists of state zone members" do - before do - @zone = create(:zone, :name => "state", :zone_members => []) - @zone.members.create(:zoneable => create(:state)) - end - it "should return the kind of zone member" do - @zone.kind.should == "state" - end - end - end -end diff --git a/core/spec/requests/address_spec.rb b/core/spec/requests/address_spec.rb deleted file mode 100644 index 7946c665d0a..00000000000 --- a/core/spec/requests/address_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'spec_helper' - -describe "Address" do - before do - @product = create(:product, :name => "RoR Mug", :on_hand => 1) - @product.save - - @order = create(:order_with_totals, :state => 'cart') - @order.stub(:available_payment_methods => [create(:bogus_payment_method, :environment => 'test') ]) - - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - Spree::Order.last.update_column(:email, "funk@groove.com") - - address = "order_bill_address_attributes" - @country_css = "#{address}_country_id" - @state_select_css = "##{address}_state_id" - @state_name_css = "##{address}_state_name" - end - - it "shows the state collection selection for a country having states", :js => true do - canada = create(:country, :name => "Canada", :states_required => true) - Factory(:state, :name => "Ontario", :country => canada) - - click_button "Checkout" - select canada.name, :from => @country_css - page.find(@state_select_css).should be_visible - page.find(@state_name_css).should_not be_visible - end - - it "shows the state input field for a country with states required but for which states are not defined", :js => true do - italy = create(:country, :name => "Italy", :states_required => true) - click_button "Checkout" - - select italy.name, :from => @country_css - page.find(@state_select_css).should_not be_visible - page.find(@state_name_css).should be_visible - page.should_not have_selector("input#{@state_name_css}[disabled]") - end - - it "shows a disabled state input field for a country where states are not required", :js => true do - france = create(:country, :name => "France", :states_required => false) - click_button "Checkout" - - select france.name, :from => @country_css - page.find(@state_select_css).should_not be_visible - page.find(@state_name_css).should_not be_visible - end - - it "should clear the state name when selecting a country without states required", :js =>true do - italy = create(:country, :name => "Italy", :states_required => true) - france = create(:country, :name => "France", :states_required => false) - - click_button "Checkout" - select italy.name, :from => @country_css - page.find(@state_name_css).set("Toscana") - - select france.name, :from => @country_css - page.find(@state_name_css).should have_content('') - end -end \ No newline at end of file diff --git a/core/spec/requests/admin/configuration/analytics_tracker_spec.rb b/core/spec/requests/admin/configuration/analytics_tracker_spec.rb deleted file mode 100644 index 2e07f69319d..00000000000 --- a/core/spec/requests/admin/configuration/analytics_tracker_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -describe "Analytics Tracker" do - stub_authorization! - - context "index" do - before(:each) do - 2.times { create(:tracker, :environment => "test") } - visit spree.admin_path - click_link "Configuration" - click_link "Analytics Tracker" - end - - it "should have the right content" do - page.should have_content("Analytics Trackers") - end - - it "should have the right tabular values displayed" do - within_row(1) do - column_text(1).should == "A100" - column_text(2).should == "Test" - column_text(3).should == "Yes" - end - - within_row(2) do - column_text(1).should == "A100" - column_text(2).should == "Test" - column_text(3).should == "Yes" - end - end - end - - context "create" do - before(:each) do - visit spree.admin_path - click_link "Configuration" - click_link "Analytics Tracker" - end - - it "should be able to create a new analytics tracker" do - click_link "admin_new_tracker_link" - fill_in "tracker_analytics_id", :with => "A100" - select "Test", :from => "tracker-env" - click_button "Create" - - page.should have_content("successfully created!") - within_row(1) do - column_text(1).should == "A100" - column_text(2).should == "Test" - column_text(3).should == "Yes" - end - end - end -end diff --git a/core/spec/requests/admin/configuration/general_settings_spec.rb b/core/spec/requests/admin/configuration/general_settings_spec.rb deleted file mode 100644 index 2e47963b450..00000000000 --- a/core/spec/requests/admin/configuration/general_settings_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -describe "General Settings" do - stub_authorization! - - before(:each) do - visit spree.admin_path - click_link "Configuration" - click_link "General Settings" - end - - context "visiting general settings (admin)" do - it "should have the right content" do - page.should have_content("General Settings") - find("#site_name").value.should == "Spree Demo Site" - find("#site_url").value.should == "demo.spreecommerce.com" - end - end - - context "editing general settings (admin)" do - it "should be able to update the site name" do - fill_in "site_name", :with => "Spree Demo Site99" - click_button "Update" - - assert_successful_update_message(:general_settings) - find("#site_name").value.should == "Spree Demo Site99" - end - end -end diff --git a/core/spec/requests/admin/configuration/inventory_settings_spec.rb b/core/spec/requests/admin/configuration/inventory_settings_spec.rb deleted file mode 100644 index 06335630971..00000000000 --- a/core/spec/requests/admin/configuration/inventory_settings_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe "Inventory Settings" do - stub_authorization! - - context "changing settings" do - before(:each) do - reset_spree_preferences do |config| - config.allow_backorders = true - end - - visit spree.admin_path - click_link "Configuration" - click_link "Inventory Settings" - end - - it "should have the right content" do - page.should have_content("Inventory Settings") - page.should have_content("Show out-of-stock products") - page.should have_content("Allow Backorders") - end - - it "should be able to toggle displaying zero stock products" do - uncheck "preferences_show_zero_stock_products" - click_button "Update" - assert_successful_update_message(:inventory_settings) - assert_preference_unset(:show_zero_stock_products) - end - - it "should be able to toggle allowing backorders" do - uncheck "preferences_allow_backorders" - click_button "Update" - assert_successful_update_message(:inventory_settings) - assert_preference_unset(:allow_backorders) - end - end -end diff --git a/core/spec/requests/admin/configuration/mail_methods_spec.rb b/core/spec/requests/admin/configuration/mail_methods_spec.rb deleted file mode 100644 index 11f3b9d7b4e..00000000000 --- a/core/spec/requests/admin/configuration/mail_methods_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe "Mail Methods" do - stub_authorization! - - before(:each) do - visit spree.admin_path - click_link "Configuration" - end - - context "index" do - before(:each) do - create(:mail_method) - click_link "Mail Methods" - end - - it "should be able to display information about existing mail methods" do - within_row(1) do - column_text(1).should == "Test" - column_text(2).should == "Yes" - end - end - end - - context "create" do - it "should be able to create a new mail method" do - click_link "Mail Methods" - click_link "admin_new_mail_method_link" - page.should have_content("New Mail Method") - click_button "Create" - page.should have_content("successfully created!") - end - end - - context "edit" do - let!(:mail_method) { create(:mail_method, :preferred_smtp_password => "haxme") } - - before do - click_link "Mail Methods" - end - - it "should be able to edit an existing mail method" do - within_row(1) { click_icon :edit } - - fill_in "mail_method_preferred_mail_bcc", :with => "spree@example.com99" - click_button "Update" - page.should have_content("successfully updated!") - - within_row(1) { click_icon :edit } - find_field("mail_method_preferred_mail_bcc").value.should == "spree@example.com99" - end - - # Regression test for #2094 - it "does not clear password if not provided" do - mail_method.preferred_smtp_password.should == "haxme" - within_row(1) { click_icon :edit } - click_button "Update" - page.should have_content("successfully updated!") - - mail_method.reload - mail_method.preferred_smtp_password.should_not be_blank - end - - end -end diff --git a/core/spec/requests/admin/configuration/payment_methods_spec.rb b/core/spec/requests/admin/configuration/payment_methods_spec.rb deleted file mode 100644 index 748eabf4481..00000000000 --- a/core/spec/requests/admin/configuration/payment_methods_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'spec_helper' - -describe "Payment Methods" do - stub_authorization! - - before(:each) do - visit spree.admin_path - click_link "Configuration" - end - - context "admin visiting payment methods listing page" do - it "should display existing payment methods" do - create(:payment_method) - click_link "Payment Methods" - - within("table#listing_payment_methods") do - find('th:nth-child(1)').text.should == "Name" - find('th:nth-child(2)').text.should == "Provider" - find('th:nth-child(3)').text.should == "Environment" - find('th:nth-child(4)').text.should == "Active" - end - - within('table#listing_payment_methods') do - page.should have_content("Spree::PaymentMethod::Check") - end - end - end - - context "admin creating a new payment method" do - it "should be able to create a new payment method" do - click_link "Payment Methods" - click_link "admin_new_payment_methods_link" - page.should have_content("New Payment Method") - fill_in "payment_method_name", :with => "check90" - fill_in "payment_method_description", :with => "check90 desc" - select "PaymentMethod::Check", :from => "gtwy-type" - click_button "Create" - page.should have_content("successfully created!") - end - end - - context "admin editing a payment method" do - before(:each) do - create(:payment_method) - click_link "Payment Methods" - within("table#listing_payment_methods") do - click_icon(:edit) - end - end - - it "should be able to edit an existing payment method" do - fill_in "payment_method_name", :with => "Payment 99" - click_button "Update" - page.should have_content("successfully updated!") - find_field("payment_method_name").value.should == "Payment 99" - end - - it "should display validation errors" do - fill_in "payment_method_name", :with => "" - click_button "Update" - page.should have_content("Name can't be blank") - end - end -end diff --git a/core/spec/requests/admin/configuration/shipping_methods_spec.rb b/core/spec/requests/admin/configuration/shipping_methods_spec.rb deleted file mode 100644 index 5017783654a..00000000000 --- a/core/spec/requests/admin/configuration/shipping_methods_spec.rb +++ /dev/null @@ -1,239 +0,0 @@ -require 'spec_helper' -require 'active_record/fixtures' - -describe "Shipping Methods" do - stub_authorization! - - let!(:address) { create(:address, :state => create(:state)) } - let!(:zone) { Spree::Zone.find_by_name("GlobalZone") || create(:global_zone) } - let!(:shipping_method) { create(:shipping_method, :zone => zone) } - - before(:each) do - # HACK: To work around no email prompting on check out - Spree::Order.any_instance.stub(:require_email => false) - create(:payment_method, :environment => 'test') - @product = create(:product, :name => "Mug") - - visit spree.admin_path - click_link "Configuration" - end - - - context "show" do - it "should display exisiting shipping methods" do - click_link "Shipping Methods" - - within_row(1) do - column_text(1).should == shipping_method.name - column_text(2).should == zone.name - column_text(3).should == "Flat Rate (per order)" - column_text(4).should == "Both" - end - end - end - - context "create" do - it "should be able to create a new shipping method" do - click_link "Shipping Methods" - click_link "admin_new_shipping_method_link" - page.should have_content("New Shipping Method") - fill_in "shipping_method_name", :with => "bullock cart" - click_button "Create" - page.should have_content("successfully created!") - page.should have_content("Editing Shipping Method") - end - end - - # Regression test for #1331 - context "update" do - it "can change the calculator", :js => true do - click_link "Shipping Methods" - within("#listing_shipping_methods") do - click_icon :edit - end - - click_button "Update" - page.should_not have_content("Shipping method is not found") - end - end - - context "availability", :js => true do - before(:each) do - @shipping_category = create(:shipping_category, :name => "Default") - click_link "Shipping Methods" - click_link "admin_new_shipping_method_link" - end - - context "when rule is no products match" do - context "when match rules are satisfied" do - it "shows the right shipping method on checkout" do - fill_in "shipping_method_name", :with => "Standard" - select shipping_method.zone.name, :from => "shipping_method_zone_id" - select @shipping_category.name, :from => "shipping_method_shipping_category_id" - check "shipping_method_match_none" - click_button "Create" - - visit spree.root_path - click_link "Mug" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - select address.country.name, :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - page.should have_content("Standard") - end - end - - context "when match rules aren't satisfied" do - before { @product.shipping_category = @shipping_category; @product.save } - - it "shows the right shipping method on checkout" do - fill_in "shipping_method_name", :with => "Standard" - select shipping_method.zone.name, :from => "shipping_method_zone_id" - select @shipping_category.name, :from => "shipping_method_shipping_category_id" - check "shipping_method_match_none" - click_button "Create" - - visit spree.root_path - click_link "Mug" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - select address.country.name, :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - page.should_not have_content("Standard") - end - end - end - - context "when rule is all products match" do - context "when match rules are satisfied" do - before { @product.shipping_category = @shipping_category; @product.save } - - it "shows the right shipping method on checkout" do - fill_in "shipping_method_name", :with => "Standard" - select shipping_method.zone.name, :from => "shipping_method_zone_id" - select @shipping_category.name, :from => "shipping_method_shipping_category_id" - check "shipping_method_match_all" - click_button "Create" - - visit spree.root_path - click_link "Mug" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - select address.country.name, :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - page.should have_content("Standard") - end - end - - context "when match rules aren't satisfied" do - it "shows the right shipping method on checkout" do - fill_in "shipping_method_name", :with => "Standard" - select shipping_method.zone.name, :from => "shipping_method_zone_id" - select @shipping_category.name, :from => "shipping_method_shipping_category_id" - check "shipping_method_match_all" - click_button "Create" - - visit spree.root_path - click_link "Mug" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - select address.country.name, :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - page.should_not have_content("Standard") - end - end - end - - context "when rule is at least one products match" do - before(:each) do - create(:product, :name => "Shirt") - end - - context "when match rules are satisfied" do - before { @product.shipping_category = @shipping_category; @product.save } - - it "shows the right shipping method on checkout" do - fill_in "shipping_method_name", :with => "Standard" - select zone.name, :from => "shipping_method_zone_id" - select @shipping_category.name, :from => "shipping_method_shipping_category_id" - check "shipping_method_match_one" - click_button "Create" - - visit spree.root_path - click_link "Mug" - click_button "Add To Cart" - click_link "Home" - click_link "Shirt" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - select address.country.name, :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - page.should have_content("Standard") - end - end - - context "when match rules aren't satisfied" do - it "shows the right shipping method on checkout" do - fill_in "shipping_method_name", :with => "Standard" - select zone.name, :from => "shipping_method_zone_id" - select @shipping_category.name, :from => "shipping_method_shipping_category_id" - check "shipping_method_match_one" - click_button "Create" - - visit spree.root_path - click_link "Mug" - click_button "Add To Cart" - click_link "Home" - click_link "Shirt" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - select address.country.name, :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - page.should_not have_content("Standard") - end - end - end - end -end diff --git a/core/spec/requests/admin/configuration/states_spec.rb b/core/spec/requests/admin/configuration/states_spec.rb deleted file mode 100644 index 38a8afd18d5..00000000000 --- a/core/spec/requests/admin/configuration/states_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -describe "States" do - stub_authorization! - - let!(:country) { create(:country) } - - before(:each) do - Spree::Config[:default_country_id] = country.id - - visit spree.admin_path - click_link "Configuration" - end - - context "admin visiting states listing" do - let!(:state) { create(:state, :country => country) } - - it "should correctly display the states" do - click_link "States" - page.should have_content(state.name) - end - end - - context "creating and editing states" do - it "should allow an admin to edit existing states", :js => true do - click_link "States" - wait_until do - page.should have_selector('#country', :visible => true) - end - select country.name, :from => "Country" - click_link "new_state_link" - fill_in "state_name", :with => "Calgary" - fill_in "Abbreviation", :with => "CL" - click_button "Create" - page.should have_content("successfully created!") - page.should have_content("Calgary") - end - - it "should show validation errors", :js => true do - click_link "States" - select country.name, :from => "country" - - wait_until do - page.should have_selector("#new_state_link", :visible => true) - end - click_link "new_state_link" - - fill_in "state_name", :with => "" - fill_in "Abbreviation", :with => "" - click_button "Create" - page.should have_content("Name can't be blank") - end - end -end diff --git a/core/spec/requests/admin/configuration/tax_categories_spec.rb b/core/spec/requests/admin/configuration/tax_categories_spec.rb deleted file mode 100644 index d4ddfcaa22f..00000000000 --- a/core/spec/requests/admin/configuration/tax_categories_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe "Tax Categories" do - stub_authorization! - - before(:each) do - visit spree.admin_path - click_link "Configuration" - end - - context "admin visiting tax categories list" do - it "should display the existing tax categories" do - create(:tax_category, :name => "Clothing", :description => "For Clothing") - click_link "Tax Categories" - page.should have_content("Listing Tax Categories") - within_row(1) do - column_text(1).should == "Clothing" - column_text(2).should == "For Clothing" - column_text(3).should == "False" - end - end - end - - context "admin creating new tax category" do - before(:each) do - click_link "Tax Categories" - click_link "admin_new_tax_categories_link" - end - - it "should be able to create new tax category" do - page.should have_content("New Tax Category") - fill_in "tax_category_name", :with => "sports goods" - fill_in "tax_category_description", :with => "sports goods desc" - click_button "Create" - page.should have_content("successfully created!") - end - - it "should show validation errors if there are any" do - click_button "Create" - page.should have_content("Name can't be blank") - end - end - - context "admin editing a tax category" do - it "should be able to update an existing tax category" do - create(:tax_category) - click_link "Tax Categories" - within_row(1) { click_icon :edit } - fill_in "tax_category_description", :with => "desc 99" - click_button "Update" - page.should have_content("successfully updated!") - page.should have_content("desc 99") - end - end -end diff --git a/core/spec/requests/admin/configuration/tax_rates_spec.rb b/core/spec/requests/admin/configuration/tax_rates_spec.rb deleted file mode 100644 index ee60ce405bd..00000000000 --- a/core/spec/requests/admin/configuration/tax_rates_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -describe "Tax Rates" do - stub_authorization! - - let!(:tax_rate) { create(:tax_rate, :calculator => stub_model(Spree::Calculator)) } - - before do - visit spree.admin_path - click_link "Configuration" - end - - # Regression test for #535 - it "can see a tax rate in the list if the tax category has been deleted" do - tax_rate.tax_category.mark_deleted! - lambda { click_link "Tax Rates" }.should_not raise_error - within("table tbody td:nth-child(3)") do - page.should have_content("N/A") - end - end - - # Regression test for #1422 - it "can create a new tax rate" do - click_link "Tax Rates" - click_link "New Tax Rate" - fill_in "Rate", :with => "0.05" - click_button "Create" - page.should have_content("Tax Rate has been successfully created!") - end -end diff --git a/core/spec/requests/admin/configuration/taxonomies_spec.rb b/core/spec/requests/admin/configuration/taxonomies_spec.rb deleted file mode 100644 index f6e260a31af..00000000000 --- a/core/spec/requests/admin/configuration/taxonomies_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe "Taxonomies" do - stub_authorization! - - before(:each) do - visit spree.admin_path - click_link "Configuration" - end - - context "show" do - it "should display existing taxonomies" do - create(:taxonomy, :name => 'Brand') - create(:taxonomy, :name => 'Categories') - click_link "Taxonomies" - within_row(1) { page.should have_content("Brand") } - within_row(2) { page.should have_content("Categories") } - end - end - - context "create" do - before(:each) do - click_link "Taxonomies" - click_link "admin_new_taxonomy_link" - end - - it "should allow an admin to create a new taxonomy" do - page.should have_content("New Taxonomy") - fill_in "taxonomy_name", :with => "sports" - click_button "Create" - page.should have_content("successfully created!") - end - - it "should display validation errors" do - fill_in "taxonomy_name", :with => "" - click_button "Create" - page.should have_content("can't be blank") - end - end - - context "edit" do - it "should allow an admin to update an existing taxonomy" do - create(:taxonomy) - click_link "Taxonomies" - within_row(1) { click_icon :edit } - fill_in "taxonomy_name", :with => "sports 99" - click_button "Update" - page.should have_content("successfully updated!") - page.should have_content("sports 99") - end - end -end diff --git a/core/spec/requests/admin/configuration/zones_spec.rb b/core/spec/requests/admin/configuration/zones_spec.rb deleted file mode 100644 index 7c5c8b4cc5c..00000000000 --- a/core/spec/requests/admin/configuration/zones_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -describe "Zones" do - stub_authorization! - - before(:each) do - Spree::Zone.delete_all - visit spree.admin_path - click_link "Configuration" - end - - context "show" do - it "should display existing zones" do - create(:zone, :name => "eastern", :description => "zone is eastern") - create(:zone, :name => "western", :description => "cool san fran") - click_link "Zones" - - within_row(1) { page.should have_content("eastern") } - within_row(2) { page.should have_content("western") } - - click_link "zones_order_by_description_title" - - within_row(1) { page.should have_content("western") } - within_row(2) { page.should have_content("eastern") } - end - end - - context "create" do - it "should allow an admin to create a new zone" do - click_link "Zones" - click_link "admin_new_zone_link" - page.should have_content("New Zone") - fill_in "zone_name", :with => "japan" - fill_in "zone_description", :with => "japanese time zone" - click_button "Create" - page.should have_content("successfully created!") - end - end -end diff --git a/core/spec/requests/admin/homepage_spec.rb b/core/spec/requests/admin/homepage_spec.rb deleted file mode 100644 index 28accd602b3..00000000000 --- a/core/spec/requests/admin/homepage_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'spec_helper' - -describe "Homepage" do - stub_authorization! - - context "visiting the homepage" do - before(:each) do - visit spree.admin_path - end - - it "should have the header text 'Listing Orders'" do - within('h1') { page.should have_content("Listing Orders") } - end - - it "should have a link to overview" do - page.find_link("Overview")['/admin'] - end - - it "should have a link to orders" do - page.find_link("Orders")['/admin/orders'] - end - - it "should have a link to products" do - page.find_link("Products")['/admin/products'] - end - - it "should have a link to reports" do - page.find_link("Reports")['/admin/reports'] - end - - it "should have a link to configuration" do - page.find_link("Configuration")['/admin/configurations'] - end - end - - context "visiting the products tab" do - before(:each) do - visit spree.admin_products_path - end - - it "should have a link to products" do - within('#sub-menu') { page.find_link("Products")['/admin/products'] } - end - - it "should have a link to option types" do - within('#sub-menu') { page.find_link("Option Types")['/admin/option_types'] } - end - - it "should have a link to properties" do - within('#sub-menu') { page.find_link("Properties")['/admin/properties'] } - end - - it "should have a link to prototypes" do - within('#sub-menu') { page.find_link("Prototypes")['/admin/prototypes'] } - end - - end -end diff --git a/core/spec/requests/admin/orders/adjustments_spec.rb b/core/spec/requests/admin/orders/adjustments_spec.rb deleted file mode 100644 index e86253d5ded..00000000000 --- a/core/spec/requests/admin/orders/adjustments_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'spec_helper' - -describe "Adjustments" do - stub_authorization! - - before(:each) do - visit spree.admin_path - order = create(:order, :completed_at => "2011-02-01 12:36:15", :number => "R100") - create(:adjustment, :adjustable => order) - click_link "Orders" - within_row(1) { click_icon :edit } - click_link "Adjustments" - end - - context "admin managing adjustments" do - it "should display the correct values for existing order adjustments" do - within_row(1) do - column_text(2).should == "Shipping" - column_text(3).should == "$100.00" - end - end - end - - context "admin creating a new adjustment" do - before(:each) do - click_link "New Adjustment" - end - - context "successfully" do - it "should create a new adjustment" do - fill_in "adjustment_amount", :with => "10" - fill_in "adjustment_label", :with => "rebate" - click_button "Continue" - page.should have_content("successfully created!") - end - end - - context "with validation errors" do - it "should not create a new adjustment" do - fill_in "adjustment_amount", :with => "" - fill_in "adjustment_label", :with => "" - click_button "Continue" - page.should have_content("Label can't be blank") - page.should have_content("Amount is not a number") - end - end - end - - context "admin editing an adjustment" do - before(:each) do - within_row(1) { click_icon :edit } - end - - context "successfully" do - it "should update the adjustment" do - fill_in "adjustment_amount", :with => "99" - fill_in "adjustment_label", :with => "rebate 99" - click_button "Continue" - page.should have_content("successfully updated!") - page.should have_content("rebate 99") - page.should have_content("$99.00") - end - end - - context "with validation errors" do - it "should not update the adjustment" do - fill_in "adjustment_amount", :with => "" - fill_in "adjustment_label", :with => "" - click_button "Continue" - page.should have_content("Label can't be blank") - page.should have_content("Amount is not a number") - end - end - end -end diff --git a/core/spec/requests/admin/orders/customer_details_spec.rb b/core/spec/requests/admin/orders/customer_details_spec.rb deleted file mode 100644 index 61340018b8b..00000000000 --- a/core/spec/requests/admin/orders/customer_details_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'spec_helper' - -describe "Customer Details" do - stub_authorization! - - let(:shipping_method) { create(:shipping_method, :display_on => "front_end") } - let(:order) { create(:completed_order_with_totals) } - let(:country) do - create(:country, :name => "Kangaland") - end - - let(:state) do - create(:state, :name => "Alabama", :country => country) - end - - before do - reset_spree_preferences do |config| - config.default_country_id = country.id - config.company = true - end - - create(:shipping_method, :display_on => "front_end") - create(:order_with_inventory_unit_shipped, :completed_at => "2011-02-01 12:36:15") - ship_address = create(:address, :country => country, :state => state) - bill_address = create(:address, :country => country, :state => state) - create(:user, :email => 'foobar@example.com', - :ship_address => ship_address, - :bill_address => bill_address) - - visit spree.admin_path - click_link "Orders" - within('table#listing_orders') { click_icon(:edit) } - end - - context "editing an order", :js => true do - it "should be able to populate customer details for an existing order" do - click_link "Customer Details" - fill_in "customer_search", :with => "foobar" - sleep(3) - page.execute_script %Q{ $('.ui-menu-item a:contains("foobar@example.com")').trigger("mouseenter").click(); } - - ["ship_address", "bill_address"].each do |address| - find_field("order_#{address}_attributes_firstname").value.should == "John" - find_field("order_#{address}_attributes_lastname").value.should == "Doe" - find_field("order_#{address}_attributes_company").value.should == "Company" - find_field("order_#{address}_attributes_address1").value.should == "10 Lovely Street" - find_field("order_#{address}_attributes_address2").value.should == "Northwest" - find_field("order_#{address}_attributes_city").value.should == "Herndon" - find_field("order_#{address}_attributes_zipcode").value.should == "20170" - find_field("order_#{address}_attributes_state_id").value.should == state.id.to_s - find_field("order_#{address}_attributes_country_id").value.should == country.id.to_s - find_field("order_#{address}_attributes_phone").value.should == "123-456-7890" - end - end - - it "should be able to update customer details for an existing order" do - order.ship_address = create(:address) - order.save! - - click_link "Customer Details" - ["ship", "bill"].each do |type| - fill_in "order_#{type}_address_attributes_firstname", :with => "John 99" - fill_in "order_#{type}_address_attributes_lastname", :with => "Doe" - fill_in "order_#{type}_address_attributes_lastname", :with => "Company" - fill_in "order_#{type}_address_attributes_address1", :with => "100 first lane" - fill_in "order_#{type}_address_attributes_address2", :with => "#101" - fill_in "order_#{type}_address_attributes_city", :with => "Bethesda" - fill_in "order_#{type}_address_attributes_zipcode", :with => "20170" - select "Alabama", :from => "order_#{type}_address_attributes_state_id" - fill_in "order_#{type}_address_attributes_phone", :with => "123-456-7890" - end - - click_button "Continue" - - click_link "Customer Details" - find_field('order_ship_address_attributes_firstname').value.should == "John 99" - end - end - - it "should show validation errors" do - click_link "Customer Details" - click_button "Continue" - page.should have_content("Shipping address first name can't be blank") - end - - - # Regression test for #942 - context "errors when no shipping methods are available" do - before do - Spree::ShippingMethod.delete_all - end - - specify do - click_link "Customer Details" - # Need to fill in valid information so it passes validations - fill_in "order_ship_address_attributes_firstname", :with => "John 99" - fill_in "order_ship_address_attributes_lastname", :with => "Doe" - fill_in "order_ship_address_attributes_lastname", :with => "Company" - fill_in "order_ship_address_attributes_address1", :with => "100 first lane" - fill_in "order_ship_address_attributes_address2", :with => "#101" - fill_in "order_ship_address_attributes_city", :with => "Bethesda" - fill_in "order_ship_address_attributes_zipcode", :with => "20170" - fill_in "order_ship_address_attributes_state_name", :with => "Alabama" - fill_in "order_ship_address_attributes_phone", :with => "123-456-7890" - lambda { click_button "Continue" }.should_not raise_error(NoMethodError) - end - - - end - -end diff --git a/core/spec/requests/admin/orders/listing_spec.rb b/core/spec/requests/admin/orders/listing_spec.rb deleted file mode 100644 index 1c2203d6ecf..00000000000 --- a/core/spec/requests/admin/orders/listing_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -require 'spec_helper' - -describe "Orders Listing" do - stub_authorization! - - before(:each) do - create(:order, :created_at => Time.now + 1.day, :completed_at => Time.now + 1.day, :number => "R100") - create(:order, :created_at => Time.now - 1.day, :completed_at => Time.now - 1.day, :number => "R200") - visit spree.admin_path - end - - context "listing orders" do - before(:each) do - click_link "Orders" - end - - it "should list existing orders" do - within_row(1) do - column_text(2).should == "R100" - column_text(3).should == "cart" - end - - within_row(2) do - column_text(2).should == "R200" - end - end - - it "should be able to sort the orders listing" do - # default is completed_at desc - within_row(1) { page.should have_content("R100") } - within_row(2) { page.should have_content("R200") } - - click_link "Completed At" - - # Completed at desc - within_row(1) { page.should have_content("R200") } - within_row(2) { page.should have_content("R100") } - - within('table#listing_orders thead') { click_link "Number" } - - # number asc - within_row(1) { page.should have_content("R100") } - within_row(2) { page.should have_content("R200") } - end - end - - context "searching orders" do - before(:each) do - click_link "Orders" - end - - it "should be able to search orders" do - fill_in "q_number_cont", :with => "R200" - click_icon :search - within_row(1) do - page.should have_content("R200") - end - - # Ensure that the other order doesn't show up - within("table#listing_orders") { page.should_not have_content("R100") } - end - - it "should be able to search orders using only completed at input" do - fill_in "q_created_at_gt", :with => Date.today - click_icon :search - within_row(1) { page.should have_content("R100") } - - # Ensure that the other order doesn't show up - within("table#listing_orders") { page.should_not have_content("R200") } - end - end -end diff --git a/core/spec/requests/admin/orders/order_details_spec.rb b/core/spec/requests/admin/orders/order_details_spec.rb deleted file mode 100644 index afa31cd41a0..00000000000 --- a/core/spec/requests/admin/orders/order_details_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# coding: utf-8 -require 'spec_helper' - -describe "Order Details" do - stub_authorization! - - context "edit order page" do - - before do - reset_spree_preferences do |config| - config.allow_backorders = true - end - create(:country) - end - - after(:each) { I18n.reload! } - - let(:product) { create(:product, :name => 'spree t-shirt', :on_hand => 5, :price => 19.99) } - let(:order) { create(:order, :completed_at => "2011-02-01 12:36:15", :number => "R100") } - - it "should allow me to edit order details", :js => true do - order.add_variant(product.master, 2) - order.inventory_units.each do |iu| - iu.update_attribute_without_callbacks('state', 'sold') - end - - visit spree.admin_path - click_link "Orders" - - within_row(1) do - click_link "R100" - end - - page.should have_content("spree t-shirt") - page.should have_content("$39.98") - click_link "Edit" - fill_in "order_line_items_attributes_0_quantity", :with => "1" - click_button "Update" - page.should have_content("Total: $19.99") - end - - it "should render details properly" do - order.state = :complete - order.currency = 'GBP' - order.save! - - visit spree.edit_admin_order_path(order) - - find(".page-title").text.strip.should == "Order #R100" - - within ".additional-info" do - find(".state").text.should == "complete" - find("#shipment_status").text.should == "none" - find("#payment_status").text.should == "none" - end - - I18n.backend.store_translations I18n.locale, - :shipment_states => { :missing => 'some text' }, - :payment_states => { :missing => 'other text' } - - visit spree.edit_admin_order_path(order) - - within ".additional-info" do - find("#order_total").text.should == "£0.00" - find("#shipment_status").text.should == "some text" - find("#payment_status").text.should == "other text" - end - - end - end -end diff --git a/core/spec/requests/admin/orders/payments_spec.rb b/core/spec/requests/admin/orders/payments_spec.rb deleted file mode 100644 index 871ff746be4..00000000000 --- a/core/spec/requests/admin/orders/payments_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'spec_helper' - -describe "Payments" do - stub_authorization! - - before(:each) do - - reset_spree_preferences do |config| - config.allow_backorders = true - end - - @order = create(:completed_order_with_totals, :number => "R100", :state => "complete") - product = create(:product, :name => 'spree t-shirt', :on_hand => 5) - product.master.count_on_hand = 5 - product.master.save - @order.add_variant(product.master, 2) - @order.update! - - @order.inventory_units.each do |iu| - iu.update_attribute_without_callbacks('state', 'sold') - end - @order.update! - - end - - context "payment methods" do - - before(:each) do - create(:payment, :order => @order, :amount => @order.outstanding_balance, :payment_method => create(:bogus_payment_method, :environment => 'test')) - visit spree.admin_path - click_link "Orders" - within_row(1) do - click_link "R100" - end - end - - it "should be able to list and create payment methods for an order", :js => true do - - click_link "Payments" - find("#payment_status").text.should == "BALANCE DUE" - within_row(1) do - column_text(2).should == "$49.98" - column_text(3).should == "Credit Card" - column_text(4).should == "PENDING" - end - - click_icon :void - find("#payment_status").text.should == "BALANCE DUE" - page.should have_content("Payment Updated") - - within_row(1) do - column_text(2).should == "$49.98" - column_text(3).should == "Credit Card" - column_text(4).should == "VOID" - end - - click_on "New Payment" - page.should have_content("New Payment") - click_button "Update" - page.should have_content("successfully created!") - - click_icon(:capture) - find("#payment_status").text.should == "PAID" - - page.should_not have_css('#new_payment_section') - end - - # Regression test for #1269 - it "cannot create a payment for an order with no payment methods" do - Spree::PaymentMethod.delete_all - @order.payments.delete_all - - visit spree.new_admin_order_payment_path(@order) - page.should have_content("You cannot create a payment for an order without any payment methods defined.") - page.should have_content("Please define some payment methods first.") - end - - # Regression tests for #1453 - context "with a check payment" do - before do - @order.payments.delete_all - create(:payment, :order => @order, - :state => "checkout", - :amount => @order.outstanding_balance, - :payment_method => create(:bogus_payment_method, :environment => 'test')) - end - - it "capturing a check payment from a new order" do - visit spree.admin_order_payments_path(@order) - click_icon(:capture) - page.should_not have_content("Cannot perform requested operation") - page.should have_content("Payment Updated") - end - - it "voids a check payment from a new order" do - visit spree.admin_order_payments_path(@order) - click_icon(:void) - page.should have_content("Payment Updated") - end - end - - end -end diff --git a/core/spec/requests/admin/orders/return_authorizations_spec.rb b/core/spec/requests/admin/orders/return_authorizations_spec.rb deleted file mode 100644 index 79f189bd80b..00000000000 --- a/core/spec/requests/admin/orders/return_authorizations_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'spec_helper' - -describe "return authorizations" do - stub_authorization! - - let!(:order) { create(:completed_order_with_totals) } - - before do - order.inventory_units.update_all("state = 'shipped'") - create(:return_authorization, - :order => order, - :state => 'authorized', - :inventory_units => order.inventory_units) - end - - # Regression test for #1107 - it "doesn't blow up when receiving a return authorization" do - visit spree.admin_path - click_link "Orders" - click_link order.number - click_link "Return Authorizations" - click_link "Edit" - lambda { click_button "receive" }.should_not raise_error(ActiveRecord::UnknownAttributeError) - end - -end diff --git a/core/spec/requests/admin/orders/shipments_spec.rb b/core/spec/requests/admin/orders/shipments_spec.rb deleted file mode 100644 index 1b1e29fb68a..00000000000 --- a/core/spec/requests/admin/orders/shipments_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -describe "Shipments" do - stub_authorization! - - let!(:order) { OrderWalkthrough.up_to(:complete) } - - before(:each) do - # Clear all the shipments and then re-create them in this test - - order.shipments.delete_all - reset_spree_preferences do |config| - config.allow_backorders = true - end - - visit spree.admin_path - click_link "Orders" - within_row(1) { click_link order.number } - end - - it "should be able to create and list shipments for an order", :js => true do - - click_link "Shipments" - - click_on "New Shipment" - check "inventory_units_1" - click_button "Create" - page.should have_content("successfully created!") - order.reload - order.shipments.count.should == 1 - - click_link "Shipments" - shipment = order.shipments.last - - within_row(1) do - column_text(1).should == shipment.number - column_text(5).should == "Pending" - click_icon(:edit) - end - - page.should have_content("##{shipment.number}") - end - -end diff --git a/core/spec/requests/admin/products/edit/images_spec.rb b/core/spec/requests/admin/products/edit/images_spec.rb deleted file mode 100644 index 084b6ac2255..00000000000 --- a/core/spec/requests/admin/products/edit/images_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe "Product Images" do - stub_authorization! - - context "uploading and editing an image", :js => true do - it "should allow an admin to upload and edit an image for a product" do - Spree::Image.attachment_definitions[:attachment].delete :storage - - create(:product) - - visit spree.admin_path - click_link "Products" - click_icon(:edit) - click_link "Images" - click_link "new_image_link" - absolute_path = Rails.root + "../../spec/support/ror_ringer.jpeg" - attach_file('image_attachment', absolute_path) - click_button "Update" - page.should have_content("successfully created!") - click_icon(:edit) - fill_in "image_alt", :with => "ruby on rails t-shirt" - click_button "Update" - page.should have_content("successfully updated!") - page.should have_content("ruby on rails t-shirt") - end - end -end diff --git a/core/spec/requests/admin/products/edit/products_spec.rb b/core/spec/requests/admin/products/edit/products_spec.rb deleted file mode 100644 index 2c7287803c7..00000000000 --- a/core/spec/requests/admin/products/edit/products_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# encoding: UTF-8 -require 'spec_helper' - -describe 'Product Details' do - stub_authorization! - - context 'editing a product' do - let(:available_on) { Time.now } - it 'should list the product details' do - create(:product, :name => 'Bún thịt nướng', :permalink => 'bun-thit-nuong', :sku => 'A100', - :description => 'lorem ipsum', :available_on => available_on, :count_on_hand => 10) - - visit spree.admin_path - click_link 'Products' - within_row(1) { click_icon :edit } - - click_link 'Product Details' - - find('.page-title').text.strip.should == 'Editing Product “Bún thịt nướng”' - find('input#product_name').value.should == 'Bún thịt nướng' - find('input#product_permalink').value.should == 'bun-thit-nuong' - find('textarea#product_description').text.strip.should == 'lorem ipsum' - find('input#product_price').value.should == '19.99' - find('input#product_cost_price').value.should == '17.00' - find('input#product_available_on').value.should_not be_blank - find('input#product_sku').value.should == 'A100' - end - - it "should handle permalink changes" do - create(:product, :name => 'Bún thịt nướng', :permalink => 'bun-thit-nuong', :sku => 'A100', - :description => 'lorem ipsum', :available_on => '2011-01-01 01:01:01', :count_on_hand => 10) - - visit spree.admin_path - click_link 'Products' - within('table.index tbody tr:nth-child(1)') do - click_icon(:edit) - end - - fill_in "product_permalink", :with => 'random-permalink-value' - click_button "Update" - page.should have_content("successfully updated!") - - fill_in "product_permalink", :with => '' - click_button "Update" - within('#product_permalink_field') { page.should have_content("can't be blank") } - - click_button "Update" - within('#product_permalink_field') { page.should have_content("can't be blank") } - - fill_in "product_permalink", :with => 'another-random-permalink-value' - click_button "Update" - page.should have_content("successfully updated!") - - end - end -end diff --git a/core/spec/requests/admin/products/edit/taxons_spec.rb b/core/spec/requests/admin/products/edit/taxons_spec.rb deleted file mode 100644 index ee405bfe4c9..00000000000 --- a/core/spec/requests/admin/products/edit/taxons_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe "Product Taxons" do - stub_authorization! - - context "managing taxons" do - def selected_taxons - find("#product_taxon_ids").value.split(',').map(&:to_i) - end - - it "should allow an admin to manage taxons", :js => true do - taxon_1 = create(:taxon) - taxon_2 = create(:taxon, :name => 'Clothing') - product = create(:product) - product.taxons << taxon_1 - - visit spree.admin_path - click_link "Products" - within("table.index") do - click_icon :edit - end - - find(".select2-search-choice").text.should == taxon_1.name - selected_taxons.should =~ [taxon_1.id] - select2("#product_taxons_field", "Clothing") - click_button "Update" - selected_taxons.should =~ [taxon_1.id, taxon_2.id] - - # Regression test for #2139 - all("#s2id_product_taxon_ids .select2-search-choice").count.should == 2 - end - end -end diff --git a/core/spec/requests/admin/products/edit/variants_spec.rb b/core/spec/requests/admin/products/edit/variants_spec.rb deleted file mode 100644 index 862013da678..00000000000 --- a/core/spec/requests/admin/products/edit/variants_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'spec_helper' - -describe "Product Variants" do - stub_authorization! - - before(:each) do - visit spree.admin_path - end - - context "editing variant option types", :js => true do - it "should allow an admin to create option types for a variant" do - create(:product) - - click_link "Products" - - within_row(1) { click_icon :edit } - - within('#sidebar') { click_link "Variants" } - page.should have_content("To add variants, you must first define") - end - - it "should allow an admin to create a variant if there are option types" do - click_link "Products" - click_link "Option Types" - click_link "new_option_type_link" - fill_in "option_type_name", :with => "shirt colors" - fill_in "option_type_presentation", :with => "colors" - click_button "Create" - page.should have_content("successfully created!") - - within('#new_add_option_value') { click_link "Add Option Value" } - page.find('table tr:last td.name input').set('color') - page.find('table tr:last td.presentation input').set('black') - click_button "Update" - page.should have_content("successfully updated!") - - create(:product) - - visit spree.admin_path - click_link "Products" - within('table.index tbody tr:nth-child(1)') do - click_icon :edit - end - - select "color", :from => "Option Types" - click_button "Update" - page.should have_content("successfully updated!") - - within('#sidebar') { click_link "Variants" } - click_link "New Variant" - fill_in "variant_sku", :with => "A100" - click_button "Create" - page.should have_content("successfully created!") - within(".index") do - page.should have_content("19.99") - page.should have_content("A100") - end - end - end -end diff --git a/core/spec/requests/admin/products/option_types_spec.rb b/core/spec/requests/admin/products/option_types_spec.rb deleted file mode 100644 index 8eb2cde0504..00000000000 --- a/core/spec/requests/admin/products/option_types_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -describe "Option Types" do - stub_authorization! - - before(:each) do - visit spree.admin_path - click_link "Products" - end - - context "listing option types" do - it "should list existing option types" do - create(:option_type, :name => "tshirt-color", :presentation => "Color") - create(:option_type, :name => "tshirt-size", :presentation => "Size") - - click_link "Option Types" - within("table#listing_option_types") do - page.should have_content("Color") - page.should have_content("tshirt-color") - page.should have_content("Size") - page.should have_content("tshirt-size") - end - end - end - - context "creating a new option type" do - it "should allow an admin to create a new option type", :js => true do - click_link "Option Types" - click_link "new_option_type_link" - page.should have_content("New Option Type") - fill_in "option_type_name", :with => "shirt colors" - fill_in "option_type_presentation", :with => "colors" - click_button "Create" - page.should have_content("successfully created!") - - click_link "Add Option Value" - page.find('table tr:last td.name input').set('color') - page.find('table tr:last td.presentation input').set('black') - click_button "Update" - page.should have_content("successfully updated!") - end - end - - context "editing an existing option type" do - it "should allow an admin to update an existing option type" do - create(:option_type, :name => "tshirt-color", :presentation => "Color") - create(:option_type, :name => "tshirt-size", :presentation => "Size") - click_link "Option Types" - within('table#listing_option_types') { click_link "Edit" } - fill_in "option_type_name", :with => "foo-size 99" - click_button "Update" - page.should have_content("successfully updated!") - page.should have_content("foo-size 99") - end - end -end diff --git a/core/spec/requests/admin/products/products_spec.rb b/core/spec/requests/admin/products/products_spec.rb deleted file mode 100644 index 2230e97be99..00000000000 --- a/core/spec/requests/admin/products/products_spec.rb +++ /dev/null @@ -1,211 +0,0 @@ -require 'spec_helper' - -describe "Products" do - stub_authorization! - - context "as admin user" do - before(:each) do - visit spree.admin_path - end - - context "listing products" do - context "sorting" do - before do - create(:product, :name => 'apache baseball cap', :price => 10) - create(:product, :name => 'zomg shirt', :price => 5) - end - - it "should list existing products with correct sorting by name" do - click_link "Products" - # Name ASC - within_row(1) { page.should have_content('apache baseball cap') } - within_row(2) { page.should have_content("zomg shirt") } - - # Name DESC - click_link "admin_products_listing_name_title" - within_row(1) { page.should have_content("zomg shirt") } - within_row(2) { page.should have_content('apache baseball cap') } - end - - it "should list existing products with correct sorting by price" do - click_link "Products" - - # Name ASC (default) - within_row(1) { page.should have_content('apache baseball cap') } - within_row(2) { page.should have_content("zomg shirt") } - - # Price DESC - click_link "admin_products_listing_price_title" - within_row(1) { page.should have_content("zomg shirt") } - within_row(2) { page.should have_content('apache baseball cap') } - end - end - end - - context "searching products" do - it "should be able to search deleted products", :js => true do - create(:product, :name => 'apache baseball cap', :deleted_at => "2011-01-06 18:21:13") - create(:product, :name => 'zomg shirt') - - click_link "Products" - page.should have_content("zomg shirt") - page.should_not have_content("apache baseball cap") - check "Show Deleted" - click_icon :search - page.should have_content("zomg shirt") - page.should have_content("apache baseball cap") - uncheck "Show Deleted" - click_icon :search - page.should have_content("zomg shirt") - page.should_not have_content("apache baseball cap") - end - - it "should be able to search products by their properties" do - create(:product, :name => 'apache baseball cap', :sku => "A100") - create(:product, :name => 'apache baseball cap2', :sku => "B100") - create(:product, :name => 'zomg shirt') - - click_link "Products" - fill_in "q_name_cont", :with => "ap" - click_icon :search - page.should have_content("apache baseball cap") - page.should have_content("apache baseball cap2") - page.should_not have_content("zomg shirt") - - fill_in "q_variants_including_master_sku_cont", :with => "A1" - click_icon :search - page.should have_content("apache baseball cap") - page.should_not have_content("apache baseball cap2") - page.should_not have_content("zomg shirt") - end - end - context "creating a new product from a prototype" do - - include_context "product prototype" - - before(:each) do - @option_type_prototype = prototype - @property_prototype = create(:prototype, :name => "Random") - click_link "Products" - click_link "admin_new_product" - within('#new_product') do - page.should have_content("SKU") - end - end - - it "should allow an admin to create a new product and variants from a prototype", :js => true do - fill_in "product_name", :with => "Baseball Cap" - fill_in "product_sku", :with => "B100" - fill_in "product_price", :with => "100" - fill_in "product_available_on", :with => "2012/01/24" - select "Size", :from => "Prototype" - check "Large" - click_button "Create" - page.should have_content("successfully created!") - Spree::Product.last.variants.length.should == 1 - end - - it "should not display variants when prototype does not contain option types", :js => true do - select "Random", :from => "Prototype" - - fill_in "product_name", :with => "Baseball Cap" - - page.should_not have_content("Variants") - end - - it "should keep option values selected if validation fails", :js => true do - select "Size", :from => "Prototype" - check "Large" - click_button "Create" - page.should have_content("Name can't be blank") - field_labeled("Size").should be_checked - field_labeled("Large").should be_checked - field_labeled("Small").should_not be_checked - end - - end - - context "creating a new product" do - before(:each) do - click_link "Products" - click_link "admin_new_product" - within('#new_product') do - page.should have_content("SKU") - end - end - - it "should allow an admin to create a new product", :js => true do - fill_in "product_name", :with => "Baseball Cap" - fill_in "product_sku", :with => "B100" - fill_in "product_price", :with => "100" - fill_in "product_available_on", :with => "2012/01/24" - click_button "Create" - page.should have_content("successfully created!") - fill_in "product_on_hand", :with => "100" - click_button "Update" - page.should have_content("successfully updated!") - end - - it "should show validation errors", :js => true do - click_button "Create" - page.should have_content("Name can't be blank") - end - - # Regression test for #2097 - it "can set the count on hand to a null value", :js => true do - fill_in "product_name", :with => "Baseball Cap" - fill_in "product_price", :with => "100" - click_button "Create" - page.should have_content("successfully created!") - fill_in "product_on_hand", :with => "" - click_button "Update" - page.should_not have_content("spree_products.count_on_hand may not be NULL") - page.should have_content("successfully updated!") - end - end - - context "cloning a product", :js => true do - it "should allow an admin to clone a product" do - create(:product) - - click_link "Products" - within_row(1) do - click_icon :copy - end - - page.should have_content("Product has been cloned") - end - - context "cloning a deleted product" do - it "should allow an admin to clone a deleted product" do - create(:product, :name => "apache baseball cap") - - click_link "Products" - check "Show Deleted" - click_button "Search" - - page.should have_content("apache baseball cap") - - within_row(1) do - click_icon :copy - end - - page.should have_content("Product has been cloned") - end - end - end - - context 'updating a product', :js => true do - let(:product) { create(:product) } - - it 'should parse correctly available_on' do - visit spree.admin_product_path(product) - fill_in "product_available_on", :with => "2012/12/25" - click_button "Update" - page.should have_content("successfully updated!") - Spree::Product.last.available_on.should == 'Tue, 25 Dec 2012 00:00:00 UTC +00:00' - end - end - - end -end diff --git a/core/spec/requests/admin/products/properties_spec.rb b/core/spec/requests/admin/products/properties_spec.rb deleted file mode 100644 index 9bd3c14d639..00000000000 --- a/core/spec/requests/admin/products/properties_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'spec_helper' - -describe "Properties" do - stub_authorization! - - before(:each) do - visit spree.admin_path - click_link "Products" - end - - context "listing product properties" do - it "should list the existing product properties" do - create(:property, :name => 'shirt size', :presentation => 'size') - create(:property, :name => 'shirt fit', :presentation => 'fit') - - click_link "Properties" - within_row(1) do - column_text(1).should == "shirt size" - column_text(2).should == "size" - end - - within_row(2) do - column_text(1).should == "shirt fit" - column_text(2).should == "fit" - end - end - end - - context "creating a property" do - it "should allow an admin to create a new product property", :js => true do - click_link "Properties" - click_link "new_property_link" - within('#new_property') { page.should have_content("New Property") } - - fill_in "property_name", :with => "color of band" - fill_in "property_presentation", :with => "color" - click_button "Create" - page.should have_content("successfully created!") - end - end - - context "editing a property" do - before(:each) do - create(:property) - click_link "Properties" - within_row(1) { click_icon :edit } - end - - it "should allow an admin to edit an existing product property" do - fill_in "property_name", :with => "model 99" - click_button "Update" - page.should have_content("successfully updated!") - page.should have_content("model 99") - end - - it "should show validation errors" do - fill_in "property_name", :with => "" - click_button "Update" - page.should have_content("Name can't be blank") - end - end -end diff --git a/core/spec/requests/admin/products/prototypes_spec.rb b/core/spec/requests/admin/products/prototypes_spec.rb deleted file mode 100644 index 8d141e2e1a3..00000000000 --- a/core/spec/requests/admin/products/prototypes_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'spec_helper' - -describe "Prototypes" do - stub_authorization! - - context "listing prototypes" do - it "should be able to list existing prototypes" do - create(:property, :name => "model", :presentation => "Model") - create(:property, :name => "brand", :presentation => "Brand") - create(:property, :name => "shirt_fabric", :presentation => "Fabric") - create(:property, :name => "shirt_sleeve_length", :presentation => "Sleeve") - create(:property, :name => "mug_type", :presentation => "Type") - create(:property, :name => "bag_type", :presentation => "Type") - create(:property, :name => "manufacturer", :presentation => "Manufacturer") - create(:property, :name => "bag_size", :presentation => "Size") - create(:property, :name => "mug_size", :presentation => "Size") - create(:property, :name => "gender", :presentation => "Gender") - create(:property, :name => "shirt_fit", :presentation => "Fit") - create(:property, :name => "bag_material", :presentation => "Material") - create(:property, :name => "shirt_type", :presentation => "Type") - p = create(:prototype, :name => "Shirt") - %w( brand gender manufacturer model shirt_fabric shirt_fit shirt_sleeve_length shirt_type ).each do |prop| - p.properties << Spree::Property.find_by_name(prop) - end - p = create(:prototype, :name => "Mug") - %w( mug_size mug_type ).each do |prop| - p.properties << Spree::Property.find_by_name(prop) - end - p = create(:prototype, :name => "Bag") - %w( bag_type bag_material ).each do |prop| - p.properties << Spree::Property.find_by_name(prop) - end - - visit spree.admin_path - click_link "Products" - click_link "Prototypes" - - within_row(1) { column_text(1).should == "Shirt" } - within_row(2) { column_text(1).should == "Mug" } - within_row(3) { column_text(1).should == "Bag" } - end - end - - context "creating a prototype" do - it "should allow an admin to create a new product prototype", :js => true do - visit spree.admin_path - click_link "Products" - click_link "Prototypes" - click_link "new_prototype_link" - within('#new_prototype') { page.should have_content("New Prototype") } - fill_in "prototype_name", :with => "male shirts" - click_button "Create" - page.should have_content("successfully created!") - click_link "Prototypes" - within_row(1) { click_icon :edit } - fill_in "prototype_name", :with => "Shirt 99" - click_button "Update" - page.should have_content("successfully updated!") - page.should have_content("Shirt 99") - end - end - - context "editing a prototype" do - it "should allow to empty its properties" do - model_property = create(:property, :name => "model", :presentation => "Model") - brand_property = create(:property, :name => "brand", :presentation => "Brand") - - shirt_prototype = create(:prototype, :name => "Shirt", :properties => []) - %w( brand model ).each do |prop| - shirt_prototype.properties << Spree::Property.find_by_name(prop) - end - - visit spree.admin_path - click_link "Products" - click_link "Prototypes" - - click_on "Edit" - property_ids = find_field("prototype_property_ids").value.map(&:to_i) - property_ids.should =~ [model_property.id, brand_property.id] - - unselect "Brand", :from => "prototype_property_ids" - unselect "Model", :from => "prototype_property_ids" - - click_button 'Update' - - click_on "Edit" - - find_field("prototype_property_ids").value.should be_empty - end - end -end diff --git a/core/spec/requests/admin/products/variant_spec.rb b/core/spec/requests/admin/products/variant_spec.rb deleted file mode 100644 index 2c4b34dd2a4..00000000000 --- a/core/spec/requests/admin/products/variant_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'spec_helper' - -describe "Variants" do - stub_authorization! - - context "creating a new variant" do - it "should allow an admin to create a new variant" do - product = create(:product_with_option_types, :price => "1.99", :cost_price => "1.00", :weight => "2.5", :height => "3.0", :width => "1.0", :depth => "1.5") - - product.options.each do |option| - create(:option_value, :option_type => option.option_type) - end - - visit spree.admin_path - click_link "Products" - within_row(1) { click_icon :edit } - click_link "Variants" - click_on "New Variant" - find('input#variant_price').value.should == "1.99" - find('input#variant_cost_price').value.should == "1.00" - find('input#variant_weight').value.should == "2.50" - find('input#variant_height').value.should == "3.00" - find('input#variant_width').value.should == "1.00" - find('input#variant_depth').value.should == "1.50" - end - end -end diff --git a/core/spec/requests/admin/reports_spec.rb b/core/spec/requests/admin/reports_spec.rb deleted file mode 100644 index c1ffec731d1..00000000000 --- a/core/spec/requests/admin/reports_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -describe "Reports" do - stub_authorization! - - context "visiting the admin reports page" do - it "should have the right content" do - visit spree.admin_path - click_link "Reports" - click_link "Sales Total" - - page.should have_content("Sales Totals") - page.should have_content("Item Total") - page.should have_content("Adjustment Total") - page.should have_content("Sales Total") - end - end - - context "searching the admin reports page" do - before do - order = create(:order) - order.update_attributes_without_callbacks({:adjustment_total => 100}) - order.completed_at = Time.now - order.save! - - order = create(:order) - order.update_attributes_without_callbacks({:adjustment_total => 200}) - order.completed_at = Time.now - order.save! - - #incomplete order - order = create(:order) - order.update_attributes_without_callbacks({:adjustment_total => 50}) - order.save! - - order = create(:order) - order.update_attributes_without_callbacks({:adjustment_total => 200}) - order.completed_at = 3.years.ago - order.created_at = 3.years.ago - order.save! - - order = create(:order) - order.update_attributes_without_callbacks({:adjustment_total => 200}) - order.completed_at = 3.years.from_now - order.created_at = 3.years.from_now - order.save! - end - - it "should allow me to search for reports" do - visit spree.admin_path - click_link "Reports" - click_link "Sales Total" - - fill_in "q_created_at_gt", :with => 1.week.ago - fill_in "q_created_at_lt", :with => 1.week.from_now - click_button "Search" - - page.should have_content("$300.00") - end - end -end diff --git a/core/spec/requests/cart_spec.rb b/core/spec/requests/cart_spec.rb deleted file mode 100644 index 81be9c406f2..00000000000 --- a/core/spec/requests/cart_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' - -describe "Cart" do - it "shows cart icon on non-cart pages" do - visit spree.root_path - lambda { find("li#link-to-cart a") }.should_not raise_error(Capybara::ElementNotFound) - end - - it "hides cart icon on cart page" do - visit spree.cart_path - lambda { find("li#link-to-cart a") }.should raise_error(Capybara::ElementNotFound) - end - - it "prevents double clicking the remove button on cart", :js => true do - @product = create(:product, :name => "RoR Mug", :on_hand => 1) - - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - - # prevent form submit to verify button is disabled - page.execute_script("$('#update-cart').submit(function(){return false;})") - - page.should_not have_selector('button#update-button[disabled]') - page.find(:css, '.delete img').click - page.should have_selector('button#update-button[disabled]') - end - - # Regression test for #2006 - it "does not error out with a 404 when GET'ing to /orders/populate" do - visit '/orders/populate' - within(".error") do - page.should have_content(I18n.t(:populate_get_error)) - end - end - - it 'allows you to remove an item from the cart', :js => true do - create(:product, :name => "RoR Mug", :on_hand => 1) - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - within("#line_items") do - click_link "delete_line_item_1" - end - page.should_not have_content("Line items quantity must be an integer") - end -end diff --git a/core/spec/requests/checkout_spec.rb b/core/spec/requests/checkout_spec.rb deleted file mode 100644 index 55170b644af..00000000000 --- a/core/spec/requests/checkout_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'spec_helper' - -describe "Checkout" do - let(:country) { create(:country, :name => "Kangaland",:states_required => true) } - before do - create(:state, :name => "Victoria", :country => country) - end - - context "visitor makes checkout as guest without registration" do - before(:each) do - Spree::Product.delete_all - @product = create(:product, :name => "RoR Mug") - @product.on_hand = 1 - @product.save - create(:zone) - end - - context "when backordering is disabled" do - before(:each) do - reset_spree_preferences do |config| - config.allow_backorders = false - end - end - - it "should warn the user about out of stock items" do - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - - @product.on_hand = 0 - @product.save - - click_button "Checkout" - - within(:css, "span.out-of-stock") { page.should have_content("Out of Stock") } - end - end - - context "defaults to use billing address" do - before do - shipping_method = create(:shipping_method) - shipping_method.zone.zone_members << Spree::ZoneMember.create(:zoneable => country) - - @order = OrderWalkthrough.up_to(:address) - @order.stub(:available_payment_methods => [ create(:bogus_payment_method, :environment => 'test') ]) - - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - Spree::Order.last.update_column(:email, "ryan@spreecommerce.com") - click_button "Checkout" - end - - it "should default checkbox to checked" do - find('input#order_use_billing').should be_checked - end - - it "should remain checked when used and visitor steps back to address step", :js => true do - address = "order_bill_address_attributes" - fill_in "#{address}_firstname", :with => "Ryan" - fill_in "#{address}_lastname", :with => "Bigg" - fill_in "#{address}_address1", :with => "143 Swan Street" - fill_in "#{address}_city", :with => "Richmond" - select "Kangaland", :from => "#{address}_country_id" - select "Victoria", :from => "#{address}_state_id" - fill_in "#{address}_zipcode", :with => "12345" - fill_in "#{address}_phone", :with => "(555) 5555-555" - click_button "Save and Continue" - click_link "Address" - - find('input#order_use_billing').should be_checked - end - end - - context "and likes to double click buttons" do - before(:each) do - order = OrderWalkthrough.up_to(:delivery) - order.stub :confirmation_required? => true - - order.reload - order.update! - - Spree::CheckoutController.any_instance.stub(:current_order => order) - Spree::CheckoutController.any_instance.stub(:skip_state_validation? => true) - end - - it "prevents double clicking the payment button on checkout", :js => true do - visit spree.checkout_state_path(:payment) - - # prevent form submit to verify button is disabled - page.execute_script("$('#checkout_form_payment').submit(function(){return false;})") - - page.should_not have_selector('input.button[disabled]') - click_button "Save and Continue" - page.should have_selector('input.button[disabled]') - end - - it "prevents double clicking the confirm button on checkout", :js => true do - visit spree.checkout_state_path(:confirm) - - # prevent form submit to verify button is disabled - page.execute_script("$('#checkout_form_confirm').submit(function(){return false;})") - - page.should_not have_selector('input.button[disabled]') - click_button "Place Order" - page.should have_selector('input.button[disabled]') - end - - # Regression test for #1596 - context "full checkout" do - before do - create(:payment_method) - Spree::ShippingMethod.delete_all - shipping_method = create(:shipping_method) - calculator = Spree::Calculator::PerItem.create!({:calculable => shipping_method}, :without_protection => true) - shipping_method.calculator = calculator - shipping_method.save - - @product.shipping_category = shipping_method.shipping_category - @product.save! - end - - it "does not break the per-item shipping method calculator", :js => true do - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - click_button "Checkout" - Spree::Order.last.update_column(:email, "ryan@spreecommerce.com") - - address = "order_bill_address_attributes" - fill_in "#{address}_firstname", :with => "Ryan" - fill_in "#{address}_lastname", :with => "Bigg" - fill_in "#{address}_address1", :with => "143 Swan Street" - fill_in "#{address}_city", :with => "Richmond" - select "Kangaland", :from => "#{address}_country_id" - select "Victoria", :from => "#{address}_state_id" - fill_in "#{address}_zipcode", :with => "12345" - fill_in "#{address}_phone", :with => "(555) 5555-555" - - click_button "Save and Continue" - page.should_not have_content("undefined method `promotion'") - end - end - end - end -end diff --git a/core/spec/requests/order_spec.rb b/core/spec/requests/order_spec.rb deleted file mode 100644 index e4ded6072a8..00000000000 --- a/core/spec/requests/order_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'spec_helper' - -describe 'orders' do - let(:order) { OrderWalkthrough.up_to(:complete) } - - it "can visit an order" do - # Regression test for current_user call on orders/show - lambda { visit spree.order_path(order) }.should_not raise_error - end - - it "should have credit card info if paid with credit card" do - create(:payment, :order => order) - visit spree.order_path(order) - within '.payment-info' do - page.should have_content "Ending in 1111" - end - end - - it "should have payment method name visible if not paid with credit card" do - create(:check_payment, :order => order) - visit spree.order_path(order) - within '.payment-info' do - page.should have_content "Check" - end - end -end diff --git a/core/spec/requests/products_spec.rb b/core/spec/requests/products_spec.rb deleted file mode 100644 index de1073eec19..00000000000 --- a/core/spec/requests/products_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'spec_helper' - -describe "Visiting Products" do - include_context "custom products" - - before(:each) do - visit spree.root_path - end - - it "should be able to show the shopping cart after adding a product to it" do - click_link "Ruby on Rails Ringer T-Shirt" - - page.should have_content("$19.99") - - click_button 'add-to-cart-button' - page.should have_content("Shopping Cart") - end - - it "should be able to search for a product" do - fill_in "keywords", :with => "shirt" - click_button "Search" - - page.all('ul.product-listing li').size.should == 1 - end - - it "should be able to visit brand Ruby on Rails" do - within(:css, '#taxonomies') { click_link "Ruby on Rails" } - - page.all('ul.product-listing li').size.should == 7 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - array = ["Ruby on Rails Bag", - "Ruby on Rails Baseball Jersey", - "Ruby on Rails Jr. Spaghetti", - "Ruby on Rails Mug", - "Ruby on Rails Ringer T-Shirt", - "Ruby on Rails Stein", - "Ruby on Rails Tote"] - tmp.sort!.should == array - end - - it "should be able to visit brand Ruby" do - within(:css, '#taxonomies') { click_link "Ruby" } - - page.all('ul.product-listing li').size.should == 1 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - tmp.sort!.should == ["Ruby Baseball Jersey"] - end - - it "should be able to visit brand Apache" do - within(:css, '#taxonomies') { click_link "Apache" } - - page.all('ul.product-listing li').size.should == 1 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - tmp.sort!.should == ["Apache Baseball Jersey"] - end - - it "should be able to visit category Clothing" do - click_link "Clothing" - - page.all('ul.product-listing li').size.should == 5 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - tmp.sort!.should == ["Apache Baseball Jersey", - "Ruby Baseball Jersey", - "Ruby on Rails Baseball Jersey", - "Ruby on Rails Jr. Spaghetti", - "Ruby on Rails Ringer T-Shirt"] - end - - it "should be able to visit category Mugs" do - click_link "Mugs" - - page.all('ul.product-listing li').size.should == 2 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - tmp.sort!.should == ["Ruby on Rails Mug", "Ruby on Rails Stein"] - end - - it "should be able to visit category Bags" do - click_link "Bags" - - page.all('ul.product-listing li').size.should == 2 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - tmp.sort!.should == ["Ruby on Rails Bag", "Ruby on Rails Tote"] - end - - it "should be able to display products priced under 10 dollars" do - within(:css, '#taxonomies') { click_link "Ruby on Rails" } - check "Price_Range_Under_$10.00" - within(:css, '#sidebar_products_search') { click_button "Search" } - page.should have_content("No products found") - end - - it "should be able to display products priced between 15 and 18 dollars" do - within(:css, '#taxonomies') { click_link "Ruby on Rails" } - check "Price_Range_$15.00_-_$18.00" - within(:css, '#sidebar_products_search') { click_button "Search" } - - page.all('ul.product-listing li').size.should == 3 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - tmp.sort!.should == ["Ruby on Rails Mug", "Ruby on Rails Stein", "Ruby on Rails Tote"] - end - - it "should be able to display products priced between 15 and 18 dollars across multiple pages" do - Spree::Config.products_per_page = 2 - within(:css, '#taxonomies') { click_link "Ruby on Rails" } - check "Price_Range_$15.00_-_$18.00" - within(:css, '#sidebar_products_search') { click_button "Search" } - - page.all('ul.product-listing li').size.should == 2 - products = page.all('ul.product-listing li a[itemprop=name]') - products.count.should == 2 - - find('nav.pagination .next a').click - products = page.all('ul.product-listing li a[itemprop=name]') - products.count.should == 1 - end - - it "should be able to display products priced 18 dollars and above" do - within(:css, '#taxonomies') { click_link "Ruby on Rails" } - check "Price_Range_$18.00_-_$20.00" - check "Price_Range_$20.00_or_over" - within(:css, '#sidebar_products_search') { click_button "Search" } - - page.all('ul.product-listing li').size.should == 4 - tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact - tmp.delete("") - tmp.sort!.should == ["Ruby on Rails Bag", - "Ruby on Rails Baseball Jersey", - "Ruby on Rails Jr. Spaghetti", - "Ruby on Rails Ringer T-Shirt"] - end - - it "should be able to put a product without a description in the cart" do - product = FactoryGirl.create(:simple_product, :description => nil, :name => 'Sample', :price => '19.99') - visit spree.product_path(product) - page.should have_content "This product has no description" - click_button 'add-to-cart-button' - page.should have_content "This product has no description" - end -end diff --git a/core/spec/requests/taxons_spec.rb b/core/spec/requests/taxons_spec.rb deleted file mode 100644 index e7ca5ea740f..00000000000 --- a/core/spec/requests/taxons_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' - -describe "viewing products" do - let!(:taxonomy) { create(:taxonomy, :name => "Category") } - let!(:clothing) { taxonomy.root.children.create(:name => "Clothing") } - let!(:t_shirts) { clothing.children.create(:name => "T-Shirts") } - let!(:xxl) { t_shirts.children.create(:name => "XXL") } - let!(:product) do - product = create(:product, :name => "Superman T-Shirt") - product.taxons << t_shirts - end - - # Regression test for #1796 - it "can see a taxon's products, even if that taxon has child taxons" do - visit '/t/category/clothing/t-shirts' - page.should have_content("Superman T-Shirt") - end -end diff --git a/core/spec/requests/template_rendering_spec.rb b/core/spec/requests/template_rendering_spec.rb deleted file mode 100644 index 37a49624510..00000000000 --- a/core/spec/requests/template_rendering_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe "Template rendering" do - - context "with layout option set to 'application' in the configuration" do - - before do - @app_layout = Rails.root.join('app/views/layouts', 'application.html.erb') - File.open(@app_layout, 'w') do |app_layout| - app_layout.puts "I am the application layout" - end - Spree::Config.set(:layout => 'application') - end - - it "should render application layout" do - visit spree.root_path - page.should_not have_content('Spree Demo Site') - page.should have_content('I am the application layout') - end - - after do - FileUtils.rm(@app_layout) - end - - end - - context "without any layout option" do - - it "should render default layout" do - visit spree.root_path - page.should_not have_content('I am the application layout') - page.should have_content('Spree Demo Site') - end - - end - -end diff --git a/core/spec/spec_helper.rb b/core/spec/spec_helper.rb index c7b03b069b1..5662d25af49 100644 --- a/core/spec/spec_helper.rb +++ b/core/spec/spec_helper.rb @@ -1,126 +1,59 @@ +if ENV["COVERAGE"] + # Run Coverage report + require 'simplecov' + SimpleCov.start do + add_group 'Controllers', 'app/controllers' + add_group 'Helpers', 'app/helpers' + add_group 'Mailers', 'app/mailers' + add_group 'Models', 'app/models' + add_group 'Views', 'app/views' + add_group 'Jobs', 'app/jobs' + add_group 'Libraries', 'lib' + end +end + # This file is copied to ~/spec when you run 'ruby script/generate rspec' # from the project root directory. ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../dummy/config/environment", __FILE__) -require 'rspec/rails' -# Requires supporting files with custom matchers and macros, etc, -# in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} +begin + require File.expand_path("../dummy/config/environment", __FILE__) +rescue LoadError + puts "Could not load dummy application. Please ensure you have run `bundle exec rake test_app`" +end +require 'rspec/rails' require 'database_cleaner' +require 'ffaker' + +Dir["./spec/support/**/*.rb"].sort.each { |f| require f } -require 'spree/core/testing_support/factories' -require 'spree/core/testing_support/controller_requests' -require 'spree/core/testing_support/authorization_helpers' -require 'spree/core/testing_support/preferences' -require 'spree/core/testing_support/flash' +if ENV["CHECK_TRANSLATIONS"] + require "spree/testing_support/i18n" +end -require 'spree/core/url_helpers' -require 'paperclip/matchers' +require 'spree/testing_support/factories' +require 'spree/testing_support/preferences' RSpec.configure do |config| + config.color = true + config.infer_spec_type_from_file_location! config.mock_with :rspec - config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_path = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures") - #config.include Devise::TestHelpers, :type => :controller # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, comment the following line or assign false # instead of true. - config.use_transactional_fixtures = false + config.use_transactional_fixtures = true - config.before(:each) do - if example.metadata[:js] - DatabaseCleaner.strategy = :truncation - else - DatabaseCleaner.strategy = :transaction - end - end - - config.before(:each) do - DatabaseCleaner.start + config.before :each do + Rails.cache.clear reset_spree_preferences end - config.after(:each) do - DatabaseCleaner.clean - end - config.include FactoryGirl::Syntax::Methods - config.include Spree::Core::UrlHelpers - config.include Spree::Core::TestingSupport::ControllerRequests - config.include Spree::Core::TestingSupport::Preferences - config.include Spree::Core::TestingSupport::Flash - - config.include Paperclip::Shoulda::Matchers -end - -shared_context "custom products" do - before(:each) do - reset_spree_preferences do |config| - config.allow_backorders = true - end - - taxonomy = FactoryGirl.create(:taxonomy, :name => 'Categories') - root = taxonomy.root - clothing_taxon = FactoryGirl.create(:taxon, :name => 'Clothing', :parent_id => root.id) - bags_taxon = FactoryGirl.create(:taxon, :name => 'Bags', :parent_id => root.id) - mugs_taxon = FactoryGirl.create(:taxon, :name => 'Mugs', :parent_id => root.id) - - taxonomy = FactoryGirl.create(:taxonomy, :name => 'Brands') - root = taxonomy.root - apache_taxon = FactoryGirl.create(:taxon, :name => 'Apache', :parent_id => root.id) - rails_taxon = FactoryGirl.create(:taxon, :name => 'Ruby on Rails', :parent_id => root.id) - ruby_taxon = FactoryGirl.create(:taxon, :name => 'Ruby', :parent_id => root.id) + config.include Spree::TestingSupport::Preferences - FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Ringer T-Shirt', :price => '19.99', :taxons => [rails_taxon, clothing_taxon]) - FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Mug', :price => '15.99', :taxons => [rails_taxon, mugs_taxon]) - FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Tote', :price => '15.99', :taxons => [rails_taxon, bags_taxon]) - FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Bag', :price => '22.99', :taxons => [rails_taxon, bags_taxon]) - FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Baseball Jersey', :price => '19.99', :taxons => [rails_taxon, clothing_taxon]) - FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Stein', :price => '16.99', :taxons => [rails_taxon, mugs_taxon]) - FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Jr. Spaghetti', :price => '19.99', :taxons => [rails_taxon, clothing_taxon]) - FactoryGirl.create(:custom_product, :name => 'Ruby Baseball Jersey', :price => '19.99', :taxons => [ruby_taxon, clothing_taxon]) - FactoryGirl.create(:custom_product, :name => 'Apache Baseball Jersey', :price => '19.99', :taxons => [apache_taxon, clothing_taxon]) - end + config.fail_fast = ENV['FAIL_FAST'] || false end - - - -shared_context "product prototype" do - - def build_option_type_with_values(name, values) - ot = FactoryGirl.create(:option_type, :name => name) - values.each do |val| - ot.option_values.create({:name => val.downcase, :presentation => val}, :without_protection => true) - end - ot - end - - let(:product_attributes) do - # FactoryGirl.attributes_for is un-deprecated! - # https://github.com/thoughtbot/factory_girl/issues/274#issuecomment-3592054 - FactoryGirl.attributes_for(:simple_product) - end - - let(:prototype) do - size = build_option_type_with_values("size", %w(Small Medium Large)) - FactoryGirl.create(:prototype, :name => "Size", :option_types => [ size ]) - end - - let(:option_values_hash) do - hash = {} - prototype.option_types.each do |i| - hash[i.id.to_s] = i.option_value_ids - end - hash - end - -end - - - -PAYMENT_STATES = Spree::Payment.state_machine.states.keys unless defined? PAYMENT_STATES -SHIPMENT_STATES = Spree::Shipment.state_machine.states.keys unless defined? SHIPMENT_STATES -ORDER_STATES = Spree::Order.state_machine.states.keys unless defined? ORDER_STATES diff --git a/core/spec/support/big_decimal.rb b/core/spec/support/big_decimal.rb new file mode 100644 index 00000000000..48bac649622 --- /dev/null +++ b/core/spec/support/big_decimal.rb @@ -0,0 +1,5 @@ +class BigDecimal + def inspect + "#" + end +end diff --git a/core/spec/support/capybara_ext.rb b/core/spec/support/capybara_ext.rb deleted file mode 100644 index abf0ca2cd71..00000000000 --- a/core/spec/support/capybara_ext.rb +++ /dev/null @@ -1,35 +0,0 @@ -module CapybaraExt - def page! - save_and_open_page - end - - def click_icon(type) - find(".icon-#{type}").click - end - - def within_row(num, &block) - within("table.index tbody tr:nth-child(#{num})", &block) - end - - def column_text(num) - find("td:nth-child(#{num})").text - end - - def select2(within, value) - script = %Q{ - $('#{within} .select2-search-field input').val('#{value}') - $('#{within} .select2-search-field input').keydown(); - } - page.execute_script(script) - - # Wait for list to populate... - wait_until do - page.find(".select2-highlighted").visible? - end - page.execute_script("$('.select2-highlighted').mouseup();") - end -end - -RSpec.configure do |c| - c.include CapybaraExt -end diff --git a/core/spec/support/concerns/default_price_spec.rb b/core/spec/support/concerns/default_price_spec.rb new file mode 100644 index 00000000000..352a83315e5 --- /dev/null +++ b/core/spec/support/concerns/default_price_spec.rb @@ -0,0 +1,28 @@ +shared_examples_for "default_price" do + let(:model) { described_class } + subject(:instance) { FactoryGirl.build(model.name.demodulize.downcase.to_sym) } + + describe '.has_one :default_price' do + let(:default_price_association) { model.reflect_on_association(:default_price) } + + it 'should be a has one association' do + expect(default_price_association.macro).to eql :has_one + end + + it 'should have a dependent destroy' do + expect(default_price_association.options[:dependent]).to eql :destroy + end + + it 'should have the class name of Spree::Price' do + expect(default_price_association.options[:class_name]).to eql 'Spree::Price' + end + end + + describe '#default_price' do + subject { instance.default_price } + + its(:class) { should eql Spree::Price } + end + + its(:has_default_price?) { should be_truthy } +end diff --git a/core/spec/support/order_walkthrough.rb b/core/spec/support/order_walkthrough.rb deleted file mode 100644 index 2deb2d445a5..00000000000 --- a/core/spec/support/order_walkthrough.rb +++ /dev/null @@ -1,60 +0,0 @@ -class OrderWalkthrough - def self.up_to(state) - # A payment method must exist for an order to proceed through the Address state - unless Spree::PaymentMethod.exists? - Factory(:payment_method) - end - - # A payment method must exist for an order to proceed through the Address state - unless Spree::ShippingMethod.exists? - Factory(:shipping_method) - end - - order = Spree::Order.create!(:email => "spree@example.com") - add_line_item!(order) - order.next! - - end_state_position = states.index(state.to_sym) - states[0..end_state_position].each do |state| - send(state, order) - end - - order - end - - private - - def self.add_line_item!(order) - order.line_items << FactoryGirl.create(:line_item) - order.save - end - - def self.address(order) - - order.bill_address = FactoryGirl.create(:address) - order.ship_address = FactoryGirl.create(:address) - order.next! - end - - def self.delivery(order) - order.shipping_method = Spree::ShippingMethod.first - order.next! - end - - def self.payment(order) - order.payments.create!({:payment_method => Spree::PaymentMethod.first, :amount => order.total}, :without_protection => true) - # TODO: maybe look at some way of making this payment_state change automatic - order.payment_state = 'paid' - order.next! - end - - def self.complete(order) - #noop? - end - - def self.states - [:address, :delivery, :payment, :complete] - end - -end - diff --git a/core/spec/support/rake.rb b/core/spec/support/rake.rb new file mode 100644 index 00000000000..e9807c6706d --- /dev/null +++ b/core/spec/support/rake.rb @@ -0,0 +1,13 @@ +require "rake" + +shared_context "rake" do + let(:task_name) { self.class.top_level_description } + let(:task_path) { "lib/tasks/#{task_name.split(":").first}" } + subject { Rake::Task[task_name] } + + before do + Rake::Task.define_task(:environment) + load File.expand_path(Rails.root + "../../#{task_path}.rake") + subject.reenable + end +end diff --git a/core/spec/support/ror_ringer.jpeg b/core/spec/support/ror_ringer.jpeg deleted file mode 100644 index 3fcf764bf4d..00000000000 Binary files a/core/spec/support/ror_ringer.jpeg and /dev/null differ diff --git a/core/spree_core.gemspec b/core/spree_core.gemspec index 0da923b9c41..b158b291e48 100644 --- a/core/spree_core.gemspec +++ b/core/spree_core.gemspec @@ -5,37 +5,40 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'spree_core' s.version = version - s.summary = 'Core e-commerce functionality for the Spree project.' - s.description = 'Required dependency for Spree' + s.summary = 'The bare bones necessary for Spree.' + s.description = 'The bare bones necessary for Spree.' - s.required_ruby_version = '>= 1.8.7' + s.required_ruby_version = '>= 1.9.3' s.author = 'Sean Schofield' s.email = 'sean@spreecommerce.com' s.homepage = 'http://spreecommerce.com' - s.rubyforge_project = 'spree_core' + s.license = %q{BSD-3} s.files = Dir['LICENSE', 'README.md', 'app/**/*', 'config/**/*', 'lib/**/*', 'db/**/*', 'vendor/**/*'] s.require_path = 'lib' - s.requirements << 'none' - s.add_dependency 'acts_as_list', '= 0.1.4' - s.add_dependency 'awesome_nested_set', '2.1.5' + s.add_dependency 'activemerchant', '~> 1.47.0' + s.add_dependency 'acts_as_list', '~> 0.3' + s.add_dependency 'awesome_nested_set', '~> 3.0.1' + s.add_dependency 'carmen', '~> 1.0.0' + s.add_dependency 'cancancan', '~> 1.9.2' + s.add_dependency 'deface', '~> 1.0.0' + s.add_dependency 'ffaker', '~> 1.16' + s.add_dependency 'font-awesome-rails', '~> 4.0' + s.add_dependency 'friendly_id', '~> 5.0.4' + s.add_dependency 'highline', '~> 1.6.18' # Necessary for the install generator + s.add_dependency 'json', '~> 1.7' + s.add_dependency 'kaminari', '~> 0.15', '>= 0.15.1' + s.add_dependency 'monetize', '~> 1.1' + s.add_dependency 'paperclip', '~> 4.2.0' + s.add_dependency 'paranoia', '~> 2.1.0' + s.add_dependency 'premailer-rails' + s.add_dependency 'rails', '~> 4.1.11' + s.add_dependency 'ransack', '~> 1.4.1' + s.add_dependency 'state_machine', '1.2.0' + s.add_dependency 'stringex', '~> 1.5.1' + s.add_dependency 'truncate_html', '0.9.2' + s.add_dependency 'twitter_cldr', '~> 3.0' - s.add_dependency 'jquery-rails', '~> 2.0' - s.add_dependency 'select2-rails', '~> 3.2' - - s.add_dependency 'highline', '= 1.6.11' - s.add_dependency 'state_machine', '= 1.1.2' - s.add_dependency 'ffaker', '~> 1.12.0' - s.add_dependency 'paperclip', '~> 2.8' - s.add_dependency 'aws-sdk', '~> 1.3.4' - s.add_dependency 'ransack', '~> 0.7.0' - s.add_dependency 'activemerchant', '= 1.28.0' - s.add_dependency 'rails', '~> 3.2.9' - s.add_dependency 'kaminari', '0.13.0' - s.add_dependency 'deface', '>= 0.9.0' - s.add_dependency 'stringex', '~> 1.3.2' - s.add_dependency 'cancan', '1.6.7' - s.add_dependency 'money', '5.0.0' - s.add_dependency 'rabl', '0.7.2' + s.add_development_dependency 'email_spec', '~> 1.6' end diff --git a/core/vendor/assets/fonts/fontawesome-webfont.eot b/core/vendor/assets/fonts/fontawesome-webfont.eot deleted file mode 100755 index 89070c1e63c..00000000000 Binary files a/core/vendor/assets/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/core/vendor/assets/fonts/fontawesome-webfont.svg b/core/vendor/assets/fonts/fontawesome-webfont.svg deleted file mode 100755 index 1245f92c2ed..00000000000 --- a/core/vendor/assets/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/core/vendor/assets/fonts/fontawesome-webfont.ttf b/core/vendor/assets/fonts/fontawesome-webfont.ttf deleted file mode 100755 index c17e9f8d100..00000000000 Binary files a/core/vendor/assets/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/core/vendor/assets/fonts/fontawesome-webfont.woff b/core/vendor/assets/fonts/fontawesome-webfont.woff deleted file mode 100755 index 09f2469a1f7..00000000000 Binary files a/core/vendor/assets/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/core/vendor/assets/images/flags/ad.png b/core/vendor/assets/images/flags/ad.png deleted file mode 100755 index 625ca84f9ec..00000000000 Binary files a/core/vendor/assets/images/flags/ad.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ae.png b/core/vendor/assets/images/flags/ae.png deleted file mode 100755 index ef3a1ecfccd..00000000000 Binary files a/core/vendor/assets/images/flags/ae.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/af.png b/core/vendor/assets/images/flags/af.png deleted file mode 100755 index a4742e299f5..00000000000 Binary files a/core/vendor/assets/images/flags/af.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ag.png b/core/vendor/assets/images/flags/ag.png deleted file mode 100755 index 556d5504dc2..00000000000 Binary files a/core/vendor/assets/images/flags/ag.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ai.png b/core/vendor/assets/images/flags/ai.png deleted file mode 100755 index 74ed29d9261..00000000000 Binary files a/core/vendor/assets/images/flags/ai.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/al.png b/core/vendor/assets/images/flags/al.png deleted file mode 100755 index 92354cb6e25..00000000000 Binary files a/core/vendor/assets/images/flags/al.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/am.png b/core/vendor/assets/images/flags/am.png deleted file mode 100755 index 344a2a86c43..00000000000 Binary files a/core/vendor/assets/images/flags/am.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/an.png b/core/vendor/assets/images/flags/an.png deleted file mode 100755 index 633e4b89fde..00000000000 Binary files a/core/vendor/assets/images/flags/an.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ar.png b/core/vendor/assets/images/flags/ar.png deleted file mode 100755 index e5ef8f1fcdd..00000000000 Binary files a/core/vendor/assets/images/flags/ar.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/as.png b/core/vendor/assets/images/flags/as.png deleted file mode 100755 index 32f30e4ce4e..00000000000 Binary files a/core/vendor/assets/images/flags/as.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/at.png b/core/vendor/assets/images/flags/at.png deleted file mode 100755 index 0f15f34f288..00000000000 Binary files a/core/vendor/assets/images/flags/at.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/au.png b/core/vendor/assets/images/flags/au.png deleted file mode 100755 index a01389a745d..00000000000 Binary files a/core/vendor/assets/images/flags/au.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/aw.png b/core/vendor/assets/images/flags/aw.png deleted file mode 100755 index a3579c2d621..00000000000 Binary files a/core/vendor/assets/images/flags/aw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ax.png b/core/vendor/assets/images/flags/ax.png deleted file mode 100755 index 1eea80a7b73..00000000000 Binary files a/core/vendor/assets/images/flags/ax.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/az.png b/core/vendor/assets/images/flags/az.png deleted file mode 100755 index 4ee9fe5ced2..00000000000 Binary files a/core/vendor/assets/images/flags/az.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ba.png b/core/vendor/assets/images/flags/ba.png deleted file mode 100755 index c77499249c9..00000000000 Binary files a/core/vendor/assets/images/flags/ba.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bb.png b/core/vendor/assets/images/flags/bb.png deleted file mode 100755 index 0df19c71d20..00000000000 Binary files a/core/vendor/assets/images/flags/bb.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bd.png b/core/vendor/assets/images/flags/bd.png deleted file mode 100755 index 076a8bf87c0..00000000000 Binary files a/core/vendor/assets/images/flags/bd.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/be.png b/core/vendor/assets/images/flags/be.png deleted file mode 100755 index d86ebc800a6..00000000000 Binary files a/core/vendor/assets/images/flags/be.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bf.png b/core/vendor/assets/images/flags/bf.png deleted file mode 100755 index ab5ce8fe123..00000000000 Binary files a/core/vendor/assets/images/flags/bf.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bg.png b/core/vendor/assets/images/flags/bg.png deleted file mode 100755 index 0469f0607dc..00000000000 Binary files a/core/vendor/assets/images/flags/bg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bh.png b/core/vendor/assets/images/flags/bh.png deleted file mode 100755 index ea8ce68761b..00000000000 Binary files a/core/vendor/assets/images/flags/bh.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bi.png b/core/vendor/assets/images/flags/bi.png deleted file mode 100755 index 5cc2e30cfc4..00000000000 Binary files a/core/vendor/assets/images/flags/bi.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bj.png b/core/vendor/assets/images/flags/bj.png deleted file mode 100755 index 1cc8b458a4c..00000000000 Binary files a/core/vendor/assets/images/flags/bj.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bm.png b/core/vendor/assets/images/flags/bm.png deleted file mode 100755 index c0c7aead8df..00000000000 Binary files a/core/vendor/assets/images/flags/bm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bn.png b/core/vendor/assets/images/flags/bn.png deleted file mode 100755 index 8fb09849e9b..00000000000 Binary files a/core/vendor/assets/images/flags/bn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bo.png b/core/vendor/assets/images/flags/bo.png deleted file mode 100755 index ce7ba522aa7..00000000000 Binary files a/core/vendor/assets/images/flags/bo.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/br.png b/core/vendor/assets/images/flags/br.png deleted file mode 100755 index 9b1a5538b26..00000000000 Binary files a/core/vendor/assets/images/flags/br.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bs.png b/core/vendor/assets/images/flags/bs.png deleted file mode 100755 index 639fa6cfa9c..00000000000 Binary files a/core/vendor/assets/images/flags/bs.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bt.png b/core/vendor/assets/images/flags/bt.png deleted file mode 100755 index 1d512dfff42..00000000000 Binary files a/core/vendor/assets/images/flags/bt.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bv.png b/core/vendor/assets/images/flags/bv.png deleted file mode 100755 index 160b6b5b79d..00000000000 Binary files a/core/vendor/assets/images/flags/bv.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bw.png b/core/vendor/assets/images/flags/bw.png deleted file mode 100755 index fcb10394152..00000000000 Binary files a/core/vendor/assets/images/flags/bw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/by.png b/core/vendor/assets/images/flags/by.png deleted file mode 100755 index 504774ec10e..00000000000 Binary files a/core/vendor/assets/images/flags/by.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/bz.png b/core/vendor/assets/images/flags/bz.png deleted file mode 100755 index be63ee1c623..00000000000 Binary files a/core/vendor/assets/images/flags/bz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ca.png b/core/vendor/assets/images/flags/ca.png deleted file mode 100755 index 1f204193ae5..00000000000 Binary files a/core/vendor/assets/images/flags/ca.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/catalonia.png b/core/vendor/assets/images/flags/catalonia.png deleted file mode 100644 index 5041e308e3a..00000000000 Binary files a/core/vendor/assets/images/flags/catalonia.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cc.png b/core/vendor/assets/images/flags/cc.png deleted file mode 100755 index aed3d3b4e44..00000000000 Binary files a/core/vendor/assets/images/flags/cc.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cd.png b/core/vendor/assets/images/flags/cd.png deleted file mode 100644 index 5e489424884..00000000000 Binary files a/core/vendor/assets/images/flags/cd.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cf.png b/core/vendor/assets/images/flags/cf.png deleted file mode 100755 index da687bdce92..00000000000 Binary files a/core/vendor/assets/images/flags/cf.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cg.png b/core/vendor/assets/images/flags/cg.png deleted file mode 100755 index a859792ef32..00000000000 Binary files a/core/vendor/assets/images/flags/cg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ch.png b/core/vendor/assets/images/flags/ch.png deleted file mode 100755 index 242ec01aaf5..00000000000 Binary files a/core/vendor/assets/images/flags/ch.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ci.png b/core/vendor/assets/images/flags/ci.png deleted file mode 100755 index 3f2c62eb4d7..00000000000 Binary files a/core/vendor/assets/images/flags/ci.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ck.png b/core/vendor/assets/images/flags/ck.png deleted file mode 100755 index 746d3d6f758..00000000000 Binary files a/core/vendor/assets/images/flags/ck.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cl.png b/core/vendor/assets/images/flags/cl.png deleted file mode 100755 index 29c6d61bd4f..00000000000 Binary files a/core/vendor/assets/images/flags/cl.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cm.png b/core/vendor/assets/images/flags/cm.png deleted file mode 100755 index f65c5bd5a79..00000000000 Binary files a/core/vendor/assets/images/flags/cm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cn.png b/core/vendor/assets/images/flags/cn.png deleted file mode 100755 index 89144146219..00000000000 Binary files a/core/vendor/assets/images/flags/cn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/co.png b/core/vendor/assets/images/flags/co.png deleted file mode 100755 index a118ff4a146..00000000000 Binary files a/core/vendor/assets/images/flags/co.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cr.png b/core/vendor/assets/images/flags/cr.png deleted file mode 100755 index c7a37317940..00000000000 Binary files a/core/vendor/assets/images/flags/cr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cs.png b/core/vendor/assets/images/flags/cs.png deleted file mode 100755 index 8254790ca72..00000000000 Binary files a/core/vendor/assets/images/flags/cs.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cu.png b/core/vendor/assets/images/flags/cu.png deleted file mode 100755 index 083f1d611c9..00000000000 Binary files a/core/vendor/assets/images/flags/cu.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cv.png b/core/vendor/assets/images/flags/cv.png deleted file mode 100755 index a63f7eaf63c..00000000000 Binary files a/core/vendor/assets/images/flags/cv.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cx.png b/core/vendor/assets/images/flags/cx.png deleted file mode 100755 index 48e31adbf4c..00000000000 Binary files a/core/vendor/assets/images/flags/cx.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cy.png b/core/vendor/assets/images/flags/cy.png deleted file mode 100755 index 5b1ad6c0788..00000000000 Binary files a/core/vendor/assets/images/flags/cy.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/cz.png b/core/vendor/assets/images/flags/cz.png deleted file mode 100755 index c8403dd21fd..00000000000 Binary files a/core/vendor/assets/images/flags/cz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/de.png b/core/vendor/assets/images/flags/de.png deleted file mode 100755 index ac4a9773627..00000000000 Binary files a/core/vendor/assets/images/flags/de.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/dj.png b/core/vendor/assets/images/flags/dj.png deleted file mode 100755 index 582af364f8a..00000000000 Binary files a/core/vendor/assets/images/flags/dj.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/dk.png b/core/vendor/assets/images/flags/dk.png deleted file mode 100755 index e2993d3c59a..00000000000 Binary files a/core/vendor/assets/images/flags/dk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/dm.png b/core/vendor/assets/images/flags/dm.png deleted file mode 100755 index 5fbffcba3cb..00000000000 Binary files a/core/vendor/assets/images/flags/dm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/do.png b/core/vendor/assets/images/flags/do.png deleted file mode 100755 index 5a04932d879..00000000000 Binary files a/core/vendor/assets/images/flags/do.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/dz.png b/core/vendor/assets/images/flags/dz.png deleted file mode 100755 index 335c2391d39..00000000000 Binary files a/core/vendor/assets/images/flags/dz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ec.png b/core/vendor/assets/images/flags/ec.png deleted file mode 100755 index 0caa0b1e785..00000000000 Binary files a/core/vendor/assets/images/flags/ec.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ee.png b/core/vendor/assets/images/flags/ee.png deleted file mode 100755 index 0c82efb7dde..00000000000 Binary files a/core/vendor/assets/images/flags/ee.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/eg.png b/core/vendor/assets/images/flags/eg.png deleted file mode 100755 index 8a3f7a10b57..00000000000 Binary files a/core/vendor/assets/images/flags/eg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/eh.png b/core/vendor/assets/images/flags/eh.png deleted file mode 100755 index 90a1195b47a..00000000000 Binary files a/core/vendor/assets/images/flags/eh.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/england.png b/core/vendor/assets/images/flags/england.png deleted file mode 100755 index 3a7311d5617..00000000000 Binary files a/core/vendor/assets/images/flags/england.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/er.png b/core/vendor/assets/images/flags/er.png deleted file mode 100755 index 13065ae99cc..00000000000 Binary files a/core/vendor/assets/images/flags/er.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/es.png b/core/vendor/assets/images/flags/es.png deleted file mode 100755 index c2de2d7111e..00000000000 Binary files a/core/vendor/assets/images/flags/es.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/et.png b/core/vendor/assets/images/flags/et.png deleted file mode 100755 index 2e893fa056c..00000000000 Binary files a/core/vendor/assets/images/flags/et.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/europeanunion.png b/core/vendor/assets/images/flags/europeanunion.png deleted file mode 100644 index d6d87115808..00000000000 Binary files a/core/vendor/assets/images/flags/europeanunion.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/fam.png b/core/vendor/assets/images/flags/fam.png deleted file mode 100755 index cf50c759eb2..00000000000 Binary files a/core/vendor/assets/images/flags/fam.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/fi.png b/core/vendor/assets/images/flags/fi.png deleted file mode 100755 index 14ec091b802..00000000000 Binary files a/core/vendor/assets/images/flags/fi.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/fj.png b/core/vendor/assets/images/flags/fj.png deleted file mode 100755 index cee998892eb..00000000000 Binary files a/core/vendor/assets/images/flags/fj.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/fk.png b/core/vendor/assets/images/flags/fk.png deleted file mode 100755 index ceaeb27decb..00000000000 Binary files a/core/vendor/assets/images/flags/fk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/fm.png b/core/vendor/assets/images/flags/fm.png deleted file mode 100755 index 066bb247389..00000000000 Binary files a/core/vendor/assets/images/flags/fm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/fo.png b/core/vendor/assets/images/flags/fo.png deleted file mode 100755 index cbceb809eb9..00000000000 Binary files a/core/vendor/assets/images/flags/fo.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/fr.png b/core/vendor/assets/images/flags/fr.png deleted file mode 100755 index 8332c4ec23c..00000000000 Binary files a/core/vendor/assets/images/flags/fr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ga.png b/core/vendor/assets/images/flags/ga.png deleted file mode 100755 index 0e0d434363a..00000000000 Binary files a/core/vendor/assets/images/flags/ga.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gb.png b/core/vendor/assets/images/flags/gb.png deleted file mode 100644 index ff701e19f6d..00000000000 Binary files a/core/vendor/assets/images/flags/gb.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gd.png b/core/vendor/assets/images/flags/gd.png deleted file mode 100755 index 9ab57f5489b..00000000000 Binary files a/core/vendor/assets/images/flags/gd.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ge.png b/core/vendor/assets/images/flags/ge.png deleted file mode 100755 index 728d97078df..00000000000 Binary files a/core/vendor/assets/images/flags/ge.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gf.png b/core/vendor/assets/images/flags/gf.png deleted file mode 100755 index 8332c4ec23c..00000000000 Binary files a/core/vendor/assets/images/flags/gf.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gh.png b/core/vendor/assets/images/flags/gh.png deleted file mode 100755 index 4e2f8965914..00000000000 Binary files a/core/vendor/assets/images/flags/gh.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gi.png b/core/vendor/assets/images/flags/gi.png deleted file mode 100755 index e76797f62fe..00000000000 Binary files a/core/vendor/assets/images/flags/gi.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gl.png b/core/vendor/assets/images/flags/gl.png deleted file mode 100755 index ef12a73bf96..00000000000 Binary files a/core/vendor/assets/images/flags/gl.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gm.png b/core/vendor/assets/images/flags/gm.png deleted file mode 100755 index 0720b667aff..00000000000 Binary files a/core/vendor/assets/images/flags/gm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gn.png b/core/vendor/assets/images/flags/gn.png deleted file mode 100755 index ea660b01fae..00000000000 Binary files a/core/vendor/assets/images/flags/gn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gp.png b/core/vendor/assets/images/flags/gp.png deleted file mode 100755 index dbb086d0012..00000000000 Binary files a/core/vendor/assets/images/flags/gp.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gq.png b/core/vendor/assets/images/flags/gq.png deleted file mode 100755 index ebe20a28de0..00000000000 Binary files a/core/vendor/assets/images/flags/gq.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gr.png b/core/vendor/assets/images/flags/gr.png deleted file mode 100755 index 8651ade7cbe..00000000000 Binary files a/core/vendor/assets/images/flags/gr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gs.png b/core/vendor/assets/images/flags/gs.png deleted file mode 100755 index 7ef0bf598d9..00000000000 Binary files a/core/vendor/assets/images/flags/gs.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gt.png b/core/vendor/assets/images/flags/gt.png deleted file mode 100755 index c43a70d3642..00000000000 Binary files a/core/vendor/assets/images/flags/gt.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gu.png b/core/vendor/assets/images/flags/gu.png deleted file mode 100755 index 92f37c05330..00000000000 Binary files a/core/vendor/assets/images/flags/gu.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gw.png b/core/vendor/assets/images/flags/gw.png deleted file mode 100755 index b37bcf06bf2..00000000000 Binary files a/core/vendor/assets/images/flags/gw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/gy.png b/core/vendor/assets/images/flags/gy.png deleted file mode 100755 index 22cbe2f5914..00000000000 Binary files a/core/vendor/assets/images/flags/gy.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/hk.png b/core/vendor/assets/images/flags/hk.png deleted file mode 100755 index d5c380ca9d8..00000000000 Binary files a/core/vendor/assets/images/flags/hk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/hm.png b/core/vendor/assets/images/flags/hm.png deleted file mode 100755 index a01389a745d..00000000000 Binary files a/core/vendor/assets/images/flags/hm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/hn.png b/core/vendor/assets/images/flags/hn.png deleted file mode 100755 index 96f838859fd..00000000000 Binary files a/core/vendor/assets/images/flags/hn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/hr.png b/core/vendor/assets/images/flags/hr.png deleted file mode 100755 index 696b515460d..00000000000 Binary files a/core/vendor/assets/images/flags/hr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ht.png b/core/vendor/assets/images/flags/ht.png deleted file mode 100755 index 416052af772..00000000000 Binary files a/core/vendor/assets/images/flags/ht.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/hu.png b/core/vendor/assets/images/flags/hu.png deleted file mode 100755 index 7baafe44ddc..00000000000 Binary files a/core/vendor/assets/images/flags/hu.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/id.png b/core/vendor/assets/images/flags/id.png deleted file mode 100755 index c6bc0fafac7..00000000000 Binary files a/core/vendor/assets/images/flags/id.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ie.png b/core/vendor/assets/images/flags/ie.png deleted file mode 100755 index 26baa31e182..00000000000 Binary files a/core/vendor/assets/images/flags/ie.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/il.png b/core/vendor/assets/images/flags/il.png deleted file mode 100755 index 2ca772d0b79..00000000000 Binary files a/core/vendor/assets/images/flags/il.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/in.png b/core/vendor/assets/images/flags/in.png deleted file mode 100755 index e4d7e81a98d..00000000000 Binary files a/core/vendor/assets/images/flags/in.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/io.png b/core/vendor/assets/images/flags/io.png deleted file mode 100755 index 3e74b6a3164..00000000000 Binary files a/core/vendor/assets/images/flags/io.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/iq.png b/core/vendor/assets/images/flags/iq.png deleted file mode 100755 index 878a351403a..00000000000 Binary files a/core/vendor/assets/images/flags/iq.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ir.png b/core/vendor/assets/images/flags/ir.png deleted file mode 100755 index c5fd136aee5..00000000000 Binary files a/core/vendor/assets/images/flags/ir.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/is.png b/core/vendor/assets/images/flags/is.png deleted file mode 100755 index b8f6d0f0667..00000000000 Binary files a/core/vendor/assets/images/flags/is.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/it.png b/core/vendor/assets/images/flags/it.png deleted file mode 100755 index 89692f74f05..00000000000 Binary files a/core/vendor/assets/images/flags/it.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ja.png b/core/vendor/assets/images/flags/ja.png deleted file mode 100755 index 325fbad3ffd..00000000000 Binary files a/core/vendor/assets/images/flags/ja.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/jm.png b/core/vendor/assets/images/flags/jm.png deleted file mode 100755 index 7be119e03d2..00000000000 Binary files a/core/vendor/assets/images/flags/jm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/jo.png b/core/vendor/assets/images/flags/jo.png deleted file mode 100755 index 11bd4972b6d..00000000000 Binary files a/core/vendor/assets/images/flags/jo.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ke.png b/core/vendor/assets/images/flags/ke.png deleted file mode 100755 index 51879adf17c..00000000000 Binary files a/core/vendor/assets/images/flags/ke.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/kg.png b/core/vendor/assets/images/flags/kg.png deleted file mode 100755 index 0a818f67ea3..00000000000 Binary files a/core/vendor/assets/images/flags/kg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/kh.png b/core/vendor/assets/images/flags/kh.png deleted file mode 100755 index 30f6bb1b9b6..00000000000 Binary files a/core/vendor/assets/images/flags/kh.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ki.png b/core/vendor/assets/images/flags/ki.png deleted file mode 100755 index 2dcce4b33ff..00000000000 Binary files a/core/vendor/assets/images/flags/ki.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/km.png b/core/vendor/assets/images/flags/km.png deleted file mode 100755 index 812b2f56c5a..00000000000 Binary files a/core/vendor/assets/images/flags/km.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/kn.png b/core/vendor/assets/images/flags/kn.png deleted file mode 100755 index febd5b486f3..00000000000 Binary files a/core/vendor/assets/images/flags/kn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/kp.png b/core/vendor/assets/images/flags/kp.png deleted file mode 100755 index d3d509aa874..00000000000 Binary files a/core/vendor/assets/images/flags/kp.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/kr.png b/core/vendor/assets/images/flags/kr.png deleted file mode 100755 index 9c0a78eb942..00000000000 Binary files a/core/vendor/assets/images/flags/kr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/kw.png b/core/vendor/assets/images/flags/kw.png deleted file mode 100755 index 96546da328a..00000000000 Binary files a/core/vendor/assets/images/flags/kw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ky.png b/core/vendor/assets/images/flags/ky.png deleted file mode 100755 index 15c5f8e4775..00000000000 Binary files a/core/vendor/assets/images/flags/ky.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/kz.png b/core/vendor/assets/images/flags/kz.png deleted file mode 100755 index 45a8c887424..00000000000 Binary files a/core/vendor/assets/images/flags/kz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/la.png b/core/vendor/assets/images/flags/la.png deleted file mode 100755 index e28acd018a2..00000000000 Binary files a/core/vendor/assets/images/flags/la.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/lb.png b/core/vendor/assets/images/flags/lb.png deleted file mode 100755 index d0d452bf868..00000000000 Binary files a/core/vendor/assets/images/flags/lb.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/lc.png b/core/vendor/assets/images/flags/lc.png deleted file mode 100644 index a47d065541b..00000000000 Binary files a/core/vendor/assets/images/flags/lc.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/li.png b/core/vendor/assets/images/flags/li.png deleted file mode 100755 index 6469909c013..00000000000 Binary files a/core/vendor/assets/images/flags/li.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/lk.png b/core/vendor/assets/images/flags/lk.png deleted file mode 100755 index 088aad6db95..00000000000 Binary files a/core/vendor/assets/images/flags/lk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/lr.png b/core/vendor/assets/images/flags/lr.png deleted file mode 100755 index 89a5bc7e707..00000000000 Binary files a/core/vendor/assets/images/flags/lr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ls.png b/core/vendor/assets/images/flags/ls.png deleted file mode 100755 index 33fdef101f7..00000000000 Binary files a/core/vendor/assets/images/flags/ls.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/lt.png b/core/vendor/assets/images/flags/lt.png deleted file mode 100755 index c8ef0da0919..00000000000 Binary files a/core/vendor/assets/images/flags/lt.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/lu.png b/core/vendor/assets/images/flags/lu.png deleted file mode 100755 index 4cabba98ae7..00000000000 Binary files a/core/vendor/assets/images/flags/lu.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/lv.png b/core/vendor/assets/images/flags/lv.png deleted file mode 100755 index 49b69981085..00000000000 Binary files a/core/vendor/assets/images/flags/lv.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ly.png b/core/vendor/assets/images/flags/ly.png deleted file mode 100755 index b163a9f8a06..00000000000 Binary files a/core/vendor/assets/images/flags/ly.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ma.png b/core/vendor/assets/images/flags/ma.png deleted file mode 100755 index f386770280b..00000000000 Binary files a/core/vendor/assets/images/flags/ma.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mc.png b/core/vendor/assets/images/flags/mc.png deleted file mode 100755 index 1aa830f121a..00000000000 Binary files a/core/vendor/assets/images/flags/mc.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/md.png b/core/vendor/assets/images/flags/md.png deleted file mode 100755 index 4e92c189044..00000000000 Binary files a/core/vendor/assets/images/flags/md.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/me.png b/core/vendor/assets/images/flags/me.png deleted file mode 100644 index ac7253558ab..00000000000 Binary files a/core/vendor/assets/images/flags/me.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mg.png b/core/vendor/assets/images/flags/mg.png deleted file mode 100755 index d2715b3d0e1..00000000000 Binary files a/core/vendor/assets/images/flags/mg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mh.png b/core/vendor/assets/images/flags/mh.png deleted file mode 100755 index fb523a8c39d..00000000000 Binary files a/core/vendor/assets/images/flags/mh.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mk.png b/core/vendor/assets/images/flags/mk.png deleted file mode 100755 index db173aaff21..00000000000 Binary files a/core/vendor/assets/images/flags/mk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ml.png b/core/vendor/assets/images/flags/ml.png deleted file mode 100755 index 2cec8ba440b..00000000000 Binary files a/core/vendor/assets/images/flags/ml.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mm.png b/core/vendor/assets/images/flags/mm.png deleted file mode 100755 index f464f67ffb4..00000000000 Binary files a/core/vendor/assets/images/flags/mm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mn.png b/core/vendor/assets/images/flags/mn.png deleted file mode 100755 index 9396355db45..00000000000 Binary files a/core/vendor/assets/images/flags/mn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mo.png b/core/vendor/assets/images/flags/mo.png deleted file mode 100755 index deb801dda24..00000000000 Binary files a/core/vendor/assets/images/flags/mo.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mp.png b/core/vendor/assets/images/flags/mp.png deleted file mode 100755 index 298d588b14b..00000000000 Binary files a/core/vendor/assets/images/flags/mp.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mq.png b/core/vendor/assets/images/flags/mq.png deleted file mode 100755 index 010143b3867..00000000000 Binary files a/core/vendor/assets/images/flags/mq.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mr.png b/core/vendor/assets/images/flags/mr.png deleted file mode 100755 index 319546b1008..00000000000 Binary files a/core/vendor/assets/images/flags/mr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ms.png b/core/vendor/assets/images/flags/ms.png deleted file mode 100755 index d4cbb433d8f..00000000000 Binary files a/core/vendor/assets/images/flags/ms.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mt.png b/core/vendor/assets/images/flags/mt.png deleted file mode 100755 index 00af94871de..00000000000 Binary files a/core/vendor/assets/images/flags/mt.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mu.png b/core/vendor/assets/images/flags/mu.png deleted file mode 100755 index b7fdce1bdd7..00000000000 Binary files a/core/vendor/assets/images/flags/mu.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mv.png b/core/vendor/assets/images/flags/mv.png deleted file mode 100755 index 5073d9ec47c..00000000000 Binary files a/core/vendor/assets/images/flags/mv.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mw.png b/core/vendor/assets/images/flags/mw.png deleted file mode 100755 index 13886e9f8bf..00000000000 Binary files a/core/vendor/assets/images/flags/mw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mx.png b/core/vendor/assets/images/flags/mx.png deleted file mode 100755 index 5bc58ab3e35..00000000000 Binary files a/core/vendor/assets/images/flags/mx.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/my.png b/core/vendor/assets/images/flags/my.png deleted file mode 100755 index 9034cbab2c0..00000000000 Binary files a/core/vendor/assets/images/flags/my.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/mz.png b/core/vendor/assets/images/flags/mz.png deleted file mode 100755 index 76405e063d4..00000000000 Binary files a/core/vendor/assets/images/flags/mz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/na.png b/core/vendor/assets/images/flags/na.png deleted file mode 100755 index 63358c67df9..00000000000 Binary files a/core/vendor/assets/images/flags/na.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/nc.png b/core/vendor/assets/images/flags/nc.png deleted file mode 100755 index 2cad2837823..00000000000 Binary files a/core/vendor/assets/images/flags/nc.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ne.png b/core/vendor/assets/images/flags/ne.png deleted file mode 100755 index d85f424f38d..00000000000 Binary files a/core/vendor/assets/images/flags/ne.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/nf.png b/core/vendor/assets/images/flags/nf.png deleted file mode 100755 index f9bcdda12ca..00000000000 Binary files a/core/vendor/assets/images/flags/nf.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ng.png b/core/vendor/assets/images/flags/ng.png deleted file mode 100755 index 3eea2e02075..00000000000 Binary files a/core/vendor/assets/images/flags/ng.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ni.png b/core/vendor/assets/images/flags/ni.png deleted file mode 100755 index 3969aaaaee4..00000000000 Binary files a/core/vendor/assets/images/flags/ni.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/nl.png b/core/vendor/assets/images/flags/nl.png deleted file mode 100755 index fe44791e32b..00000000000 Binary files a/core/vendor/assets/images/flags/nl.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/no.png b/core/vendor/assets/images/flags/no.png deleted file mode 100755 index 160b6b5b79d..00000000000 Binary files a/core/vendor/assets/images/flags/no.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/np.png b/core/vendor/assets/images/flags/np.png deleted file mode 100755 index aeb058b7ea8..00000000000 Binary files a/core/vendor/assets/images/flags/np.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/nr.png b/core/vendor/assets/images/flags/nr.png deleted file mode 100755 index 705fc337ccd..00000000000 Binary files a/core/vendor/assets/images/flags/nr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/nu.png b/core/vendor/assets/images/flags/nu.png deleted file mode 100755 index c3ce4aedda9..00000000000 Binary files a/core/vendor/assets/images/flags/nu.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/nz.png b/core/vendor/assets/images/flags/nz.png deleted file mode 100755 index 10d6306d174..00000000000 Binary files a/core/vendor/assets/images/flags/nz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/om.png b/core/vendor/assets/images/flags/om.png deleted file mode 100755 index 2ffba7e8c43..00000000000 Binary files a/core/vendor/assets/images/flags/om.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pa.png b/core/vendor/assets/images/flags/pa.png deleted file mode 100755 index 9b2ee9a7809..00000000000 Binary files a/core/vendor/assets/images/flags/pa.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pe.png b/core/vendor/assets/images/flags/pe.png deleted file mode 100755 index 62a04977fb2..00000000000 Binary files a/core/vendor/assets/images/flags/pe.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pf.png b/core/vendor/assets/images/flags/pf.png deleted file mode 100755 index 771a0f65225..00000000000 Binary files a/core/vendor/assets/images/flags/pf.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pg.png b/core/vendor/assets/images/flags/pg.png deleted file mode 100755 index 10d6233496c..00000000000 Binary files a/core/vendor/assets/images/flags/pg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ph.png b/core/vendor/assets/images/flags/ph.png deleted file mode 100755 index b89e15935d9..00000000000 Binary files a/core/vendor/assets/images/flags/ph.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pk.png b/core/vendor/assets/images/flags/pk.png deleted file mode 100755 index e9df70ca4d6..00000000000 Binary files a/core/vendor/assets/images/flags/pk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pl.png b/core/vendor/assets/images/flags/pl.png deleted file mode 100755 index d413d010b5b..00000000000 Binary files a/core/vendor/assets/images/flags/pl.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pm.png b/core/vendor/assets/images/flags/pm.png deleted file mode 100755 index ba91d2c7a0d..00000000000 Binary files a/core/vendor/assets/images/flags/pm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pn.png b/core/vendor/assets/images/flags/pn.png deleted file mode 100755 index aa9344f575b..00000000000 Binary files a/core/vendor/assets/images/flags/pn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pr.png b/core/vendor/assets/images/flags/pr.png deleted file mode 100755 index 82d9130da45..00000000000 Binary files a/core/vendor/assets/images/flags/pr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ps.png b/core/vendor/assets/images/flags/ps.png deleted file mode 100755 index f5f547762ed..00000000000 Binary files a/core/vendor/assets/images/flags/ps.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pt.png b/core/vendor/assets/images/flags/pt.png deleted file mode 100755 index ece79801506..00000000000 Binary files a/core/vendor/assets/images/flags/pt.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/pw.png b/core/vendor/assets/images/flags/pw.png deleted file mode 100755 index 6178b254a5d..00000000000 Binary files a/core/vendor/assets/images/flags/pw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/py.png b/core/vendor/assets/images/flags/py.png deleted file mode 100755 index cb8723c0640..00000000000 Binary files a/core/vendor/assets/images/flags/py.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/qa.png b/core/vendor/assets/images/flags/qa.png deleted file mode 100755 index ed4c621fa71..00000000000 Binary files a/core/vendor/assets/images/flags/qa.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/re.png b/core/vendor/assets/images/flags/re.png deleted file mode 100755 index 8332c4ec23c..00000000000 Binary files a/core/vendor/assets/images/flags/re.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ro.png b/core/vendor/assets/images/flags/ro.png deleted file mode 100755 index 57e74a6510d..00000000000 Binary files a/core/vendor/assets/images/flags/ro.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/rs.png b/core/vendor/assets/images/flags/rs.png deleted file mode 100644 index 9439a5b605d..00000000000 Binary files a/core/vendor/assets/images/flags/rs.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ru.png b/core/vendor/assets/images/flags/ru.png deleted file mode 100755 index 47da4214fd9..00000000000 Binary files a/core/vendor/assets/images/flags/ru.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/rw.png b/core/vendor/assets/images/flags/rw.png deleted file mode 100755 index 535649178a8..00000000000 Binary files a/core/vendor/assets/images/flags/rw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sa.png b/core/vendor/assets/images/flags/sa.png deleted file mode 100755 index b4641c7e8b0..00000000000 Binary files a/core/vendor/assets/images/flags/sa.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sb.png b/core/vendor/assets/images/flags/sb.png deleted file mode 100755 index a9937ccf091..00000000000 Binary files a/core/vendor/assets/images/flags/sb.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sc.png b/core/vendor/assets/images/flags/sc.png deleted file mode 100755 index 39ee37184e0..00000000000 Binary files a/core/vendor/assets/images/flags/sc.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/scotland.png b/core/vendor/assets/images/flags/scotland.png deleted file mode 100755 index a0e57b4122a..00000000000 Binary files a/core/vendor/assets/images/flags/scotland.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sd.png b/core/vendor/assets/images/flags/sd.png deleted file mode 100755 index eaab69eb787..00000000000 Binary files a/core/vendor/assets/images/flags/sd.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/se.png b/core/vendor/assets/images/flags/se.png deleted file mode 100755 index 1994653dac1..00000000000 Binary files a/core/vendor/assets/images/flags/se.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sg.png b/core/vendor/assets/images/flags/sg.png deleted file mode 100755 index dd34d612107..00000000000 Binary files a/core/vendor/assets/images/flags/sg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sh.png b/core/vendor/assets/images/flags/sh.png deleted file mode 100755 index 4b1d2a29107..00000000000 Binary files a/core/vendor/assets/images/flags/sh.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/si.png b/core/vendor/assets/images/flags/si.png deleted file mode 100755 index bb1476ff5fe..00000000000 Binary files a/core/vendor/assets/images/flags/si.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sj.png b/core/vendor/assets/images/flags/sj.png deleted file mode 100755 index 160b6b5b79d..00000000000 Binary files a/core/vendor/assets/images/flags/sj.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sk.png b/core/vendor/assets/images/flags/sk.png deleted file mode 100755 index 7ccbc8274ad..00000000000 Binary files a/core/vendor/assets/images/flags/sk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sl.png b/core/vendor/assets/images/flags/sl.png deleted file mode 100755 index 12d812d29fa..00000000000 Binary files a/core/vendor/assets/images/flags/sl.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sm.png b/core/vendor/assets/images/flags/sm.png deleted file mode 100755 index 3df2fdcf8c0..00000000000 Binary files a/core/vendor/assets/images/flags/sm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sn.png b/core/vendor/assets/images/flags/sn.png deleted file mode 100755 index eabb71db4e8..00000000000 Binary files a/core/vendor/assets/images/flags/sn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/so.png b/core/vendor/assets/images/flags/so.png deleted file mode 100755 index 4a1ea4b29b3..00000000000 Binary files a/core/vendor/assets/images/flags/so.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sr.png b/core/vendor/assets/images/flags/sr.png deleted file mode 100755 index 5eff9271d28..00000000000 Binary files a/core/vendor/assets/images/flags/sr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/st.png b/core/vendor/assets/images/flags/st.png deleted file mode 100755 index 2978557b19d..00000000000 Binary files a/core/vendor/assets/images/flags/st.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sv.png b/core/vendor/assets/images/flags/sv.png deleted file mode 100755 index 24987990b73..00000000000 Binary files a/core/vendor/assets/images/flags/sv.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sy.png b/core/vendor/assets/images/flags/sy.png deleted file mode 100755 index f5ce30dcb79..00000000000 Binary files a/core/vendor/assets/images/flags/sy.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/sz.png b/core/vendor/assets/images/flags/sz.png deleted file mode 100755 index 914ee861d41..00000000000 Binary files a/core/vendor/assets/images/flags/sz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tc.png b/core/vendor/assets/images/flags/tc.png deleted file mode 100755 index 8fc1156bec3..00000000000 Binary files a/core/vendor/assets/images/flags/tc.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/td.png b/core/vendor/assets/images/flags/td.png deleted file mode 100755 index 667f21fd9d5..00000000000 Binary files a/core/vendor/assets/images/flags/td.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tf.png b/core/vendor/assets/images/flags/tf.png deleted file mode 100755 index 80529a43619..00000000000 Binary files a/core/vendor/assets/images/flags/tf.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tg.png b/core/vendor/assets/images/flags/tg.png deleted file mode 100755 index 3aa00ad4dfa..00000000000 Binary files a/core/vendor/assets/images/flags/tg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/th.png b/core/vendor/assets/images/flags/th.png deleted file mode 100755 index dd8ba91719b..00000000000 Binary files a/core/vendor/assets/images/flags/th.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tj.png b/core/vendor/assets/images/flags/tj.png deleted file mode 100755 index 617bf6455f6..00000000000 Binary files a/core/vendor/assets/images/flags/tj.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tk.png b/core/vendor/assets/images/flags/tk.png deleted file mode 100755 index 67b8c8cb519..00000000000 Binary files a/core/vendor/assets/images/flags/tk.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tl.png b/core/vendor/assets/images/flags/tl.png deleted file mode 100755 index 77da181e9c5..00000000000 Binary files a/core/vendor/assets/images/flags/tl.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tm.png b/core/vendor/assets/images/flags/tm.png deleted file mode 100755 index 828020ecd0f..00000000000 Binary files a/core/vendor/assets/images/flags/tm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tn.png b/core/vendor/assets/images/flags/tn.png deleted file mode 100755 index 183cdd3dc98..00000000000 Binary files a/core/vendor/assets/images/flags/tn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/to.png b/core/vendor/assets/images/flags/to.png deleted file mode 100755 index f89b8ba755f..00000000000 Binary files a/core/vendor/assets/images/flags/to.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tr.png b/core/vendor/assets/images/flags/tr.png deleted file mode 100755 index be32f77e991..00000000000 Binary files a/core/vendor/assets/images/flags/tr.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tt.png b/core/vendor/assets/images/flags/tt.png deleted file mode 100755 index 2a11c1e20ac..00000000000 Binary files a/core/vendor/assets/images/flags/tt.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tv.png b/core/vendor/assets/images/flags/tv.png deleted file mode 100755 index 28274c5fb40..00000000000 Binary files a/core/vendor/assets/images/flags/tv.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tw.png b/core/vendor/assets/images/flags/tw.png deleted file mode 100755 index f31c654c99c..00000000000 Binary files a/core/vendor/assets/images/flags/tw.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/tz.png b/core/vendor/assets/images/flags/tz.png deleted file mode 100755 index c00ff796142..00000000000 Binary files a/core/vendor/assets/images/flags/tz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ua.png b/core/vendor/assets/images/flags/ua.png deleted file mode 100755 index 09563a21941..00000000000 Binary files a/core/vendor/assets/images/flags/ua.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ug.png b/core/vendor/assets/images/flags/ug.png deleted file mode 100755 index 33f4affadee..00000000000 Binary files a/core/vendor/assets/images/flags/ug.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/um.png b/core/vendor/assets/images/flags/um.png deleted file mode 100755 index c1dd9654b07..00000000000 Binary files a/core/vendor/assets/images/flags/um.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/us.png b/core/vendor/assets/images/flags/us.png deleted file mode 100755 index 10f451fe85c..00000000000 Binary files a/core/vendor/assets/images/flags/us.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/uy.png b/core/vendor/assets/images/flags/uy.png deleted file mode 100755 index 31d948a067f..00000000000 Binary files a/core/vendor/assets/images/flags/uy.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/uz.png b/core/vendor/assets/images/flags/uz.png deleted file mode 100755 index fef5dc1709d..00000000000 Binary files a/core/vendor/assets/images/flags/uz.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/va.png b/core/vendor/assets/images/flags/va.png deleted file mode 100755 index b31eaf225d6..00000000000 Binary files a/core/vendor/assets/images/flags/va.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/vc.png b/core/vendor/assets/images/flags/vc.png deleted file mode 100755 index 8fa17b0612b..00000000000 Binary files a/core/vendor/assets/images/flags/vc.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ve.png b/core/vendor/assets/images/flags/ve.png deleted file mode 100755 index 00c90f9aff0..00000000000 Binary files a/core/vendor/assets/images/flags/ve.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/vg.png b/core/vendor/assets/images/flags/vg.png deleted file mode 100755 index 41569079865..00000000000 Binary files a/core/vendor/assets/images/flags/vg.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/vi.png b/core/vendor/assets/images/flags/vi.png deleted file mode 100755 index ed26915a323..00000000000 Binary files a/core/vendor/assets/images/flags/vi.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/vn.png b/core/vendor/assets/images/flags/vn.png deleted file mode 100755 index ec7cd48a346..00000000000 Binary files a/core/vendor/assets/images/flags/vn.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/vu.png b/core/vendor/assets/images/flags/vu.png deleted file mode 100755 index b3397bc63d7..00000000000 Binary files a/core/vendor/assets/images/flags/vu.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/wales.png b/core/vendor/assets/images/flags/wales.png deleted file mode 100755 index e0d7cee1107..00000000000 Binary files a/core/vendor/assets/images/flags/wales.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/wf.png b/core/vendor/assets/images/flags/wf.png deleted file mode 100755 index 9f9558734f0..00000000000 Binary files a/core/vendor/assets/images/flags/wf.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ws.png b/core/vendor/assets/images/flags/ws.png deleted file mode 100755 index c16950802ea..00000000000 Binary files a/core/vendor/assets/images/flags/ws.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/ye.png b/core/vendor/assets/images/flags/ye.png deleted file mode 100755 index 468dfad0386..00000000000 Binary files a/core/vendor/assets/images/flags/ye.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/yt.png b/core/vendor/assets/images/flags/yt.png deleted file mode 100755 index c298f378bee..00000000000 Binary files a/core/vendor/assets/images/flags/yt.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/za.png b/core/vendor/assets/images/flags/za.png deleted file mode 100755 index 57c58e2119f..00000000000 Binary files a/core/vendor/assets/images/flags/za.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/zm.png b/core/vendor/assets/images/flags/zm.png deleted file mode 100755 index c25b07beef8..00000000000 Binary files a/core/vendor/assets/images/flags/zm.png and /dev/null differ diff --git a/core/vendor/assets/images/flags/zw.png b/core/vendor/assets/images/flags/zw.png deleted file mode 100755 index 53c97259b9b..00000000000 Binary files a/core/vendor/assets/images/flags/zw.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png b/core/vendor/assets/images/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png deleted file mode 100755 index 5b5dab2ab7b..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png b/core/vendor/assets/images/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png deleted file mode 100755 index e44f861be1c..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_flat_55_ffffff_40x100.png b/core/vendor/assets/images/jquery-ui/ui-bg_flat_55_ffffff_40x100.png deleted file mode 100755 index ac8b229af95..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_flat_55_ffffff_40x100.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_flat_75_ffffff_40x100.png b/core/vendor/assets/images/jquery-ui/ui-bg_flat_75_ffffff_40x100.png deleted file mode 100755 index ac8b229af95..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_flat_75_ffffff_40x100.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png b/core/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png deleted file mode 100755 index 42ccba269b6..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png b/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png deleted file mode 100755 index 5dcfaa9a016..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png b/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png deleted file mode 100755 index 7226bdbbbde..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png b/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png deleted file mode 100755 index b47a4da5243..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-icons_0073ea_256x240.png b/core/vendor/assets/images/jquery-ui/ui-icons_0073ea_256x240.png deleted file mode 100755 index 6b852b618ad..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-icons_0073ea_256x240.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-icons_454545_256x240.png b/core/vendor/assets/images/jquery-ui/ui-icons_454545_256x240.png deleted file mode 100755 index 59bd45b907c..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-icons_454545_256x240.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-icons_666666_256x240.png b/core/vendor/assets/images/jquery-ui/ui-icons_666666_256x240.png deleted file mode 100755 index f87de1ca1dc..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-icons_666666_256x240.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-icons_ff0084_256x240.png b/core/vendor/assets/images/jquery-ui/ui-icons_ff0084_256x240.png deleted file mode 100755 index 938307146b3..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-icons_ff0084_256x240.png and /dev/null differ diff --git a/core/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png b/core/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png deleted file mode 100755 index 42f8f992c72..00000000000 Binary files a/core/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png and /dev/null differ diff --git a/core/vendor/assets/javascripts/handlebars.js b/core/vendor/assets/javascripts/handlebars.js deleted file mode 100644 index 05346370a20..00000000000 --- a/core/vendor/assets/javascripts/handlebars.js +++ /dev/null @@ -1,1920 +0,0 @@ -// lib/handlebars/base.js - -/*jshint eqnull:true*/ -this.Handlebars = {}; - -(function(Handlebars) { - -Handlebars.VERSION = "1.0.rc.1"; - -Handlebars.helpers = {}; -Handlebars.partials = {}; - -Handlebars.registerHelper = function(name, fn, inverse) { - if(inverse) { fn.not = inverse; } - this.helpers[name] = fn; -}; - -Handlebars.registerPartial = function(name, str) { - this.partials[name] = str; -}; - -Handlebars.registerHelper('helperMissing', function(arg) { - if(arguments.length === 2) { - return undefined; - } else { - throw new Error("Could not find property '" + arg + "'"); - } -}); - -var toString = Object.prototype.toString, functionType = "[object Function]"; - -Handlebars.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse || function() {}, fn = options.fn; - - - var ret = ""; - var type = toString.call(context); - - if(type === functionType) { context = context.call(this); } - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if(type === "[object Array]") { - if(context.length > 0) { - return Handlebars.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - return fn(context); - } -}); - -Handlebars.K = function() {}; - -Handlebars.createFrame = Object.create || function(object) { - Handlebars.K.prototype = object; - var obj = new Handlebars.K(); - Handlebars.K.prototype = null; - return obj; -}; - -Handlebars.registerHelper('each', function(context, options) { - var fn = options.fn, inverse = options.inverse; - var ret = "", data; - - if (options.data) { - data = Handlebars.createFrame(options.data); - } - - if(context && context.length > 0) { - for(var i=0, j=context.length; i 2) { - expected.push("'" + this.terminals_[p] + "'"); - } - if (this.lexer.showPosition) { - errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; - } else { - errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); - } - this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); - } - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(this.lexer.yytext); - lstack.push(this.lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = this.lexer.yyleng; - yytext = this.lexer.yytext; - yylineno = this.lexer.yylineno; - yyloc = this.lexer.yylloc; - if (recovering > 0) - recovering--; - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; - if (ranges) { - yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; - } - r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); - if (typeof r !== "undefined") { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; -} -}; -/* Jison generated lexer */ -var lexer = (function(){ -var lexer = ({EOF:1, -parseError:function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, -setInput:function (input) { - this._input = input; - this._more = this._less = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; - if (this.options.ranges) this.yylloc.range = [0,0]; - this.offset = 0; - return this; - }, -input:function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) this.yylloc.range[1]++; - - this._input = this._input.slice(1); - return ch; - }, -unput:function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length-len-1); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length-1); - this.matched = this.matched.substr(0, this.matched.length-1); - - if (lines.length-1) this.yylineno -= lines.length-1; - var r = this.yylloc.range; - - this.yylloc = {first_line: this.yylloc.first_line, - last_line: this.yylineno+1, - first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: - this.yylloc.first_column - len - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - return this; - }, -more:function () { - this._more = true; - return this; - }, -less:function (n) { - this.unput(this.match.slice(n)); - }, -pastInput:function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, -upcomingInput:function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20-next.length); - } - return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); - }, -showPosition:function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c+"^"; - }, -next:function () { - if (this.done) { - return this.EOF; - } - if (!this._input) this.done = true; - - var token, - match, - tempMatch, - index, - col, - lines; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i=0;i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (!this.options.flex) break; - } - } - if (match) { - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) this.yylineno += lines.length; - this.yylloc = {first_line: this.yylloc.last_line, - last_line: this.yylineno+1, - first_column: this.yylloc.last_column, - last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); - if (this.done && this._input) this.done = false; - if (token) return token; - else return; - } - if (this._input === "") { - return this.EOF; - } else { - return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), - {text: "", token: null, line: this.yylineno}); - } - }, -lex:function lex() { - var r = this.next(); - if (typeof r !== 'undefined') { - return r; - } else { - return this.lex(); - } - }, -begin:function begin(condition) { - this.conditionStack.push(condition); - }, -popState:function popState() { - return this.conditionStack.pop(); - }, -_currentRules:function _currentRules() { - return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; - }, -topState:function () { - return this.conditionStack[this.conditionStack.length-2]; - }, -pushState:function begin(condition) { - this.begin(condition); - }}); -lexer.options = {}; -lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { - -var YYSTATE=YY_START -switch($avoiding_name_collisions) { -case 0: - if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); - if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); - if(yy_.yytext) return 14; - -break; -case 1: return 14; -break; -case 2: - if(yy_.yytext.slice(-1) !== "\\") this.popState(); - if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); - return 14; - -break; -case 3: return 24; -break; -case 4: return 16; -break; -case 5: return 20; -break; -case 6: return 19; -break; -case 7: return 19; -break; -case 8: return 23; -break; -case 9: return 23; -break; -case 10: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; -break; -case 11: return 22; -break; -case 12: return 35; -break; -case 13: return 34; -break; -case 14: return 34; -break; -case 15: return 37; -break; -case 16: /*ignore whitespace*/ -break; -case 17: this.popState(); return 18; -break; -case 18: this.popState(); return 18; -break; -case 19: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29; -break; -case 20: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29; -break; -case 21: yy_.yytext = yy_.yytext.substr(1); return 27; -break; -case 22: return 31; -break; -case 23: return 31; -break; -case 24: return 30; -break; -case 25: return 34; -break; -case 26: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 34; -break; -case 27: return 'INVALID'; -break; -case 28: return 5; -break; -} -}; -lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[} ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; -lexer.conditions = {"mu":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"INITIAL":{"rules":[0,1,28],"inclusive":true}}; -return lexer;})() -parser.lexer = lexer; -function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; -return new Parser; -})(); -if (typeof require !== 'undefined' && typeof exports !== 'undefined') { -exports.parser = handlebars; -exports.Parser = handlebars.Parser; -exports.parse = function () { return handlebars.parse.apply(handlebars, arguments); } -exports.main = function commonjsMain(args) { - if (!args[1]) - throw new Error('Usage: '+args[0]+' FILE'); - var source, cwd; - if (typeof process !== 'undefined') { - source = require('fs').readFileSync(require('path').resolve(args[1]), "utf8"); - } else { - source = require("file").path(require("file").cwd()).join(args[1]).read({charset: "utf-8"}); - } - return exports.parser.parse(source); -} -if (typeof module !== 'undefined' && require.main === module) { - exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args); -} -}; -; -// lib/handlebars/compiler/base.js -Handlebars.Parser = handlebars; - -Handlebars.parse = function(string) { - Handlebars.Parser.yy = Handlebars.AST; - return Handlebars.Parser.parse(string); -}; - -Handlebars.print = function(ast) { - return new Handlebars.PrintVisitor().accept(ast); -}; - -Handlebars.logger = { - DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, - - // override in the host environment - log: function(level, str) {} -}; - -Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); }; -; -// lib/handlebars/compiler/ast.js -(function() { - - Handlebars.AST = {}; - - Handlebars.AST.ProgramNode = function(statements, inverse) { - this.type = "program"; - this.statements = statements; - if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } - }; - - Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { - this.type = "mustache"; - this.escaped = !unescaped; - this.hash = hash; - - var id = this.id = rawParams[0]; - var params = this.params = rawParams.slice(1); - - // a mustache is an eligible helper if: - // * its id is simple (a single part, not `this` or `..`) - var eligibleHelper = this.eligibleHelper = id.isSimple; - - // a mustache is definitely a helper if: - // * it is an eligible helper, and - // * it has at least one parameter or hash segment - this.isHelper = eligibleHelper && (params.length || hash); - - // if a mustache is an eligible helper but not a definite - // helper, it is ambiguous, and will be resolved in a later - // pass or at runtime. - }; - - Handlebars.AST.PartialNode = function(id, context) { - this.type = "partial"; - - // TODO: disallow complex IDs - - this.id = id; - this.context = context; - }; - - var verifyMatch = function(open, close) { - if(open.original !== close.original) { - throw new Handlebars.Exception(open.original + " doesn't match " + close.original); - } - }; - - Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { - verifyMatch(mustache.id, close); - this.type = "block"; - this.mustache = mustache; - this.program = program; - this.inverse = inverse; - - if (this.inverse && !this.program) { - this.isInverse = true; - } - }; - - Handlebars.AST.ContentNode = function(string) { - this.type = "content"; - this.string = string; - }; - - Handlebars.AST.HashNode = function(pairs) { - this.type = "hash"; - this.pairs = pairs; - }; - - Handlebars.AST.IdNode = function(parts) { - this.type = "ID"; - this.original = parts.join("."); - - var dig = [], depth = 0; - - for(var i=0,l=parts.length; i": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - var escapeChar = function(chr) { - return escape[chr] || "&"; - }; - - Handlebars.Utils = { - escapeExpression: function(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof Handlebars.SafeString) { - return string.toString(); - } else if (string == null || string === false) { - return ""; - } - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - }, - - isEmpty: function(value) { - if (typeof value === "undefined") { - return true; - } else if (value === null) { - return true; - } else if (value === false) { - return true; - } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { - return true; - } else { - return false; - } - } - }; -})();; -// lib/handlebars/compiler/compiler.js - -/*jshint eqnull:true*/ -Handlebars.Compiler = function() {}; -Handlebars.JavaScriptCompiler = function() {}; - -(function(Compiler, JavaScriptCompiler) { - // the foundHelper register will disambiguate helper lookup from finding a - // function in a context. This is necessary for mustache compatibility, which - // requires that context functions in blocks are evaluated by blockHelperMissing, - // and then proceed as if the resulting value was provided to blockHelperMissing. - - Compiler.prototype = { - compiler: Compiler, - - disassemble: function() { - var opcodes = this.opcodes, opcode, out = [], params, param; - - for (var i=0, l=opcodes.length; i 0) { - this.source[1] = this.source[1] + ", " + locals.join(", "); - } - - // Generate minimizer alias mappings - if (!this.isChild) { - var aliases = []; - for (var alias in this.context.aliases) { - this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; - } - } - - if (this.source[1]) { - this.source[1] = "var " + this.source[1].substring(2) + ";"; - } - - // Merge children - if (!this.isChild) { - this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; - } - - if (!this.environment.isSimple) { - this.source.push("return buffer;"); - } - - var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; - - for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } - return "stack" + this.stackSlot; - }, - - popStack: function() { - var item = this.compileStack.pop(); - - if (item instanceof Literal) { - return item.value; - } else { - this.stackSlot--; - return item; - } - }, - - topStack: function() { - var item = this.compileStack[this.compileStack.length - 1]; - - if (item instanceof Literal) { - return item.value; - } else { - return item; - } - }, - - quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') + '"'; - }, - - setupHelper: function(paramSize, name) { - var params = []; - this.setupParams(paramSize, params); - var foundHelper = this.nameLookup('helpers', name, 'helper'); - - return { - params: params, - name: foundHelper, - callParams: ["depth0"].concat(params).join(", "), - helperMissingParams: ["depth0", this.quotedString(name)].concat(params).join(", ") - }; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(paramSize, params) { - var options = [], contexts = [], param, inverse, program; - - options.push("hash:" + this.popStack()); - - inverse = this.popStack(); - program = this.popStack(); - - // Avoid setting fn and inverse if neither are set. This allows - // helpers to do a check for `if (options.fn)` - if (program || inverse) { - if (!program) { - this.context.aliases.self = "this"; - program = "self.noop"; - } - - if (!inverse) { - this.context.aliases.self = "this"; - inverse = "self.noop"; - } - - options.push("inverse:" + inverse); - options.push("fn:" + program); - } - - for(var i=0; i)[^>]*|#([\w\-]*))$/; + +// $(html) "looks like html" rule change +jQuery.fn.init = function( selector, context, rootjQuery ) { + var match; + + if ( selector && typeof selector === "string" && !jQuery.isPlainObject( context ) && + (match = rquickExpr.exec( selector )) && match[1] ) { + // This is an HTML string according to the "old" rules; is it still? + if ( selector.charAt( 0 ) !== "<" ) { + migrateWarn("$(html) HTML strings must start with '<' character"); + } + // Now process using loose rules; let pre-1.8 play too + if ( context && context.context ) { + // jQuery object as context; parseHTML expects a DOM object + context = context.context; + } + if ( jQuery.parseHTML ) { + return oldInit.call( this, jQuery.parseHTML( jQuery.trim(selector), context, true ), + context, rootjQuery ); + } + } + return oldInit.apply( this, arguments ); +}; +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.uaMatch = function( ua ) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || + /(webkit)[ \/]([\w.]+)/.exec( ua ) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || + /(msie) ([\w.]+)/.exec( ua ) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; +}; + +matched = jQuery.uaMatch( navigator.userAgent ); +browser = {}; + +if ( matched.browser ) { + browser[ matched.browser ] = true; + browser.version = matched.version; +} + +// Chrome is Webkit, but Webkit is also Safari. +if ( browser.chrome ) { + browser.webkit = true; +} else if ( browser.webkit ) { + browser.safari = true; +} + +jQuery.browser = browser; + +// Warn if the code tries to get jQuery.browser +migrateWarnProp( jQuery, "browser", browser, "jQuery.browser is deprecated" ); + +jQuery.sub = function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + migrateWarn( "jQuery.sub() is deprecated" ); + return jQuerySub; +}; + + +var oldFnData = jQuery.fn.data; + +jQuery.fn.data = function( name ) { + var ret, evt, + elem = this[0]; + + // Handles 1.7 which has this behavior and 1.8 which doesn't + if ( elem && name === "events" && arguments.length === 1 ) { + ret = jQuery.data( elem, name ); + evt = jQuery._data( elem, name ); + if ( ( ret === undefined || ret === evt ) && evt !== undefined ) { + migrateWarn("Use of jQuery.fn.data('events') is deprecated"); + return evt; + } + } + return oldFnData.apply( this, arguments ); +}; + + +var rscriptType = /\/(java|ecma)script/i, + oldSelf = jQuery.fn.andSelf || jQuery.fn.addBack, + oldFragment = jQuery.buildFragment; + +jQuery.fn.andSelf = function() { + migrateWarn("jQuery.fn.andSelf() replaced by jQuery.fn.addBack()"); + return oldSelf.apply( this, arguments ); +}; + +// Since jQuery.clean is used internally on older versions, we only shim if it's missing +if ( !jQuery.clean ) { + jQuery.clean = function( elems, context, fragment, scripts ) { + // Set context per 1.8 logic + context = context || document; + context = !context.nodeType && context[0] || context; + context = context.ownerDocument || context; + + migrateWarn("jQuery.clean() is deprecated"); + + var i, elem, handleScript, jsTags, + ret = []; + + jQuery.merge( ret, jQuery.buildFragment( elems, context ).childNodes ); + + // Complex logic lifted directly from jQuery 1.8 + if ( fragment ) { + // Special handling of each script element + handleScript = function( elem ) { + // Check if we consider it executable + if ( !elem.type || rscriptType.test( elem.type ) ) { + // Detach the script and store it in the scripts array (if provided) or the fragment + // Return truthy to indicate that it has been handled + return scripts ? + scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) : + fragment.appendChild( elem ); + } + }; + + for ( i = 0; (elem = ret[i]) != null; i++ ) { + // Check if we're done after handling an executable script + if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) { + // Append to fragment and handle embedded scripts + fragment.appendChild( elem ); + if ( typeof elem.getElementsByTagName !== "undefined" ) { + // handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration + jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript ); + + // Splice the scripts into ret after their former ancestor and advance our index beyond them + ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); + i += jsTags.length; + } + } + } + } + + return ret; + }; +} + +jQuery.buildFragment = function( elems, context, scripts, selection ) { + var ret, + warning = "jQuery.buildFragment() is deprecated"; + + // Set context per 1.8 logic + context = context || document; + context = !context.nodeType && context[0] || context; + context = context.ownerDocument || context; + + try { + ret = oldFragment.call( jQuery, elems, context, scripts, selection ); + + // jQuery < 1.8 required arrayish context; jQuery 1.9 fails on it + } catch( x ) { + ret = oldFragment.call( jQuery, elems, context.nodeType ? [ context ] : context[ 0 ], scripts, selection ); + + // Success from tweaking context means buildFragment was called by the user + migrateWarn( warning ); + } + + // jQuery < 1.9 returned an object instead of the fragment itself + if ( !ret.fragment ) { + migrateWarnProp( ret, "fragment", ret, warning ); + migrateWarnProp( ret, "cacheable", false, warning ); + } + + return ret; +}; + +var eventAdd = jQuery.event.add, + eventRemove = jQuery.event.remove, + eventTrigger = jQuery.event.trigger, + oldToggle = jQuery.fn.toggle, + oldLive = jQuery.fn.live, + oldDie = jQuery.fn.die, + ajaxEvents = "ajaxStart|ajaxStop|ajaxSend|ajaxComplete|ajaxError|ajaxSuccess", + rajaxEvent = new RegExp( "\\b(?:" + ajaxEvents + ")\\b" ), + rhoverHack = /(?:^|\s)hover(\.\S+|)\b/, + hoverHack = function( events ) { + if ( typeof( events ) != "string" || jQuery.event.special.hover ) { + return events; + } + if ( rhoverHack.test( events ) ) { + migrateWarn("'hover' pseudo-event is deprecated, use 'mouseenter mouseleave'"); + } + return events && events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +// Event props removed in 1.9, put them back if needed; no practical way to warn them +if ( jQuery.event.props && jQuery.event.props[ 0 ] !== "attrChange" ) { + jQuery.event.props.unshift( "attrChange", "attrName", "relatedNode", "srcElement" ); +} + +// Undocumented jQuery.event.handle was "deprecated" in jQuery 1.7 +migrateWarnProp( jQuery.event, "handle", jQuery.event.dispatch, "jQuery.event.handle is undocumented and deprecated" ); + +// Support for 'hover' pseudo-event and ajax event warnings +jQuery.event.add = function( elem, types, handler, data, selector ){ + if ( elem !== document && rajaxEvent.test( types ) ) { + migrateWarn( "AJAX events should be attached to document: " + types ); + } + eventAdd.call( this, elem, hoverHack( types || "" ), handler, data, selector ); +}; +jQuery.event.remove = function( elem, types, handler, selector, mappedTypes ){ + eventRemove.call( this, elem, hoverHack( types ) || "", handler, selector, mappedTypes ); +}; + +jQuery.fn.error = function() { + var args = Array.prototype.slice.call( arguments, 0); + migrateWarn("jQuery.fn.error() is deprecated"); + args.splice( 0, 0, "error" ); + if ( arguments.length ) { + return this.bind.apply( this, args ); + } + // error event should not bubble to window, although it does pre-1.7 + this.triggerHandler.apply( this, args ); + return this; +}; + +jQuery.fn.toggle = function( fn, fn2 ) { + + // Don't mess with animation or css toggles + if ( !jQuery.isFunction( fn ) || !jQuery.isFunction( fn2 ) ) { + return oldToggle.apply( this, arguments ); + } + migrateWarn("jQuery.fn.toggle(handler, handler...) is deprecated"); + + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); +}; + +jQuery.fn.live = function( types, data, fn ) { + migrateWarn("jQuery.fn.live() is deprecated"); + if ( oldLive ) { + return oldLive.apply( this, arguments ); + } + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; +}; + +jQuery.fn.die = function( types, fn ) { + migrateWarn("jQuery.fn.die() is deprecated"); + if ( oldDie ) { + return oldDie.apply( this, arguments ); + } + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; +}; + +// Turn global events into document-triggered events +jQuery.event.trigger = function( event, data, elem, onlyHandlers ){ + if ( !elem & !rajaxEvent.test( event ) ) { + migrateWarn( "Global events are undocumented and deprecated" ); + } + return eventTrigger.call( this, event, data, elem || document, onlyHandlers ); +}; +jQuery.each( ajaxEvents.split("|"), + function( _, name ) { + jQuery.event.special[ name ] = { + setup: function() { + var elem = this; + + // The document needs no shimming; must be !== for oldIE + if ( elem !== document ) { + jQuery.event.add( document, name + "." + jQuery.guid, function() { + jQuery.event.trigger( name, null, elem, true ); + }); + jQuery._data( this, name, jQuery.guid++ ); + } + return false; + }, + teardown: function() { + if ( this !== document ) { + jQuery.event.remove( document, name + "." + jQuery._data( this, name ) ); + } + return false; + } + }; + } +); + + +})( jQuery, window ); diff --git a/core/vendor/assets/javascripts/jquery.alerts/images/important.gif b/core/vendor/assets/javascripts/jquery.alerts/images/important.gif deleted file mode 100755 index 41d49438fd0..00000000000 Binary files a/core/vendor/assets/javascripts/jquery.alerts/images/important.gif and /dev/null differ diff --git a/core/vendor/assets/javascripts/jquery.alerts/images/info.gif b/core/vendor/assets/javascripts/jquery.alerts/images/info.gif deleted file mode 100755 index c81828d1c22..00000000000 Binary files a/core/vendor/assets/javascripts/jquery.alerts/images/info.gif and /dev/null differ diff --git a/core/vendor/assets/javascripts/jquery.alerts/images/title.gif b/core/vendor/assets/javascripts/jquery.alerts/images/title.gif deleted file mode 100755 index f92b59665df..00000000000 Binary files a/core/vendor/assets/javascripts/jquery.alerts/images/title.gif and /dev/null differ diff --git a/core/vendor/assets/javascripts/jquery.horizontalNav.js b/core/vendor/assets/javascripts/jquery.horizontalNav.js deleted file mode 100755 index a008355a25f..00000000000 --- a/core/vendor/assets/javascripts/jquery.horizontalNav.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * jQuery Horizontal Navigation 1.0 - * https://github.com/sebnitu/horizontalNav - * - * By Sebastian Nitu - Copyright 2012 - All rights reserved - * Author URL: http://sebnitu.com - */ -(function($) { - - $.fn.horizontalNav = function(options) { - - // Extend our default options with those provided. - var opts = $.extend({}, $.fn.horizontalNav.defaults, options); - - return this.each(function () { - - // Save our object - var $this = $(this); - - // Build element specific options - // This lets me access options with this syntax: o.optionName - var o = $.meta ? $.extend({}, opts, $this.data()) : opts; - - // Save the wrapper. The wrapper is the element that - // we figure out what the full width should be - if ($this.is('ul')) { - var ul_wrap = $this.parent(); - } else { - var ul_wrap = $this; - } - - // let's append a clearfixing element to the ul wrapper - ul_wrap.css({ 'zoom' : '1' }).append('
    '); - $('.clearHorizontalNav').css({ - 'display' : 'block', - 'overflow' : 'hidden', - 'visibility' : 'hidden', - 'width' : 0, - 'height' : 0, - 'clear' : 'both' - }); - - // Grab elements we'll need and add some default styles - var ul = $this.is('ul') ? $this : ul_wrap.find('> ul'), // The unordered list element - li = ul.find('> li'), // All list items - li_last = li.last(), // Last list item - li_count = li.size(), // The number of navigation elements - li_a = li.find('> a'); // Remove padding from the links - - // If set to responsive, re-construct after every browser resize - if ( o.responsive === true ) { - // Only need to do this for IE7 and below - // or if we set tableDisplay to false - if ( (o.tableDisplay != true) || ($.browser.msie && parseInt($.browser.version, 10) <= 7) ) { - resizeTrigger( _construct, o.responsiveDelay ); - } - } - - // Initiate the plugin - _construct(); - - // Returns the true inner width of an element - // Essentially it's the inner width without padding. - function trueInnerWidth(element) { - return element.innerWidth() - ( - parseInt(element.css('padding-left')) + parseInt(element.css('padding-right')) - ); - } - - // Call funcion on browser resize - function resizeTrigger(callback, delay) { - // Delay before function is called - delay = delay || 100; - // Call function on resize - var resizeTimer; - $(window).resize(function() { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(function() { - callback(); - }, delay); - }); - } - - // The heavy lifting of this plugin. This is where we - // find and set the appropriate widths for list items - function _construct() { - - if ( (o.tableDisplay != true) || ($.browser.msie && parseInt($.browser.version, 10) <= 7) ) { - - // IE7 doesn't support the "display: table" method - // so we need to do it the hard way. - - // Add some styles - ul.css({ 'float' : 'left' }); - li.css({ 'float' : 'left', 'width' : 'auto' }); - li_a.css({ 'padding-left' : 0, 'padding-right' : 0 }); - - // Grabbing widths and doing some math - var ul_width = trueInnerWidth(ul), - ul_width_outer = ul.outerWidth(true), - ul_width_extra = ul_width_outer - ul_width, - - full_width = trueInnerWidth(ul_wrap), - extra_width = (full_width - ul_width_extra) - ul_width, - li_padding = Math.floor( extra_width / li_count ); - - // Cycle through the list items and give them widths - li.each(function(index) { - var li_width = trueInnerWidth( $(this) ); - $(this).css({ 'width' : (li_width + li_padding) + 'px' }); - }); - - // Get the leftover pixels after we set every itms width - var li_last_width = trueInnerWidth(li_last) + ( (full_width - ul_width_extra) - trueInnerWidth(ul) ); - // I hate to do this but for some reason Firefox (v13.0) and IE are always - // one pixel off when rendering. So this is a quick fix for that. - if ($.browser.mozilla || $.browser.msie) { - li_last_width = li_last_width - 1; - } - // Add the leftovers to the last navigation item - li_last.css({ 'width' : li_last_width + 'px' }); - - } else { - // Every modern browser supports the "display: table" method - // so this is the best way to do it for them. - ul.css({ 'display' : 'table', 'float' : 'none', 'width' : '100%' }); - li.css({ 'display' : 'table-cell', 'float' : 'none' }); - } - } - - }); // @end of return this.each() - - }; - - $.fn.horizontalNav.defaults = { - responsive : true, - responsiveDelay : 100, - tableDisplay : true - }; - -})(jQuery); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/bg.jpg b/core/vendor/assets/javascripts/jquery.jstree/themes/apple/bg.jpg deleted file mode 100755 index 3aad05d8fad..00000000000 Binary files a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/bg.jpg and /dev/null differ diff --git a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/d.png b/core/vendor/assets/javascripts/jquery.jstree/themes/apple/d.png deleted file mode 100755 index 2463ba6df91..00000000000 Binary files a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/d.png and /dev/null differ diff --git a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/style.css b/core/vendor/assets/javascripts/jquery.jstree/themes/apple/style.css deleted file mode 100755 index 0ce803ccc34..00000000000 --- a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/style.css +++ /dev/null @@ -1,61 +0,0 @@ -/* - * jsTree apple theme 1.0 - * Supported features: dots/no-dots, icons/no-icons, focused, loading - * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search - */ - -.jstree-apple > ul { background:url("bg.jpg") left top repeat; } -.jstree-apple li, -.jstree-apple ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; } -.jstree-apple li { background-position:-90px 0; background-repeat:repeat-y; } -.jstree-apple li.jstree-last { background:transparent; } -.jstree-apple .jstree-open > ins { background-position:-72px 0; } -.jstree-apple .jstree-closed > ins { background-position:-54px 0; } -.jstree-apple .jstree-leaf > ins { background-position:-36px 0; } - -.jstree-apple a { border-radius:4px; -moz-border-radius:4px; -webkit-border-radius:4px; text-shadow:1px 1px 1px white; } -.jstree-apple .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 3px 0 1px; text-shadow:1px 1px 1px silver; } -.jstree-apple .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 3px 0 1px; } -.jstree-apple a .jstree-icon { background-position:-56px -20px; } -.jstree-apple a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; } - -.jstree-apple.jstree-focused { background:white; } - -.jstree-apple .jstree-no-dots li, -.jstree-apple .jstree-no-dots .jstree-leaf > ins { background:transparent; } -.jstree-apple .jstree-no-dots .jstree-open > ins { background-position:-18px 0; } -.jstree-apple .jstree-no-dots .jstree-closed > ins { background-position:0 0; } - -.jstree-apple .jstree-no-icons a .jstree-icon { display:none; } - -.jstree-apple .jstree-search { font-style:italic; } - -.jstree-apple .jstree-no-icons .jstree-checkbox { display:inline-block; } -.jstree-apple .jstree-no-checkboxes .jstree-checkbox { display:none !important; } -.jstree-apple .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; } -.jstree-apple .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; } -.jstree-apple .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; } -.jstree-apple .jstree-checked > a > .checkbox:hover { background-position:-38px -37px; } -.jstree-apple .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; } -.jstree-apple .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; } - -#vakata-dragged.jstree-apple ins { background:transparent !important; } -/*#vakata-dragged.jstree-apple .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }*/ -/*#vakata-dragged.jstree-apple .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }*/ -/*#jstree-marker.jstree-apple { background:url("d.png") -41px -57px no-repeat !important; text-indent:-100px; }*/ - -.jstree-apple a.jstree-search { color:aqua; } -.jstree-apple .jstree-locked a { color:silver; cursor:default; } - -#vakata-contextmenu.jstree-apple-context, -#vakata-contextmenu.jstree-apple-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; } -#vakata-contextmenu.jstree-apple-context li { } -#vakata-contextmenu.jstree-apple-context a { color:black; } -#vakata-contextmenu.jstree-apple-context a:hover, -#vakata-contextmenu.jstree-apple-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; } -#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a, -#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; } -#vakata-contextmenu.jstree-apple-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; } -#vakata-contextmenu.jstree-apple-context li ul { margin-left:-4px; } - -/* TODO: IE6 support - the `>` selectors */ \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/throbber.gif b/core/vendor/assets/javascripts/jquery.jstree/themes/apple/throbber.gif deleted file mode 100755 index 5b33f7e54f4..00000000000 Binary files a/core/vendor/assets/javascripts/jquery.jstree/themes/apple/throbber.gif and /dev/null differ diff --git a/core/vendor/assets/javascripts/jquery.payment.js b/core/vendor/assets/javascripts/jquery.payment.js new file mode 100644 index 00000000000..5bc7ef0adb6 --- /dev/null +++ b/core/vendor/assets/javascripts/jquery.payment.js @@ -0,0 +1,497 @@ +// Generated by CoffeeScript 1.4.0 +(function() { + var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlash, hasTextSelected, luhnCheck, reFormatCardNumber, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, setCardType, + __slice = [].slice, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, + _this = this; + + $ = jQuery; + + $.payment = {}; + + $.payment.fn = {}; + + $.fn.payment = function() { + var args, method; + method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + return $.payment.fn[method].apply(this, args); + }; + + defaultFormat = /(\d{1,4})/g; + + cards = [ + { + type: 'maestro', + pattern: /^(5018|5020|5038|6304|6759|676[1-3])/, + format: defaultFormat, + length: [12, 13, 14, 15, 16, 17, 18, 19], + cvcLength: [3], + luhn: true + }, { + type: 'dinersclub', + pattern: /^(36|38|30[0-5])/, + format: defaultFormat, + length: [14], + cvcLength: [3], + luhn: true + }, { + type: 'laser', + pattern: /^(6706|6771|6709)/, + format: defaultFormat, + length: [16, 17, 18, 19], + cvcLength: [3], + luhn: true + }, { + type: 'jcb', + pattern: /^35/, + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'unionpay', + pattern: /^62/, + format: defaultFormat, + length: [16, 17, 18, 19], + cvcLength: [3], + luhn: false + }, { + type: 'discover', + pattern: /^(6011|65|64[4-9]|622)/, + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'mastercard', + pattern: /^5[1-5]/, + format: defaultFormat, + length: [16], + cvcLength: [3], + luhn: true + }, { + type: 'amex', + pattern: /^3[47]/, + format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, + length: [15], + cvcLength: [3, 4], + luhn: true + }, { + type: 'visa', + pattern: /^4/, + format: defaultFormat, + length: [13, 14, 15, 16], + cvcLength: [3], + luhn: true + } + ]; + + cardFromNumber = function(num) { + var card, _i, _len; + num = (num + '').replace(/\D/g, ''); + for (_i = 0, _len = cards.length; _i < _len; _i++) { + card = cards[_i]; + if (card.pattern.test(num)) { + return card; + } + } + }; + + cardFromType = function(type) { + var card, _i, _len; + for (_i = 0, _len = cards.length; _i < _len; _i++) { + card = cards[_i]; + if (card.type === type) { + return card; + } + } + }; + + luhnCheck = function(num) { + var digit, digits, odd, sum, _i, _len; + odd = true; + sum = 0; + digits = (num + '').split('').reverse(); + for (_i = 0, _len = digits.length; _i < _len; _i++) { + digit = digits[_i]; + digit = parseInt(digit, 10); + if ((odd = !odd)) { + digit *= 2; + } + if (digit > 9) { + digit -= 9; + } + sum += digit; + } + return sum % 10 === 0; + }; + + hasTextSelected = function($target) { + var _ref; + if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) { + return true; + } + if (typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? typeof _ref.createRange === "function" ? _ref.createRange().text : void 0 : void 0 : void 0) { + return true; + } + return false; + }; + + reFormatCardNumber = function(e) { + var _this = this; + return setTimeout(function() { + var $target, value; + $target = $(e.currentTarget); + value = $target.val(); + value = $.payment.formatCardNumber(value); + return $target.val(value); + }); + }; + + formatCardNumber = function(e) { + var $target, card, digit, length, re, upperLength, value; + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + $target = $(e.currentTarget); + value = $target.val(); + card = cardFromNumber(value + digit); + length = (value.replace(/\D/g, '') + digit).length; + upperLength = 16; + if (card) { + upperLength = card.length[card.length.length - 1]; + } + if (length >= upperLength) { + return; + } + if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { + return; + } + if (card && card.type === 'amex') { + re = /^(\d{4}|\d{4}\s\d{6})$/; + } else { + re = /(?:^|\s)(\d{4})$/; + } + if (re.test(value)) { + e.preventDefault(); + return $target.val(value + ' ' + digit); + } else if (re.test(value + digit)) { + e.preventDefault(); + return $target.val(value + digit + ' '); + } + }; + + formatBackCardNumber = function(e) { + var $target, value; + $target = $(e.currentTarget); + value = $target.val(); + if (e.meta) { + return; + } + if (e.which !== 8) { + return; + } + if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { + return; + } + if (/\d\s$/.test(value)) { + e.preventDefault(); + return $target.val(value.replace(/\d\s$/, '')); + } else if (/\s\d?$/.test(value)) { + e.preventDefault(); + return $target.val(value.replace(/\s\d?$/, '')); + } + }; + + formatExpiry = function(e) { + var $target, digit, val; + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + $target = $(e.currentTarget); + val = $target.val() + digit; + if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { + e.preventDefault(); + return $target.val("0" + val + " / "); + } else if (/^\d\d$/.test(val)) { + e.preventDefault(); + return $target.val("" + val + " / "); + } + }; + + formatForwardExpiry = function(e) { + var $target, digit, val; + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + $target = $(e.currentTarget); + val = $target.val(); + if (/^\d\d$/.test(val)) { + return $target.val("" + val + " / "); + } + }; + + formatForwardSlash = function(e) { + var $target, slash, val; + slash = String.fromCharCode(e.which); + if (slash !== '/') { + return; + } + $target = $(e.currentTarget); + val = $target.val(); + if (/^\d$/.test(val) && val !== '0') { + return $target.val("0" + val + " / "); + } + }; + + formatBackExpiry = function(e) { + var $target, value; + if (e.meta) { + return; + } + $target = $(e.currentTarget); + value = $target.val(); + if (e.which !== 8) { + return; + } + if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { + return; + } + if (/\d(\s|\/)+$/.test(value)) { + e.preventDefault(); + return $target.val(value.replace(/\d(\s|\/)*$/, '')); + } else if (/\s\/\s?\d?$/.test(value)) { + e.preventDefault(); + return $target.val(value.replace(/\s\/\s?\d?$/, '')); + } + }; + + restrictNumeric = function(e) { + var input; + if (e.metaKey || e.ctrlKey) { + return true; + } + if (e.which === 32) { + return false; + } + if (e.which === 0) { + return true; + } + if (e.which < 33) { + return true; + } + input = String.fromCharCode(e.which); + return !!/[\d\s]/.test(input); + }; + + restrictCardNumber = function(e) { + var $target, card, digit, value; + $target = $(e.currentTarget); + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + if (hasTextSelected($target)) { + return; + } + value = ($target.val() + digit).replace(/\D/g, ''); + card = cardFromNumber(value); + if (card) { + return value.length <= card.length[card.length.length - 1]; + } else { + return value.length <= 16; + } + }; + + restrictExpiry = function(e) { + var $target, digit, value; + $target = $(e.currentTarget); + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + if (hasTextSelected($target)) { + return; + } + value = $target.val() + digit; + value = value.replace(/\D/g, ''); + if (value.length > 6) { + return false; + } + }; + + restrictCVC = function(e) { + var $target, digit, val; + $target = $(e.currentTarget); + digit = String.fromCharCode(e.which); + if (!/^\d+$/.test(digit)) { + return; + } + val = $target.val() + digit; + return val.length <= 4; + }; + + setCardType = function(e) { + var $target, allTypes, card, cardType, val; + $target = $(e.currentTarget); + val = $target.val(); + cardType = $.payment.cardType(val) || 'unknown'; + if (!$target.hasClass(cardType)) { + allTypes = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = cards.length; _i < _len; _i++) { + card = cards[_i]; + _results.push(card.type); + } + return _results; + })(); + $target.removeClass('unknown'); + $target.removeClass(allTypes.join(' ')); + $target.addClass(cardType); + $target.toggleClass('identified', cardType !== 'unknown'); + return $target.trigger('payment.cardType', cardType); + } + }; + + $.payment.fn.formatCardCVC = function() { + this.payment('restrictNumeric'); + this.on('keypress', restrictCVC); + return this; + }; + + $.payment.fn.formatCardExpiry = function() { + this.payment('restrictNumeric'); + this.on('keypress', restrictExpiry); + this.on('keypress', formatExpiry); + this.on('keypress', formatForwardSlash); + this.on('keypress', formatForwardExpiry); + this.on('keydown', formatBackExpiry); + return this; + }; + + $.payment.fn.formatCardNumber = function() { + this.payment('restrictNumeric'); + this.on('keypress', restrictCardNumber); + this.on('keypress', formatCardNumber); + this.on('keydown', formatBackCardNumber); + this.on('keyup', setCardType); + this.on('paste', reFormatCardNumber); + return this; + }; + + $.payment.fn.restrictNumeric = function() { + this.on('keypress', restrictNumeric); + return this; + }; + + $.payment.fn.cardExpiryVal = function() { + return $.payment.cardExpiryVal($(this).val()); + }; + + $.payment.cardExpiryVal = function(value) { + var month, prefix, year, _ref; + value = value.replace(/\s/g, ''); + _ref = value.split('/', 2), month = _ref[0], year = _ref[1]; + if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) { + prefix = (new Date).getFullYear(); + prefix = prefix.toString().slice(0, 2); + year = prefix + year; + } + month = parseInt(month, 10); + year = parseInt(year, 10); + return { + month: month, + year: year + }; + }; + + $.payment.validateCardNumber = function(num) { + var card, _ref; + num = (num + '').replace(/\s+|-/g, ''); + if (!/^\d+$/.test(num)) { + return false; + } + card = cardFromNumber(num); + if (!card) { + return false; + } + return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num)); + }; + + $.payment.validateCardExpiry = function(month, year) { + var currentTime, expiry, prefix, _ref; + if (typeof month === 'object' && 'month' in month) { + _ref = month, month = _ref.month, year = _ref.year; + } + if (!(month && year)) { + return false; + } + month = $.trim(month); + year = $.trim(year); + if (!/^\d+$/.test(month)) { + return false; + } + if (!/^\d+$/.test(year)) { + return false; + } + if (!(parseInt(month, 10) <= 12)) { + return false; + } + if (year.length === 2) { + prefix = (new Date).getFullYear(); + prefix = prefix.toString().slice(0, 2); + year = prefix + year; + } + expiry = new Date(year, month); + currentTime = new Date; + expiry.setMonth(expiry.getMonth() - 1); + expiry.setMonth(expiry.getMonth() + 1, 1); + return expiry > currentTime; + }; + + $.payment.validateCardCVC = function(cvc, type) { + var _ref, _ref1; + cvc = $.trim(cvc); + if (!/^\d+$/.test(cvc)) { + return false; + } + if (type) { + return _ref = cvc.length, __indexOf.call((_ref1 = cardFromType(type)) != null ? _ref1.cvcLength : void 0, _ref) >= 0; + } else { + return cvc.length >= 3 && cvc.length <= 4; + } + }; + + $.payment.cardType = function(num) { + var _ref; + if (!num) { + return null; + } + return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null; + }; + + $.payment.formatCardNumber = function(num) { + var card, groups, upperLength, _ref; + card = cardFromNumber(num); + if (!card) { + return num; + } + upperLength = card.length[card.length.length - 1]; + num = num.replace(/\D/g, ''); + num = num.slice(0, +upperLength + 1 || 9e9); + if (card.format.global) { + return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0; + } else { + groups = card.format.exec(num); + if (groups != null) { + groups.shift(); + } + return groups != null ? groups.join(' ') : void 0; + } + }; + +}).call(this); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.tokeninput.js b/core/vendor/assets/javascripts/jquery.tokeninput.js deleted file mode 100755 index 87641a57a54..00000000000 --- a/core/vendor/assets/javascripts/jquery.tokeninput.js +++ /dev/null @@ -1,860 +0,0 @@ -/* - * jQuery Plugin: Tokenizing Autocomplete Text Entry - * Version 1.6.0 - * - * Copyright (c) 2009 James Smith (http://loopj.com) - * Licensed jointly under the GPL and MIT licenses, - * choose which one suits your project best! - * - */ - -(function ($) { -// Default settings -var DEFAULT_SETTINGS = { - // Search settings - method: "GET", - contentType: "json", - queryParam: "q", - searchDelay: 300, - minChars: 1, - propertyToSearch: "name", - jsonContainer: null, - - // Display settings - hintText: "Type in a search term", - noResultsText: "No results", - searchingText: "Searching...", - deleteText: "×", - animateDropdown: true, - - // Tokenization settings - tokenLimit: null, - tokenDelimiter: ",", - preventDuplicates: false, - - // Output settings - tokenValue: "id", - - // Prepopulation settings - prePopulate: null, - processPrePopulate: false, - - // Manipulation settings - idPrefix: "token-input-", - - // Formatters - resultsFormatter: function(item){ return "
  • " + item[this.propertyToSearch]+ "
  • " }, - tokenFormatter: function(item) { return "
  • " + item[this.propertyToSearch] + "

  • " }, - - // Callbacks - onResult: null, - onAdd: null, - onDelete: null, - onReady: null -}; - -// Default classes to use when theming -var DEFAULT_CLASSES = { - tokenList: "token-input-list", - token: "token-input-token", - tokenDelete: "token-input-delete-token", - selectedToken: "token-input-selected-token", - highlightedToken: "token-input-highlighted-token", - dropdown: "token-input-dropdown", - dropdownItem: "token-input-dropdown-item", - dropdownItem2: "token-input-dropdown-item2", - selectedDropdownItem: "token-input-selected-dropdown-item", - inputToken: "token-input-input-token" -}; - -// Input box position "enum" -var POSITION = { - BEFORE: 0, - AFTER: 1, - END: 2 -}; - -// Keys "enum" -var KEY = { - BACKSPACE: 8, - TAB: 9, - ENTER: 13, - ESCAPE: 27, - SPACE: 32, - PAGE_UP: 33, - PAGE_DOWN: 34, - END: 35, - HOME: 36, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - NUMPAD_ENTER: 108, - COMMA: 188 -}; - -// Additional public (exposed) methods -var methods = { - init: function(url_or_data_or_function, options) { - var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); - - return this.each(function () { - $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); - }); - }, - clear: function() { - this.data("tokenInputObject").clear(); - return this; - }, - add: function(item) { - this.data("tokenInputObject").add(item); - return this; - }, - remove: function(item) { - this.data("tokenInputObject").remove(item); - return this; - }, - get: function() { - return this.data("tokenInputObject").getTokens(); - } -} - -// Expose the .tokenInput function to jQuery as a plugin -$.fn.tokenInput = function (method) { - // Method calling and initialization logic - if(methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else { - return methods.init.apply(this, arguments); - } -}; - -// TokenList class for each input -$.TokenList = function (input, url_or_data, settings) { - // - // Initialization - // - - // Configure the data source - if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { - // Set the url to query against - settings.url = url_or_data; - - // If the URL is a function, evaluate it here to do our initalization work - var url = computeURL(); - - // Make a smart guess about cross-domain if it wasn't explicitly specified - if(settings.crossDomain === undefined) { - if(url.indexOf("://") === -1) { - settings.crossDomain = false; - } else { - settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); - } - } - } else if(typeof(url_or_data) === "object") { - // Set the local data to search through - settings.local_data = url_or_data; - } - - // Build class names - if(settings.classes) { - // Use custom class names - settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); - } else if(settings.theme) { - // Use theme-suffixed default class names - settings.classes = {}; - $.each(DEFAULT_CLASSES, function(key, value) { - settings.classes[key] = value + "-" + settings.theme; - }); - } else { - settings.classes = DEFAULT_CLASSES; - } - - - // Save the tokens - var saved_tokens = []; - - // Keep track of the number of tokens in the list - var token_count = 0; - - // Basic cache to save on db hits - var cache = new $.TokenList.Cache(); - - // Keep track of the timeout, old vals - var timeout; - var input_val; - - // Create a new text input an attach keyup events - var input_box = $("") - .css({ - outline: "none" - }) - .attr("id", settings.idPrefix + input.id) - .focus(function () { - if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { - show_dropdown_hint(); - } - }) - .blur(function () { - hide_dropdown(); - $(this).val(""); - }) - .bind("keyup keydown blur update", resize_input) - .keydown(function (event) { - var previous_token; - var next_token; - - switch(event.keyCode) { - case KEY.LEFT: - case KEY.RIGHT: - case KEY.UP: - case KEY.DOWN: - if(!$(this).val()) { - previous_token = input_token.prev(); - next_token = input_token.next(); - - if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { - // Check if there is a previous/next token and it is selected - if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { - deselect_token($(selected_token), POSITION.BEFORE); - } else { - deselect_token($(selected_token), POSITION.AFTER); - } - } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) { - // We are moving left, select the previous token if it exists - select_token($(previous_token.get(0))); - } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) { - // We are moving right, select the next token if it exists - select_token($(next_token.get(0))); - } - } else { - var dropdown_item = null; - - if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { - dropdown_item = $(selected_dropdown_item).next(); - } else { - dropdown_item = $(selected_dropdown_item).prev(); - } - - if(dropdown_item.length) { - select_dropdown_item(dropdown_item); - } - return false; - } - break; - - case KEY.BACKSPACE: - previous_token = input_token.prev(); - - if(!$(this).val().length) { - if(selected_token) { - delete_token($(selected_token)); - hidden_input.change(); - } else if(previous_token.length) { - select_token($(previous_token.get(0))); - } - - return false; - } else if($(this).val().length === 1) { - hide_dropdown(); - } else { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search();}, 5); - } - break; - - case KEY.TAB: - case KEY.ENTER: - case KEY.NUMPAD_ENTER: - case KEY.COMMA: - if(selected_dropdown_item) { - add_token($(selected_dropdown_item).data("tokeninput")); - hidden_input.change(); - return false; - } - break; - - case KEY.ESCAPE: - hide_dropdown(); - return true; - - default: - if(String.fromCharCode(event.which)) { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search();}, 5); - } - break; - } - }); - - // Keep a reference to the original input box - var hidden_input = $(input) - .hide() - .val("") - .focus(function () { - input_box.focus(); - }) - .blur(function () { - input_box.blur(); - }); - - // Keep a reference to the selected token and dropdown item - var selected_token = null; - var selected_token_index = 0; - var selected_dropdown_item = null; - - // The list to store the token items in - var token_list = $("
      ") - .addClass(settings.classes.tokenList) - .click(function (event) { - var li = $(event.target).closest("li"); - if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { - toggle_select_token(li); - } else { - // Deselect selected token - if(selected_token) { - deselect_token($(selected_token), POSITION.END); - } - - // Focus input box - input_box.focus(); - } - }) - .mouseover(function (event) { - var li = $(event.target).closest("li"); - if(li && selected_token !== this) { - li.addClass(settings.classes.highlightedToken); - } - }) - .mouseout(function (event) { - var li = $(event.target).closest("li"); - if(li && selected_token !== this) { - li.removeClass(settings.classes.highlightedToken); - } - }) - .insertBefore(hidden_input); - - // The token holding the input box - var input_token = $("
    • ") - .addClass(settings.classes.inputToken) - .appendTo(token_list) - .append(input_box); - - // The list to store the dropdown items in - var dropdown = $("
      ") - .addClass(settings.classes.dropdown) - .appendTo("body") - .hide(); - - // Magic element to help us resize the text input - var input_resizer = $("") - .insertAfter(input_box) - .css({ - position: "absolute", - top: -9999, - left: -9999, - width: "auto", - fontSize: input_box.css("fontSize"), - fontFamily: input_box.css("fontFamily"), - fontWeight: input_box.css("fontWeight"), - letterSpacing: input_box.css("letterSpacing"), - whiteSpace: "nowrap" - }); - - // Pre-populate list if items exist - hidden_input.val(""); - var li_data = settings.prePopulate || hidden_input.data("pre"); - if(settings.processPrePopulate && $.isFunction(settings.onResult)) { - li_data = settings.onResult.call(hidden_input, li_data); - } - if(li_data && li_data.length) { - $.each(li_data, function (index, value) { - insert_token(value); - checkTokenLimit(); - }); - } - - // Initialization is done - if($.isFunction(settings.onReady)) { - settings.onReady.call(); - } - - // - // Public functions - // - - this.clear = function() { - token_list.children("li").each(function() { - if ($(this).children("input").length === 0) { - delete_token($(this)); - } - }); - } - - this.add = function(item) { - add_token(item); - } - - this.remove = function(item) { - token_list.children("li").each(function() { - if ($(this).children("input").length === 0) { - var currToken = $(this).data("tokeninput"); - var match = true; - for (var prop in item) { - if (item[prop] !== currToken[prop]) { - match = false; - break; - } - } - if (match) { - delete_token($(this)); - } - } - }); - } - - this.getTokens = function() { - return saved_tokens; - } - - // - // Private functions - // - - function checkTokenLimit() { - if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { - input_box.hide(); - hide_dropdown(); - return; - } - } - - function resize_input() { - if(input_val === (input_val = input_box.val())) {return;} - - // Enter new content into resizer and resize input accordingly - var escaped = input_val.replace(/&/g, '&').replace(/\s/g,' ').replace(//g, '>'); - input_resizer.html(escaped); - input_box.width(input_resizer.width() + 30); - } - - function is_printable_character(keycode) { - return ((keycode >= 48 && keycode <= 90) || // 0-1a-z - (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * . - (keycode >= 186 && keycode <= 192) || // ; = , - . / ^ - (keycode >= 219 && keycode <= 222)); // ( \ ) ' - } - - // Inner function to a token to the list - function insert_token(item) { - var this_token = settings.tokenFormatter(item); - this_token = $(this_token) - .addClass(settings.classes.token) - .insertBefore(input_token); - - // The 'delete token' button - $("" + settings.deleteText + "") - .addClass(settings.classes.tokenDelete) - .appendTo(this_token) - .click(function () { - delete_token($(this).parent()); - hidden_input.change(); - return false; - }); - - // Store data on the token - var token_data = {"id": item.id}; - token_data[settings.propertyToSearch] = item[settings.propertyToSearch]; - $.data(this_token.get(0), "tokeninput", item); - - // Save this token for duplicate checking - saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); - selected_token_index++; - - // Update the hidden input - update_hidden_input(saved_tokens, hidden_input); - - token_count += 1; - - // Check the token limit - if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { - input_box.hide(); - hide_dropdown(); - } - - return this_token; - } - - // Add a token to the token list based on user input - function add_token (item) { - var callback = settings.onAdd; - - // See if the token already exists and select it if we don't want duplicates - if(token_count > 0 && settings.preventDuplicates) { - var found_existing_token = null; - token_list.children().each(function () { - var existing_token = $(this); - var existing_data = $.data(existing_token.get(0), "tokeninput"); - if(existing_data && existing_data.id === item.id) { - found_existing_token = existing_token; - return false; - } - }); - - if(found_existing_token) { - select_token(found_existing_token); - input_token.insertAfter(found_existing_token); - input_box.focus(); - return; - } - } - - // Insert the new tokens - if(settings.tokenLimit == null || token_count < settings.tokenLimit) { - insert_token(item); - checkTokenLimit(); - } - - // Clear input box - input_box.val(""); - - // Don't show the help dropdown, they've got the idea - hide_dropdown(); - - // Execute the onAdd callback if defined - if($.isFunction(callback)) { - callback.call(hidden_input,item); - } - } - - // Select a token in the token list - function select_token (token) { - token.addClass(settings.classes.selectedToken); - selected_token = token.get(0); - - // Hide input box - input_box.val(""); - - // Hide dropdown if it is visible (eg if we clicked to select token) - hide_dropdown(); - } - - // Deselect a token in the token list - function deselect_token (token, position) { - token.removeClass(settings.classes.selectedToken); - selected_token = null; - - if(position === POSITION.BEFORE) { - input_token.insertBefore(token); - selected_token_index--; - } else if(position === POSITION.AFTER) { - input_token.insertAfter(token); - selected_token_index++; - } else { - input_token.appendTo(token_list); - selected_token_index = token_count; - } - - // Show the input box and give it focus again - input_box.focus(); - } - - // Toggle selection of a token in the token list - function toggle_select_token(token) { - var previous_selected_token = selected_token; - - if(selected_token) { - deselect_token($(selected_token), POSITION.END); - } - - if(previous_selected_token === token.get(0)) { - deselect_token(token, POSITION.END); - } else { - select_token(token); - } - } - - // Delete a token from the token list - function delete_token (token) { - // Remove the id from the saved list - var token_data = $.data(token.get(0), "tokeninput"); - var callback = settings.onDelete; - - var index = token.prevAll().length; - if(index > selected_token_index) index--; - - // Delete the token - token.remove(); - selected_token = null; - - // Show the input box and give it focus again - input_box.focus(); - - // Remove this token from the saved list - saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); - if(index < selected_token_index) selected_token_index--; - - // Update the hidden input - update_hidden_input(saved_tokens, hidden_input); - - token_count -= 1; - - if(settings.tokenLimit !== null) { - input_box - .show() - .val("") - .focus(); - } - - // Execute the onDelete callback if defined - if($.isFunction(callback)) { - callback.call(hidden_input,token_data); - } - } - - // Update the hidden input box value - function update_hidden_input(saved_tokens, hidden_input) { - var token_values = $.map(saved_tokens, function (el) { - return el[settings.tokenValue]; - }); - hidden_input.val(token_values.join(settings.tokenDelimiter)); - - } - - // Hide and clear the results dropdown - function hide_dropdown () { - dropdown.hide().empty(); - selected_dropdown_item = null; - } - - function show_dropdown() { - dropdown - .css({ - position: "absolute", - top: $(token_list).offset().top + $(token_list).outerHeight(), - left: $(token_list).offset().left, - zindex: 999 - }) - .show(); - } - - function show_dropdown_searching () { - if(settings.searchingText) { - dropdown.html("

      "+settings.searchingText+"

      "); - show_dropdown(); - } - } - - function show_dropdown_hint () { - if(settings.hintText) { - dropdown.html("

      "+settings.hintText+"

      "); - show_dropdown(); - } - } - - // Highlight the query part of the search term - function highlight_term(value, term) { - return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - } - - function find_value_and_highlight_term(template, value, term) { - return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); - } - - // Populate the results dropdown with some results - function populate_dropdown (query, results) { - if(results && results.length) { - dropdown.empty(); - var dropdown_ul = $("
        ") - .appendTo(dropdown) - .mouseover(function (event) { - select_dropdown_item($(event.target).closest("li")); - }) - .mousedown(function (event) { - add_token($(event.target).closest("li").data("tokeninput")); - hidden_input.change(); - return false; - }) - .hide(); - - $.each(results, function(index, value) { - var this_li = settings.resultsFormatter(value); - - this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query); - - this_li = $(this_li).appendTo(dropdown_ul); - - if(index % 2) { - this_li.addClass(settings.classes.dropdownItem); - } else { - this_li.addClass(settings.classes.dropdownItem2); - } - - if(index === 0) { - select_dropdown_item(this_li); - } - - $.data(this_li.get(0), "tokeninput", value); - }); - - show_dropdown(); - - if(settings.animateDropdown) { - dropdown_ul.slideDown("fast"); - } else { - dropdown_ul.show(); - } - } else { - if(settings.noResultsText) { - dropdown.html("

        "+settings.noResultsText+"

        "); - show_dropdown(); - } - } - } - - // Highlight an item in the results dropdown - function select_dropdown_item (item) { - if(item) { - if(selected_dropdown_item) { - deselect_dropdown_item($(selected_dropdown_item)); - } - - item.addClass(settings.classes.selectedDropdownItem); - selected_dropdown_item = item.get(0); - } - } - - // Remove highlighting from an item in the results dropdown - function deselect_dropdown_item (item) { - item.removeClass(settings.classes.selectedDropdownItem); - selected_dropdown_item = null; - } - - // Do a search and show the "searching" dropdown if the input is longer - // than settings.minChars - function do_search() { - var query = input_box.val().toLowerCase(); - - if(query && query.length) { - if(selected_token) { - deselect_token($(selected_token), POSITION.AFTER); - } - - if(query.length >= settings.minChars) { - show_dropdown_searching(); - clearTimeout(timeout); - - timeout = setTimeout(function(){ - run_search(query); - }, settings.searchDelay); - } else { - hide_dropdown(); - } - } - } - - // Do the actual search - function run_search(query) { - var cache_key = query + computeURL(); - var cached_results = cache.get(cache_key); - if(cached_results) { - populate_dropdown(query, cached_results); - } else { - // Are we doing an ajax search or local data search? - if(settings.url) { - var url = computeURL(); - // Extract exisiting get params - var ajax_params = {}; - ajax_params.data = {}; - if(url.indexOf("?") > -1) { - var parts = url.split("?"); - ajax_params.url = parts[0]; - - var param_array = parts[1].split("&"); - $.each(param_array, function (index, value) { - var kv = value.split("="); - ajax_params.data[kv[0]] = kv[1]; - }); - } else { - ajax_params.url = url; - } - - // Prepare the request - ajax_params.data[settings.queryParam] = query; - ajax_params.type = settings.method; - ajax_params.dataType = settings.contentType; - if(settings.crossDomain) { - ajax_params.dataType = "jsonp"; - } - - // Attach the success callback - ajax_params.success = function(results) { - if($.isFunction(settings.onResult)) { - results = settings.onResult.call(hidden_input, results); - } - cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results); - - // only populate the dropdown if the results are associated with the active search query - if(input_box.val().toLowerCase() === query) { - populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); - } - }; - - // Make the request - $.ajax(ajax_params); - } else if(settings.local_data) { - // Do the search through local data - var results = $.grep(settings.local_data, function (row) { - return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; - }); - - if($.isFunction(settings.onResult)) { - results = settings.onResult.call(hidden_input, results); - } - cache.add(cache_key, results); - populate_dropdown(query, results); - } - } - } - - // compute the dynamic URL - function computeURL() { - var url = settings.url; - if(typeof settings.url == 'function') { - url = settings.url.call(); - } - return url; - } -}; - -// Really basic cache for the results -$.TokenList.Cache = function (options) { - var settings = $.extend({ - max_size: 500 - }, options); - - var data = {}; - var size = 0; - - var flush = function () { - data = {}; - size = 0; - }; - - this.add = function (query, results) { - if(size > settings.max_size) { - flush(); - } - - if(!data[query]) { - size += 1; - } - - data[query] = results; - }; - - this.get = function (query) { - return data[query]; - }; -}; -}(jQuery)); diff --git a/core/vendor/assets/javascripts/jquery.validate/additional-methods.min.js b/core/vendor/assets/javascripts/jquery.validate/additional-methods.min.js deleted file mode 100644 index ff5dff3f379..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/additional-methods.min.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * jQuery Validation Plugin 1.8.1 - * - * http://bassistance.de/jquery-plugins/jquery-plugin-validation/ - * http://docs.jquery.com/Plugins/Validation - * - * Copyright (c) 2006 - 2011 Jörn Zaefferer - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - */ -(function(){function a(b){return b.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[0-9.(),;:!?%#$'"_+=\/-]*/g,"")}jQuery.validator.addMethod("maxWords",function(b,c,d){return this.optional(c)||a(b).match(/\b\w+\b/g).length=d},jQuery.validator.format("Please enter at least {0} words."));jQuery.validator.addMethod("rangeWords", -function(b,c,d){return this.optional(c)||a(b).match(/\b\w+\b/g).length>=d[0]&&b.match(/bw+b/g).length9&&a.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/)},"Please specify a valid phone number");jQuery.validator.addMethod("phoneUK",function(a,b){return this.optional(b)||a.length>9&&a.match(/^(\(?(0|\+44)[1-9]{1}\d{1,4}?\)?\s?\d{3,4}\s?\d{3,4})$/)},"Please specify a valid phone number"); -jQuery.validator.addMethod("mobileUK",function(a,b){return this.optional(b)||a.length>9&&a.match(/^((0|\+44)7(5|6|7|8|9){1}\d{2}\s?\d{6})$/)},"Please specify a valid mobile number");jQuery.validator.addMethod("strippedminlength",function(a,b,c){return jQuery(a).text().length>=c},jQuery.validator.format("Please enter at least {0} characters")); -jQuery.validator.addMethod("email2",function(a,b){return this.optional(b)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(a)},jQuery.validator.messages.email); -jQuery.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)}, -jQuery.validator.messages.url); -jQuery.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9-]+/.test(a))return false;a=a.replace(/\D/g,"");b=0;if(c.mastercard)b|=1;if(c.visa)b|=2;if(c.amex)b|=4;if(c.dinersclub)b|=8;if(c.enroute)b|=16;if(c.discover)b|=32;if(c.jcb)b|=64;if(c.unknown)b|=128;if(c.all)b=255;if(b&1&&/^(51|52|53|54|55)/.test(a))return a.length==16;if(b&2&&/^(4)/.test(a))return a.length==16;if(b&4&&/^(34|37)/.test(a))return a.length==15;if(b&8&&/^(300|301|302|303|304|305|36|38)/.test(a))return a.length==14;if(b& -16&&/^(2014|2149)/.test(a))return a.length==15;if(b&32&&/^(6011)/.test(a))return a.length==16;if(b&64&&/^(3)/.test(a))return a.length==16;if(b&64&&/^(2131|1800)/.test(a))return a.length==15;if(b&128)return true;return false},"Please enter a valid credit card number."); -jQuery.validator.addMethod("ipv4",function(a,b){return this.optional(b)||/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i.test(a)},"Please enter a valid IP v4 address."); -jQuery.validator.addMethod("ipv6",function(a,b){return this.optional(b)||/^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test(a)},"Please enter a valid IP v6 address."); diff --git a/core/vendor/assets/javascripts/jquery.validate/jquery.validate.min.js b/core/vendor/assets/javascripts/jquery.validate/jquery.validate.min.js deleted file mode 100644 index 882c699e42b..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/jquery.validate.min.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * jQuery Validation Plugin 1.8.1 - * - * http://bassistance.de/jquery-plugins/jquery-plugin-validation/ - * http://docs.jquery.com/Plugins/Validation - * - * Copyright (c) 2006 - 2011 Jörn Zaefferer - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - */ -(function(c){c.extend(c.fn,{validate:function(a){if(this.length){var b=c.data(this[0],"validator");if(b)return b;b=new c.validator(a,this[0]);c.data(this[0],"validator",b);if(b.settings.onsubmit){this.find("input, button").filter(".cancel").click(function(){b.cancelSubmit=true});b.settings.submitHandler&&this.find("input, button").filter(":submit").click(function(){b.submitButton=this});this.submit(function(d){function e(){if(b.settings.submitHandler){if(b.submitButton)var f=c("").attr("name", -b.submitButton.name).val(b.submitButton.value).appendTo(b.currentForm);b.settings.submitHandler.call(b,b.currentForm);b.submitButton&&f.remove();return false}return true}b.settings.debug&&d.preventDefault();if(b.cancelSubmit){b.cancelSubmit=false;return e()}if(b.form()){if(b.pendingRequest){b.formSubmitted=true;return false}return e()}else{b.focusInvalid();return false}})}return b}else a&&a.debug&&window.console&&console.warn("nothing selected, can't validate, returning nothing")},valid:function(){if(c(this[0]).is("form"))return this.validate().form(); -else{var a=true,b=c(this[0].form).validate();this.each(function(){a&=b.element(this)});return a}},removeAttrs:function(a){var b={},d=this;c.each(a.split(/\s/),function(e,f){b[f]=d.attr(f);d.removeAttr(f)});return b},rules:function(a,b){var d=this[0];if(a){var e=c.data(d.form,"validator").settings,f=e.rules,g=c.validator.staticRules(d);switch(a){case "add":c.extend(g,c.validator.normalizeRule(b));f[d.name]=g;if(b.messages)e.messages[d.name]=c.extend(e.messages[d.name],b.messages);break;case "remove":if(!b){delete f[d.name]; -return g}var h={};c.each(b.split(/\s/),function(j,i){h[i]=g[i];delete g[i]});return h}}d=c.validator.normalizeRules(c.extend({},c.validator.metadataRules(d),c.validator.classRules(d),c.validator.attributeRules(d),c.validator.staticRules(d)),d);if(d.required){e=d.required;delete d.required;d=c.extend({required:e},d)}return d}});c.extend(c.expr[":"],{blank:function(a){return!c.trim(""+a.value)},filled:function(a){return!!c.trim(""+a.value)},unchecked:function(a){return!a.checked}});c.validator=function(a, -b){this.settings=c.extend(true,{},c.validator.defaults,a);this.currentForm=b;this.init()};c.validator.format=function(a,b){if(arguments.length==1)return function(){var d=c.makeArray(arguments);d.unshift(a);return c.validator.format.apply(this,d)};if(arguments.length>2&&b.constructor!=Array)b=c.makeArray(arguments).slice(1);if(b.constructor!=Array)b=[b];c.each(b,function(d,e){a=a.replace(RegExp("\\{"+d+"\\}","g"),e)});return a};c.extend(c.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error", -validClass:"valid",errorElement:"label",focusInvalid:true,errorContainer:c([]),errorLabelContainer:c([]),onsubmit:true,ignore:[],ignoreTitle:false,onfocusin:function(a){this.lastActive=a;if(this.settings.focusCleanup&&!this.blockFocusCleanup){this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass);this.addWrapper(this.errorsFor(a)).hide()}},onfocusout:function(a){if(!this.checkable(a)&&(a.name in this.submitted||!this.optional(a)))this.element(a)}, -onkeyup:function(a){if(a.name in this.submitted||a==this.lastElement)this.element(a)},onclick:function(a){if(a.name in this.submitted)this.element(a);else a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(a,b,d){a.type==="radio"?this.findByName(a.name).addClass(b).removeClass(d):c(a).addClass(b).removeClass(d)},unhighlight:function(a,b,d){a.type==="radio"?this.findByName(a.name).removeClass(b).addClass(d):c(a).removeClass(b).addClass(d)}},setDefaults:function(a){c.extend(c.validator.defaults, -a)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",accept:"Please enter a value with a valid extension.",maxlength:c.validator.format("Please enter no more than {0} characters."), -minlength:c.validator.format("Please enter at least {0} characters."),rangelength:c.validator.format("Please enter a value between {0} and {1} characters long."),range:c.validator.format("Please enter a value between {0} and {1}."),max:c.validator.format("Please enter a value less than or equal to {0}."),min:c.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:false,prototype:{init:function(){function a(e){var f=c.data(this[0].form,"validator");e="on"+e.type.replace(/^validate/, -"");f.settings[e]&&f.settings[e].call(f,this[0])}this.labelContainer=c(this.settings.errorLabelContainer);this.errorContext=this.labelContainer.length&&this.labelContainer||c(this.currentForm);this.containers=c(this.settings.errorContainer).add(this.settings.errorLabelContainer);this.submitted={};this.valueCache={};this.pendingRequest=0;this.pending={};this.invalid={};this.reset();var b=this.groups={};c.each(this.settings.groups,function(e,f){c.each(f.split(/\s/),function(g,h){b[h]=e})});var d=this.settings.rules; -c.each(d,function(e,f){d[e]=c.validator.normalizeRule(f)});c(this.currentForm).validateDelegate(":text, :password, :file, select, textarea","focusin focusout keyup",a).validateDelegate(":radio, :checkbox, select, option","click",a);this.settings.invalidHandler&&c(this.currentForm).bind("invalid-form.validate",this.settings.invalidHandler)},form:function(){this.checkForm();c.extend(this.submitted,this.errorMap);this.invalid=c.extend({},this.errorMap);this.valid()||c(this.currentForm).triggerHandler("invalid-form", -[this]);this.showErrors();return this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(a){this.lastElement=a=this.clean(a);this.prepareElement(a);this.currentElements=c(a);var b=this.check(a);if(b)delete this.invalid[a.name];else this.invalid[a.name]=true;if(!this.numberOfInvalids())this.toHide=this.toHide.add(this.containers);this.showErrors();return b},showErrors:function(a){if(a){c.extend(this.errorMap, -a);this.errorList=[];for(var b in a)this.errorList.push({message:a[b],element:this.findByName(b)[0]});this.successList=c.grep(this.successList,function(d){return!(d.name in a)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){c.fn.resetForm&&c(this.currentForm).resetForm();this.submitted={};this.prepareForm();this.hideErrors();this.elements().removeClass(this.settings.errorClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)}, -objectLength:function(a){var b=0,d;for(d in a)b++;return b},hideErrors:function(){this.addWrapper(this.toHide).hide()},valid:function(){return this.size()==0},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{c(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(a){}},findLastActive:function(){var a=this.lastActive;return a&&c.grep(this.errorList,function(b){return b.element.name== -a.name}).length==1&&a},elements:function(){var a=this,b={};return c(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, [disabled]").not(this.settings.ignore).filter(function(){!this.name&&a.settings.debug&&window.console&&console.error("%o has no name assigned",this);if(this.name in b||!a.objectLength(c(this).rules()))return false;return b[this.name]=true})},clean:function(a){return c(a)[0]},errors:function(){return c(this.settings.errorElement+"."+this.settings.errorClass, -this.errorContext)},reset:function(){this.successList=[];this.errorList=[];this.errorMap={};this.toShow=c([]);this.toHide=c([]);this.currentElements=c([])},prepareForm:function(){this.reset();this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset();this.toHide=this.errorsFor(a)},check:function(a){a=this.clean(a);if(this.checkable(a))a=this.findByName(a.name).not(this.settings.ignore)[0];var b=c(a).rules(),d=false,e;for(e in b){var f={method:e,parameters:b[e]};try{var g= -c.validator.methods[e].call(this,a.value.replace(/\r/g,""),a,f.parameters);if(g=="dependency-mismatch")d=true;else{d=false;if(g=="pending"){this.toHide=this.toHide.not(this.errorsFor(a));return}if(!g){this.formatAndAdd(a,f);return false}}}catch(h){this.settings.debug&&window.console&&console.log("exception occured when checking element "+a.id+", check the '"+f.method+"' method",h);throw h;}}if(!d){this.objectLength(b)&&this.successList.push(a);return true}},customMetaMessage:function(a,b){if(c.metadata){var d= -this.settings.meta?c(a).metadata()[this.settings.meta]:c(a).metadata();return d&&d.messages&&d.messages[b]}},customMessage:function(a,b){var d=this.settings.messages[a];return d&&(d.constructor==String?d:d[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+ -a.name+"
        ")},formatAndAdd:function(a,b){var d=this.defaultMessage(a,b.method),e=/\$?\{(\d+)\}/g;if(typeof d=="function")d=d.call(this,b.parameters,a);else if(e.test(d))d=jQuery.format(d.replace(e,"{$1}"),b.parameters);this.errorList.push({message:d,element:a});this.errorMap[a.name]=d;this.submitted[a.name]=d},addWrapper:function(a){if(this.settings.wrapper)a=a.add(a.parent(this.settings.wrapper));return a},defaultShowErrors:function(){for(var a=0;this.errorList[a];a++){var b=this.errorList[a]; -this.settings.highlight&&this.settings.highlight.call(this,b.element,this.settings.errorClass,this.settings.validClass);this.showLabel(b.element,b.message)}if(this.errorList.length)this.toShow=this.toShow.add(this.containers);if(this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight){a=0;for(b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass)}this.toHide=this.toHide.not(this.toShow); -this.hideErrors();this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return c(this.errorList).map(function(){return this.element})},showLabel:function(a,b){var d=this.errorsFor(a);if(d.length){d.removeClass().addClass(this.settings.errorClass);d.attr("generated")&&d.html(b)}else{d=c("<"+this.settings.errorElement+"/>").attr({"for":this.idOrName(a),generated:true}).addClass(this.settings.errorClass).html(b|| -"");if(this.settings.wrapper)d=d.hide().show().wrap("<"+this.settings.wrapper+"/>").parent();this.labelContainer.append(d).length||(this.settings.errorPlacement?this.settings.errorPlacement(d,c(a)):d.insertAfter(a))}if(!b&&this.settings.success){d.text("");typeof this.settings.success=="string"?d.addClass(this.settings.success):this.settings.success(d)}this.toShow=this.toShow.add(d)},errorsFor:function(a){var b=this.idOrName(a);return this.errors().filter(function(){return c(this).attr("for")==b})}, -idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(a){var b=this.currentForm;return c(document.getElementsByName(a)).map(function(d,e){return e.form==b&&e.name==a&&e||null})},getLength:function(a,b){switch(b.nodeName.toLowerCase()){case "select":return c("option:selected",b).length;case "input":if(this.checkable(b))return this.findByName(b.name).filter(":checked").length}return a.length}, -depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):true},dependTypes:{"boolean":function(a){return a},string:function(a,b){return!!c(a,b.form).length},"function":function(a,b){return a(b)}},optional:function(a){return!c.validator.methods.required.call(this,c.trim(a.value),a)&&"dependency-mismatch"},startRequest:function(a){if(!this.pending[a.name]){this.pendingRequest++;this.pending[a.name]=true}},stopRequest:function(a,b){this.pendingRequest--;if(this.pendingRequest< -0)this.pendingRequest=0;delete this.pending[a.name];if(b&&this.pendingRequest==0&&this.formSubmitted&&this.form()){c(this.currentForm).submit();this.formSubmitted=false}else if(!b&&this.pendingRequest==0&&this.formSubmitted){c(this.currentForm).triggerHandler("invalid-form",[this]);this.formSubmitted=false}},previousValue:function(a){return c.data(a,"previousValue")||c.data(a,"previousValue",{old:null,valid:true,message:this.defaultMessage(a,"remote")})}},classRuleSettings:{required:{required:true}, -email:{email:true},url:{url:true},date:{date:true},dateISO:{dateISO:true},dateDE:{dateDE:true},number:{number:true},numberDE:{numberDE:true},digits:{digits:true},creditcard:{creditcard:true}},addClassRules:function(a,b){a.constructor==String?this.classRuleSettings[a]=b:c.extend(this.classRuleSettings,a)},classRules:function(a){var b={};(a=c(a).attr("class"))&&c.each(a.split(" "),function(){this in c.validator.classRuleSettings&&c.extend(b,c.validator.classRuleSettings[this])});return b},attributeRules:function(a){var b= -{};a=c(a);for(var d in c.validator.methods){var e=a.attr(d);if(e)b[d]=e}b.maxlength&&/-1|2147483647|524288/.test(b.maxlength)&&delete b.maxlength;return b},metadataRules:function(a){if(!c.metadata)return{};var b=c.data(a.form,"validator").settings.meta;return b?c(a).metadata()[b]:c(a).metadata()},staticRules:function(a){var b={},d=c.data(a.form,"validator");if(d.settings.rules)b=c.validator.normalizeRule(d.settings.rules[a.name])||{};return b},normalizeRules:function(a,b){c.each(a,function(d,e){if(e=== -false)delete a[d];else if(e.param||e.depends){var f=true;switch(typeof e.depends){case "string":f=!!c(e.depends,b.form).length;break;case "function":f=e.depends.call(b,b)}if(f)a[d]=e.param!==undefined?e.param:true;else delete a[d]}});c.each(a,function(d,e){a[d]=c.isFunction(e)?e(b):e});c.each(["minlength","maxlength","min","max"],function(){if(a[this])a[this]=Number(a[this])});c.each(["rangelength","range"],function(){if(a[this])a[this]=[Number(a[this][0]),Number(a[this][1])]});if(c.validator.autoCreateRanges){if(a.min&& -a.max){a.range=[a.min,a.max];delete a.min;delete a.max}if(a.minlength&&a.maxlength){a.rangelength=[a.minlength,a.maxlength];delete a.minlength;delete a.maxlength}}a.messages&&delete a.messages;return a},normalizeRule:function(a){if(typeof a=="string"){var b={};c.each(a.split(/\s/),function(){b[this]=true});a=b}return a},addMethod:function(a,b,d){c.validator.methods[a]=b;c.validator.messages[a]=d!=undefined?d:c.validator.messages[a];b.length<3&&c.validator.addClassRules(a,c.validator.normalizeRule(a))}, -methods:{required:function(a,b,d){if(!this.depend(d,b))return"dependency-mismatch";switch(b.nodeName.toLowerCase()){case "select":return(a=c(b).val())&&a.length>0;case "input":if(this.checkable(b))return this.getLength(a,b)>0;default:return c.trim(a).length>0}},remote:function(a,b,d){if(this.optional(b))return"dependency-mismatch";var e=this.previousValue(b);this.settings.messages[b.name]||(this.settings.messages[b.name]={});e.originalMessage=this.settings.messages[b.name].remote;this.settings.messages[b.name].remote= -e.message;d=typeof d=="string"&&{url:d}||d;if(this.pending[b.name])return"pending";if(e.old===a)return e.valid;e.old=a;var f=this;this.startRequest(b);var g={};g[b.name]=a;c.ajax(c.extend(true,{url:d,mode:"abort",port:"validate"+b.name,dataType:"json",data:g,success:function(h){f.settings.messages[b.name].remote=e.originalMessage;var j=h===true;if(j){var i=f.formSubmitted;f.prepareElement(b);f.formSubmitted=i;f.successList.push(b);f.showErrors()}else{i={};h=h||f.defaultMessage(b,"remote");i[b.name]= -e.message=c.isFunction(h)?h(a):h;f.showErrors(i)}e.valid=j;f.stopRequest(b,j)}},d));return"pending"},minlength:function(a,b,d){return this.optional(b)||this.getLength(c.trim(a),b)>=d},maxlength:function(a,b,d){return this.optional(b)||this.getLength(c.trim(a),b)<=d},rangelength:function(a,b,d){a=this.getLength(c.trim(a),b);return this.optional(b)||a>=d[0]&&a<=d[1]},min:function(a,b,d){return this.optional(b)||a>=d},max:function(a,b,d){return this.optional(b)||a<=d},range:function(a,b,d){return this.optional(b)|| -a>=d[0]&&a<=d[1]},email:function(a,b){return this.optional(b)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(a)}, -url:function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)}, -date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a))},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(a)},number:function(a,b){return this.optional(b)||/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},creditcard:function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9-]+/.test(a))return false;var d=0,e=0,f=false;a=a.replace(/\D/g,"");for(var g=a.length-1;g>= -0;g--){e=a.charAt(g);e=parseInt(e,10);if(f)if((e*=2)>9)e-=9;d+=e;f=!f}return d%10==0},accept:function(a,b,d){d=typeof d=="string"?d.replace(/,/g,"|"):"png|jpe?g|gif";return this.optional(b)||a.match(RegExp(".("+d+")$","i"))},equalTo:function(a,b,d){d=c(d).unbind(".validate-equalTo").bind("blur.validate-equalTo",function(){c(b).valid()});return a==d.val()}}});c.format=c.validator.format})(jQuery); -(function(c){var a={};if(c.ajaxPrefilter)c.ajaxPrefilter(function(d,e,f){e=d.port;if(d.mode=="abort"){a[e]&&a[e].abort();a[e]=f}});else{var b=c.ajax;c.ajax=function(d){var e=("port"in d?d:c.ajaxSettings).port;if(("mode"in d?d:c.ajaxSettings).mode=="abort"){a[e]&&a[e].abort();return a[e]=b.apply(this,arguments)}return b.apply(this,arguments)}}})(jQuery); -(function(c){!jQuery.event.special.focusin&&!jQuery.event.special.focusout&&document.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.handle.call(this,e)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)},handler:function(e){arguments[0]=c.event.fix(e);arguments[0].type=b;return c.event.handle.apply(this,arguments)}}});c.extend(c.fn,{validateDelegate:function(a, -b,d){return this.bind(b,function(e){var f=c(e.target);if(f.is(a))return d.apply(f,arguments)})}})})(jQuery); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ar.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ar.js deleted file mode 100644 index eb9e707bbe2..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ar.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin into arabic. - * Locale: AR - */ -jQuery.extend(jQuery.validator.messages, { - required: "هذا الحقل إلزامي", - remote: "يرجى تصحيح هذا الحقل للمتابعة", - email: "رجاء إدخال عنوان بريد إلكتروني صحيح", - url: "رجاء إدخال عنوان موقع إلكتروني صحيح", - date: "رجاء إدخال تاريخ صحيح", - dateISO: "رجاء إدخال تاريخ صحيح (ISO)", - number: "رجاء إدخال عدد بطريقة صحيحة", - digits: "رجاء إدخال أرقام فقط", - creditcard: "رجاء إدخال رقم بطاقة ائتمان صحيح", - equalTo: "رجاء إدخال نفس القيمة", - accept: "رجاء إدخال ملف بامتداد موافق عليه", - maxlength: jQuery.validator.format("الحد الأقصى لعدد الحروف هو {0}"), - minlength: jQuery.validator.format("الحد الأدنى لعدد الحروف هو {0}"), - rangelength: jQuery.validator.format("عدد الحروف يجب أن يكون بين {0} و {1}"), - range: jQuery.validator.format("رجاء إدخال عدد قيمته بين {0} و {1}"), - max: jQuery.validator.format("رجاء إدخال عدد أقل من أو يساوي (0}"), - min: jQuery.validator.format("رجاء إدخال عدد أكبر من أو يساوي (0}") -}); - diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_bg.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_bg.js deleted file mode 100644 index 4c91da20fa1..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_bg.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: BG - */ -jQuery.extend(jQuery.validator.messages, { - required: "Полето е задължително.", - remote: "Моля, въведете правилната стойност.", - email: "Моля, въведете валиден email.", - url: "Моля, въведете валидно URL.", - date: "Моля, въведете валидна дата.", - dateISO: "Моля, въведете валидна дата (ISO).", - number: "Моля, въведете валиден номер.", - digits: "Моля, въведете само цифри", - creditcard: "Моля, въведете валиден номер на кредитна карта.", - equalTo: "Моля, въведете същата стойност отново.", - accept: "Моля, въведете стойност с валидно разширение.", - maxlength: $.validator.format("Моля, въведете повече от {0} символа."), - minlength: $.validator.format("Моля, въведете поне {0} символа."), - rangelength: $.validator.format("Моля, въведете стойност с дължина между {0} и {1} символа."), - range: $.validator.format("Моля, въведете стойност между {0} и {1}."), - max: $.validator.format("Моля, въведете стойност по-малка или равна на {0}."), - min: $.validator.format("Моля, въведете стойност по-голяма или равна на {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ca.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ca.js deleted file mode 100644 index 3082bc40ad3..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ca.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: CA - */ -jQuery.extend(jQuery.validator.messages, { - required: "Aquest camp és obligatori.", - remote: "Si us plau, omple aquest camp.", - email: "Si us plau, escriu una adreça de correu-e vàlida", - url: "Si us plau, escriu una URL vàlida.", - date: "Si us plau, escriu una data vàlida.", - dateISO: "Si us plau, escriu una data (ISO) vàlida.", - number: "Si us plau, escriu un número enter vàlid.", - digits: "Si us plau, escriu només dígits.", - creditcard: "Si us plau, escriu un número de tarjeta vàlid.", - equalTo: "Si us plau, escriu el maateix valor de nou.", - accept: "Si us plau, escriu un valor amb una extensió acceptada.", - maxlength: jQuery.validator.format("Si us plau, no escriguis més de {0} caracters."), - minlength: jQuery.validator.format("Si us plau, no escriguis menys de {0} caracters."), - rangelength: jQuery.validator.format("Si us plau, escriu un valor entre {0} i {1} caracters."), - range: jQuery.validator.format("Si us plau, escriu un valor entre {0} i {1}."), - max: jQuery.validator.format("Si us plau, escriu un valor menor o igual a {0}."), - min: jQuery.validator.format("Si us plau, escriu un valor major o igual a {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_cn.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_cn.js deleted file mode 100644 index 87434605e7d..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_cn.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: CN - */ -jQuery.extend(jQuery.validator.messages, { - required: "必选字段", - remote: "请修正该字段", - email: "请输入正确格式的电子邮件", - url: "请输入合法的网址", - date: "请输入合法的日期", - dateISO: "请输入合法的日期 (ISO).", - number: "请输入合法的数字", - digits: "只能输入整数", - creditcard: "请输入合法的信用卡号", - equalTo: "请再次输入相同的值", - accept: "请输入拥有合法后缀名的字符串", - maxlength: jQuery.validator.format("请输入一个长度最多是 {0} 的字符串"), - minlength: jQuery.validator.format("请输入一个长度最少是 {0} 的字符串"), - rangelength: jQuery.validator.format("请输入一个长度介于 {0} 和 {1} 之间的字符串"), - range: jQuery.validator.format("请输入一个介于 {0} 和 {1} 之间的值"), - max: jQuery.validator.format("请输入一个最大为 {0} 的值"), - min: jQuery.validator.format("请输入一个最小为 {0} 的值") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_cs.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_cs.js deleted file mode 100644 index 4d9f5a8061a..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_cs.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: CS - */ -jQuery.extend(jQuery.validator.messages, { - required: "Tento údaj je povinný.", - remote: "Prosím, opravte tento údaj.", - email: "Prosím, zadejte platný e-mail.", - url: "Prosím, zadejte platné URL.", - date: "Prosím, zadejte platné datum.", - dateISO: "Prosím, zadejte platné datum (ISO).", - number: "Prosím, zadejte číslo.", - digits: "Prosím, zadávejte pouze číslice.", - creditcard: "Prosím, zadejte číslo kreditní karty.", - equalTo: "Prosím, zadejte znovu stejnou hodnotu.", - accept: "Prosím, zadejte soubor se správnou příponou.", - maxlength: jQuery.validator.format("Prosím, zadejte nejvíce {0} znaků."), - minlength: jQuery.validator.format("Prosím, zadejte nejméně {0} znaků."), - rangelength: jQuery.validator.format("Prosím, zadejte od {0} do {1} znaků."), - range: jQuery.validator.format("Prosím, zadejte hodnotu od {0} do {1}."), - max: jQuery.validator.format("Prosím, zadejte hodnotu menší nebo rovnu {0}."), - min: jQuery.validator.format("Prosím, zadejte hodnotu větší nebo rovnu {0}.") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_da.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_da.js deleted file mode 100644 index b93eba6f4bc..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_da.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: DA - */ -jQuery.extend(jQuery.validator.messages, { - required: "Dette felt er påkrævet.", - maxlength: jQuery.validator.format("Indtast højst {0} tegn."), - minlength: jQuery.validator.format("Indtast mindst {0} tegn."), - rangelength: jQuery.validator.format("Indtast mindst {0} og højst {1} tegn."), - email: "Indtast en gyldig email-adresse.", - url: "Indtast en gyldig URL.", - date: "Indtast en gyldig dato.", - number: "Indtast et tal.", - digits: "Indtast kun cifre.", - equalTo: "Indtast den samme værdi igen.", - range: jQuery.validator.format("Angiv en værdi mellem {0} og {1}."), - max: jQuery.validator.format("Angiv en værdi der højst er {0}."), - min: jQuery.validator.format("Angiv en værdi der mindst er {0}."), - creditcard: "Indtast et gyldigt kreditkortnummer." -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_de.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_de.js deleted file mode 100644 index 8726eadbfbe..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_de.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Language: DE - * Skipped date/dateISO/number. - */ -jQuery.extend(jQuery.validator.messages, { - required: "Dieses Feld ist ein Pflichtfeld.", - maxlength: jQuery.validator.format("Geben Sie bitte maximal {0} Zeichen ein."), - minlength: jQuery.validator.format("Geben Sie bitte mindestens {0} Zeichen ein."), - rangelength: jQuery.validator.format("Geben Sie bitte mindestens {0} und maximal {1} Zeichen ein."), - email: "Geben Sie bitte eine gültige E-Mail Adresse ein.", - url: "Geben Sie bitte eine gültige URL ein.", - dateDE: "Bitte geben Sie ein gültiges Datum ein.", - numberDE: "Geben Sie bitte eine Nummer ein.", - digits: "Geben Sie bitte nur Ziffern ein.", - equalTo: "Bitte denselben Wert wiederholen.", - range: jQuery.validator.format("Geben Sie bitten einen Wert zwischen {0} und {1}."), - max: jQuery.validator.format("Geben Sie bitte einen Wert kleiner oder gleich {0} ein."), - min: jQuery.validator.format("Geben Sie bitte einen Wert größer oder gleich {0} ein."), - creditcard: "Geben Sie bitte ein gültige Kreditkarten-Nummer ein." -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_el.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_el.js deleted file mode 100644 index de24ced3632..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_el.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: EL - */ -jQuery.extend(jQuery.validator.messages, { - required: "Αυτό το πεδίο είναι υποχρεωτικό.", - remote: "Παρακαλώ διορθώστε αυτό το πεδίο.", - email: "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email.", - url: "Παρακαλώ εισάγετε ένα έγκυρο URL.", - date: "Παρακαλώ εισάγετε μια έγκυρη ημερομηνία.", - dateISO: "Παρακαλώ εισάγετε μια έγκυρη ημερομηνία (ISO).", - number: "Παρακαλώ εισάγετε έναν έγκυρο αριθμό.", - digits: "Παρακαλώ εισάγετε μόνο αριθμητικά ψηφία.", - creditcard: "Παρακαλώ εισάγετε έναν έγκυρο αριθμό πιστωτικής κάρτας.", - equalTo: "Παρακαλώ εισάγετε την ίδια τιμή ξανά.", - accept: "Παρακαλώ εισάγετε μια τιμή με έγκυρη επέκταση αρχείου.", - maxlength: $.validator.format("Παρακαλώ εισάγετε μέχρι και {0} χαρακτήρες."), - minlength: $.validator.format("Παρακαλώ εισάγετε τουλάχιστον {0} χαρακτήρες."), - rangelength: $.validator.format("Παρακαλώ εισάγετε μια τιμή με μήκος μεταξύ {0} και {1} χαρακτήρων."), - range: $.validator.format("Παρακαλώ εισάγετε μια τιμή μεταξύ {0} και {1}."), - max: $.validator.format("Παρακαλώ εισάγετε μια τιμή μικρότερη ή ίση του {0}."), - min: $.validator.format("Παρακαλώ εισάγετε μια τιμή μεγαλύτερη ή ίση του {0}.") -}); - diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_es.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_es.js deleted file mode 100644 index b9a24145daf..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_es.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: ES - */ -jQuery.extend(jQuery.validator.messages, { - required: "Este campo es obligatorio.", - remote: "Por favor, rellena este campo.", - email: "Por favor, escribe una dirección de correo válida", - url: "Por favor, escribe una URL válida.", - date: "Por favor, escribe una fecha válida.", - dateISO: "Por favor, escribe una fecha (ISO) válida.", - number: "Por favor, escribe un número entero válido.", - digits: "Por favor, escribe sólo dígitos.", - creditcard: "Por favor, escribe un número de tarjeta válido.", - equalTo: "Por favor, escribe el mismo valor de nuevo.", - accept: "Por favor, escribe un valor con una extensión aceptada.", - maxlength: jQuery.validator.format("Por favor, no escribas más de {0} caracteres."), - minlength: jQuery.validator.format("Por favor, no escribas menos de {0} caracteres."), - rangelength: jQuery.validator.format("Por favor, escribe un valor entre {0} y {1} caracteres."), - range: jQuery.validator.format("Por favor, escribe un valor entre {0} y {1}."), - max: jQuery.validator.format("Por favor, escribe un valor menor o igual a {0}."), - min: jQuery.validator.format("Por favor, escribe un valor mayor o igual a {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_fa.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_fa.js deleted file mode 100644 index db0a6e2048a..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_fa.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: FA - */ -jQuery.extend(jQuery.validator.messages, { - required: "تکمیل این فیلد اجباری است.", - remote: "لطفا این فیلد را تصحیح کنید.", - email: ".لطفا یک ایمیل صحیح وارد کنید", - url: "لطفا آدرس صحیح وارد کنید.", - date: "لطفا یک تاریخ صحیح وارد کنید", - dateISO: "لطفا تاریخ صحیح وارد کنید (ISO).", - number: "لطفا عدد صحیح وارد کنید.", - digits: "لطفا تنها رقم وارد کنید", - creditcard: "لطفا کریدیت کارت صحیح وارد کنید.", - equalTo: "لطفا مقدار برابری وارد کنید", - accept: "لطفا مقداری وارد کنید که ", - maxlength: jQuery.validator.format("لطفا بیشتر از {0} حرف وارد نکنید."), - minlength: jQuery.validator.format("لطفا کمتر از {0} حرف وارد نکنید."), - rangelength: jQuery.validator.format("لطفا مقداری بین {0} تا {1} حرف وارد کنید."), - range: jQuery.validator.format("لطفا مقداری بین {0} تا {1} حرف وارد کنید."), - max: jQuery.validator.format("لطفا مقداری کمتر از {0} حرف وارد کنید."), - min: jQuery.validator.format("لطفا مقداری بیشتر از {0} حرف وارد کنید.") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_fi.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_fi.js deleted file mode 100644 index e25f2bfba1b..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_fi.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: FI - */ -jQuery.extend(jQuery.validator.messages, { - required: "Tämä kenttä on pakollinen.", - maxlength: jQuery.validator.format("Voit syöttää enintään {0} merkkiä."), - minlength: jQuery.validator.format("Vähintään {0} merkkiä."), - rangelength: jQuery.validator.format("Syötä vähintään {0} ja enintään {1} merkkiä."), - email: "Syö:tä oikea sähköpostiosoite.", - url: "Syötä oikea URL osoite.", - date: "Syötä oike päivämäärä.", - dateISO: "Syötä oike päivämäärä (VVVV-MM-DD).", - number: "Syötä numero.", - digits: "Syötä pelkästään numeroita.", - equalTo: "Syötä sama arvo uudestaan.", - range: jQuery.validator.format("Syötä arvo {0} ja {1} väliltä."), - max: jQuery.validator.format("Syötä arvo joka on yhtä suuri tai suurempi kuin {0}."), - min: jQuery.validator.format("Syötä arvo joka on pienempi tai yhtä suuri kuin {0}."), - creditcard: "Syötä voimassa oleva luottokorttinumero." -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_fr.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_fr.js deleted file mode 100644 index e79667f41dc..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_fr.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: FR - */ -jQuery.extend(jQuery.validator.messages, { - required: "Ce champ est requis.", - remote: "Veuillez remplir ce champ pour continuer.", - email: "Veuillez entrer une adresse email valide.", - url: "Veuillez entrer une URL valide.", - date: "Veuillez entrer une date valide.", - dateISO: "Veuillez entrer une date valide (ISO).", - number: "Veuillez entrer un nombre valide.", - digits: "Veuillez entrer (seulement) une valeur numérique.", - creditcard: "Veuillez entrer un numéro de carte de crédit valide.", - equalTo: "Veuillez entrer une nouvelle fois la même valeur.", - accept: "Veuillez entrer une valeur avec une extension valide.", - maxlength: jQuery.validator.format("Veuillez ne pas entrer plus de {0} caractères."), - minlength: jQuery.validator.format("Veuillez entrer au moins {0} caractères."), - rangelength: jQuery.validator.format("Veuillez entrer entre {0} et {1} caractères."), - range: jQuery.validator.format("Veuillez entrer une valeur entre {0} et {1}."), - max: jQuery.validator.format("Veuillez entrer une valeur inférieure ou égale à {0}."), - min: jQuery.validator.format("Veuillez entrer une valeur supérieure ou égale à {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_he.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_he.js deleted file mode 100644 index 8e6e1fb1f9a..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_he.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: HE - */ -jQuery.extend(jQuery.validator.messages, { - required: ".השדה הזה הינו שדה חובה", - remote: "נא לתקן שדה זה.", - email: "נא למלא כתובת דוא\"ל חוקית", - url: "נא למלא כתובת אינטרנט חוקית.", - date: "נא למלא תאריך חוקי", - dateISO: "נא למלא תאריך חוקי (ISO).", - number: "נא למלא מספר.", - digits: ".נא למלא רק מספרים", - creditcard: "נא למלא מספר כרטיס אשראי חוקי.", - equalTo: "נא למלא את אותו ערך שוב.", - accept: "נא למלא ערך עם סיומת חוקית.", - maxlength: jQuery.validator.format(".נא לא למלא יותר מ- {0} תווים"), - minlength: jQuery.validator.format("נא למלא לפחות {0} תווים."), - rangelength: jQuery.validator.format("נא למלא ערך בין {0} ל- {1} תווים."), - range: jQuery.validator.format("נא למלא ערך בין {0} ל- {1}."), - max: jQuery.validator.format("נא למלא ערך קטן או שווה ל- {0}."), - min: jQuery.validator.format("נא למלא ערך גדול או שווה ל- {0}.") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_hu.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_hu.js deleted file mode 100644 index b5e41af2d53..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_hu.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Language: HU - * Skipped dateISO/DE, numberDE - */ -jQuery.extend(jQuery.validator.messages, { - required: "Kötelező megadni.", - maxlength: jQuery.validator.format("Legfeljebb {0} karakter hosszú legyen."), - minlength: jQuery.validator.format("Legalább {0} karakter hosszú legyen."), - rangelength: jQuery.validator.format("Legalább {0} és legfeljebb {1} karakter hosszú legyen."), - email: "Érvényes e-mail címnek kell lennie.", - url: "Érvényes URL-nek kell lennie.", - date: "Dátumnak kell lennie.", - number: "Számnak kell lennie.", - digits: "Csak számjegyek lehetnek.", - equalTo: "Meg kell egyeznie a két értéknek.", - range: jQuery.validator.format("{0} és {1} közé kell esnie."), - max: jQuery.validator.format("Nem lehet nagyobb, mint {0}."), - min: jQuery.validator.format("Nem lehet kisebb, mint {0}."), - creditcard: "Érvényes hitelkártyaszámnak kell lennie." -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_it.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_it.js deleted file mode 100644 index a1a7998474d..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_it.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: IT - */ -jQuery.extend(jQuery.validator.messages, { - required: "Campo obbligatorio.", - remote: "Controlla questo campo.", - email: "Inserisci un indirizzo email valido.", - url: "Inserisci un indirizzo web valido.", - date: "Inserisci una data valida.", - dateISO: "Inserisci una data valida (ISO).", - number: "Inserisci un numero valido.", - digits: "Inserisci solo numeri.", - creditcard: "Inserisci un numero di carta di credito valido.", - equalTo: "Il valore non corrisponde.", - accept: "Inserisci un valore con un'estensione valida.", - maxlength: jQuery.validator.format("Non inserire più di {0} caratteri."), - minlength: jQuery.validator.format("Inserisci almeno {0} caratteri."), - rangelength: jQuery.validator.format("Inserisci un valore compreso tra {0} e {1} caratteri."), - range: jQuery.validator.format("Inserisci un valore compreso tra {0} e {1}."), - max: jQuery.validator.format("Inserisci un valore minore o uguale a {0}."), - min: jQuery.validator.format("Inserisci un valore maggiore o uguale a {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ja.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ja.js deleted file mode 100644 index f5b564e4b2b..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ja.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: JA (Japanese) - */ -jQuery.extend(jQuery.validator.messages, { - required: "このフィールドは必須です。", - remote: "このフィールドを修正してください。", - email: "有効なEメールアドレスを入力してください。", - url: "有効なURLを入力してください。", - date: "有効な日付を入力してください。", - dateISO: "有効な日付(ISO)を入力してください。", - number: "有効な数字を入力してください。", - digits: "数字のみを入力してください。", - creditcard: "有効なクレジットカード番号を入力してください。", - equalTo: "同じ値をもう一度入力してください。", - accept: "有効な拡張子を含む値を入力してください。", - maxlength: jQuery.format("{0} 文字以内で入力してください。"), - minlength: jQuery.format("{0} 文字以上で入力してください。"), - rangelength: jQuery.format("{0} 文字から {1} 文字までの値を入力してください。"), - range: jQuery.format("{0} から {1} までの値を入力してください。"), - max: jQuery.format("{0} 以下の値を入力してください。"), - min: jQuery.format("{1} 以上の値を入力してください。") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_kk.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_kk.js deleted file mode 100644 index a0a305e1a86..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_kk.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: KK - */ -jQuery.extend(jQuery.validator.messages, { - required: "Бұл өрісті міндетті түрде толтырыңыз.", - remote: "Дұрыс мағына енгізуіңізді сұраймыз.", - email: "Нақты электронды поштаңызды енгізуіңізді сұраймыз.", - url: "Нақты URL-ды енгізуіңізді сұраймыз.", - date: "Нақты URL-ды енгізуіңізді сұраймыз.", - dateISO: "Нақты ISO форматымен сәйкес датасын енгізуіңізді сұраймыз.", - number: "Күнді енгізуіңізді сұраймыз.", - digits: "Тек қана сандарды енгізуіңізді сұраймыз.", - creditcard: "Несие картасының нөмірін дұрыс енгізуіңізді сұраймыз.", - equalTo: "Осы мәнді қайта енгізуіңізді сұраймыз.", - accept: "Файлдың кеңейтуін дұрыс таңдаңыз.", - maxlength: jQuery.format("Ұзындығы {0} символдан көр болмасын."), - minlength: jQuery.format("Ұзындығы {0} символдан аз болмасын."), - rangelength: jQuery.format("Ұзындығы {0}-{1} дейін мән енгізуіңізді сұраймыз."), - range: jQuery.format("Пожалуйста, введите число от {0} до {1}. - {0} - {1} санын енгізуіңізді сұраймыз."), - max: jQuery.format("{0} аз немесе тең санын енгізуіңіді сұраймыз."), - min: jQuery.format("{0} көп немесе тең санын енгізуіңізді сұраймыз.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_lt.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_lt.js deleted file mode 100644 index d9572437e59..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_lt.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin in lithuanian. - * Locale: LT - */ -jQuery.extend(jQuery.validator.messages, { - required: "Šis laukas yra privalomas.", - remote: "Prašau pataisyti šį lauką.", - email: "Prašau įvesti teisingą elektroninio pašto adresą.", - url: "Prašau įvesti teisingą URL.", - date: "Prašau įvesti teisingą datą.", - dateISO: "Prašau įvesti teisingą datą (ISO).", - number: "Prašau įvesti teisingą skaičių.", - digits: "Prašau naudoti tik skaitmenis.", - creditcard: "Prašau įvesti teisingą kreditinės kortelės numerį.", - equalTo: "Prašau įvestį tą pačią reikšmę dar kartą.", - accept: "Prašau įvesti reikšmę su teisingu plėtiniu.", - maxlength: $.format("Prašau įvesti ne daugiau kaip {0} simbolių."), - minlength: $.format("Prašau įvesti bent {0} simbolius."), - rangelength: $.format("Prašau įvesti reikšmes, kurių ilgis nuo {0} iki {1} simbolių."), - range: $.format("Prašau įvesti reikšmę intervale nuo {0} iki {1}."), - max: $.format("Prašau įvesti reikšmę mažesnę arba lygią {0}."), - min: $.format("Prašau įvesti reikšmę didesnę arba lygią {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_lv.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_lv.js deleted file mode 100644 index 9dec157a49f..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_lv.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: LV - */ -jQuery.extend(jQuery.validator.messages, { - required: "Šis lauks ir obligāts.", - remote: "Lūdzu, pārbaudiet šo lauku.", - email: "Lūdzu, ievadiet derīgu e-pasta adresi.", - url: "Lūdzu, ievadiet derīgu URL adresi.", - date: "Lūdzu, ievadiet derīgu datumu.", - dateISO: "Lūdzu, ievadiet derīgu datumu (ISO).", - number: "Lūdzu, ievadiet derīgu numuru.", - digits: "Lūdzu, ievadiet tikai ciparus.", - creditcard: "Lūdzu, ievadiet derīgu kredītkartes numuru.", - equalTo: "Lūdzu, ievadiet to pašu vēlreiz.", - accept: "Lūdzu, ievadiet vērtību ar derīgu paplašinājumu.", - maxlength: jQuery.validator.format("Lūdzu, ievadiet ne vairāk kā {0} rakstzīmes."), - minlength: jQuery.validator.format("Lūdzu, ievadiet vismaz {0} rakstzīmes."), - rangelength: jQuery.validator.format("Lūdzu ievadiet {0} līdz {1} rakstzīmes."), - range: jQuery.validator.format("Lūdzu, ievadiet skaitli no {0} līdz {1}."), - max: jQuery.validator.format("Lūdzu, ievadiet skaitli, kurš ir mazāks vai vienāds ar {0}."), - min: jQuery.validator.format("Lūdzu, ievadiet skaitli, kurš ir lielāks vai vienāds ar {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_nl.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_nl.js deleted file mode 100644 index 450e7411e9b..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_nl.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: NL - */ -jQuery.extend(jQuery.validator.messages, { - required: "Dit is een verplicht veld.", - remote: "Controleer dit veld.", - email: "Vul hier een geldig e-mailadres in.", - url: "Vul hier een geldige URL in.", - date: "Vul hier een geldige datum in.", - dateISO: "Vul hier een geldige datum in (ISO-formaat).", - number: "Vul hier een geldig getal in.", - digits: "Vul hier alleen getallen in.", - creditcard: "Vul hier een geldig creditcardnummer in.", - equalTo: "Vul hier dezelfde waarde in.", - accept: "Vul hier een waarde in met een geldige extensie.", - maxlength: jQuery.validator.format("Vul hier maximaal {0} tekens in."), - minlength: jQuery.validator.format("Vul hier minimaal {0} tekens in."), - rangelength: jQuery.validator.format("Vul hier een waarde in van minimaal {0} en maximaal {1} tekens."), - range: jQuery.validator.format("Vul hier een waarde in van minimaal {0} en maximaal {1}."), - max: jQuery.validator.format("Vul hier een waarde in kleiner dan of gelijk aan {0}."), - min: jQuery.validator.format("Vul hier een waarde in groter dan of gelijk aan {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_no.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_no.js deleted file mode 100644 index eae1a214a97..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_no.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: NO (Norwegian) - */ -jQuery.extend(jQuery.validator.messages, { - required: "Dette feltet er obligatorisk.", - maxlength: jQuery.validator.format("Maksimalt {0} tegn."), - minlength: jQuery.validator.format("Minimum {0} tegn."), - rangelength: jQuery.validator.format("Angi minimum {0} og maksimum {1} tegn."), - email: "Oppgi en gyldig epostadresse.", - url: "Angi en gyldig URL.", - date: "Angi en gyldig dato.", - dateISO: "Angi en gyldig dato (&ARING;&ARING;&ARING;&ARING;-MM-DD).", - dateSE: "Angi en gyldig dato.", - number: "Angi et gyldig nummer.", - numberSE: "Angi et gyldig nummer.", - digits: "Skriv kun tall.", - equalTo: "Skriv samme verdi igjen.", - range: jQuery.validator.format("Angi en verdi mellom {0} og {1}."), - max: jQuery.validator.format("Angi en verdi som er større eller lik {0}."), - min: jQuery.validator.format("Angi en verdi som er mindre eller lik {0}."), - creditcard: "Angi et gyldig kredittkortnummer." -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_pl.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_pl.js deleted file mode 100644 index 90b7ac94f6f..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_pl.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Language: PL - */ -jQuery.extend(jQuery.validator.messages, { - required: "To pole jest wymagane.", - remote: "Proszę o wypełnienie tego pola.", - email: "Proszę o podanie prawidłowego adresu email.", - url: "Proszę o podanie prawidłowego URL.", - date: "Proszę o podanie prawidłowej daty.", - dateISO: "Proszę o podanie prawidłowej daty (ISO).", - number: "Proszę o podanie prawidłowej liczby.", - digits: "Proszę o podanie samych cyfr.", - creditcard: "Proszę o podanie prawidłowej karty kredytowej.", - equalTo: "Proszę o podanie tej samej wartości ponownie.", - accept: "Proszę o podanie wartości z prawidłowym rozszerzeniem.", - maxlength: jQuery.validator.format("Proszę o podanie nie więcej niż {0} znaków."), - minlength: jQuery.validator.format("Proszę o podanie przynajmniej {0} znaków."), - rangelength: jQuery.validator.format("Proszę o podanie wartości o długości od {0} do {1} znaków."), - range: jQuery.validator.format("Proszę o podanie wartości z przedziału od {0} do {1}."), - max: jQuery.validator.format("Proszę o podanie wartości mniejszej bądź równej {0}."), - min: jQuery.validator.format("Proszę o podanie wartości większej bądź równej {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ptbr.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ptbr.js deleted file mode 100644 index 0d44ce30e28..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ptbr.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Translated default messages for the jQuery validation plugin. - * Language: PT_BR - * Translator: Francisco Ernesto Teixeira - */ -jQuery.extend(jQuery.validator.messages, { - required: "Este campo é requerido.", - remote: "Por favor, corrija este campo.", - email: "Por favor, forneça um endereço eletrônico válido.", - url: "Por favor, forneça uma URL válida.", - date: "Por favor, forneça uma data válida.", - dateISO: "Por favor, forneça uma data válida (ISO).", - dateDE: "Bitte geben Sie ein gültiges Datum ein.", - number: "Por favor, forneça um número válida.", - numberDE: "Bitte geben Sie eine Nummer ein.", - digits: "Por favor, forneça somente dígitos.", - creditcard: "Por favor, forneça um cartão de crédito válido.", - equalTo: "Por favor, forneça o mesmo valor novamente.", - accept: "Por favor, forneça um valor com uma extensão válida.", - maxlength: jQuery.validator.format("Por favor, forneça não mais que {0} caracteres."), - minlength: jQuery.validator.format("Por favor, forneça ao menos {0} caracteres."), - rangelength: jQuery.validator.format("Por favor, forneça um valor entre {0} e {1} caracteres de comprimento."), - range: jQuery.validator.format("Por favor, forneça um valor entre {0} e {1}."), - max: jQuery.validator.format("Por favor, forneça um valor menor ou igual a {0}."), - min: jQuery.validator.format("Por favor, forneça um valor maior ou igual a {0}.") -}); - -jQuery.validator.addMethod("datePTBR", function(value) { - return this.optional(element) || /^\d\d?\/\d\d?\/\d\d\d?\d?$/.test(value); -}, "Por favor, forneça uma data válida."); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ptpt.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ptpt.js deleted file mode 100644 index de84e569c5c..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ptpt.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Translated default messages for the jQuery validation plugin. - * Locale: PT_PT - */ -jQuery.extend(jQuery.validator.messages, { - required: "Campo de preenchimento obrigatório.", - remote: "Por favor, corrija este campo.", - email: "Por favor, introduza um endereço eletrónico válido.", - url: "Por favor, introduza um URL válido.", - date: "Por favor, introduza uma data válida.", - dateISO: "Por favor, introduza uma data válida (ISO).", - number: "Por favor, introduza um número válido.", - digits: "Por favor, introduza apenas dígitos.", - creditcard: "Por favor, introduza um número de cartão de crédito válido.", - equalTo: "Por favor, introduza de novo o mesmo valor.", - accept: "Por favor, introduza um ficheiro com uma extensão válida.", - maxlength: jQuery.validator.format("Por favor, não introduza mais do que {0} caracteres."), - minlength: jQuery.validator.format("Por favor, introduza pelo menos {0} caracteres."), - rangelength: jQuery.validator.format("Por favor, introduza entre {0} e {1} caracteres."), - range: jQuery.validator.format("Por favor, introduza um valor entre {0} e {1}."), - max: jQuery.validator.format("Por favor, introduza um valor menor ou igual a {0}."), - min: jQuery.validator.format("Por favor, introduza um valor maior ou igual a {0}.") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ro.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ro.js deleted file mode 100644 index ae9a67cf900..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ro.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: RO - */ -jQuery.extend(jQuery.validator.messages, { - required: "Acest câmp este obligatoriu.", - remote: "Te rugăm să completezi acest câmp.", - email: "Te rugăm să introduci o adresă de email validă", - url: "Te rugăm sa introduci o adresă URL validă.", - date: "Te rugăm să introduci o dată corectă.", - dateISO: "Te rugăm să introduci o dată (ISO) corectă.", - number: "Te rugăm să introduci un număr întreg valid.", - digits: "Te rugăm să introduci doar cifre.", - creditcard: "Te rugăm să introduci un numar de carte de credit valid.", - equalTo: "Te rugăm să reintroduci valoarea.", - accept: "Te rugăm să introduci o valoare cu o extensie validă.", - maxlength: jQuery.validator.format("Te rugăm să nu introduci mai mult de {0} caractere."), - minlength: jQuery.validator.format("Te rugăm să introduci cel puțin {0} caractere."), - rangelength: jQuery.validator.format("Te rugăm să introduci o valoare între {0} și {1} caractere."), - range: jQuery.validator.format("Te rugăm să introduci o valoare între {0} și {1}."), - max: jQuery.validator.format("Te rugăm să introduci o valoare egal sau mai mică decât {0}."), - min: jQuery.validator.format("Te rugăm să introduci o valoare egal sau mai mare decât {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ru.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ru.js deleted file mode 100644 index a0776029b49..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ru.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: RU - */ -jQuery.extend(jQuery.validator.messages, { - required: "Это поле необходимо заполнить.", - remote: "Пожалуйста, введите правильное значение.", - email: "Пожалуйста, введите корретный адрес электронной почты.", - url: "Пожалуйста, введите корректный URL.", - date: "Пожалуйста, введите корректную дату.", - dateISO: "Пожалуйста, введите корректную дату в формате ISO.", - number: "Пожалуйста, введите число.", - digits: "Пожалуйста, вводите только цифры.", - creditcard: "Пожалуйста, введите правильный номер кредитной карты.", - equalTo: "Пожалуйста, введите такое же значение ещё раз.", - accept: "Пожалуйста, выберите файл с правильным расширением.", - maxlength: jQuery.validator.format("Пожалуйста, введите не больше {0} символов."), - minlength: jQuery.validator.format("Пожалуйста, введите не меньше {0} символов."), - rangelength: jQuery.validator.format("Пожалуйста, введите значение длиной от {0} до {1} символов."), - range: jQuery.validator.format("Пожалуйста, введите число от {0} до {1}."), - max: jQuery.validator.format("Пожалуйста, введите число, меньшее или равное {0}."), - min: jQuery.validator.format("Пожалуйста, введите число, большее или равное {0}.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_si.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_si.js deleted file mode 100644 index 7983ba25d0b..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_si.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: SI (Slovenian) - */ -jQuery.extend(jQuery.validator.messages, { - required: "To polje je obvezno.", - remote: "Vpis v tem polju ni v pravi obliki.", - email: "Prosimo, vnesite pravi email naslov.", - url: "Prosimo, vnesite pravi URL.", - date: "Prosimo, vnesite pravi datum.", - dateISO: "Prosimo, vnesite pravi datum (ISO).", - number: "Prosimo, vnesite pravo številko.", - digits: "Prosimo, vnesite samo številke.", - creditcard: "Prosimo, vnesite pravo številko kreditne kartice.", - equalTo: "Prosimo, ponovno vnesite enako vsebino.", - accept: "Prosimo, vnesite vsebino z pravo končnico.", - maxlength: $.validator.format("Prosimo, da ne vnašate več kot {0} znakov."), - minlength: $.validator.format("Prosimo, vnesite vsaj {0} znakov."), - rangelength: $.validator.format("Prosimo, vnesite od {0} do {1} znakov."), - range: $.validator.format("Prosimo, vnesite vrednost med {0} in {1}."), - max: $.validator.format("Prosimo, vnesite vrednost manjšo ali enako {0}."), - min: $.validator.format("Prosimo, vnesite vrednost večjo ali enako {0}.") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_sk.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_sk.js deleted file mode 100644 index 77100704c34..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_sk.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Language: SK - * Skipped dateISO/DE, numberDE - */ -jQuery.extend(jQuery.validator.messages, { - required: "Povinné zadať.", - maxlength: jQuery.validator.format("Maximálne {0} znakov."), - minlength: jQuery.validator.format("Minimálne {0} znakov."), - rangelength: jQuery.validator.format("Minimálne {0} a Maximálne {0} znakov."), - email: "E-mailová adresa musí byť platná.", - url: "URL musí byť platný.", - date: "Musí byť dátum.", - number: "Musí byť číslo.", - digits: "Môže obsahovať iba číslice.", - equalTo: "Dva hodnoty sa musia rovnať.", - range: jQuery.validator.format("Musí byť medzi {0} a {1}."), - max: jQuery.validator.format("Nemôže byť viac ako{0}."), - min: jQuery.validator.format("Nemôže byť menej ako{0}."), - creditcard: "Číslo platobnej karty musí byť platné." -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_sr.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_sr.js deleted file mode 100644 index beb6520b042..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_sr.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: SR - */ -jQuery.extend(jQuery.validator.messages, { - required: "Поље је обавезно.", - remote: "Средите ово поље.", - email: "Унесите исправну и-мејл адресу", - url: "Унесите исправан URL.", - date: "Унесите исправан датум.", - dateISO: "Унесите исправан датум (ISO).", - number: "Унесите исправан број.", - digits: "Унесите само цифе.", - creditcard: "Унесите исправан број кредитне картице.", - equalTo: "Унесите исту вредност поново.", - accept: "Унесите вредност са одговарајућом екстензијом.", - maxlength: $.validator.format("Унесите мање од {0}карактера."), - minlength: $.validator.format("Унесите барем {0} карактера."), - rangelength: $.validator.format("Унесите вредност дугачку између {0} и {1} карактера."), - range: $.validator.format("Унесите вредност између {0} и {1}."), - max: $.validator.format("Унесите вредност мању или једнаку {0}."), - min: $.validator.format("Унесите вредност већу или једнаку {0}.") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_th.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_th.js deleted file mode 100644 index 1014645e809..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_th.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: TH (Thai) - */ -jQuery.extend(jQuery.validator.messages, { - required: "โปรดระบุ", - remote: "โปรดแก้ไขให้ถูกต้อง", - email: "โปรดระบุที่อยู่อีเมล์ที่ถูกต้อง", - url: "โปรดระบุ URL ที่ถูกต้อง", - date: "โปรดระบุวันที่ ที่ถูกต้อง", - dateISO: "โปรดระบุวันที่ ที่ถูกต้อง (ระบบ ISO).", - number: "โปรดระบุทศนิยมที่ถูกต้อง", - digits: "โปรดระบุจำนวนเต็มที่ถูกต้อง", - creditcard: "โปรดระบุรหัสบัตรเครดิตที่ถูกต้อง", - equalTo: "โปรดระบุค่าเดิมอีกครั้ง", - accept: "โปรดระบุค่าที่มีส่วนขยายที่ถูกต้อง", - maxlength: jQuery.validator.format("โปรดอย่าระบุค่าที่ยาวกว่า {0} อักขระ"), - minlength: jQuery.validator.format("โปรดอย่าระบุค่าที่สั้นกว่า {0} อักขระ"), - rangelength: jQuery.validator.format("โปรดอย่าระบุค่าความยาวระหว่าง {0} ถึง {1} อักขระ"), - range: jQuery.validator.format("โปรดระบุค่าระหว่าง {0} และ {1}"), - max: jQuery.validator.format("โปรดระบุค่าน้อยกว่าหรือเท่ากับ {0}"), - min: jQuery.validator.format("โปรดระบุค่ามากกว่าหรือเท่ากับ {0}") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_tr.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_tr.js deleted file mode 100644 index bd51d7f124a..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_tr.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Language: TR - * Author: kara - */ -jQuery.extend(jQuery.validator.messages, { - required: "Bu alanın doldurulması zorunludur.", - remote: "Lütfen bu alanı düzeltin.", - email: "Lütfen geçerli bir e-posta adresi giriniz.", - url: "Lütfen geçerli bir web adresi (URL) giriniz.", - date: "Lütfen geçerli bir tarih giriniz.", - dateISO: "Lütfen geçerli bir tarih giriniz(ISO formatında)", - number: "Lütfen geçerli bir sayı giriniz.", - digits: "Lütfen sadece sayısal karakterler giriniz.", - creditcard: "Lütfen geçerli bir kredi kartı giriniz.", - equalTo: "Lütfen aynı değeri tekrar giriniz.", - accept: "Lütfen geçerli uzantıya sahip bir değer giriniz.", - maxlength: jQuery.validator.format("Lütfen en fazla {0} karakter uzunluğunda bir değer giriniz."), - minlength: jQuery.validator.format("Lütfen en az {0} karakter uzunluğunda bir değer giriniz."), - rangelength: jQuery.validator.format("Lütfen en az {0} ve en fazla {1} uzunluğunda bir değer giriniz."), - range: jQuery.validator.format("Lütfen {0} ile {1} arasında bir değer giriniz."), - max: jQuery.validator.format("Lütfen {0} değerine eşit ya da daha küçük bir değer giriniz."), - min: jQuery.validator.format("Lütfen {0} değerine eşit ya da daha büyük bir değer giriniz.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ua.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_ua.js deleted file mode 100644 index 49896cbbed2..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ua.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Language: UA (Ukrainian) - * Author: maserg - */ -jQuery.extend(jQuery.validator.messages, { - required: "Це поле необхідно заповнити.", - remote: "Будь ласка, введіть правильне значення.", - email: "Будь ласка, введіть коректну адресу електронної пошти.", - url: "Будь ласка, введіть коректний URL.", - date: "Будь ласка, введіть коректну дату.", - dateISO: "Будь ласка, введіть коректну дату у форматі ISO.", - number: "Будь ласка, введіть число.", - digits: "Вводите потрібно лише цифри.", - creditcard: "Будь ласка, введіть правильний номер кредитної карти.", - equalTo: "Будь ласка, введіть таке ж значення ще раз.", - accept: "Будь ласка, виберіть файл з правильним розширенням.", - maxlength: jQuery.validator.format("Будь ласка, введіть не більше {0} символів."), - minlength: jQuery.validator.format("Будь ласка, введіть не менше {0} символів."), - rangelength: jQuery.validator.format("Будь ласка, введіть значення довжиною від {0} до {1} символів."), - range: jQuery.validator.format("Будь ласка, введіть число від {0} до {1}."), - max: jQuery.validator.format("Будь ласка, введіть число, менше або рівно {0}."), - min: jQuery.validator.format("Будь ласка, введіть число, більше або рівно {0}.") -}); diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_vi.js b/core/vendor/assets/javascripts/jquery.validate/localization/messages_vi.js deleted file mode 100644 index 5c02351af21..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/messages_vi.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Translated default messages for the jQuery validation plugin. - * Locale: VI (Vietnamese) - */ -jQuery.extend(jQuery.validator.messages, { - required: "Hãy nhập.", - remote: "Hãy sửa cho đúng.", - email: "Hãy nhập email.", - url: "Hãy nhập URL.", - date: "Hãy nhập ngày.", - dateISO: "Hãy nhập ngày (ISO).", - number: "Hãy nhập số.", - digits: "Hãy nhập chữ số.", - creditcard: "Hãy nhập số thẻ tín dụng.", - equalTo: "Hãy nhập thêm lần nữa.", - accept: "Phần mở rộng không đúng.", - maxlength: jQuery.format("Hãy nhập từ {0} kí tự trở xuống."), - minlength: jQuery.format("Hãy nhập từ {0} kí tự trở lên."), - rangelength: jQuery.format("Hãy nhập từ {0} đến {1} kí tự."), - range: jQuery.format("Hãy nhập từ {0} đến {1}."), - max: jQuery.format("Hãy nhập từ {0} trở xuống."), - min: jQuery.format("Hãy nhập từ {1} trở lên.") -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/methods_de.js b/core/vendor/assets/javascripts/jquery.validate/localization/methods_de.js deleted file mode 100644 index 3df614906e1..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/methods_de.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Localized default methods for the jQuery validation plugin. - * Locale: DE - */ -jQuery.extend(jQuery.validator.methods, { - date: function(value, element) { - return this.optional(element) || /^\d\d?\.\d\d?\.\d\d\d?\d?$/.test(value); - }, - number: function(value, element) { - return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:\.\d{3})+)(?:,\d+)?$/.test(value); - } -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/methods_nl.js b/core/vendor/assets/javascripts/jquery.validate/localization/methods_nl.js deleted file mode 100644 index c79e15490b7..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/methods_nl.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Localized default methods for the jQuery validation plugin. - * Locale: NL - */ -jQuery.extend(jQuery.validator.methods, { - date: function(value, element) { - return this.optional(element) || /^\d\d?[\.\/-]\d\d?[\.\/-]\d\d\d?\d?$/.test(value); - } -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/methods_pt.js b/core/vendor/assets/javascripts/jquery.validate/localization/methods_pt.js deleted file mode 100644 index 0e34f22fec0..00000000000 --- a/core/vendor/assets/javascripts/jquery.validate/localization/methods_pt.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Localized default methods for the jQuery validation plugin. - * Locale: PT_BR - */ -jQuery.extend(jQuery.validator.methods, { - date: function(value, element) { - return this.optional(element) || /^\d\d?\/\d\d?\/\d\d\d?\d?$/.test(value); - } -}); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jsuri.js b/core/vendor/assets/javascripts/jsuri.js new file mode 100644 index 00000000000..dcf60f97966 --- /dev/null +++ b/core/vendor/assets/javascripts/jsuri.js @@ -0,0 +1,2 @@ +/*! jsUri v1.1.1 | https://github.com/derek-watson/jsUri */ +var Query=function(a){"use strict";var b=function(a){var b=[],c,d,e,f;if(typeof a=="undefined"||a===null||a==="")return b;a.indexOf("?")===0&&(a=a.substring(1)),d=a.toString().split(/[&;]/);for(c=0;c0&&(a+="&"),a+=d.join("=");return a.length>0?"?"+a:a},e=function(a){a=decodeURIComponent(a),a=a.replace("+"," ");return a},f=function(a){var b,d;for(d=0;d0&&c.push([a,b]);return this},j=function(a,b,d){var f=-1,g,j;if(arguments.length===3){for(g=0;g> 1) : parseInt(o.left, 10) + mid) + 'px', - top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + 'px' - }) - } - - el.setAttribute('aria-role', 'progressbar') - self.lines(el, self.opts) - - if (!useCssAnimations) { - // No CSS animation support, use setTimeout() instead - var i = 0 - , fps = o.fps - , f = fps/o.speed - , ostep = (1-o.opacity) / (f*o.trail / 100) - , astep = f/o.lines - - ;(function anim() { - i++; - for (var s=o.lines; s; s--) { - var alpha = Math.max(1-(i+s*astep)%f * ostep, o.opacity) - self.opacity(el, o.lines-s, alpha, o) - } - self.timeout = self.el && setTimeout(anim, ~~(1000/fps)) - })() - } - return self - }, - - stop: function() { - var el = this.el - if (el) { - clearTimeout(this.timeout) - if (el.parentNode) el.parentNode.removeChild(el) - this.el = undefined - } - return this - }, - - lines: function(el, o) { - var i = 0 - , seg - - function fill(color, shadow) { - return css(createEl(), { - position: 'absolute', - width: (o.length+o.width) + 'px', - height: o.width + 'px', - background: color, - boxShadow: shadow, - transformOrigin: 'left', - transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)', - borderRadius: (o.corners * o.width>>1) + 'px' - }) - } - - for (; i < o.lines; i++) { - seg = css(createEl(), { - position: 'absolute', - top: 1+~(o.width/2) + 'px', - transform: o.hwaccel ? 'translate3d(0,0,0)' : '', - opacity: o.opacity, - animation: useCssAnimations && addAnimation(o.opacity, o.trail, i, o.lines) + ' ' + 1/o.speed + 's linear infinite' - }) - - if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'})) - - ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)'))) - } - return el - }, - - opacity: function(el, i, val) { - if (i < el.childNodes.length) el.childNodes[i].style.opacity = val - } - - }) - - ///////////////////////////////////////////////////////////////////////// - // VML rendering for IE - ///////////////////////////////////////////////////////////////////////// - - /** - * Check and init VML support - */ - ;(function() { - - function vml(tag, attr) { - return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr) - } - - var s = css(createEl('group'), {behavior: 'url(#default#VML)'}) - - if (!vendor(s, 'transform') && s.adj) { - - // VML support detected. Insert CSS rule ... - sheet.addRule('.spin-vml', 'behavior:url(#default#VML)') - - Spinner.prototype.lines = function(el, o) { - var r = o.length+o.width - , s = 2*r - - function grp() { - return css( - vml('group', { - coordsize: s + ' ' + s, - coordorigin: -r + ' ' + -r - }), - { width: s, height: s } - ) - } - - var margin = -(o.width+o.length)*2 + 'px' - , g = css(grp(), {position: 'absolute', top: margin, left: margin}) - , i - - function seg(i, dx, filter) { - ins(g, - ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}), - ins(css(vml('roundrect', {arcsize: o.corners}), { - width: r, - height: o.width, - left: o.radius, - top: -o.width>>1, - filter: filter - }), - vml('fill', {color: o.color, opacity: o.opacity}), - vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change - ) - ) - ) - } - - if (o.shadow) - for (i = 1; i <= o.lines; i++) - seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)') - - for (i = 1; i <= o.lines; i++) seg(i) - return ins(el, g) - } - - Spinner.prototype.opacity = function(el, i, val, o) { - var c = el.firstChild - o = o.shadow && o.lines || 0 - if (c && i+o < c.childNodes.length) { - c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild - if (c) c.opacity = val - } - } - } - else - useCssAnimations = vendor(s, 'animation') - })() - - if (typeof define == 'function' && define.amd) - define(function() { return Spinner }) - else - window.Spinner = Spinner - -}(window, document) diff --git a/core/vendor/assets/javascripts/trunk8.js b/core/vendor/assets/javascripts/trunk8.js deleted file mode 100644 index 4efeeca8b57..00000000000 --- a/core/vendor/assets/javascripts/trunk8.js +++ /dev/null @@ -1,233 +0,0 @@ -/**! - * trunk8 v1.2.3 - * https://github.com/rviscomi/trunk8 - * - * Copyright 2012 Rick Viscomi - * Released under the MIT License. - * - * Date: September 9, 2012 - */ - -(function ($) { - var methods, - utils, - SIDES = { - /* cen...ter */ - center: 'center', - /* ...left */ - left: 'left', - /* right... */ - right: 'right' - }, - WIDTH = { - auto: 'auto' - }, - settings = { - fill: '…', - lines: 1, - side: SIDES.right, - tooltip: true, - width: WIDTH.auto - }; - - function truncate() { - var width = settings.width, - side = settings.side, - fill = settings.fill, - line_height = utils.getLineHeight(this) * settings.lines, - str = this.data('trunk8') || this.text(), - length = str.length, - max_bite = '', - lower, upper, - bite_size, - bite; - - /* Reset the field to the original string. */ - this.html(str).data('trunk8', str); - - if (width === WIDTH.auto) { - /* Assuming there is no "overflow: hidden". */ - if (this.height() <= line_height) { - /* Text is already at the optimal trunkage. */ - return; - } - - /* Binary search technique for finding the optimal trunkage. */ - /* Find the maximum bite without overflowing. */ - lower = 0; - upper = length - 1; - - while (lower <= upper) { - bite_size = lower + ((upper - lower) >> 1); - - bite = utils.eatStr(str, side, length - bite_size, fill); - - this.html(bite); - - /* Check for overflow. */ - if (this.height() > line_height) { - upper = bite_size - 1; - } - else { - lower = bite_size + 1; - - /* Save the bigger bite. */ - max_bite = (max_bite.length > bite.length) ? max_bite : bite; - } - } - - /* Reset the content to eliminate possible existing scroll bars. */ - this.html(''); - - /* Display the biggest bite. */ - this.html(max_bite); - - if (settings.tooltip) { - this.attr('title', str); - } - } - else if (!isNaN(width)) { - bite_size = length - width; - - bite = utils.eatStr(str, side, bite_size, fill); - - this.html(bite); - - if (settings.tooltip) { - this.attr('title', str); - } - } - else { - $.error('Invalid width "' + width + '".'); - } - } - - methods = { - init: function (options) { - settings = $.extend(settings, options); - - return this.each(function () { - truncate.call($(this)); - }); - }, - - /** Updates the text value of the elements while maintaining truncation. */ - update: function (new_string) { - return this.each(function () { - /* Update text. */ - if (new_string) { - $(this).data('trunk8', new_string); - } - - /* Truncate accordingly. */ - truncate.call($(this)); - }); - }, - - revert: function () { - return this.each(function () { - /* Get original text. */ - var text = $(this).data('trunk8'); - - /* Revert element to original text. */ - $(this).html(text); - }); - }, - - /** Returns this instance's settings object. NOT CHAINABLE. */ - getSettings: function () { - return settings; - } - }; - - utils = { - /** Replaces [bite_size] [side]-most chars in [str] with [fill]. */ - eatStr: function (str, side, bite_size, fill) { - var length = str.length, - key = utils.eatStr.generateKey.apply(null, arguments), - half_length, - half_bite_size; - - /* If the result is already in the cache, return it. */ - if (utils.eatStr.cache[key]) { - return utils.eatStr.cache[key]; - } - - /* Common error handling. */ - if ((typeof str !== 'string') || (length === 0)) { - $.error('Invalid source string "' + str + '".'); - } - if ((bite_size < 0) || (bite_size > length)) { - $.error('Invalid bite size "' + bite_size + '".'); - } - else if (bite_size === 0) { - /* No bite should show no truncation. */ - return str; - } - if (typeof (fill + '') !== 'string') { - $.error('Fill unable to be converted to a string.'); - } - - /* Compute the result, store it in the cache, and return it. */ - switch (side) { - case SIDES.right: - /* str... */ - return utils.eatStr.cache[key] = - $.trim(str.substr(0, length - bite_size)) + fill; - - case SIDES.left: - /* ...str */ - return utils.eatStr.cache[key] = - fill + $.trim(str.substr(bite_size)); - - case SIDES.center: - /* Bit-shift to the right by one === Math.floor(x / 2) */ - half_length = length >> 1; // halve the length - half_bite_size = bite_size >> 1; // halve the bite_size - - /* st...r */ - return utils.eatStr.cache[key] = - $.trim(utils.eatStr(str.substr(0, length - half_length), SIDES.right, bite_size - half_bite_size, '')) + - fill + - $.trim(utils.eatStr(str.substr(length - half_length), SIDES.left, half_bite_size, '')); - - default: - $.error('Invalid side "' + side + '".'); - } - }, - - getLineHeight: function (elem) { - var html = $(elem).html(), - wrapper_id = 'line-height-test', - line_height; - - /* Set the content to a small single character and wrap. */ - $(elem).html('i').wrap('
        '); - - /* Calculate the line height by measuring the wrapper.*/ - line_height = $('#' + wrapper_id).innerHeight(); - - /* Remove the wrapper and reset the content. */ - $(elem).html(html).unwrap(); - - return line_height; - } - }; - - utils.eatStr.cache = {}; - utils.eatStr.generateKey = function () { - return Array.prototype.join.call(arguments, ''); - }; - - $.fn.trunk8 = function (method) { - if (methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } - else if (typeof method === 'object' || !method) { - return methods.init.apply(this, arguments); - } - else { - $.error('Method ' + method + ' does not exist on jQuery.trunk8'); - } - }; -})(jQuery); \ No newline at end of file diff --git a/core/vendor/assets/stylesheets/font-awesome-ie7.css b/core/vendor/assets/stylesheets/font-awesome-ie7.css deleted file mode 100755 index c1dc3ac6b38..00000000000 --- a/core/vendor/assets/stylesheets/font-awesome-ie7.css +++ /dev/null @@ -1,645 +0,0 @@ -[class^="icon-"], -[class*=" icon-"] { - font-family: FontAwesome; - font-style: normal; - font-weight: normal; -} -.btn.dropdown-toggle [class^="icon-"], -.btn.dropdown-toggle [class*=" icon-"] { - /* keeps button heights with and without icons the same */ - - line-height: 1.4em; -} -.icon-large { - font-size: 1.3333em; -} -.icon-glass { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-music { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-search { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-envelope { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-heart { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-star { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-star-empty { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-user { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-film { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-th-large { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-th { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-th-list { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-ok { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-remove { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-zoom-in { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-zoom-out { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-off { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-signal { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-cog { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-trash { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-home { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-file { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-time { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-road { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-download-alt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-download { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-upload { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-inbox { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-play-circle { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-repeat { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-refresh { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-list-alt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-lock { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-flag { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-headphones { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-volume-off { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-volume-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-volume-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-qrcode { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-barcode { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-tag { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-tags { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-book { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-bookmark { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-print { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-camera { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-font { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-bold { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-italic { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-text-height { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-text-width { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-align-left { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-align-center { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-align-right { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-align-justify { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-list { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-indent-left { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-indent-right { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-facetime-video { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-picture { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-pencil { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-map-marker { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-adjust { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-tint { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-edit { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-share { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-check { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-move { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-step-backward { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-fast-backward { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-backward { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-play { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-pause { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-stop { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-forward { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-fast-forward { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-step-forward { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-eject { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-chevron-left { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-chevron-right { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-plus-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-minus-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-remove-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-ok-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-question-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-info-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-screenshot { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-remove-circle { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-ok-circle { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-ban-circle { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-arrow-left { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-arrow-right { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-arrow-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-arrow-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-share-alt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-resize-full { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-resize-small { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-plus { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-minus { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-asterisk { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-exclamation-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-gift { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-leaf { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-fire { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-eye-open { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-eye-close { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-warning-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-plane { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-calendar { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-random { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-comment { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-magnet { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-chevron-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-chevron-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-retweet { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-shopping-cart { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-folder-close { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-folder-open { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-resize-vertical { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-resize-horizontal { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-bar-chart { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-twitter-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-facebook-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-camera-retro { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-key { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-cogs { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-comments { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-thumbs-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-thumbs-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-star-half { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-heart-empty { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-signout { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-linkedin-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-pushpin { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-external-link { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-signin { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-trophy { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-github-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-upload-alt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-lemon { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-phone { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-check-empty { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-bookmark-empty { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-phone-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-twitter { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-facebook { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-github { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-unlock { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-credit-card { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-rss { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-hdd { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-bullhorn { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-bell { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-certificate { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-hand-right { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-hand-left { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-hand-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-hand-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-circle-arrow-left { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-circle-arrow-right { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-circle-arrow-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-circle-arrow-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-globe { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-wrench { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-tasks { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-filter { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-briefcase { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-fullscreen { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-group { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-link { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-cloud { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-beaker { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-cut { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-copy { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-paper-clip { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-save { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-sign-blank { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-reorder { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-list-ul { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-list-ol { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-strikethrough { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-underline { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-table { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-magic { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-truck { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-pinterest { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-pinterest-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-google-plus-sign { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-google-plus { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-money { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-caret-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-caret-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-caret-left { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-caret-right { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-columns { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-sort { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-sort-down { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-sort-up { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-envelope-alt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-linkedin { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-undo { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-legal { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-dashboard { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-comment-alt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-comments-alt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-bolt { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-sitemap { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-umbrella { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-paste { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} -.icon-user-md { - *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); -} diff --git a/dash/.rspec b/dash/.rspec deleted file mode 100644 index 53607ea52b7..00000000000 --- a/dash/.rspec +++ /dev/null @@ -1 +0,0 @@ ---colour diff --git a/dash/Guardfile b/dash/Guardfile deleted file mode 100644 index 4f2814fecbb..00000000000 --- a/dash/Guardfile +++ /dev/null @@ -1,13 +0,0 @@ -guard 'rspec', :version => 2, :spec_paths => %w(spec), - :cli => (File.read('.rspec').split("\n").join(' ') if File.exists?('.rspec')) do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/requests/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch("spec/spec_helper.rb") { "spec" } - watch("config/routes.rb") - watch("app/controllers/application_controller.rb") { "spec/controllers" } - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } -end diff --git a/dash/LICENSE b/dash/LICENSE deleted file mode 100644 index 74f73e35ac3..00000000000 --- a/dash/LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2007-2012, Spree Commerce, Inc. and other contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name Spree nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/dash/README.md b/dash/README.md deleted file mode 100644 index 28176481515..00000000000 --- a/dash/README.md +++ /dev/null @@ -1,15 +0,0 @@ -Overview Dashboard -================== - -Core extension to display Spree Analytics - -Testing -------- - -You need to do a quick one-time creation of a test application and then you can use it to run the tests. - - bundle exec rake test_app - -Then run the tests - - bundle exec rake spec diff --git a/dash/Rakefile b/dash/Rakefile deleted file mode 100644 index ed2585a9ba8..00000000000 --- a/dash/Rakefile +++ /dev/null @@ -1,28 +0,0 @@ -require 'rake' -require 'rake/testtask' -require 'rake/packagetask' -require 'rubygems/package_task' -require 'rspec/core/rake_task' -require 'spree/core/testing_support/common_rake' - -RSpec::Core::RakeTask.new - -task :default => :spec - -spec = eval(File.read('spree_dash.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc 'Release to gemcutter' -task :release do - version = File.read(File.expand_path('../../SPREE_VERSION', __FILE__)).strip - cmd = 'cd pkg && gem push spree_dash-#{version}.gem'; puts cmd; system cmd -end - -desc 'Generates a dummy app for testing' -task :test_app do - ENV['LIB_NAME'] = 'spree/dash' - Rake::Task['common:test_app'].invoke -end diff --git a/dash/app/assets/images/analytics_dashboard_preview.jpg b/dash/app/assets/images/analytics_dashboard_preview.jpg deleted file mode 100644 index 210d9199cb6..00000000000 Binary files a/dash/app/assets/images/analytics_dashboard_preview.jpg and /dev/null differ diff --git a/dash/app/controllers/spree/admin/analytics_controller.rb b/dash/app/controllers/spree/admin/analytics_controller.rb deleted file mode 100644 index 27f8f091102..00000000000 --- a/dash/app/controllers/spree/admin/analytics_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Spree - class Admin::AnalyticsController < Admin::BaseController - - def sign_up - redirect_if_registered and return - @store = { - :first_name => '', - :last_name => '', - :email => try_spree_current_user.email, - :currency => 'USD', - :time_zone => Time.zone, - :name => Spree::Config.site_name, - :url => format_url(Spree::Config.site_url) - } - end - - def register - redirect_if_registered and return - @store = params[:store] - @store[:url] = format_url(@store[:url]) - - unless @store.has_key? :terms_of_service - flash[:error] = t(:agree_to_terms_of_service) - return render :sign_up - end - - unless @store.has_key? :privacy_policy - flash[:error] = t(:agree_to_privacy_policy) - return render :sign_up - end - - begin - @store = Spree::Dash::Jirafe.register(@store) - Spree::Dash::Config.app_id = @store[:app_id] - Spree::Dash::Config.app_token = @store[:app_token] - Spree::Dash::Config.site_id = @store[:site_id] - Spree::Dash::Config.token = @store[:site_token] - flash[:notice] = t(:successfully_signed_up_for_analytics) - redirect_to admin_path - rescue Spree::Dash::JirafeException => e - flash[:error] = e.message - render :sign_up - end - end - - def edit - - end - - def update - Spree::Dash::Config.app_id = params[:app_id] - Spree::Dash::Config.app_token = params[:app_token] - Spree::Dash::Config.site_id = params[:site_id] - Spree::Dash::Config.token = params[:token] - flash[:success] = t(:jirafe_settings_updated, :scope => "spree.dash") - redirect_to admin_analytics_path - end - - private - - def redirect_if_registered - if Spree::Dash::Config.configured? - flash[:success] = t(:already_signed_up_for_analytics) - redirect_to admin_path and return true - end - end - - def format_url(url) - url =~ /^http/ ? url : "http://#{url}" - end - - end -end diff --git a/dash/app/controllers/spree/admin/overview_controller.rb b/dash/app/controllers/spree/admin/overview_controller.rb deleted file mode 100644 index 4e698888ea5..00000000000 --- a/dash/app/controllers/spree/admin/overview_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Spree - class Admin::OverviewController < Admin::BaseController - - JIRAFE_LOCALES = { :english => 'en_US', - :french => 'fr_FR', - :german => 'de_DE', - :japanese => 'ja_JA' } - - def index - if JIRAFE_LOCALES.values.include? params[:locale] - Spree::Dash::Config.locale = params[:locale] - end - end - - end -end diff --git a/dash/app/helpers/spree/admin/overview_helper.rb b/dash/app/helpers/spree/admin/overview_helper.rb deleted file mode 100644 index 0f6b8d7daf8..00000000000 --- a/dash/app/helpers/spree/admin/overview_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Spree - module Admin - module OverviewHelper - - def jirafe_locale_links - Spree::Admin::OverviewController::JIRAFE_LOCALES.collect do |langage, locale| - link_to image_tag("flags/#{locale.split('_')[1].downcase}.png", :alt => langage.to_s.titleize), admin_path(:locale => locale), :class => 'with-tip', :title => langage.to_s.titleize, :data => {:'tip-color' => 'green'} - end - end - - end - end -end diff --git a/dash/app/helpers/spree/analytics_helper.rb b/dash/app/helpers/spree/analytics_helper.rb deleted file mode 100644 index 02d1a4226c2..00000000000 --- a/dash/app/helpers/spree/analytics_helper.rb +++ /dev/null @@ -1,236 +0,0 @@ -module Spree - module AnalyticsHelper - - def spree_analytics - render :partial => 'spree/analytics/header' - end - - def analytics_tags - tags = { :id => Spree::Dash::Config.site_id.to_s } - tags.merge! product_analytics_tags - tags.merge! taxon_analytics_tags - tags.merge! keywords_analytics_tags - tags.merge! cart_analytics_tags - tags.merge! completed_analytics_tags - end - - def product_analytics_tags - return {} unless @product - { :product => { :name => @product.name, - :price => @product.price, - :sku => @product.sku, - :categories => @product.taxons.map(&:permalink) - } - } - end - - def taxon_analytics_tags - return {} unless @taxon - { :category => { :name => @taxon.permalink } } - end - - def keywords_analytics_tags - return {} unless params[:keywords] - { :search => { :keyword => u(params[:keywords]) } } - end - - def cart_analytics_tags - return {} unless @order and @order.cart? - { :cart => { :total => @order.total, - :products => products_for_order } - } - end - - def completed_analytics_tags - return {} unless @order and @order.complete? - { :confirm => { :orderid => @order.number, - :total => @order.total, - :shipping => @order.ship_total, - :tax => @order.tax_total, - :discount => @order.adjustment_total, - :subtotal => @order.item_total, - :products => products_for_order } - } - end - - def products_for_order - @order.line_items.map do |line_item| - variant = line_item.variant - { - :name => variant.name, - :qty => line_item.quantity, - :price => variant.price, - :sku => variant.sku, - :categories => variant.product.taxons.map(&:permalink) - } - end - end - - CURRENCIES = [ - ["Australian Dollar", "AUD"], - ["New Zealand Dollar", "NZD"], - ["US Dollar", "USD"], - ["Euro", "EUR"], - ["British Pound", "GBP"], - ["Japanese Yen", "JPY"], - ["Afghanistan Afghani", "AFA"], - ["Albanian Lek", "ALL"], - ["Algerian Dinar", "DZD"], - ["Andorran Franc", "ADF"], - ["Andorran Peseta", "ADP"], - ["Angolan New Kwanza", "AON"], - ["Argentine Peso", "ARS"], - ["Aruban Florin", "AWG"], - ["Austrian Schilling", "ATS"], - ["Bahamanian Dollar", "BSD"], - ["Bahraini Dinar", "BHD"], - ["Bangladeshi Taka", "BDT"], - ["Barbados Dollar", "BBD"], - ["Belgian Franc", "BEF"], - ["Belize Dollar", "BZD"], - ["Bermudian Dollar", "BMD"], - ["Bhutan Ngultrum", "BTN"], - ["Bolivian Boliviano", "BOB"], - ["Botswana Pula", "BWP"], - ["Brazilian Real", "BRL"], - ["Brunei Dollar", "BND"], - ["Bulgarian Lev", "BGL"], - ["Burundi Franc", "BIF"], - ["CFA Franc BCEAO", "XOF"], - ["CFA Franc BEAC", "XAF"], - ["Cambodian Riel", "KHR"], - ["Canadian Dollar", "CAD"], - ["Cape Verde Escudo", "CVE"], - ["Cayman Islands Dollar", "KYD"], - ["Central Pacific Franc", "CFP"], - ["Chilean Peso", "CLP"], - ["Chinese Yuan Renminbi", "CNY"], - ["Colombian Peso", "COP"], - ["Comoros Franc", "KMF"], - ["Costa Rican Colon", "CRC"], - ["Croatian Kuna", "HRK"], - ["Cuban Peso", "CUP"], - ["Cyprus Pound", "CYP"], - ["Czech Koruna", "CSK"], - ["Danish Krone", "DKK"], - ["Djibouti Franc", "DJF"], - ["Dominican R. Peso", "DOP"], - ["Dutch Guilder", "NLG"], - ["ECU", "XEU"], - ["East Caribbean Dollar", "XCD"], - ["Ecuador Sucre", "ECS"], - ["Egyptian Pound", "EGP"], - ["El Salvador Colon", "SVC"], - ["Estonian Kroon", "EEK"], - ["Ethiopian Birr", "ETB"], - ["Falkland Islands Pound", "FKP"], - ["Fiji Dollar", "FJD"], - ["Finnish Markka", "FIM"], - ["French Franc", "FRF"], - ["Gambian Dalasi", "GMD"], - ["German Mark", "DEM"], - ["Ghanaian Cedi", "GHC"], - ["Gibraltar Pound", "GIP"], - ["Greek Drachma", "GRD"], - ["Guatemalan Quetzal", "GTQ"], - ["Guinea Franc", "GNF"], - ["Guyanese Dollar", "GYD"], - ["Haitian Gourde", "HTG"], - ["Honduran Lempira", "HNL"], - ["Hong Kong Dollar", "HKD"], - ["Hungarian Forint", "HUF"], - ["Iceland Krona", "ISK"], - ["Indian Rupee", "INR"], - ["Indonesian Rupiah", "IDR"], - ["Iranian Rial", "IRR"], - ["Iraqi Dinar", "IQD"], - ["Irish Punt", "IEP"], - ["Israeli New Shekel", "ILS"], - ["Italian Lira", "ITL"], - ["Jamaican Dollar", "JMD"], - ["Jordanian Dinar", "JOD"], - ["Kazakhstan Tenge", "KZT"], - ["Kenyan Shilling", "KES"], - ["Kuwaiti Dinar", "KWD"], - ["Lao Kip", "LAK"], - ["Latvian Lats", "LVL"], - ["Lebanese Pound", "LBP"], - ["Lesotho Loti", "LSL"], - ["Liberian Dollar", "LRD"], - ["Libyan Dinar", "LYD"], - ["Lithuanian Litas", "LTL"], - ["Luxembourg Franc", "LUF"], - ["Macau Pataca", "MOP"], - ["Malagasy Franc", "MGF"], - ["Malawi Kwacha", "MWK"], - ["Malaysian Ringgit", "MYR"], - ["Maldive Rufiyaa", "MVR"], - ["Maltese Lira", "MTL"], - ["Mauritanian Ouguiya", "MRO"], - ["Mauritius Rupee", "MUR"], - ["Mexican Peso", "MXP"], - ["Mongolian Tugrik", "MNT"], - ["Moroccan Dirham", "MAD"], - ["Mozambique Metical", "MZM"], - ["Myanmar Kyat", "MMK"], - ["NL Antillian Guilder", "ANG"], - ["Namibia Dollar", "NAD"], - ["Nepalese Rupee", "NPR"], - ["Nicaraguan Cordoba Oro", "NIO"], - ["Nigerian Naira", "NGN"], - ["North Korean Won", "KPW"], - ["Norwegian Kroner", "NOK"], - ["Omani Rial", "OMR"], - ["Pakistan Rupee", "PKR"], - ["Panamanian Balboa", "PAB"], - ["Papua New Guinea Kina", "PGK"], - ["Paraguay Guarani", "PYG"], - ["Peruvian Nuevo Sol", "PEN"], - ["Philippine Peso", "PHP"], - ["Polish Zloty", "PLZ"], - ["Portuguese Escudo", "PTE"], - ["Qatari Rial", "QAR"], - ["Romanian Leu", "ROL"], - ["Russian Rouble", "RUB"], - ["Samoan Tala", "WST"], - ["Sao Tome/Principe Dobra", "STD"], - ["Saudi Riyal", "SAR"], - ["Seychelles Rupee", "SCR"], - ["Sierra Leone Leone", "SLL"], - ["Singapore Dollar", "SGD"], - ["Slovak Koruna", "SKK"], - ["Slovenian Tolar", "SIT"], - ["Solomon Islands Dollar", "SBD"], - ["Somali Shilling", "SOS"], - ["South African Rand", "ZAR"], - ["South-Korean Won", "KRW"], - ["Spanish Peseta", "ESP"], - ["Sri Lanka Rupee", "LKR"], - ["St. Helena Pound", "SHP"], - ["Sudanese Dinar", "SDD"], - ["Sudanese Pound", "SDP"], - ["Suriname Guilder", "SRG"], - ["Swaziland Lilangeni", "SZL"], - ["Swedish Krona", "SEK"], - ["Swiss Franc", "CHF"], - ["Syrian Pound", "SYP"], - ["Taiwan Dollar", "TWD"], - ["Tanzanian Shilling", "TZS"], - ["Thai Baht", "THB"], - ["Tonga Pa'anga", "TOP"], - ["Trinidad/Tobago Dollar", "TTD"], - ["Tunisian Dinar", "TND"], - ["Turkish Lira", "TRL"], - ["Uganda Shilling", "UGS"], - ["Ukraine Hryvnia", "UAH"], - ["Uruguayan Peso", "UYP"], - ["Utd. Arab Emir. Dirham", "AED"], - ["Vanuatu Vatu", "VUV"], - ["Venezuelan Bolivar", "VEB"], - ["Vietnamese Dong", "VND"], - ["Yugoslav Dinar", "YUN"], - ["Zambian Kwacha", "ZMK"], - ["ZWD","Zimbabwe Dollar"] - ] - end -end diff --git a/dash/app/models/spree/dash_configuration.rb b/dash/app/models/spree/dash_configuration.rb deleted file mode 100644 index 12f5a0e543b..00000000000 --- a/dash/app/models/spree/dash_configuration.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Spree - class DashConfiguration < Preferences::Configuration - preference :app_id, :string - preference :app_token, :string - preference :site_id, :string - preference :token, :string - preference :locale, :string, :default => 'en_US' - - def configured? - preferred_app_id.present? and preferred_site_id.present? and preferred_token.present? - end - end -end diff --git a/dash/app/overrides/analytics_header.rb b/dash/app/overrides/analytics_header.rb deleted file mode 100644 index 6dd7a4d9ffd..00000000000 --- a/dash/app/overrides/analytics_header.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/admin/shared/_configuration_menu", - :name => "add_dashboard_sidebar_link", - :insert_bottom => ".sidebar", - :text => "<%= configurations_sidebar_menu_item t(:jirafe), admin_analytics_path %>", - :original => 'a74f177275dc303c9cd5994b2e24e027434c3cbb') diff --git a/dash/app/views/spree/admin/analytics/edit.html.erb b/dash/app/views/spree/admin/analytics/edit.html.erb deleted file mode 100644 index 631427add1b..00000000000 --- a/dash/app/views/spree/admin/analytics/edit.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -

        <%= t(:header, :scope => "spree.dash.jirafe") %>

        - -

        <%= t(:explanation, :scope => "spree.dash.jirafe") %>

        - -<%= form_tag admin_analytics_path, :method => :put do %> -
        - <%= label_tag 'app_id', t(:app_id, :scope => "spree.dash.jirafe") %>
        - <%= text_field_tag 'app_id', Spree::Dash::Config.app_id %> -
        -
        - <%= label_tag 'app_token', t(:app_token, :scope => "spree.dash.jirafe") %>
        - <%= text_field_tag 'app_token', Spree::Dash::Config.app_token, :size => 40 %> -
        -
        - <%= label_tag 'site_id', t(:site_id, :scope => "spree.dash.jirafe") %>
        - <%= text_field_tag 'site_id', Spree::Dash::Config.site_id %> -
        -
        - <%= label_tag 'token', t(:token, :scope => "spree.dash.jirafe") %>
        - <%= text_field_tag 'token', Spree::Dash::Config.token, :size => 40 %> -
        -
        - <%= submit_tag "Update" %> -
        -<% end %> - diff --git a/dash/app/views/spree/admin/analytics/sign_up.html.erb b/dash/app/views/spree/admin/analytics/sign_up.html.erb deleted file mode 100644 index c5499bb2a85..00000000000 --- a/dash/app/views/spree/admin/analytics/sign_up.html.erb +++ /dev/null @@ -1,61 +0,0 @@ -<% content_for :page_title do %> - <%= t(:analytics_sign_up) %> -<% end %> - -<% content_for :page_title_classes do %> - align-center -<% end %> - -<%= form_tag admin_analytics_register_path do %> -
        -
        - <%= label_tag :first_name %> - <%= text_field_tag 'store[first_name]', @store[:first_name], :size => 50 %> -
        - -
        - <%= label_tag :last_name %> - <%= text_field_tag 'store[last_name]', @store[:last_name], :size => 50 %> -
        - -
        - <%= label_tag :email %> - <%= email_field_tag 'store[email]', @store[:email], :size => 50 %> -
        - -
        - <%= label_tag :store_name %> - <%= text_field_tag 'store[name]', @store[:name], :size => 50 %> -
        - -
        - <%= label_tag :store_url %> - <%= url_field_tag 'store[url]', @store[:url], :size => 50 %> -
        - -
        - <%= label_tag :currency %> - <%= select_tag 'store[currency]', options_for_select(Spree::AnalyticsHelper::CURRENCIES, @store[:currency]), :class => 'select2' %> -
        - -
        - <%= label_tag :time_zone %> - <%= select_tag 'store[time_zone]', time_zone_options_for_select(@store[:time_zone]), :class => 'select2' %> -
        - -
        - <%= check_box_tag 'store[terms_of_service]', @store[:terms_of_service], @store.has_key?(:terms_of_service), :size => 50 %> - <%= label_tag 'store[terms_of_service]', t(:agree_to_terms_of_service) %> (<%= t(:review) %>) -
        - -
        - <%= check_box_tag 'store[privacy_policy]', @store[:privacy_policy], @store.has_key?(:privacy_policy), :size => 50 %> - <%= label_tag 'store[privacy_policy]', t(:agree_to_privacy_policy) %> (<%= t(:review) %>) -
        -
        -
        - <%= button t(:activate), 'icon-ok' %> - <%= t(:or) %> - <%= link_to_with_icon 'icon-remove', t(:cancel), admin_path, :class => 'button' %> -
        -<% end %> diff --git a/dash/app/views/spree/admin/overview/index.html.erb b/dash/app/views/spree/admin/overview/index.html.erb deleted file mode 100644 index 1ec455aa25b..00000000000 --- a/dash/app/views/spree/admin/overview/index.html.erb +++ /dev/null @@ -1,51 +0,0 @@ -<% content_for :head do %> - <%= stylesheet_link_tag 'https://api.jirafe.com/dashboard/css/spree_ui.css', :media => 'all' %> - <%= javascript_include_tag 'https://jirafe.com/dashboard/js/spree_namespaced_ui.js' %> -<% end %> - -<% if Spree::Dash::Config.configured? %> - - <% content_for :page_actions do %> -
      • - <%= t(:choose_dashboard_locale) %>: <%= raw jirafe_locale_links.join(' ') %> -
      • - <% end %> - -
        - <%= javascript_tag :defer => 'defer' do %> - jirafe.jQuery('#jirafe').jirafe({ - api_url: 'https://api.jirafe.com/v1', - api_token: '<%= Spree::Dash::Config.token %>', - app_id: '<%= Spree::Dash::Config.app_id %>', - version: 'spree-v0.1.0', - locale: '<%= Spree::Dash::Config.locale %>' }); - setTimeout(function() { - if ($('mod-jirafe') == undefined) { - $('messages').insert ("
        • We're unable to connect with the Jirafe service for the moment. Please wait a few minutes and refresh this page later.
        "); - } - }, 10000); - <% end %> -<% else %> -
        -
        -
        -

        <%= t(:analytics_desc_header_1)%>

        -
        <%= t(:analytics_desc_header_2)%>
        -
        - -
          -
        • <%= t(:analytics_desc_list_1)%>
        • -
        • <%= t(:analytics_desc_list_2)%>
        • -
        • <%= t(:analytics_desc_list_3)%>
        • -
        • <%= t(:analytics_desc_list_4)%>
        • -
        -
        -
        - <%= link_to_with_icon 'icon-ok', t(:activate), admin_analytics_sign_up_path, :class => 'button' %> -   <%= t(:or) %>   - <%= link_to_with_icon 'icon-info-sign', t(:learn_more), "http://spreecommerce.com/blog/2012/01/31/introducing-spree-analytics/", :class => 'button', :target => '_blank' %> -
        -
        - -
        -<% end %> \ No newline at end of file diff --git a/dash/app/views/spree/analytics/_header.html.erb b/dash/app/views/spree/analytics/_header.html.erb deleted file mode 100644 index a01c60095d6..00000000000 --- a/dash/app/views/spree/analytics/_header.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% if Spree::Dash::Config.configured? %> - -<% else %> - -<% end %> diff --git a/dash/config/locales/en.yml b/dash/config/locales/en.yml deleted file mode 100644 index 704b5b8b413..00000000000 --- a/dash/config/locales/en.yml +++ /dev/null @@ -1,22 +0,0 @@ -en: - agree_to_terms_of_service: Agree to Terms of Service - agree_to_privacy_policy: Agree to Privacy Policy - already_signed_up_for_analytics: You have already signed up for Spree Analytics - successfully_signed_up_for_analytics: Successfully signed up for Spree Analytics - analytics_desc_header_1: Spree Analytics - analytics_desc_header_2: Live analytics integrated into your Spree dashboard - analytics_desc_list_1: Get live sales information as it happens - analytics_desc_list_2: Requires only a free Spree account to activate - analytics_desc_list_3: Absolutely no code to install - analytics_desc_list_4: It's completely free! - - spree: - dash: - jirafe: - header: Jirafe Analytics Settings - app_id: App ID - app_token: App Token - site_id: Site ID - token: Token - explanation: The fields below may already be populated if you chose to register with Jirafe from the admin dashboard. - jirafe_settings_updated: Jirafe Settings have been updated. diff --git a/dash/config/routes.rb b/dash/config/routes.rb deleted file mode 100644 index 0143cfa6d51..00000000000 --- a/dash/config/routes.rb +++ /dev/null @@ -1,9 +0,0 @@ -Spree::Core::Engine.routes.prepend do - match '/admin' => 'admin/overview#index', :as => :admin - - get '/admin/analytics/sign_up' => 'admin/analytics#sign_up', :as => :admin_analytics_sign_up - post '/admin/analytics/register' => 'admin/analytics#register', :as => :admin_analytics_register - - get '/jirafe' => 'admin/analytics#edit', :as => :admin_analytics - put '/jirafe' => 'admin/analytics#update', :as => :admin_analytics -end diff --git a/dash/lib/spree/dash.rb b/dash/lib/spree/dash.rb deleted file mode 100644 index 0a6a5d21b05..00000000000 --- a/dash/lib/spree/dash.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spree_core' -require 'httparty' - -module Spree - module Dash - - end -end - -require 'spree/dash/engine' -require 'spree/dash/jirafe' - -Spree::Dash::Engine.config.to_prepare do - Spree::BaseController.send :helper, 'spree/analytics' -end diff --git a/dash/lib/spree/dash/engine.rb b/dash/lib/spree/dash/engine.rb deleted file mode 100644 index 10d9ad0f97f..00000000000 --- a/dash/lib/spree/dash/engine.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Spree - module Dash - class Engine < Rails::Engine - isolate_namespace Spree - engine_name 'spree_dash' - - initializer "spree.dash.environment", :before => :load_config_initializers do |app| - Spree::Dash::Config = Spree::DashConfiguration.new - end - - def self.activate - Dir.glob(File.join(File.dirname(__FILE__), "../../../app/**/*_decorator*.rb")) do |c| - Rails.configuration.cache_classes ? require(c) : load(c) - end - end - config.to_prepare &method(:activate).to_proc - - end - end -end diff --git a/dash/lib/spree/dash/jirafe.rb b/dash/lib/spree/dash/jirafe.rb deleted file mode 100644 index eeffeb88ff3..00000000000 --- a/dash/lib/spree/dash/jirafe.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Spree - module Dash - class JirafeException < Exception; end - - class Jirafe - include HTTParty - base_uri 'https://api.jirafe.com/v1' - format :json - - class << self - def register(store) - validate_required_keys! store - store[:time_zone] = ActiveSupport::TimeZone::MAPPING[store[:time_zone]] # jirafe expects 'America/New_York' - - store = register_application(store) - store = synchronize_resources(store) - end - - def validate_required_keys!(store) - [:first_name, :url, :email, :currency, :time_zone, :name].each do |key| - unless store[key].present? - raise JirafeException, "#{key.to_s.titleize} is required" - end - end - end - - def register_application(store) - return if store[:app_id].present? && store[:app_token].present? - - options = { - :body => { - :name => store[:name], - :url => store[:url] - } - } - response = post '/applications', options - raise JirafeException, 'unable to create jirafe application' unless response.code == 200 && - response['app_id'].present? && - response['token'].present? - store[:app_id] = response['app_id'] - store[:app_token] = response['token'] - store - end - - def synchronize_resources(store) - return unless store.has_key?(:app_id) and store.has_key?(:app_token) - - options = { - :headers => { 'Content-type' => 'application/json' }, - :query => { :token => store[:app_token] }, - :body => { - :sites => [{ :description => store[:name], - :url => store[:url], - :currency => store[:currency], - :timezone => store[:time_zone], - :external_id => 1, - :site_id => store[:site_id] }], - :users => [{ :email => store[:email], - :first_name => store[:first_name], - :last_name => store[:last_name] }] - }.to_json - } - response = post "/applications/#{store[:app_id]}/resources", options - raise JirafeException, 'unable to synchronize store' unless response.code == 200 && - response['sites'].present? && - response['users'].present? - store[:site_id] = response["sites"].first["site_id"] - store[:site_token] = response["users"].first["token"] - store - end - end - end - end -end \ No newline at end of file diff --git a/dash/lib/spree_dash.rb b/dash/lib/spree_dash.rb deleted file mode 100644 index 0e896f65604..00000000000 --- a/dash/lib/spree_dash.rb +++ /dev/null @@ -1 +0,0 @@ -require 'spree/dash' \ No newline at end of file diff --git a/dash/spec/controllers/admin/analytics_controller_spec.rb b/dash/spec/controllers/admin/analytics_controller_spec.rb deleted file mode 100644 index 276ca984080..00000000000 --- a/dash/spec/controllers/admin/analytics_controller_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# Also see spec/requests/admin/analytics_spec.rb -require 'spec_helper' - -describe Spree::Admin::AnalyticsController do - before :each do - @user = create(:admin_user) - controller.stub :spree_current_user => @user - end - - it "redirects if previously registered" do - Spree::Dash::Config.should_receive(:configured?).and_return(true) - spree_get :sign_up - response.should redirect_to(spree.admin_path) - end - - describe 'Allows sign up if not registered' do - before :each do - Spree::Dash::Config.app_id = nil - Spree::Dash::Config.app_token = nil - Spree::Dash::Config.site_id = nil - Spree::Dash::Config.token = nil - end - - it "sets the defaults to preferences" do - Spree::Config.site_name = "test_site" - Spree::Config.site_url = "http://test_site.com" - spree_get :sign_up - response.should render_template("sign_up") - assigns(:store)[:url].should eq 'http://test_site.com' - assigns(:store)[:email].should eq @user.email - end - - it "must agree to terms of service" do - params = { :store => {:url => 'http://test.com' } } - spree_post :register, params - flash[:error].should match /Terms of Service/ - end - - it "must agree to privacy policy" do - params = { :store => {:terms_of_service => 'on', :url => 'http://test.com' } } - spree_post :register, params - flash[:error].should match /Privacy Policy/ - end - - end -end diff --git a/dash/spec/controllers/admin/overview_controller_spec.rb b/dash/spec/controllers/admin/overview_controller_spec.rb deleted file mode 100644 index 6fa04fb9120..00000000000 --- a/dash/spec/controllers/admin/overview_controller_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe Spree::Admin::OverviewController do - - before :each do - @user = create(:admin_user) - controller.stub :spree_current_user => @user - end - - it "sets the locale preference" do - Spree::Dash::Config.locale = 'en_EN' - spree_get :index, :locale => 'fr_FR' - Spree::Dash::Config.locale.should eq 'fr_FR' - end - -end diff --git a/dash/spec/helpers/spree/analytics_helper_spec.rb b/dash/spec/helpers/spree/analytics_helper_spec.rb deleted file mode 100644 index 3e0e9b19171..00000000000 --- a/dash/spec/helpers/spree/analytics_helper_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -require "spec_helper" - -module Spree - describe AnalyticsHelper do - - before :all do - Spree::Dash::Config.app_id = 1 - Spree::Dash::Config.site_id = 2 - end - - it "includes jirafe configuration" do - tags = helper.analytics_tags - tags[:id].should be_kind_of String - tags[:id].should eq "2" - end - - describe "Tracking Tags Customizations" do - before :each do - @product = double(:name => "Fancy Pants", - :price => "19.99", - :sku => "1234", - :taxons => [double(:permalink => "clothing/pants")]) - - @variant = double(:name => @product.name, - :price => @product.price, - :sku => @product.sku, - :product => @product) - - @order = double(:number => "R12345", - :total => "19.99", - :ship_total => "22.99", - :tax_total => "4.99", - :adjustment_total => "0.00", - :item_total => "1.99", - :cart? => false, - :complete? => false) - - end - - it "for @product" do - assign :product, @product - tags = helper.product_analytics_tags - tags[:product][:name].should eq "Fancy Pants" - tags[:product][:price].should eq "19.99" - tags[:product][:sku].should eq "1234" - tags[:product][:categories].first.should eq "clothing/pants" - end - - it "for taxons" do - assign :taxon, double(:permalink => "clothing/shirts") - tags = helper.taxon_analytics_tags - tags[:category][:name].should eq "clothing/shirts" - end - - it "for keywords" do - params[:keywords] = "rails" - tags = helper.keywords_analytics_tags - tags[:search][:keyword].should eq "rails" - end - - it "escapes keywords" do - Spree::Dash::Config.app_id = "test" - Spree::Dash::Config.token = "test" - Spree::Dash::Config.site_id " test" - params[:keywords] = "\"funny> "product1"}]) - tags = helper.cart_analytics_tags - tags[:cart][:total].should eq "19.99" - tags[:cart][:products].first[:name].should eq "product1" - end - - it "for completed order" do - @order.should_receive(:complete?).and_return(true) - assign :order, @order - helper.should_receive(:products_for_order).and_return([{:name => "product1"}]) - tags = helper.completed_analytics_tags - tags[:confirm][:orderid].should eq "R12345" - tags[:confirm][:total].should eq "19.99" - tags[:confirm][:shipping].should eq "22.99" - tags[:confirm][:tax].should eq "4.99" - tags[:confirm][:discount].should eq "0.00" - tags[:confirm][:subtotal].should eq "1.99" - tags[:confirm][:products].first[:name].should eq "product1" - end - - it "products_for_order" do - line_item = double(:variant => @variant, :quantity => "4") - assign :order, double(:line_items => [line_item]) - tags = helper.products_for_order - tags.first[:name].should eq "Fancy Pants" - tags.first[:price].should eq "19.99" - tags.first[:sku].should eq "1234" - tags.first[:qty].should eq "4" - tags.first[:categories].first.should eq "clothing/pants" - end - end - end -end diff --git a/dash/spec/requests/admin/analytics_spec.rb b/dash/spec/requests/admin/analytics_spec.rb deleted file mode 100644 index 81f519f2474..00000000000 --- a/dash/spec/requests/admin/analytics_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe "Analytics Activation" do - stub_authorization! - - before(:each) do - @user = create(:admin_user) - - Spree::Dash::Config.app_id = nil - Spree::Dash::Config.app_token = nil - Spree::Dash::Config.site_id = nil - Spree::Dash::Config.token = nil - end - - it "user can activate spree_analytics" do - Spree::Dash::Jirafe.should_receive(:register). - with(hash_including(:url => 'http://test.com')). - and_return({ :app_id => '1', :app_token => '2', :site_id => '3', :site_token => '4' }) - - visit spree.admin_analytics_sign_up_path - check 'store[terms_of_service]' - check 'store[privacy_policy]' - fill_in 'store[first_name]', :with => "test_first_name" - fill_in 'store[last_name]', :with => "test_last_name" - fill_in 'store[url]', :with => "test.com" - select '(GMT+00:00) Casablanca', :from => 'store[time_zone]' - click_button 'Activate' - - Spree::Dash::Config.app_id.should eq '1' - Spree::Dash::Config.app_token.should eq '2' - Spree::Dash::Config.site_id.should eq '3' - Spree::Dash::Config.token.should eq '4' - end - - it "can edit anayltics information" do - visit spree.admin_path - click_link "Configuration" - click_link "Jirafe" - fill_in 'app_id', :with => "1" - fill_in 'app_token', :with => "token" - fill_in 'site_id', :with => "test.com" - fill_in 'token', :with => "other_token" - click_button "Update" - - page.should have_content("Jirafe Settings have been updated.") - - Spree::Dash::Config.app_id.should eq '1' - Spree::Dash::Config.app_token.should eq 'token' - Spree::Dash::Config.site_id.should eq 'test.com' - Spree::Dash::Config.token.should eq 'other_token' - end -end diff --git a/dash/spec/requests/header_tags_spec.rb b/dash/spec/requests/header_tags_spec.rb deleted file mode 100644 index 178f035fc5f..00000000000 --- a/dash/spec/requests/header_tags_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'spec_helper' - -describe "Header Tags" do - before do - @product = create(:product, :name => "RoR Mug") - - Spree::DashConfiguration.new.app_id = 1111 - Spree::DashConfiguration.new.site_id = 2222 - Spree::DashConfiguration.new.token = "test_token" - end - - let(:analytics) { page.find("script#analytics") } - - it "includes the site_id on the home page" do - visit spree.root_path - jirafe = analytics.text =~ /jirafe= \{"id":"2222"\}/ - jirafe.should_not be_nil - end - - it "includes the product tag on the product page" do - visit spree.root_path - click_link "RoR Mug" - product = analytics.text =~ /"product":\{.*?"name":"RoR Mug".*?\}/ - product.should_not be_nil - end - - it "includes the cart tag on the cart page" do - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - cart = analytics.text =~ /"cart":\{.*?"total":"19.99".*?\}/ - cart.should_not be_nil - end - - it "includes the search tag on the results page" do - visit spree.root_path - fill_in "keywords", :with => 'mug' - click_button "Search" - search = analytics.text =~ /"search":\{.*?"keyword":"mug".*?\}/ - search.should_not be_nil - end - -end diff --git a/dash/spec/spec_helper.rb b/dash/spec/spec_helper.rb deleted file mode 100644 index b708cbc5ea7..00000000000 --- a/dash/spec/spec_helper.rb +++ /dev/null @@ -1,46 +0,0 @@ -# This file is copied to ~/spec when you run 'ruby script/generate rspec' -# from the project root directory. -ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../dummy/config/environment", __FILE__) -require 'rspec/rails' -require 'database_cleaner' -require 'spree/core/url_helpers' -require 'ffaker' - -# Requires supporting files with custom matchers and macros, etc, -# in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} - -require 'spree/core/testing_support/factories' -require 'spree/core/testing_support/controller_requests' -require 'spree/core/testing_support/authorization_helpers' - -require 'active_record/fixtures' -fixtures_dir = File.expand_path('../../../core/db/default', __FILE__) -ActiveRecord::Fixtures.create_fixtures(fixtures_dir, ['spree/countries', 'spree/zones', 'spree/zone_members', 'spree/states', 'spree/roles']) - -RSpec.configure do |config| - config.mock_with :rspec - - config.fixture_path = "#{::Rails.root}/spec/fixtures" - - config.use_transactional_fixtures = false - - config.before(:suite) do - DatabaseCleaner.strategy = :truncation, { :except => ['spree_countries', 'spree_zones', 'spree_zone_members', 'spree_states', 'spree_roles'] } - end - - config.before(:each) do - DatabaseCleaner.start - end - - config.after(:each) do - DatabaseCleaner.clean - end - - config.include FactoryGirl::Syntax::Methods - config.include Spree::Core::UrlHelpers - config.include Spree::Core::TestingSupport::ControllerRequests, :type => :controller - - config.include Rack::Test::Methods, :type => :requests -end diff --git a/dash/spree_dash.gemspec b/dash/spree_dash.gemspec deleted file mode 100644 index 45e99670a90..00000000000 --- a/dash/spree_dash.gemspec +++ /dev/null @@ -1,22 +0,0 @@ -# encoding: UTF-8 -version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip - -Gem::Specification.new do |s| - s.platform = Gem::Platform::RUBY - s.name = 'spree_dash' - s.version = version - s.summary = 'Overview dashboard for use with Spree.' - s.description = 'Required dependency for Spree' - - s.required_ruby_version = '>= 1.8.7' - s.author = 'Brian Quinn' - s.email = 'brian@spreecommerce.com' - s.homepage = 'http://spreecommerce.com' - - s.files = Dir['LICENSE', 'README.md', 'app/**/*', 'config/**/*', 'lib/**/*', 'db/**/*', 'vendor/**/*'] - s.require_path = 'lib' - s.requirements << 'none' - - s.add_dependency 'spree_core', version - s.add_dependency 'httparty', '~> 0.8.1' -end diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md new file mode 100644 index 00000000000..f4a0249631c --- /dev/null +++ b/frontend/CHANGELOG.md @@ -0,0 +1 @@ +## Spree 2.4.0 (unreleased) ## diff --git a/frontend/Gemfile b/frontend/Gemfile new file mode 100644 index 00000000000..49bfb5b7790 --- /dev/null +++ b/frontend/Gemfile @@ -0,0 +1,6 @@ +eval(File.read(File.dirname(__FILE__) + '/../common_spree_dependencies.rb')) + +gem 'spree_core', :path => '../core' +gem 'spree_api', :path => '../api' + +gemspec diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100644 index 00000000000..bef97d82cc6 --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2007-2014, Spree Commerce, Inc. and other contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Spree nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/core/README.md b/frontend/README.md similarity index 100% rename from core/README.md rename to frontend/README.md diff --git a/frontend/Rakefile b/frontend/Rakefile new file mode 100644 index 00000000000..d8673a6caa1 --- /dev/null +++ b/frontend/Rakefile @@ -0,0 +1,29 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/packagetask' +require 'rubygems/package_task' +require 'rspec/core/rake_task' +require 'spree/testing_support/common_rake' + +Bundler::GemHelper.install_tasks +RSpec::Core::RakeTask.new + +spec = eval(File.read('spree_frontend.gemspec')) +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end + +desc "Release to gemcutter" +task :release do + version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip + cmd = "cd pkg && gem push spree_frontend-#{version}.gem"; puts cmd; system cmd +end + +task :default => :spec + +desc "Generates a dummy app for testing" +task :test_app do + ENV['LIB_NAME'] = 'spree/frontend' + Rake::Task['common:test_app'].invoke +end diff --git a/frontend/app/assets/images/credit_cards/amex_cid.gif b/frontend/app/assets/images/credit_cards/amex_cid.gif new file mode 100644 index 00000000000..fb940dc511e Binary files /dev/null and b/frontend/app/assets/images/credit_cards/amex_cid.gif differ diff --git a/frontend/app/assets/images/credit_cards/credit_card.gif b/frontend/app/assets/images/credit_cards/credit_card.gif new file mode 100644 index 00000000000..2e61a23c310 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/credit_card.gif differ diff --git a/frontend/app/assets/images/credit_cards/discover_cid.gif b/frontend/app/assets/images/credit_cards/discover_cid.gif new file mode 100644 index 00000000000..083820f464e Binary files /dev/null and b/frontend/app/assets/images/credit_cards/discover_cid.gif differ diff --git a/frontend/app/assets/images/credit_cards/icons/american_express.png b/frontend/app/assets/images/credit_cards/icons/american_express.png new file mode 100644 index 00000000000..73fa1ea749d Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/american_express.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/cirrus.png b/frontend/app/assets/images/credit_cards/icons/cirrus.png new file mode 100644 index 00000000000..81065defa11 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/cirrus.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/delta.png b/frontend/app/assets/images/credit_cards/icons/delta.png new file mode 100644 index 00000000000..f7c79d9a646 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/delta.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/diners_club.png b/frontend/app/assets/images/credit_cards/icons/diners_club.png new file mode 100644 index 00000000000..02d8c9508e5 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/diners_club.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/directdebit.png b/frontend/app/assets/images/credit_cards/icons/directdebit.png new file mode 100644 index 00000000000..c76274ed01c Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/directdebit.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/discover.png b/frontend/app/assets/images/credit_cards/icons/discover.png new file mode 100644 index 00000000000..e7d199b8e56 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/discover.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/egold.png b/frontend/app/assets/images/credit_cards/icons/egold.png new file mode 100644 index 00000000000..abb2bba8cb1 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/egold.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/maestro.png b/frontend/app/assets/images/credit_cards/icons/maestro.png new file mode 100644 index 00000000000..1dd6f42cad5 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/maestro.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/master.png b/frontend/app/assets/images/credit_cards/icons/master.png new file mode 100644 index 00000000000..f8992cdfbbe Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/master.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/paypal.png b/frontend/app/assets/images/credit_cards/icons/paypal.png new file mode 100644 index 00000000000..91051c00551 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/paypal.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/solo.png b/frontend/app/assets/images/credit_cards/icons/solo.png new file mode 100644 index 00000000000..ad867f1dba3 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/solo.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/switch.png b/frontend/app/assets/images/credit_cards/icons/switch.png new file mode 100644 index 00000000000..5d8315b1ebc Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/switch.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/visa.png b/frontend/app/assets/images/credit_cards/icons/visa.png new file mode 100644 index 00000000000..7545c43fc0c Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/visa.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/visaelectron.png b/frontend/app/assets/images/credit_cards/icons/visaelectron.png new file mode 100644 index 00000000000..4b0c4a8453f Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/visaelectron.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/westernunion.png b/frontend/app/assets/images/credit_cards/icons/westernunion.png new file mode 100644 index 00000000000..2a1766ac683 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/westernunion.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/wirecard.png b/frontend/app/assets/images/credit_cards/icons/wirecard.png new file mode 100644 index 00000000000..0cc55c5b7d6 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/wirecard.png differ diff --git a/frontend/app/assets/images/credit_cards/icons/worldpay.png b/frontend/app/assets/images/credit_cards/icons/worldpay.png new file mode 100644 index 00000000000..29b89ddb1c6 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/worldpay.png differ diff --git a/frontend/app/assets/images/credit_cards/master_cid.jpg b/frontend/app/assets/images/credit_cards/master_cid.jpg new file mode 100644 index 00000000000..b27fa71a7f0 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/master_cid.jpg differ diff --git a/frontend/app/assets/images/credit_cards/visa_cid.gif b/frontend/app/assets/images/credit_cards/visa_cid.gif new file mode 100644 index 00000000000..4048a820e33 Binary files /dev/null and b/frontend/app/assets/images/credit_cards/visa_cid.gif differ diff --git a/core/app/assets/images/favicon.ico b/frontend/app/assets/images/favicon.ico similarity index 100% rename from core/app/assets/images/favicon.ico rename to frontend/app/assets/images/favicon.ico diff --git a/frontend/app/assets/images/icons/add-to-cart.png b/frontend/app/assets/images/icons/add-to-cart.png new file mode 100644 index 00000000000..d45307a75e4 Binary files /dev/null and b/frontend/app/assets/images/icons/add-to-cart.png differ diff --git a/frontend/app/assets/images/icons/checkout.png b/frontend/app/assets/images/icons/checkout.png new file mode 100644 index 00000000000..f09452a40d9 Binary files /dev/null and b/frontend/app/assets/images/icons/checkout.png differ diff --git a/frontend/app/assets/images/icons/delete.png b/frontend/app/assets/images/icons/delete.png new file mode 100755 index 00000000000..9fc6a8c4640 Binary files /dev/null and b/frontend/app/assets/images/icons/delete.png differ diff --git a/frontend/app/assets/images/icons/update.png b/frontend/app/assets/images/icons/update.png new file mode 100644 index 00000000000..c5d887fec45 Binary files /dev/null and b/frontend/app/assets/images/icons/update.png differ diff --git a/frontend/app/assets/images/spinner.gif b/frontend/app/assets/images/spinner.gif new file mode 100644 index 00000000000..877f43f662d Binary files /dev/null and b/frontend/app/assets/images/spinner.gif differ diff --git a/frontend/app/assets/images/spree/frontend/cart.png b/frontend/app/assets/images/spree/frontend/cart.png new file mode 100755 index 00000000000..2408a293989 Binary files /dev/null and b/frontend/app/assets/images/spree/frontend/cart.png differ diff --git a/core/app/assets/images/store/select_arrow.gif b/frontend/app/assets/images/spree/frontend/select_arrow.gif similarity index 100% rename from core/app/assets/images/store/select_arrow.gif rename to frontend/app/assets/images/spree/frontend/select_arrow.gif diff --git a/frontend/app/assets/javascripts/spree/frontend.js b/frontend/app/assets/javascripts/spree/frontend.js new file mode 100644 index 00000000000..e6092d593fc --- /dev/null +++ b/frontend/app/assets/javascripts/spree/frontend.js @@ -0,0 +1,5 @@ +//= require jquery.validate/jquery.validate.min +//= require spree +//= require spree/frontend/checkout +//= require spree/frontend/product +//= require spree/frontend/cart diff --git a/frontend/app/assets/javascripts/spree/frontend/cart.js.coffee b/frontend/app/assets/javascripts/spree/frontend/cart.js.coffee new file mode 100644 index 00000000000..194dfbf4205 --- /dev/null +++ b/frontend/app/assets/javascripts/spree/frontend/cart.js.coffee @@ -0,0 +1,15 @@ +Spree.ready ($) -> + if ($ 'form#update-cart').is('*') + ($ 'form#update-cart a.delete').show().one 'click', -> + ($ this).parents('.line-item').first().find('input.line_item_quantity').val 0 + ($ this).parents('form').first().submit() + false + + ($ 'form#update-cart').submit -> + ($ 'form#update-cart #update-button').attr('disabled', true) + +Spree.fetch_cart = -> + $.ajax + url: Spree.pathFor("cart_link"), + success: (data) -> + $('#link-to-cart').html data diff --git a/frontend/app/assets/javascripts/spree/frontend/checkout.js.coffee b/frontend/app/assets/javascripts/spree/frontend/checkout.js.coffee new file mode 100644 index 00000000000..7b062b128a0 --- /dev/null +++ b/frontend/app/assets/javascripts/spree/frontend/checkout.js.coffee @@ -0,0 +1,11 @@ +//= require jquery.payment +//= require_self +//= require spree/frontend/checkout/address +//= require spree/frontend/checkout/payment + +Spree.disableSaveOnClick = -> + ($ 'form.edit_order').submit -> + ($ this).find(':submit, :image').attr('disabled', true).removeClass('primary').addClass 'disabled' + +Spree.ready ($) -> + Spree.Checkout = {} diff --git a/frontend/app/assets/javascripts/spree/frontend/checkout/address.js.coffee b/frontend/app/assets/javascripts/spree/frontend/checkout/address.js.coffee new file mode 100644 index 00000000000..7fe8ee3ff99 --- /dev/null +++ b/frontend/app/assets/javascripts/spree/frontend/checkout/address.js.coffee @@ -0,0 +1,83 @@ +Spree.ready ($) -> + Spree.onAddress = () -> + if ($ '#checkout_form_address').is('*') + ($ '#checkout_form_address').validate() + + getCountryId = (region) -> + $('#' + region + 'country select').val() + + Spree.updateState = (region) -> + countryId = getCountryId(region) + if countryId? + unless Spree.Checkout[countryId]? + $.get Spree.routes.states_search, {country_id: countryId}, (data) -> + Spree.Checkout[countryId] = + states: data.states + states_required: data.states_required + Spree.fillStates(Spree.Checkout[countryId], region) + else + Spree.fillStates(Spree.Checkout[countryId], region) + + Spree.fillStates = (data, region) -> + statesRequired = data.states_required + states = data.states + + statePara = ($ '#' + region + 'state') + stateSelect = statePara.find('select') + stateInput = statePara.find('input') + stateSpanRequired = statePara.find('[id$="state-required"]') + if states.length > 0 + selected = parseInt stateSelect.val() + stateSelect.html '' + statesWithBlank = [{ name: '', id: ''}].concat(states) + $.each statesWithBlank, (idx, state) -> + opt = ($ document.createElement('option')).attr('value', state.id).html(state.name) + opt.prop 'selected', true if selected is state.id + stateSelect.append opt + + stateSelect.prop('disabled', false).show() + stateInput.hide().prop 'disabled', true + statePara.show() + stateSpanRequired.show() + stateSelect.addClass('required') if statesRequired + stateSelect.removeClass('hidden') + stateInput.removeClass('required') + else + stateSelect.hide().prop 'disabled', true + stateInput.show() + if statesRequired + stateSpanRequired.show() + stateInput.addClass('required') + else + stateInput.val '' + stateSpanRequired.hide() + stateInput.removeClass('required') + statePara.toggle(!!statesRequired) + stateInput.prop('disabled', !statesRequired) + stateInput.removeClass('hidden') + stateSelect.removeClass('required') + + ($ '#bcountry select').change -> + Spree.updateState 'b' + + ($ '#scountry select').change -> + Spree.updateState 's' + + Spree.updateState 'b' + + order_use_billing = ($ 'input#order_use_billing') + order_use_billing.change -> + update_shipping_form_state order_use_billing + + update_shipping_form_state = (order_use_billing) -> + if order_use_billing.is(':checked') + ($ '#shipping .inner').hide() + ($ '#shipping .inner input, #shipping .inner select').prop 'disabled', true + else + ($ '#shipping .inner').show() + ($ '#shipping .inner input, #shipping .inner select').prop 'disabled', false + Spree.updateState('s') + + update_shipping_form_state order_use_billing + + Spree.onAddress() diff --git a/frontend/app/assets/javascripts/spree/frontend/checkout/payment.js.coffee b/frontend/app/assets/javascripts/spree/frontend/checkout/payment.js.coffee new file mode 100644 index 00000000000..70f7cb0b706 --- /dev/null +++ b/frontend/app/assets/javascripts/spree/frontend/checkout/payment.js.coffee @@ -0,0 +1,79 @@ +Spree.ready ($) -> + Spree.onPayment = () -> + if ($ '#checkout_form_payment').is('*') + + if ($ '#existing_cards').is('*') + ($ '#payment-method-fields').hide() + ($ '#payment-methods').hide() + + ($ '#use_existing_card_yes').click -> + ($ '#payment-method-fields').hide() + ($ '#payment-methods').hide() + ($ '.existing-cc-radio').prop("disabled", false) + + ($ '#use_existing_card_no').click -> + ($ '#payment-method-fields').show() + ($ '#payment-methods').show() + ($ '.existing-cc-radio').prop("disabled", true) + + + $(".cardNumber").payment('formatCardNumber') + $(".cardExpiry").payment('formatCardExpiry') + $(".cardCode").payment('formatCardCVC') + + $(".cardNumber").change -> + $(this).parent().siblings(".ccType").val($.payment.cardType(@value)) + + ($ 'input[type="radio"][name="order[payments_attributes][][payment_method_id]"]').click(-> + ($ '#payment-methods li').hide() + ($ '#payment_method_' + @value).show() if @checked + ) + + ($ document).on('click', '#cvv_link', (event) -> + windowName = 'cvv_info' + windowOptions = 'left=20,top=20,width=500,height=500,toolbar=0,resizable=0,scrollbars=1' + window.open(($ this).attr('href'), windowName, windowOptions) + event.preventDefault() + ) + + # Activate already checked payment method if form is re-rendered + # i.e. if user enters invalid data + ($ 'input[type="radio"]:checked').click() + + $('#checkout_form_payment').submit -> + # Coupon code application may take a number of seconds. + # Informing the user that this is happening is a good way to indicate some progress to them. + # In addition to this, if the coupon code FAILS then they don't lose their just-entered payment data. + coupon_code_field = $('#order_coupon_code') + coupon_code = $.trim(coupon_code_field.val()) + if (coupon_code != '') + if $('#coupon_status').length == 0 + coupon_status = $("
        ") + coupon_code_field.parent().append(coupon_status) + else + coupon_status = $("#coupon_status") + + url = Spree.url(Spree.routes.apply_coupon_code(Spree.current_order_id), + { + order_token: Spree.current_order_token, + coupon_code: coupon_code + } + ) + + coupon_status.removeClass(); + $.ajax({ + async: false, + method: "PUT", + url: url, + success: (data) -> + coupon_code_field.val('') + coupon_status.addClass("success").html("Coupon code applied successfully.") + return true + error: (xhr) -> + handler = JSON.parse(xhr.responseText) + coupon_status.addClass("error").html(handler["error"]) + $('.continue').attr('disabled', false) + return false + }) + + Spree.onPayment() diff --git a/frontend/app/assets/javascripts/spree/frontend/product.js.coffee b/frontend/app/assets/javascripts/spree/frontend/product.js.coffee new file mode 100644 index 00000000000..6d2224b3943 --- /dev/null +++ b/frontend/app/assets/javascripts/spree/frontend/product.js.coffee @@ -0,0 +1,47 @@ +$ -> + Spree.addImageHandlers = -> + thumbnails = ($ '#product-images ul.thumbnails') + ($ '#main-image').data 'selectedThumb', ($ '#main-image img').attr('src') + thumbnails.find('li').eq(0).addClass 'selected' + thumbnails.find('a').on 'click', (event) -> + ($ '#main-image').data 'selectedThumb', ($ event.currentTarget).attr('href') + ($ '#main-image').data 'selectedThumbId', ($ event.currentTarget).parent().attr('id') + thumbnails.find('li').removeClass 'selected' + ($ event.currentTarget).parent('li').addClass 'selected' + false + + thumbnails.find('li').on 'mouseenter', (event) -> + ($ '#main-image img').attr 'src', ($ event.currentTarget).find('a').attr('href') + + thumbnails.find('li').on 'mouseleave', (event) -> + ($ '#main-image img').attr 'src', ($ '#main-image').data('selectedThumb') + + Spree.showVariantImages = (variantId) -> + ($ 'li.vtmb').hide() + ($ 'li.tmb-' + variantId).show() + currentThumb = ($ '#' + ($ '#main-image').data('selectedThumbId')) + if not currentThumb.hasClass('vtmb-' + variantId) + thumb = ($ ($ '#product-images ul.thumbnails li:visible.vtmb').eq(0)) + thumb = ($ ($ '#product-images ul.thumbnails li:visible').eq(0)) unless thumb.length > 0 + newImg = thumb.find('a').attr('href') + ($ '#product-images ul.thumbnails li').removeClass 'selected' + thumb.addClass 'selected' + ($ '#main-image img').attr 'src', newImg + ($ '#main-image').data 'selectedThumb', newImg + ($ '#main-image').data 'selectedThumbId', thumb.attr('id') + + Spree.updateVariantPrice = (variant) -> + variantPrice = variant.data('price') + ($ '.price.selling').text(variantPrice) if variantPrice + radios = ($ '#product-variants input[type="radio"]') + + if radios.length > 0 + selectedRadio = ($ '#product-variants input[type="radio"][checked="checked"]') + Spree.showVariantImages selectedRadio.attr('value') + Spree.updateVariantPrice selectedRadio + + Spree.addImageHandlers() + + radios.click (event) -> + Spree.showVariantImages @value + Spree.updateVariantPrice ($ this) diff --git a/frontend/app/assets/stylesheets/spree/frontend.css b/frontend/app/assets/stylesheets/spree/frontend.css new file mode 100644 index 00000000000..739d13cf2f1 --- /dev/null +++ b/frontend/app/assets/stylesheets/spree/frontend.css @@ -0,0 +1,6 @@ +/* +* This is a manifest file that includes stylesheets for spree_frontend + *= require normalize + *= require skeleton + *= require spree/frontend/screen +*/ diff --git a/frontend/app/assets/stylesheets/spree/frontend/_variables.scss b/frontend/app/assets/stylesheets/spree/frontend/_variables.scss new file mode 100644 index 00000000000..ae655a1d162 --- /dev/null +++ b/frontend/app/assets/stylesheets/spree/frontend/_variables.scss @@ -0,0 +1,61 @@ +/*--------------------------------------*/ +/* Colors +/*--------------------------------------*/ +$c_green: #8dba53 !default; /* Spree green */ +$c_red: #e45353 !default; /* Error red */ + +$layout_background_color: #FFFFFF !default; +$title_text_color: #404042 !default; +$body_text_color: #404042 !default; +$link_text_color: #00ADEE !default; + +$product_background_color: #FFFFFF !default; +$product_title_text_color: #404042 !default; +$product_body_text_color: #404042 !default; +$product_link_text_color: #BBBBBB !default; + +/*--------------------------------------*/ +/* Fonts import from remote +/*--------------------------------------*/ +@import url(//fonts.googleapis.com/css?family=Ubuntu:400,700,400italic,700italic|&subset=latin,cyrillic,greek,greek-ext,latin-ext,cyrillic-ext); + +/*--------------------------------------*/ +/* Font families +/*--------------------------------------*/ +$ff_base: 'Ubuntu', sans-serif !default; + +/*-------------------------------------- + | Font sizes + |-------------------------------------- + |- Navigation + | */ + $header_navigation_font_size: 14px !default; + $horizontal_navigation_font_size: 16px !default; + $main_navigation_header_font_size: 14px !default; + $main_navigation_font_size: 12px !default; +/*|------------------------------------ + |- Product Listing + | */ + $product_list_name_font_size: 12px !default; + $product_list_price_font_size: 16px !default; + $product_list_header_font_size: 20px !default; + $product_list_search_font_size: 14px !default; +/*|------------------------------------ + |- Product Details + | */ + $product_detail_name_font_size: 24px !default; + $product_detail_description_font_size: 12px !default; + $product_detail_price_font_size: 20px !default; + $product_detail_title_font_size: 14px !default; +/*|------------------------------------ + |- Basic + | */ + $heading_font_size: 24px !default; + $sub_heading_font_size: 14px !default; + $button_font_size: 12px !default; + $input_box_font_size: 13px !default; + $base_font_size: 12px !default; + $border_color: lighten($body_text_color, 60) !default; + $default_border: 1px solid $border_color !default; + $button_border_color: rgba(0, 138, 189, .75) !default; + $table_head_color: lighten($body_text_color, 60) !default; diff --git a/frontend/app/assets/stylesheets/spree/frontend/screen.css.scss b/frontend/app/assets/stylesheets/spree/frontend/screen.css.scss new file mode 100644 index 00000000000..06cb57334c8 --- /dev/null +++ b/frontend/app/assets/stylesheets/spree/frontend/screen.css.scss @@ -0,0 +1,1321 @@ +//= depend_on_asset "fontawesome-webfont.eot" +//= depend_on_asset "fontawesome-webfont.woff" +//= depend_on_asset "fontawesome-webfont.ttf" +//= depend_on_asset "fontawesome-webfont.svg" +//= depend_on_asset "spree/frontend/cart.png" + +@import 'spree/frontend/variables'; +@import 'font-awesome'; + +/*--------------------------------------*/ +/* Basic styles +/*--------------------------------------*/ +body { + font-family: $ff_base; + font-size: $base_font_size; + font-weight: 400; + color: $body_text_color; + line-height: 18px; + background-color: $layout_background_color; + -webkit-font-smoothing: antialiased; +} + +/* Line style */ +hr { + height: 0; + background-color: transparent; + color: transparent; + border: none; + border-bottom: $default_border; +} + +/* Custom text-selection colors (remove any text shadows: twitter.com/miketaylr/status/12228805301) */ +::-moz-selection{background: $link_text_color; color: $layout_background_color; text-shadow: none;} +::selection {background: $link_text_color; color: $layout_background_color; text-shadow: none;} + +/* j.mp/webkit-tap-highlight-color */ +a:link {-webkit-tap-highlight-color: $link_text_color;} + +ins {background-color: $link_text_color; color: $layout_background_color; text-decoration: none;} +mark {background-color: $link_text_color; color: $layout_background_color; font-style: italic; font-weight: bold;} + +/*--------------------------------------*/ +/* Links +/*--------------------------------------*/ +a { + text-decoration: none; + color: $link_text_color; + + &:hover { + color: darken($link_text_color, 10); + } + + &:active, &:focus { + outline: none; + } +} + +/*--------------------------------------*/ +/* Lists +/*--------------------------------------*/ +ul, ol { + margin-left: 0; + margin-top: 0; + -webkit-padding-start: 0px; + padding-left: 0; + list-style-position: inside; + + &.inline { + li { + display: inline-block; + } + } +} + +dl { + dt, dd { + display: inline-block; + width: 50%; + padding: 5px; + + &.odd { + background-color: $table_head_color; + } + } + dt { + font-weight: bold; + text-transform: uppercase; + } + dd { + margin-left: -23px; + } +} + +/*--------------------------------------*/ +/* Headers +/*--------------------------------------*/ +h1 { font-size: $heading_font_size; line-height: $heading_font_size + 10; } +h2 { font-size: $heading_font_size - 2; line-height: $heading_font_size - 2 + 10; } +h3 { font-size: $heading_font_size - 4; line-height: $heading_font_size - 4 + 10; } +h4 { font-size: $heading_font_size - 6; line-height: $heading_font_size - 6 + 10; } +h5 { font-size: $sub_heading_font_size; line-height: $sub_heading_font_size + 10; } +h6 { font-size: $sub_heading_font_size - 2; line-height: $sub_heading_font_size - 2 + 10; } + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + color: $title_text_color; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + margin-top: 0; + margin-bottom: 0; +} + +/*--------------------------------------*/ +/* Forms +/*--------------------------------------*/ +textarea, input[type="date"], +input[type="datetime"], input[type="datetime-local"], +input[type="email"], input[type="month"], input[type="number"], +input[type="password"], input[type="search"], input[type="tel"], +input[type="text"], input[type="time"], input[type="url"], +input[type="week"] { + border: $default_border; + padding: 5px; + font-family: $ff_base; + font-size: $input_box_font_size; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + &:active, &:focus { + border-color: $link_text_color; + outline: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + -o-box-shadow: none; + box-shadow: none; + } + + &.error { + border-color: $c_red; + } +} + +label.error { + display: block; + font-size: $base_font_size - 1; + color: $c_red; + margin-top: 3px; +} + +span.required { + color: $c_red; + font-weight: bold; + font-size: 1.2em; +} + +fieldset { + margin: 0; + min-width: 100%; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +input[type="submit"], input[type="button"], +input[type= "reset"], button, a.button { + background-color: $link_text_color; + background-image: none; + text-shadow: none; + color: $layout_background_color; + font-weight: bold; + font-size: $button_font_size; + font-family: $ff_base; + border: 1px solid $button_border_color; + padding: 6px 10px 5px; + vertical-align: top; + + -webkit-font-smoothing: antialiased; + + -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); + -khtml-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); + -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); + -o-box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); + -webkit-border-radius: 0px; + -khtml-border-radius: 0px; + -moz-border-radius: 0px; + -ms-border-radius: 0px; + -o-border-radius: 0px; + border-radius: 0px; + + &.large { + padding: 7px 10px; + font-size: $button_font_size + 2; + } + + &.gray { + background-color: lighten($body_text_color, 20); + border-color: lighten($body_text_color, 10); + } + + &:hover { + background-image: none; + background-color: $body_text_color; + border-color: $body_text_color; + color: $layout_background_color; + } +} + +.ie8 { + a.button { + line-height: 16px; + } +} + +input[type="checkbox"], label { + vertical-align: middle; +} + +a.button { + display: inline-block; + line-height: 15px; + margin-top: -2px; + vertical-align: bottom; +} + +/*--------------------------------------*/ +/* Footer +/*--------------------------------------*/ +footer#footer { + padding: 10px 0; + border-top: $default_border; +} + +/*--------------------------------------*/ +/* Paragraphs +/*--------------------------------------*/ +p { + padding: 10px 0; +} + +/*--------------------------------------*/ +/* Tables +/*--------------------------------------*/ +table { + thead { + background-color: $table_head_color; + text-transform: uppercase; + + tr { + th { + padding: 5px 10px; + } + } + } + + tbody, tfoot { + tr { + border-bottom: $default_border; + + td { + vertical-align: middle; + padding: 5px 10px; + } + + &.alt, &.odd { + background-color: lighten($link_text_color, 50); + } + } + } +} + +/*--------------------------------------*/ +/* Navigation +/*--------------------------------------*/ +nav#top-nav-bar { + text-align: right; + margin-top: 20px; + + ul { + li { + margin-bottom: 5px; + padding-left: 10px; + + a{ + font-weight: bold; + font-size: $header_navigation_font_size; + text-transform: uppercase; + } + } + } +} + +nav #main-nav-bar { + text-transform: uppercase; + font-weight: bold; + margin-top: 20px; + border-bottom: $default_border; + padding-bottom: 6px; + + li { + + a { + font-size: $horizontal_navigation_font_size; + padding: 5px; + } + + &:first-child { + a { + padding-left: 0 + } + } + + &#link-to-cart { + float: right; + padding-left: 24px; + background: image-url("spree/frontend/cart.png") no-repeat left center; + + &:hover { + border-color: $link_text_color; + + .amount { + border-color: $link_text_color; + } + } + + a { + font-weight: normal; + font-size: $horizontal_navigation_font_size; + color: $link_text_color; + + .amount { + font-size: $horizontal_navigation_font_size + 2; + font-weight: bold; + border-left: $default_border; + padding-left: 5px; + padding-bottom: 5px; + } + } + } + } +} + +figure#logo { + img { + padding-top:25px; + } +} + +nav#taxonomies { + .taxonomy-root { + text-transform: uppercase; + border-bottom: $default_border; + margin-bottom: 5px; + font-size: $main_navigation_header_font_size; + } + + .taxons-list { + li { + a { + font-size: $main_navigation_font_size + } + } + } +} + +#breadcrumbs { + border-bottom: $default_border; + padding: 3px 0; + margin-bottom: 15px; + + li { + a { + color: $link_text_color; + } + span { + text-transform: uppercase; + font-weight: bold; + } + } +} + +/*--------------------------------------*/ +/* Flash notices & errors +/*--------------------------------------*/ +.flash { + padding: 10px; + color: $layout_background_color; + font-weight: bold; + margin-bottom: 10px; + + &.notice { + background-color: $link_text_color; + } + &.success { + background-color: $c_green; + } + &.error { + background-color: $c_red; + } +} + +.errorExplanation { + @extend .flash.error; + @extend .flash; + + p { + font-weight: normal; + } + + ul { + list-style: disc outside; + margin-left: 30px; + + li { + font-weight: normal; + } + } +} + +/*--------------------------------------*/ +/* Main search bar +/*--------------------------------------*/ +#search-bar { + display: block; +} + +/*--------------------------------------*/ +/* Products +/*--------------------------------------*/ +[data-hook="product_show"] { + h6 { + font-size: $product_detail_title_font_size; + } +} + +.product-section-title { + text-transform: uppercase; + margin-top: 15px; +} + +.add-to-cart { + margin-top: 15px; + + input[type="number"] { + margin-right: 3px; + width: 60px; + vertical-align: middle; + padding: 8px 10px; + } +} + +span.price { + font-weight: bold; + color: $link_text_color; + + &.selling { + font-size: $product_detail_price_font_size; + } + &.diff { + font-weight: bold; + } +} + +.taxon-title { + font-size: $product_list_header_font_size; +} + +.search-results-title { + font-size: $product_list_search_font_size; +} + +ul#products { + &:after { + content: " "; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + height: 0; + } + + li { + text-align: center; + font-weight: bold; + margin-bottom: 20px; + + a { + display: block; + + &.info { + height: 35px; + margin-top: 5px; + font-size: $product_list_name_font_size; + color: $product_link_text_color; + border-bottom: $default_border; + overflow: hidden; + } + } + + .product-image { + border: $default_border; + padding: 5px; + min-height: 110px; + background-color: $product_background_color; + + &:hover { + border-color: $link_text_color; + } + + img { + max-width: 100%; /* Fluid images for product */ + } + + } + + .price { + color: $link_text_color; + font-size: $product_list_price_font_size; + padding-top: 5px; + display: block; + } + } +} + +.subtaxon-title { + text-transform: uppercase; + + a { + color: $link_text_color; + } +} + +.search-results-title { + text-transform: uppercase; + border-bottom: $default_border; + margin-bottom: 10px; +} + +#sidebar_products_search { + .navigation { + margin-bottom: 15px; + } + + .filter-title { + display: block; + font-weight: bold; + text-transform: uppercase; + border-bottom: 1px solid #ededed; + margin-bottom: 5px; + color: $link_text_color; + font-size: $base_font_size + 2; + line-height: 24px; + } +} + +.taxon { + overflow: hidden; +} + +#product-images { + #main-image { + text-align: center; + border: $default_border; + background-color: $product_background_color; + + img { + min-height: 240px; + max-width: 100%; /* Fluid images for product */ + } + } + #product-thumbnails { + li { + background-color: $product_background_color; + } + } +} + +#product-description { + .product-title { + border-bottom: $default_border; + margin-bottom: 15px; + color: $product_title_text_color; + font-size: $product_detail_name_font_size; + } + + [data-hook="product-description"] { + font-size: $product_detail_description_font_size; + color: $product_body_text_color; + } +} + +#product-thumbnails { + margin-top: 10px; + + li { + margin-right: 6px; + border: $default_border; + + img { + padding: 5px; + } + + &:hover, &.selected { + border-color: $link_text_color; + } + } +} + +#product-properties { + border: $default_border; + padding: 10px; + width: 100%; +} + +#product-variants { + ul { + li { + padding: 5px; + } + } +} + +#cart-form { + #inside-product-cart-form:after { + content: " "; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + height: 0; + } +} + +/*--------------------------------------*/ +/* Checkout +/*--------------------------------------*/ +.out-of-stock { + background: #df0000; + color: white; + padding: 5px; + padding-right: 10px; + font-weight: bold; +} + +.progress-steps { + list-style: decimal inside; + overflow: auto; + + li { + float: left; + margin-right: 20px; + font-weight: bold; + text-transform: uppercase; + padding: 5px 20px; + color: lighten($body_text_color, 20); + + &.current-first, &.current { + background-color: $link_text_color; + color: $layout_background_color; + } + + &.completed-first, &.completed { + background-color: $table_head_color; + color: $layout_background_color; + + a { + color: $layout_background_color; + } + + &:hover { + background-color: $link_text_color; + color: $layout_background_color; + + a { + color: $layout_background_color; + + &:hover { + color: $layout_background_color; + } + } + } + } + } +} + +#payment-methods { + list-style: none; + + li { + fieldset { + border: none; + padding: 0; + } + } +} + + + +#checkout-summary { + text-align: center; + border: $default_border; + margin-top: 23px; + margin-left: 0; + + h3 { + text-transform: uppercase; + font-size: $base_font_size + 2; + border-bottom: $default_border; + } + + table { + width: 100%; + + tr[data-hook="item_total"] { + td:last-child { + strong { + @extend span.price; + } + } + } + + tr[data-hook="order_total"] { + border-bottom: none; + }; + + #summary-order-total { + @extend span.price; + font-size: $base_font_size + 2; + } + } +} + +#billing, #shipping, #shipping_method, +#payment, #order_details, #order_summary { + margin-top: 10px; + border: $default_border; + padding: 10px; + + legend { + text-transform: uppercase; + font-weight: bold; + font-size: $base_font_size + 2; + color: $link_text_color; + padding: 5px; + margin-left: 15px; + } +} + +#billing { + input[type="text"], input[type="email"], + input[type="tel"], input[type="number"], + select, textarea { + width: 100%; + } +} + +#order_details, #order_summary { + padding: 0; + + div:last-child { + margin-left: -1px; + } + + .payment-info { + .cc-type { + img { + vertical-align: middle; + } + } + } + + td.price, td.total { + span { + @extend span.price; + } + } + + table tfoot { + text-align: right; + color: lighten($body_text_color, 20); + + tr { + border: none; + } + + &#order-total { + text-transform: uppercase; + font-size: $base_font_size + 4; + color: $body_text_color; + + tr { + border-top: $default_border; + + td { + padding: 10px; + } + } + } + + &#subtotal { + text-transform: uppercase; + font-size: $base_font_size + 4; + color: $body_text_color; + } + } + + .steps-data { + div.columns { + padding: 5px; + margin: 0; + + &:first-child { + margin-left: 10px; + } + } + + h6 { + border-bottom: $default_border; + margin-bottom: 5px; + } + } +} + +#shipping_method { + p { + &#minstrs { + clear: both; + + label { + width: 100%; + } + } + + label { + float: left; + font-weight: bold; + font-size: $base_font_size + 2; + margin-right: 40px; + padding: 5px; + } + } + + .shipment { + margin-bottom: 30px; + } + + .stock-shipping-method-title { + background-color: lighten($body_text_color, 70); + text-align: center; + text-transform: uppercase; + font-weight: normal; + font-size :11px; + } + + .stock-location { + text-align: center; + text-transform: uppercase; + font-size: 12px; + font-weight: normal; + background-color: $link_text_color; + color: white; + } + + .unshippable { + .stock-location { + background-color: $c_red; + } + } + + .shipping-methods { + list-style: none; + margin: 0; + padding: none; + + .shipping-method { + display: inline-block; + margin: 5px 10px 5px 0; + + label { + font-weight: bold; + + .rate-cost { + color: $link_text_color; + } + } + } + } + + table.stock-contents { + thead { + background-color: lighten($body_text_color, 70); + + th { + font-size: 11px; + font-weight: normal + } + } + tbody { + tr { + td { + text-align: center; + + &.item-name { + text-align: left; + } + } + } + } + } +} + +p[data-hook="use_billing"] { + float: right; + margin-top: -18px; + background-color: $layout_background_color; + padding: 5px; +} + +#coupon_status { + font-weight: bold; + font-size: 125%; + &.success { + color: $c_green; + } + &.error { + color: $c_red; + } +} + +/*--------------------------------------*/ +/* Cart +/*--------------------------------------*/ +table#cart-detail { + width: 100%; + border-collapse: separate; + + tbody#line_items { + tr { + + td[data-hook="cart_item_price"], td[data-hook="cart_item_total"] { + @extend span.price; + @extend span.price.selling; + } + td[data-hook="cart_item_quantity"] { + .line_item_quantity { + width: 65px; + } + } + td[data-hook="cart_item_delete"] { + .delete { + display: block; + width: 20px; + } + } + } + } +} + +div[data-hook="inside_cart_form"] { + .links { + margin-top: 15px; + text-align: right; + } + + #subtotal { + text-align: right; + text-transform: uppercase; + margin-top: 15px; + + span.order-total { + @extend span.price; + } + } +} + +#empty-cart { + margin-top: -50px; + float: left; +} + +.cart-subtotal, .cart-total { + background: #00ADEE; + + td h5 { + color: #fff; + } +} + +.adjustment:nth-child(even) { + background: #eaeaea; + +} + +/*--------------------------------------*/ +/* Account +/*--------------------------------------*/ +#existing-customer, #new-customer, #forgot-password { + h6 { + text-transform: uppercase; + } +} + +#registration { + h6 { + text-transform: uppercase; + } + + #existing-customer { + width: auto; + text-align: left; + } +} + +#user-info { + margin-bottom: 15px; + border: $default_border; + padding: 10px; + + dd { + margin-left: 0px; + } +} + +/*--------------------------------------*/ +/* Order +/*--------------------------------------*/ +#order_summary { + margin-top: 0; +} +#order { + p[data-hook="links"] { + margin-left: 10px; + overflow: auto; + } +} + +table.order-summary { + tbody { + tr { + td { + width: 10%; + text-align: center; + + &:first-child { + a { + text-transform: uppercase; + font-weight: bold; + color: $link_text_color; + } + } + } + } + } +} + +/* #Media Queries +================================================== */ + +/* Smaller than standard 960 (devices and browsers) */ +@media only screen and (max-width: 959px) { + +} + +/* Tablet Portrait size to standard 960 (devices and browsers) */ +@media only screen and (min-width: 768px) and (max-width: 959px) { + .container { + padding-left: 10px; + width: 758px; + } + footer#footer { + width: 748px; + } + p[data-hook="use_billing"] { + margin-top: -15px; + } +} + +/* All Mobile Sizes (devices and browser) */ +@media only screen and (max-width: 767px) { + + html { + -webkit-text-size-adjust: none; + } + + #order_details .steps-data div.columns, + #order_summary .steps-data div.columns { + padding: 0; + margin: 0; + + &:first-child { + margin: 0 + } + } + + nav#taxonomies { + text-align: center; + + ul { + padding-left: 0; + list-style: none; + } + } + + ul#nav-bar { + text-align: center; + } + + .steps-data div.columns { + margin-bottom: 15px; + text-align: center; + } + + #order_details, #order { + table[data-hook="order_details"] { + width: 100%; + } + } + + #update-cart { + #subtotal, .links { + width: 50%; + float: left; + text-align: left; + } + #subtotal { + text-align: right; + } + } +} + +/* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ +@media only screen and (min-width: 480px) and (max-width: 767px) { + + footer#footer { + width: auto; + } + + input, select { + vertical-align: baseline; + } + + figure#logo { + text-align: center; + } + + #link-to-login { + display: block; + text-align: center; + } + + #search-bar { + display: block; + text-align: center; + + select { + margin-bottom: 10px; + } + } + + ul#products { + margin-left: 0; + margin-right: -20px; + + li { + width: 133px; + margin-right: 10px; + } + } + + table#cart-detail, table[data-hook="order_details"] { + tbody { + tr { + td[data-hook="cart_item_description"], td[data-hook="order_item_description"] { + font-size: $base_font_size - 1; + line-height: 15px; + width: 100px; + + h4 { + font-size: $base_font_size + 2; + line-height: 17px; + margin-bottom: 10px; + } + } + td[data-hook="cart_item_price"], td[data-hook="cart_item_total"], + td[data-hook="order_item_price"], td[data-hook="order_item_total"] { + font-size: $base_font_size; + } + td[data-hook="cart_item_image"], td[data-hook="order_item_image"] { + img { + width: 70px; + } + } + } + } + } + +} + +@media only screen and (max-width: 767px) { + #empty-cart { + clear: both; + margin-top: 0; + float: none; + } +} + +@media only screen and (max-width: 767px) { + #empty-cart { + /* yes, this is ugly... */ + margin-top: 0 !important; + } +} + +/* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ +@media only screen and (max-width: 479px) { + + .progress-steps li { + padding: 0; + margin: 0; + width: 50%; + + span { + display: block; + padding: 10px 20px; + } + } + + #shipping_method p label { + float: none; + display: block; + text-align: center; + margin-right: 0; + } + + p[data-hook="use_billing"] { + float: none; + margin-top: 0; + } + + table#cart-detail, table[data-hook="order_details"] { + tbody { + tr { + td[data-hook="cart_item_description"], td[data-hook="order_item_description"] { + padding: 0 !important; + text-indent: -9999px; + + h4 { + display: none; + } + } + td[data-hook="cart_item_image"], td[data-hook="order_item_image"] { + img { + width: 70px; + } + } + td[data-hook="cart_item_price"], td[data-hook="cart_item_total"] { + font-size: $base_font_size + 2 !important; + } + } + } + } + + table.order-summary { + display: block; position: relative; width: 100%; + + thead { display: block; float: left; } + tbody { display: block; width: auto; position: relative; overflow-x: auto; white-space: nowrap; } + thead tr { display: block; } + th { display: block; } + tbody tr { display: inline-block; vertical-align: top; } + td { display: block; min-height: 1.25em; } + } + + figure#logo { + text-align: center; + } + + #link-to-login { + display: block; + text-align: center; + } + + #search-bar { + display: block; + text-align: center; + + select { + margin-bottom: 10px; + } + } + + aside#sidebar { + text-align: center; + + ul { + padding-left: 0; + + li { + list-style-type: none; + } + } + } + + ul#products { + li { + width: 142px; + margin-right: 15px; + + &.secondary, &.omega { + margin-right: 0; + } + } + } + + #content { + text-align: center; + } +} diff --git a/frontend/app/controllers/spree/checkout_controller.rb b/frontend/app/controllers/spree/checkout_controller.rb new file mode 100644 index 00000000000..6f315e1a8fe --- /dev/null +++ b/frontend/app/controllers/spree/checkout_controller.rb @@ -0,0 +1,170 @@ +module Spree + # This is somewhat contrary to standard REST convention since there is not + # actually a Checkout object. There's enough distinct logic specific to + # checkout which has nothing to do with updating an order that this approach + # is waranted. + class CheckoutController < Spree::StoreController + ssl_required + + before_action :load_order_with_lock + before_action :ensure_valid_state_lock_version, only: [:update] + before_action :set_state_if_present + + before_action :ensure_order_not_completed + before_action :ensure_checkout_allowed + before_action :ensure_sufficient_stock_lines + before_action :ensure_valid_state + + before_action :associate_user + before_action :check_authorization + before_action :apply_coupon_code + + before_action :setup_for_current_state + + helper 'spree/orders' + + rescue_from Spree::Core::GatewayError, :with => :rescue_from_spree_gateway_error + + # Updates the order and advances to the next state (when possible.) + def update + if @order.update_from_params(params, permitted_checkout_attributes, request.headers.env) + @order.temporary_address = !params[:save_user_address] + unless @order.next + flash[:error] = @order.errors.full_messages.join("\n") + redirect_to checkout_state_path(@order.state) and return + end + + if @order.completed? + @current_order = nil + flash.notice = Spree.t(:order_processed_successfully) + flash['order_completed'] = true + redirect_to completion_route + else + redirect_to checkout_state_path(@order.state) + end + else + render :edit + end + end + + private + def ensure_valid_state + unless skip_state_validation? + if (params[:state] && !@order.has_checkout_step?(params[:state])) || + (!params[:state] && !@order.has_checkout_step?(@order.state)) + @order.state = 'cart' + redirect_to checkout_state_path(@order.checkout_steps.first) + end + end + + # Fix for #4117 + # If confirmation of payment fails, redirect back to payment screen + if params[:state] == "confirm" && @order.payment_required? && @order.payments.valid.empty? + flash.keep + redirect_to checkout_state_path("payment") + end + end + + # Should be overriden if you have areas of your checkout that don't match + # up to a step within checkout_steps, such as a registration step + def skip_state_validation? + false + end + + def load_order_with_lock + @order = current_order(lock: true) + redirect_to spree.cart_path and return unless @order + end + + def ensure_valid_state_lock_version + if params[:order] && params[:order][:state_lock_version] + @order.with_lock do + unless @order.state_lock_version == params[:order].delete(:state_lock_version).to_i + flash[:error] = Spree.t(:order_already_updated) + redirect_to checkout_state_path(@order.state) and return + end + @order.increment!(:state_lock_version) + end + end + end + + def set_state_if_present + if params[:state] + redirect_to checkout_state_path(@order.state) if @order.can_go_to_state?(params[:state]) && !skip_state_validation? + @order.state = params[:state] + end + end + + def ensure_checkout_allowed + unless @order.checkout_allowed? + redirect_to spree.cart_path + end + end + + def ensure_order_not_completed + redirect_to spree.cart_path if @order.completed? + end + + def ensure_sufficient_stock_lines + if @order.insufficient_stock_lines.present? + flash[:error] = Spree.t(:inventory_error_flash_for_insufficient_quantity) + redirect_to spree.cart_path + end + end + + # Provides a route to redirect after order completion + def completion_route + spree.order_path(@order) + end + + def setup_for_current_state + method_name = :"before_#{@order.state}" + send(method_name) if respond_to?(method_name, true) + end + + def before_address + # if the user has a default address, a callback takes care of setting + # that; but if he doesn't, we need to build an empty one here + @order.bill_address ||= Address.build_default + @order.ship_address ||= Address.build_default if @order.checkout_steps.include?('delivery') + end + + def before_delivery + return if params[:order].present? + + packages = @order.shipments.map { |s| s.to_package } + @differentiator = Spree::Stock::Differentiator.new(@order, packages) + end + + def before_payment + if @order.checkout_steps.include? "delivery" + packages = @order.shipments.map { |s| s.to_package } + @differentiator = Spree::Stock::Differentiator.new(@order, packages) + @differentiator.missing.each do |variant, quantity| + @order.contents.remove(variant, quantity) + end + + # @order.contents.remove did transitively call reload in the past. + # Hiding the fact that the machine advanced already to "payment" state. + # + # As an intermediary step to optimize reloads out of high volume code path + # the reload was lifted here and will be removed by later passes. + @order.reload + end + + if try_spree_current_user && try_spree_current_user.respond_to?(:payment_sources) + @payment_sources = try_spree_current_user.payment_sources + end + end + + def rescue_from_spree_gateway_error(exception) + flash.now[:error] = Spree.t(:spree_gateway_error_flash_for_checkout) + @order.errors.add(:base, exception.message) + render :edit + end + + def check_authorization + authorize!(:edit, current_order, cookies.signed[:guest_token]) + end + end +end diff --git a/frontend/app/controllers/spree/content_controller.rb b/frontend/app/controllers/spree/content_controller.rb new file mode 100644 index 00000000000..45832017aa2 --- /dev/null +++ b/frontend/app/controllers/spree/content_controller.rb @@ -0,0 +1,23 @@ +module Spree + class ContentController < Spree::StoreController + # Don't serve local files or static assets + before_filter { render_404 if params[:path] =~ /(\.|\\)/ } + after_action :fire_visited_path, only: :show + + rescue_from ActionView::MissingTemplate, :with => :render_404 + + respond_to :html + + def show + render :action => params[:path] + end + + def cvv + render :layout => false + end + + def fire_visited_path + Spree::PromotionHandler::Page.new(current_order, params[:path]).activate + end + end +end diff --git a/frontend/app/controllers/spree/home_controller.rb b/frontend/app/controllers/spree/home_controller.rb new file mode 100644 index 00000000000..75e5e4436e5 --- /dev/null +++ b/frontend/app/controllers/spree/home_controller.rb @@ -0,0 +1,12 @@ +module Spree + class HomeController < Spree::StoreController + helper 'spree/products' + respond_to :html + + def index + @searcher = build_searcher(params.merge(include_images: true)) + @products = @searcher.retrieve_products + @taxonomies = Spree::Taxonomy.includes(root: :children) + end + end +end diff --git a/frontend/app/controllers/spree/locale_controller.rb b/frontend/app/controllers/spree/locale_controller.rb new file mode 100644 index 00000000000..2975bd24187 --- /dev/null +++ b/frontend/app/controllers/spree/locale_controller.rb @@ -0,0 +1,16 @@ +module Spree + class LocaleController < Spree::StoreController + def set + if request.referer && request.referer.starts_with?('http://' + request.host) + session['user_return_to'] = request.referer + end + if params[:locale] && I18n.available_locales.map(&:to_s).include?(params[:locale]) + session[:locale] = I18n.locale = params[:locale] + flash.notice = Spree.t(:locale_changed) + else + flash[:error] = Spree.t(:locale_not_changed) + end + redirect_back_or_default(spree.root_path) + end + end +end diff --git a/frontend/app/controllers/spree/orders_controller.rb b/frontend/app/controllers/spree/orders_controller.rb new file mode 100644 index 00000000000..8e892ac7f44 --- /dev/null +++ b/frontend/app/controllers/spree/orders_controller.rb @@ -0,0 +1,100 @@ +module Spree + class OrdersController < Spree::StoreController + ssl_required :show + + before_action :check_authorization + rescue_from ActiveRecord::RecordNotFound, :with => :render_404 + helper 'spree/products', 'spree/orders' + + respond_to :html + + before_action :assign_order_with_lock, only: :update + before_action :apply_coupon_code, only: :update + skip_before_action :verify_authenticity_token, only: [:populate] + + def show + @order = Order.find_by_number!(params[:id]) + end + + def update + if @order.contents.update_cart(order_params) + respond_with(@order) do |format| + format.html do + if params.has_key?(:checkout) + @order.next if @order.cart? + redirect_to checkout_state_path(@order.checkout_steps.first) + else + redirect_to cart_path + end + end + end + else + respond_with(@order) + end + end + + # Shows the current incomplete order from the session + def edit + @order = current_order || Order.incomplete.find_or_initialize_by(guest_token: cookies.signed[:guest_token]) + associate_user + end + + # Adds a new item to the order (creating a new order if none already exists) + def populate + populator = Spree::OrderPopulator.new(current_order(create_order_if_necessary: true), current_currency) + if populator.populate(params[:variant_id], params[:quantity], params[:options]) + respond_with(@order) do |format| + format.html { redirect_to cart_path } + end + else + flash[:error] = populator.errors.full_messages.join(" ") + redirect_back_or_default(spree.root_path) + end + end + + def empty + if @order = current_order + @order.empty! + end + + redirect_to spree.cart_path + end + + def accurate_title + if @order && @order.completed? + Spree.t(:order_number, :number => @order.number) + else + Spree.t(:shopping_cart) + end + end + + def check_authorization + cookies.permanent.signed[:guest_token] = params[:token] if params[:token] + order = Spree::Order.find_by_number(params[:id]) || current_order + + if order + authorize! :edit, order, cookies.signed[:guest_token] + else + authorize! :create, Spree::Order + end + end + + private + + def order_params + if params[:order] + params[:order].permit(*permitted_order_attributes) + else + {} + end + end + + def assign_order_with_lock + @order = current_order(lock: true) + unless @order + flash[:error] = Spree.t(:order_not_found) + redirect_to root_path and return + end + end + end +end diff --git a/frontend/app/controllers/spree/products_controller.rb b/frontend/app/controllers/spree/products_controller.rb new file mode 100644 index 00000000000..c94e00369d7 --- /dev/null +++ b/frontend/app/controllers/spree/products_controller.rb @@ -0,0 +1,45 @@ +module Spree + class ProductsController < Spree::StoreController + before_action :load_product, only: :show + before_action :load_taxon, only: :index + + rescue_from ActiveRecord::RecordNotFound, :with => :render_404 + helper 'spree/taxons' + + respond_to :html + + def index + @searcher = build_searcher(params.merge(include_images: true)) + @products = @searcher.retrieve_products + @taxonomies = Spree::Taxonomy.includes(root: :children) + end + + def show + @variants = @product.variants_including_master.active(current_currency).includes([:option_values, :images]) + @product_properties = @product.product_properties.includes(:property) + @taxon = Spree::Taxon.find(params[:taxon_id]) if params[:taxon_id] + end + + private + def accurate_title + if @product + @product.meta_title.blank? ? @product.name : @product.meta_title + else + super + end + end + + def load_product + if try_spree_current_user.try(:has_spree_role?, "admin") + @products = Product.with_deleted + else + @products = Product.active(current_currency) + end + @product = @products.friendly.find(params[:id]) + end + + def load_taxon + @taxon = Spree::Taxon.find(params[:taxon]) if params[:taxon].present? + end + end +end diff --git a/frontend/app/controllers/spree/store_controller.rb b/frontend/app/controllers/spree/store_controller.rb new file mode 100644 index 00000000000..7c9fecb160a --- /dev/null +++ b/frontend/app/controllers/spree/store_controller.rb @@ -0,0 +1,40 @@ +module Spree + class StoreController < Spree::BaseController + include Spree::Core::ControllerHelpers::Order + + skip_before_action :set_current_order, only: :cart_link + + def unauthorized + render 'spree/shared/unauthorized', :layout => Spree::Config[:layout], :status => 401 + end + + def cart_link + render :partial => 'spree/shared/link_to_cart' + fresh_when(simple_current_order) + end + + protected + # This method is placed here so that the CheckoutController + # and OrdersController can both reference it (or any other controller + # which needs it) + def apply_coupon_code + if params[:order] && params[:order][:coupon_code] + @order.coupon_code = params[:order][:coupon_code] + + handler = PromotionHandler::Coupon.new(@order).apply + + if handler.error.present? + flash.now[:error] = handler.error + respond_with(@order) { |format| format.html { render :edit } } and return + elsif handler.success + flash[:success] = handler.success + end + end + end + + def config_locale + Spree::Frontend::Config[:locale] + end + end +end + diff --git a/frontend/app/controllers/spree/taxons_controller.rb b/frontend/app/controllers/spree/taxons_controller.rb new file mode 100644 index 00000000000..4f12e94930e --- /dev/null +++ b/frontend/app/controllers/spree/taxons_controller.rb @@ -0,0 +1,28 @@ +module Spree + class TaxonsController < Spree::StoreController + rescue_from ActiveRecord::RecordNotFound, :with => :render_404 + helper 'spree/products' + + respond_to :html + + def show + @taxon = Taxon.find_by_permalink!(params[:id]) + return unless @taxon + + @searcher = build_searcher(params.merge(taxon: @taxon.id, include_images: true)) + @products = @searcher.retrieve_products + @taxonomies = Spree::Taxonomy.includes(root: :children) + end + + private + + def accurate_title + if @taxon + @taxon.seo_title + else + super + end + end + + end +end diff --git a/frontend/app/models/spree/frontend_configuration.rb b/frontend/app/models/spree/frontend_configuration.rb new file mode 100644 index 00000000000..08fbc8d926b --- /dev/null +++ b/frontend/app/models/spree/frontend_configuration.rb @@ -0,0 +1,5 @@ +module Spree + class FrontendConfiguration < Preferences::Configuration + preference :locale, :string, :default => Rails.application.config.i18n.default_locale + end +end diff --git a/frontend/app/views/spree/address/_form.html.erb b/frontend/app/views/spree/address/_form.html.erb new file mode 100644 index 00000000000..60f9e992850 --- /dev/null +++ b/frontend/app/views/spree/address/_form.html.erb @@ -0,0 +1,75 @@ +<% address_id = address_type.chars.first %> +
        > +

        > + <%= form.label :firstname, Spree.t(:first_name) %>*
        + <%= form.text_field :firstname, :class => 'required' %> +

        +

        > + <%= form.label :lastname, Spree.t(:last_name) %>*
        + <%= form.text_field :lastname, :class => 'required' %> +

        + <% if Spree::Config[:company] %> +

        > + <%= form.label :company, Spree.t(:company) %>
        + <%= form.text_field :company %> +

        + <% end %> +

        > + <%= form.label :address1, Spree.t(:street_address) %>*
        + <%= form.text_field :address1, :class => 'required' %> +

        +

        > + <%= form.label :address2, Spree.t(:street_address_2) %>
        + <%= form.text_field :address2 %> +

        +

        > + <%= form.label :city, Spree.t(:city) %>*
        + <%= form.text_field :city, :class => 'required' %> +

        +

        > + <%= form.label :country_id, Spree.t(:country) %>*
        + > + <%= form.collection_select :country_id, available_countries, :id, :name, {}, {:class => 'required'} %> + +

        + + <% if Spree::Config[:address_requires_state] %> +

        > + <% have_states = !address.country.states.empty? %> + <%= form.label :state, Spree.t(:state) %>>*
        + + <% state_elements = [ + form.collection_select(:state_id, address.country.states, + :id, :name, + {:include_blank => true}, + {:class => have_states ? 'required' : 'hidden', + :disabled => !have_states}) + + form.text_field(:state_name, + :class => !have_states ? 'required' : 'hidden', + :disabled => have_states) + ].join.gsub('"', "'").gsub("\n", "") + %> + <%= javascript_tag do -%> + $('#<%="#{address_id}state" %>').append("<%== state_elements %>"); + <% end %> +

        + + <% end %> + +

        > + <%= form.label :zipcode, Spree.t(:zip) %><% if address.require_zipcode? %>*
        <% end %> + <%= form.text_field :zipcode, :class => "#{'required' if address.require_zipcode?}" %> +

        +

        > + <%= form.label :phone, Spree.t(:phone) %><% if address.require_phone? %>*
        <% end %> + <%= form.phone_field :phone, :class => "#{'required' if address.require_phone?}" %> +

        + <% if Spree::Config[:alternative_shipping_phone] %> +

        > + <%= form.label :alternative_phone, Spree.t(:alternative_phone) %>
        + <%= form.phone_field :alternative_phone %> +

        + <% end %> +
        diff --git a/frontend/app/views/spree/checkout/_address.html.erb b/frontend/app/views/spree/checkout/_address.html.erb new file mode 100644 index 00000000000..35bc394d977 --- /dev/null +++ b/frontend/app/views/spree/checkout/_address.html.erb @@ -0,0 +1,33 @@ +
        +
        + <%= form.fields_for :bill_address do |bill_form| %> + <%= Spree.t(:billing_address) %> + <%= render :partial => 'spree/address/form', :locals => { :form => bill_form, :address_type => 'billing', :address => @order.bill_address } %> + <% end %> +
        +
        + +
        +
        + <%= form.fields_for :ship_address do |ship_form| %> + <%= Spree.t(:shipping_address) %> +

        + <%= check_box_tag 'order[use_billing]', '1', @order.shipping_eq_billing_address? %> + <%= label_tag :order_use_billing, Spree.t(:use_billing_address), :id => 'use_billing' %> +

        + <%= render :partial => 'spree/address/form', :locals => { :form => ship_form, :address_type => 'shipping', :address => @order.ship_address } %> + <% end %> +
        +
        +
        + +
        + <%= submit_tag Spree.t(:save_and_continue), :class => 'continue button primary' %> + <% if try_spree_current_user %> + +     + <%= check_box_tag 'save_user_address', '1', try_spree_current_user.respond_to?(:persist_order_address) %> + <%= label_tag :save_user_address, Spree.t(:save_my_address) %> + + <% end %> +
        diff --git a/frontend/app/views/spree/checkout/_confirm.html.erb b/frontend/app/views/spree/checkout/_confirm.html.erb new file mode 100644 index 00000000000..6dbdddb62ca --- /dev/null +++ b/frontend/app/views/spree/checkout/_confirm.html.erb @@ -0,0 +1,12 @@ +
        +
        + <%= Spree.t(:confirm) %> + <%= render :partial => 'spree/shared/order_details', :locals => { :order => @order } %> +
        + +
        + +
        + <%= submit_tag Spree.t(:place_order), :class => 'continue button primary' %> + +
        diff --git a/frontend/app/views/spree/checkout/_delivery.html.erb b/frontend/app/views/spree/checkout/_delivery.html.erb new file mode 100644 index 00000000000..3b26948331b --- /dev/null +++ b/frontend/app/views/spree/checkout/_delivery.html.erb @@ -0,0 +1,100 @@ +
        + <%= Spree.t(:delivery) %> +
        +
        + <%= form.fields_for :shipments do |ship_form| %> + +
        +

        + <%= Spree.t(:package_from) %> + <%= ship_form.object.stock_location.name %> +

        + + + + + + + + + + + + + + + + <% ship_form.object.manifest.each do |item| %> + + + + + + + <% end %> + +
        <%= Spree.t(:item) %><%= Spree.t(:qty) %><%= Spree.t(:price) %>
        <%= mini_image(item.variant) %><%= item.variant.name %><%= item.quantity %><%= display_price(item.variant) %>
        + +
        <%= Spree.t(:shipping_method) %>
        +
          + <% ship_form.object.shipping_rates.each do |rate| %> +
        • + +
        • + <% end %> +
        +
        + + <% end %> + + <% if @differentiator.try(:missing?) %> +
        +

        + <%= Spree.t(:unshippable_items) %> +

        + + + + + + + + + + + + + + + <% @differentiator.missing.each do |variant, quantity| %> + + + + + + + <% end %> + +
        <%= Spree.t(:item) %><%= Spree.t(:qty) %><%= Spree.t(:price) %>
        <%= mini_image(variant) %><%= variant.name %><%= quantity %><%= display_price(variant) %>
        +
        + <% end %> + +
        + <% if Spree::Config[:shipping_instructions] %> +

        + <%= form.label :special_instructions, Spree.t(:shipping_instructions) %>
        + <%= form.text_area :special_instructions, :cols => 40, :rows => 7 %> +

        + <% end %> +
        +
        + +
        + +
        + <%= submit_tag Spree.t(:save_and_continue), :class => 'continue button primary' %> +
        diff --git a/frontend/app/views/spree/checkout/_payment.html.erb b/frontend/app/views/spree/checkout/_payment.html.erb new file mode 100644 index 00000000000..ab46674a13f --- /dev/null +++ b/frontend/app/views/spree/checkout/_payment.html.erb @@ -0,0 +1,72 @@ +
        + <%= Spree.t(:payment_information) %> +
        + + <% if @payment_sources.present? %> +
        + <%= radio_button_tag 'use_existing_card', 'yes', true %> + +
        + <%= radio_button_tag 'use_existing_card', 'no' %> + +
        + +
        +

        + + + <% @payment_sources.each do |card| %> + + + + + + + + <% end %> + +
        <%= card.name %><%= card.display_number %><%= card.month %><%= card.year %> + <%= radio_button_tag "order[existing_card]", card.id, (card == @payment_sources.first), { class: "existing-cc-radio" } %> +
        +

        +
        + <% end %> + +
        + <% @order.available_payment_methods.each do |method| %> +

        + +

        + <% end %> +
        + +
          + <% @order.available_payment_methods.each do |method| %> +
        • +
          + <%= render :partial => "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } %> +
          +
        • + <% end %> +
        +
        +

        + <%= form.label :coupon_code %>
        + <%= form.text_field :coupon_code %> +

        +
        +
        + +
        + +
        + <%= submit_tag Spree.t(:save_and_continue), :class => 'continue button primary' %> + +
        diff --git a/frontend/app/views/spree/checkout/_summary.html.erb b/frontend/app/views/spree/checkout/_summary.html.erb new file mode 100644 index 00000000000..a1469d35666 --- /dev/null +++ b/frontend/app/views/spree/checkout/_summary.html.erb @@ -0,0 +1,65 @@ +

        <%= Spree.t(:order_summary) %>

        + + + + + + + + + <% if order.line_item_adjustments.nonzero.exists? %> + + <% order.line_item_adjustments.nonzero.promotion.eligible.group_by(&:label).each do |label, adjustments| %> + + + + + <% end %> + + <% end %> + + + <% order.all_adjustments.nonzero.tax.eligible.group_by(&:label).each do |label, adjustments| %> + + + + + <% end %> + + + <% if order.passed_checkout_step?("delivery") && order.shipments.any? %> + + + + + + <% if order.shipment_adjustments.nonzero.exists? %> + + <% order.shipment_adjustments.nonzero.promotion.eligible.group_by(&:label).each do |label, adjustments| %> + + + + + <% end %> + + <% end %> + <% end %> + + <% if order.adjustments.nonzero.eligible.exists? %> + + <% order.adjustments.nonzero.eligible.each do |adjustment| %> + <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> + + + + + <% end %> + + <% end %> + + + + + + +
        <%= Spree.t(:item_total) %>:<%= order.display_item_total.to_html %>
        <%= label %><%= Spree::Money.new(adjustments.sum(&:amount), currency: order.currency).to_html %>
        <%= label %><%= Spree::Money.new(adjustments.sum(&:amount), currency: order.currency).to_html %>
        <%= Spree.t(:shipping_total) %>:<%= Spree::Money.new(order.shipments.to_a.sum(&:cost), currency: order.currency).to_html %>
        <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: order.currency).to_html %>
        <%= adjustment.label %>:<%= adjustment.display_amount.to_html %>
        <%= Spree.t(:order_total) %>:<%= order.display_total.to_html %>
        diff --git a/frontend/app/views/spree/checkout/edit.html.erb b/frontend/app/views/spree/checkout/edit.html.erb new file mode 100644 index 00000000000..eb52d227cd4 --- /dev/null +++ b/frontend/app/views/spree/checkout/edit.html.erb @@ -0,0 +1,37 @@ +
        + <%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> + +
        +

        <%= Spree.t(:checkout) %>

        +
        <%= checkout_progress %>
        +
        + +
        +
        + <%= form_for @order, :url => update_checkout_path(@order.state), :html => { :id => "checkout_form_#{@order.state}" } do |form| %> + <% if @order.state == 'address' || !@order.email? %> +

        + <%= form.label :email %>
        + <%= form.text_field :email %> +

        + <% end %> + <%= form.hidden_field :state_lock_version %> + <%= render @order.state, :form => form %> + <% end %> +
        + <% if @order.state != 'confirm' %> +
        + <%= render :partial => 'summary', :locals => { :order => @order } %> +
        + <% end %> +
        +
        + + + +<% if I18n.locale != :en %> + <%= javascript_include_tag 'jquery.validate/localization/messages_' + I18n.locale.to_s.downcase.gsub('-', '') %> +<% end %> diff --git a/frontend/app/views/spree/checkout/payment/_check.html.erb b/frontend/app/views/spree/checkout/payment/_check.html.erb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frontend/app/views/spree/checkout/payment/_gateway.html.erb b/frontend/app/views/spree/checkout/payment/_gateway.html.erb new file mode 100644 index 00000000000..e1765ba62b7 --- /dev/null +++ b/frontend/app/views/spree/checkout/payment/_gateway.html.erb @@ -0,0 +1,32 @@ +<%= image_tag 'credit_cards/credit_card.gif', :id => 'credit-card-image' %> +<% param_prefix = "payment_source[#{payment_method.id}]" %> + +

        + <%= label_tag "name_on_card_#{payment_method.id}", Spree.t(:name_on_card) %>*
        + <%= text_field_tag "#{param_prefix}[name]", "#{@order.billing_firstname} #{@order.billing_lastname}", { id: "name_on_card_#{payment_method.id}"} %> +

        + +

        + <%= label_tag "card_number", Spree.t(:card_number) %>*
        + <% options_hash = Rails.env.production? ? {:autocomplete => 'off'} : {} %> + <%= text_field_tag "#{param_prefix}[number]", '', options_hash.merge(:id => 'card_number', :class => 'required cardNumber', :size => 19, :maxlength => 19, :autocomplete => "off") %> +   + +

        + +

        + <%= label_tag "card_expiry", Spree.t(:expiration) %>*
        + <%= text_field_tag "#{param_prefix}[expiry]", '', :id => 'card_expiry', :class => "required cardExpiry", :placeholder => "MM / YY" %> +

        + +

        + <%= label_tag "card_code", Spree.t(:card_code) %>*
        + <%= text_field_tag "#{param_prefix}[verification_value]", '', options_hash.merge(:id => 'card_code', :class => 'required cardCode', :size => 5) %> + <%= link_to "(#{Spree.t(:what_is_this)})", spree.content_path('cvv'), :target => '_blank', "data-hook" => "cvv_link", :id => "cvv_link" %> +

        + +<%= hidden_field_tag "#{param_prefix}[cc_type]", '', :id => "cc_type", :class => 'ccType' %> diff --git a/core/app/views/spree/content/cvv.html.erb b/frontend/app/views/spree/content/cvv.html.erb similarity index 96% rename from core/app/views/spree/content/cvv.html.erb rename to frontend/app/views/spree/content/cvv.html.erb index 09c26fd2272..bd063cc8767 100644 --- a/core/app/views/spree/content/cvv.html.erb +++ b/frontend/app/views/spree/content/cvv.html.erb @@ -1,5 +1,5 @@
        -

        <%= t(:what_is_a_cvv) %>

        +

        <%= Spree.t(:what_is_a_cvv) %>

        For Visa, MasterCard, and Discover cards, the card code is the last 3 digit number located on the back of your card on or above your signature line. For an American Express card, it is the 4 digits on the FRONT above the end of your card number.

        To help reduce fraud in the card-not-present environment, credit card companies have introduced a card code program. Visa calls this code Card Verification Value (CVV); MasterCard calls it Card Validation Code (CVC); Discover calls it Card ID (CID). The card code is a three- or four- digit security code that is printed on the back of cards. The number typically appears at the end of the signature panel.

        Visa
        diff --git a/frontend/app/views/spree/content/test.html.erb b/frontend/app/views/spree/content/test.html.erb new file mode 100644 index 00000000000..1588c8ca646 --- /dev/null +++ b/frontend/app/views/spree/content/test.html.erb @@ -0,0 +1 @@ +Nothing to see here. Move along now. \ No newline at end of file diff --git a/frontend/app/views/spree/home/index.html.erb b/frontend/app/views/spree/home/index.html.erb new file mode 100644 index 00000000000..a79dcd918ab --- /dev/null +++ b/frontend/app/views/spree/home/index.html.erb @@ -0,0 +1,12 @@ +<% content_for :sidebar do %> +
        + <%= render :partial => 'spree/shared/taxonomies' %> +
        +<% end %> + +
        + <% cache(cache_key_for_products) do %> + <%= render :partial => 'spree/shared/products', :locals => { :products => @products } %> + <% end %> +
        + diff --git a/core/app/views/spree/layouts/spree_application.html.erb b/frontend/app/views/spree/layouts/spree_application.html.erb similarity index 93% rename from core/app/views/spree/layouts/spree_application.html.erb rename to frontend/app/views/spree/layouts/spree_application.html.erb index 6a02e4d2008..c9d7b0e5fb6 100644 --- a/core/app/views/spree/layouts/spree_application.html.erb +++ b/frontend/app/views/spree/layouts/spree_application.html.erb @@ -19,11 +19,13 @@ <%= render :partial => 'spree/shared/sidebar' if content_for? :sidebar %> -
        " data-hook> +
        " data-hook> <%= flash_messages %> <%= yield %>
        + <%= yield :templates %> +
        <%= render :partial => 'spree/shared/footer' %> @@ -31,6 +33,5 @@
        <%= render :partial => 'spree/shared/google_analytics' %> - diff --git a/frontend/app/views/spree/orders/_adjustment_row.html.erb b/frontend/app/views/spree/orders/_adjustment_row.html.erb new file mode 100644 index 00000000000..3306bc6f96b --- /dev/null +++ b/frontend/app/views/spree/orders/_adjustment_row.html.erb @@ -0,0 +1,8 @@ +<% if adjustments.sum(&:amount) != 0 %> + +
        <%= type %>: <%= label %>
        + +
        <%= Spree::Money.new(adjustments.sum(&:amount), :currency => @order.currency) %>
        + + +<% end %> diff --git a/frontend/app/views/spree/orders/_adjustments.html.erb b/frontend/app/views/spree/orders/_adjustments.html.erb new file mode 100644 index 00000000000..c3f0f69cba9 --- /dev/null +++ b/frontend/app/views/spree/orders/_adjustments.html.erb @@ -0,0 +1,24 @@ + + <% if @order.line_item_adjustments.exists? %> + <% @order.line_item_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> + <%= render "spree/orders/adjustment_row", :label => label, :adjustments => adjustments, :type => Spree.t(:promotion) %> + <% end %> + <% end %> + + <% @order.all_adjustments.tax.eligible.group_by(&:label).each do |label, adjustments| %> + <%= render "spree/orders/adjustment_row", :label => label, :adjustments => adjustments, :type => Spree.t(:tax) %> + <% end %> + + <% @order.shipments.each do |shipment| %> + +
        <%= Spree.t(:shipping) %>: <%= shipment.shipping_method.name %>
        + +
        <%= shipment.display_discounted_cost %>
        + + + <% end %> + + <% @order.adjustments.eligible.group_by(&:label).each do |label, adjustments| %> + <%= render "spree/orders/adjustment_row", :label => label, :adjustments => adjustments, :type => Spree.t(:adjustment) %> + <% end %> + diff --git a/frontend/app/views/spree/orders/_form.html.erb b/frontend/app/views/spree/orders/_form.html.erb new file mode 100644 index 00000000000..90c38710f34 --- /dev/null +++ b/frontend/app/views/spree/orders/_form.html.erb @@ -0,0 +1,28 @@ +<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @order } %> + + + + + + + + + + + + <%= render partial: 'spree/orders/line_item', collection: order_form.object.line_items, locals: {order_form: order_form} %> + + <% if @order.adjustments.nonzero.exists? || @order.line_item_adjustments.nonzero.exists? || @order.shipment_adjustments.nonzero.exists? || @order.shipments.any? %> + + + + + <%= render "spree/orders/adjustments" %> + <% end %> + + + + +
        <%= Spree.t(:item) %><%= Spree.t(:price) %><%= Spree.t(:qty) %><%= Spree.t(:total) %>
        <%= Spree.t(:cart_subtotal, :count => @order.line_items.sum(:quantity)) %>
        +
        <%= order_form.object.display_item_total %>
        <%= Spree.t(:total) %>
        +
        <%= order_form.object.display_total %>
        diff --git a/frontend/app/views/spree/orders/_line_item.html.erb b/frontend/app/views/spree/orders/_line_item.html.erb new file mode 100644 index 00000000000..ffc974dee62 --- /dev/null +++ b/frontend/app/views/spree/orders/_line_item.html.erb @@ -0,0 +1,36 @@ +<% variant = line_item.variant -%> +<%= order_form.fields_for :line_items, line_item do |item_form| -%> + + + <% if variant.images.length == 0 %> + <%= link_to small_image(variant.product), variant.product %> + <% else %> + <%= link_to image_tag(variant.images.first.attachment.url(:small)), variant.product %> + <% end %> + + +

        <%= link_to line_item.name, product_path(variant.product) %>

        + <%= variant.options_text %> + <% if line_item.insufficient_stock? %> + + <%= Spree.t(:out_of_stock) %>  
        +
        + <% end %> + + <%= line_item_description_text(line_item.description) %> + + + + <%= line_item.single_money.to_html %> + + + <%= item_form.number_field :quantity, :min => 0, :class => "line_item_quantity", :size => 5 %> + + + <%= line_item.display_amount.to_html unless line_item.quantity.nil? %> + + + <%= link_to image_tag('icons/delete.png'), '#', :class => 'delete', :id => "delete_#{dom_id(line_item)}" %> + + +<% end -%> diff --git a/frontend/app/views/spree/orders/edit.html.erb b/frontend/app/views/spree/orders/edit.html.erb new file mode 100644 index 00000000000..f0c907f6024 --- /dev/null +++ b/frontend/app/views/spree/orders/edit.html.erb @@ -0,0 +1,47 @@ +<% @body_id = 'cart' %> +
        +

        <%= Spree.t(:shopping_cart) %>

        + + <% if @order.line_items.empty? %> + +
        +

        <%= Spree.t(:your_cart_is_empty) %>

        +

        <%= link_to Spree.t(:continue_shopping), products_path, :class => 'button continue' %>

        +
        + + <% else %> + +
        + <%= form_for @order, :url => update_cart_path, :html => {:id => 'update-cart'} do |order_form| %> +
        + +
        + <%= render :partial => 'form', :locals => { :order_form => order_form } %> +
        + + + +
        + <% end %> +
        + +
        + <%= form_tag empty_cart_path, :method => :put do %> + + <% end %> +
        + + <% end %> +
        diff --git a/frontend/app/views/spree/orders/show.html.erb b/frontend/app/views/spree/orders/show.html.erb new file mode 100644 index 00000000000..d3c48c7ddf8 --- /dev/null +++ b/frontend/app/views/spree/orders/show.html.erb @@ -0,0 +1,22 @@ +
        + <%= Spree.t(:order_number, :number => @order.number) %> +

        <%= accurate_title %>

        + <% if order_just_completed?(@order) %> + <%= Spree.t(:thank_you_for_your_order) %> + <% end %> + +
        + <%= render :partial => 'spree/shared/order_details', :locals => { :order => @order } %> + +
        + +

        + <%= link_to Spree.t(:back_to_store), spree.root_path, :class => "button" %> + <% unless order_just_completed?(@order) %> + <% if try_spree_current_user && respond_to?(:spree_account_path) %> + <%= link_to Spree.t(:my_account), spree_account_path, :class => "button" %> + <% end %> + <% end %> +

        +
        +
        diff --git a/frontend/app/views/spree/payments/_payment.html.erb b/frontend/app/views/spree/payments/_payment.html.erb new file mode 100644 index 00000000000..018a53722cd --- /dev/null +++ b/frontend/app/views/spree/payments/_payment.html.erb @@ -0,0 +1,15 @@ +<% source = payment.source %> +<% if source.is_a?(Spree::CreditCard) %> + + <% unless (cc_type = source.cc_type).blank? %> + <%= image_tag "credit_cards/icons/#{cc_type}.png" %> + <% end %> + <% if source.last_digits %> + <%= Spree.t(:ending_in) %> <%= source.last_digits %> + <% end %> + +
        + <%= source.name %> +<% else %> + <%= content_tag(:span, payment.payment_method.name) %> +<% end %> diff --git a/frontend/app/views/spree/products/_cart_form.html.erb b/frontend/app/views/spree/products/_cart_form.html.erb new file mode 100644 index 00000000000..99cec2aeaf2 --- /dev/null +++ b/frontend/app/views/spree/products/_cart_form.html.erb @@ -0,0 +1,65 @@ +<%= form_for :order, :url => populate_orders_path do |f| %> +
        + + <% if @product.variants_and_option_values(current_currency).any? %> +
        +
        <%= Spree.t(:variants) %>
        +
          + <% @product.variants_and_option_values(current_currency).each_with_index do |variant, index| %> +
        • + <%= radio_button_tag "variant_id", variant.id, index == 0, 'data-price' => variant.price_in(current_currency).money %> + <%= label_tag "variant_id_#{ variant.id }" do %> + + <%= variant_options variant %> + + <% if variant_price variant %> + <%= variant_price variant %> + <% end %> + + <% unless variant.can_supply? %> + <%= Spree.t(:out_of_stock) %> + <% end %> + <% end %> +
        • + <% end%> +
        +
        + <% else %> + <%= hidden_field_tag "variant_id", @product.master.id %> + <% end %> + + <% if @product.price_in(current_currency) and !@product.price.nil? %> +
        + +
        +
        <%= Spree.t(:price) %>
        +
        + + <%= display_price(@product) %> + + +
        + + <% if @product.master.can_supply? %> + + <% elsif @product.variants.empty? %> +
        + <%= Spree.t(:out_of_stock) %> + <% end %> +
        + +
        + <%= number_field_tag :quantity, 1, :class => 'title', :min => 1 %> + <%= button_tag :class => 'large primary', :id => 'add-to-cart-button', :type => :submit do %> + <%= Spree.t(:add_to_cart) %> + <% end %> +
        +
        + <% else %> +
        +
        +
        <%= Spree.t('product_not_available_in_this_currency') %>
        +
        + <% end %> +
        +<% end %> diff --git a/core/app/views/spree/products/_image.html.erb b/frontend/app/views/spree/products/_image.html.erb similarity index 75% rename from core/app/views/spree/products/_image.html.erb rename to frontend/app/views/spree/products/_image.html.erb index 8f02df59e3f..5390cfb5b36 100644 --- a/core/app/views/spree/products/_image.html.erb +++ b/frontend/app/views/spree/products/_image.html.erb @@ -1,5 +1,5 @@ -<% if image %> +<% if defined?(image) && image %> <%= image_tag image.attachment.url(:product), :itemprop => "image" %> <% else %> <%= product_image(@product, :itemprop => "image") %> -<% end %> \ No newline at end of file +<% end %> diff --git a/frontend/app/views/spree/products/_promotions.html.erb b/frontend/app/views/spree/products/_promotions.html.erb new file mode 100644 index 00000000000..a67f4134838 --- /dev/null +++ b/frontend/app/views/spree/products/_promotions.html.erb @@ -0,0 +1,19 @@ +<% promotions = @product.possible_promotions %> +<% if promotions.any? %> +
        +

        <%= Spree.t(:promotions) %>

        + <% promotions.each do |promotion| %> +
        +

        <%= promotion.name %>

        +

        <%= promotion.description %>

        + <% if promotion.products.any? %> +
          + <% promotion.products.each do |product| %> +
        • <%= link_to product.name, product_path(product) %>
        • + <% end %> +
        + <% end %> +
        + <% end %> +
        +<% end %> diff --git a/core/app/views/spree/products/_properties.html.erb b/frontend/app/views/spree/products/_properties.html.erb similarity index 88% rename from core/app/views/spree/products/_properties.html.erb rename to frontend/app/views/spree/products/_properties.html.erb index 2b5f9d0093a..34b873f31b8 100644 --- a/core/app/views/spree/products/_properties.html.erb +++ b/frontend/app/views/spree/products/_properties.html.erb @@ -1,5 +1,5 @@ <% unless @product_properties.empty? %> -
        <%= t('properties')%>
        +
        <%= Spree.t('properties')%>
        <% @product_properties.each do |product_property| %> diff --git a/core/app/views/spree/products/_taxons.html.erb b/frontend/app/views/spree/products/_taxons.html.erb similarity index 80% rename from core/app/views/spree/products/_taxons.html.erb rename to frontend/app/views/spree/products/_taxons.html.erb index 8ace859d754..65f6c207c4d 100644 --- a/core/app/views/spree/products/_taxons.html.erb +++ b/frontend/app/views/spree/products/_taxons.html.erb @@ -1,6 +1,6 @@ <% if !@product.taxons.blank? %>
        -
        <%= t(:look_for_similar_items) %>
        +
        <%= Spree.t(:look_for_similar_items) %>
          diff --git a/frontend/app/views/spree/products/_thumbnails.html.erb b/frontend/app/views/spree/products/_thumbnails.html.erb new file mode 100644 index 00000000000..a0bb80322e3 --- /dev/null +++ b/frontend/app/views/spree/products/_thumbnails.html.erb @@ -0,0 +1,19 @@ +<%# no need for thumbnails unless there is more than one image %> +<% if (@product.images + @product.variant_images).uniq.size > 1 %> +
            + <% @product.images.each do |i| %> +
          • + <%= link_to(image_tag(i.attachment.url(:mini)), i.attachment.url(:product)) %> +
          • + <% end %> + + <% if @product.has_variants? %> + <% @product.variant_images.each do |i| %> + <% next if @product.images.include?(i) %> +
          • + <%= link_to(image_tag(i.attachment.url(:mini)), i.attachment.url(:product)) %> +
          • + <% end %> + <% end %> +
          +<% end %> diff --git a/frontend/app/views/spree/products/index.html.erb b/frontend/app/views/spree/products/index.html.erb new file mode 100644 index 00000000000..66c9bea34d6 --- /dev/null +++ b/frontend/app/views/spree/products/index.html.erb @@ -0,0 +1,27 @@ +<% content_for :sidebar do %> +
          + <% if "spree/products" == params[:controller] && @taxon %> + <%= render :partial => 'spree/shared/filters' %> + <% else %> + <%= render :partial => 'spree/shared/taxonomies' %> + <% end %> +
          +<% end %> + + +<% if params[:keywords] %> + +
          + <% if @products.empty? %> +
          <%= Spree.t(:no_products_found) %>
          + <% else %> + <%= render :partial => 'spree/shared/products', :locals => { :products => @products, :taxon => @taxon } %> + <% end %> +
          + +<% else %> + +
          + <%= render :partial => 'spree/shared/products', :locals => { :products => @products, :taxon => @taxon } %> +
          +<% end %> diff --git a/frontend/app/views/spree/products/show.html.erb b/frontend/app/views/spree/products/show.html.erb new file mode 100644 index 00000000000..4b5cf61bcd0 --- /dev/null +++ b/frontend/app/views/spree/products/show.html.erb @@ -0,0 +1,51 @@ +<% cache [I18n.locale, current_currency, @product, @product.possible_promotions] do %> +
          + <% @body_id = 'product-details' %> + +
          +
          + +
          +
          + <%= render :partial => 'image' %> +
          +
          + <%= render :partial => 'thumbnails' %> +
          +
          + +
          + <%= render :partial => 'properties' %> +
          + +
          + <%= render :partial => 'promotions' %> +
          + +
          +
          + +
          +
          + +
          + +

          <%= @product.name %>

          + +
          + <%= product_description(@product) rescue Spree.t(:product_has_no_description) %> +
          + +
          + <%= render :partial => 'cart_form' %> +
          + +
          + + <%= render :partial => 'taxons' %> + +
          +
          + +
          +<% end %> diff --git a/frontend/app/views/spree/shared/_address.html.erb b/frontend/app/views/spree/shared/_address.html.erb new file mode 100644 index 00000000000..a2e7f59888c --- /dev/null +++ b/frontend/app/views/spree/shared/_address.html.erb @@ -0,0 +1,38 @@ +
          +
          <%= address.full_name %>
          + <% unless address.company.blank? %> +
          + <%= address.company %> +
          + <% end %> +
          +
          +
          + <%= address.address1 %> +
          + <% unless address.address2.blank? %> +
          + <%= address.address2 %> +
          + <% end %> +
          +
          + <%= address.city %> + <%= address.state_text %> + <%= address.zipcode %> +
          <%= address.country.try(:name) %>
          +
          +
          + <% unless address.phone.blank? %> +
          + <%= Spree.t(:phone) %> + <%= address.phone %> +
          + <% end %> + <% unless address.alternative_phone.blank? %> +
          + <%= Spree.t(:alternative_phone) %> + <%= address.alternative_phone %> +
          + <% end %> +
          diff --git a/core/app/views/spree/shared/_filters.html.erb b/frontend/app/views/spree/shared/_filters.html.erb similarity index 75% rename from core/app/views/spree/shared/_filters.html.erb rename to frontend/app/views/spree/shared/_filters.html.erb index 7ae545bdddc..d74d7b6a444 100644 --- a/core/app/views/spree/shared/_filters.html.erb +++ b/frontend/app/views/spree/shared/_filters.html.erb @@ -1,7 +1,6 @@ -<% filters = @taxon ? @taxon.applicable_filters : [Spree::ProductFilters.all_taxons] %> +<% filters = @taxon ? @taxon.applicable_filters : [Spree::Core::ProductFilters.all_taxons] %> <% unless filters.empty? %> <%= form_tag '', :method => :get, :id => 'sidebar_products_search' do %> - <% params[:search] ||= {} %> <%= hidden_field_tag 'per_page', params[:per_page] %> <% filters.each do |filter| %> <% labels = filter[:labels] || filter[:conds].map {|m,c| [m,m]} %> @@ -16,13 +15,13 @@ id="<%= label %>" name="search[<%= filter[:scope].to_s %>][]" value="<%= val %>" - <%= params[:search][filter[:scope]] && params[:search][filter[:scope]].include?(val.to_s) ? "checked" : "" %> /> + <%= params[:search] && params[:search][filter[:scope]] && params[:search][filter[:scope]].include?(val.to_s) ? "checked" : "" %> /> <% end %>
        <% end %> - <%= submit_tag t(:search), :name => nil %> + <%= submit_tag Spree.t(:search), :name => nil %> <% end %> <% end %> diff --git a/frontend/app/views/spree/shared/_footer.html.erb b/frontend/app/views/spree/shared/_footer.html.erb new file mode 100644 index 00000000000..9b47fa88dad --- /dev/null +++ b/frontend/app/views/spree/shared/_footer.html.erb @@ -0,0 +1,6 @@ +
        + + +
        diff --git a/frontend/app/views/spree/shared/_google_analytics.html.erb b/frontend/app/views/spree/shared/_google_analytics.html.erb new file mode 100644 index 00000000000..d9b69ccbd95 --- /dev/null +++ b/frontend/app/views/spree/shared/_google_analytics.html.erb @@ -0,0 +1,40 @@ +<% if tracker = Spree::Tracker.current %> + +<% end %> diff --git a/frontend/app/views/spree/shared/_head.html.erb b/frontend/app/views/spree/shared/_head.html.erb new file mode 100644 index 00000000000..69043e4de04 --- /dev/null +++ b/frontend/app/views/spree/shared/_head.html.erb @@ -0,0 +1,14 @@ + +<%= title %> + + +<%== meta_data_tags %> +<%= canonical_tag(current_store.url) %> +<%= favicon_link_tag 'favicon.ico' %> +<%= stylesheet_link_tag 'spree/frontend/all', :media => 'screen' %> +<%= csrf_meta_tags %> +<%= javascript_include_tag 'spree/frontend/all' %> + +<%= yield :head %> diff --git a/core/app/views/spree/shared/_header.html.erb b/frontend/app/views/spree/shared/_header.html.erb similarity index 100% rename from core/app/views/spree/shared/_header.html.erb rename to frontend/app/views/spree/shared/_header.html.erb diff --git a/frontend/app/views/spree/shared/_link_to_cart.html.erb b/frontend/app/views/spree/shared/_link_to_cart.html.erb new file mode 100644 index 00000000000..8b1f86c54ad --- /dev/null +++ b/frontend/app/views/spree/shared/_link_to_cart.html.erb @@ -0,0 +1 @@ +<%= link_to_cart %> \ No newline at end of file diff --git a/frontend/app/views/spree/shared/_main_nav_bar.html.erb b/frontend/app/views/spree/shared/_main_nav_bar.html.erb new file mode 100644 index 00000000000..ba685192b08 --- /dev/null +++ b/frontend/app/views/spree/shared/_main_nav_bar.html.erb @@ -0,0 +1,12 @@ + diff --git a/core/app/views/spree/shared/_nav_bar.html.erb b/frontend/app/views/spree/shared/_nav_bar.html.erb similarity index 100% rename from core/app/views/spree/shared/_nav_bar.html.erb rename to frontend/app/views/spree/shared/_nav_bar.html.erb diff --git a/frontend/app/views/spree/shared/_order_details.html.erb b/frontend/app/views/spree/shared/_order_details.html.erb new file mode 100644 index 00000000000..86777e3ce75 --- /dev/null +++ b/frontend/app/views/spree/shared/_order_details.html.erb @@ -0,0 +1,138 @@ +
        + + <% if order.has_step?("address") %> + +
        +
        <%= Spree.t(:billing_address) %> <%= link_to "(#{Spree.t(:edit)})", checkout_state_path(:address) unless order.completed? %>
        + <%= render :partial => 'spree/shared/address', :locals => { :address => order.bill_address } %> +
        + + <% if order.has_step?("delivery") %> +
        +
        <%= Spree.t(:shipping_address) %> <%= link_to "(#{Spree.t(:edit)})", checkout_state_path(:address) unless order.completed? %>
        + <%= render :partial => 'spree/shared/address', :locals => { :address => order.ship_address } %> +
        + +
        +
        <%= Spree.t(:shipments) %> <%= link_to "(#{Spree.t(:edit)})", checkout_state_path(:delivery) unless order.completed? %>
        +
        + <% order.shipments.each do |shipment| %> +
        + + <%= Spree.t(:shipment_details, :stock_location => shipment.stock_location.name, :shipping_method => shipment.selected_shipping_rate.name) %> +
        + <% end %> +
        + <%= render(:partial => 'spree/shared/shipment_tracking', :locals => {:order => order}) if order.shipped? %> +
        + <% end %> + <% end %> + +
        +
        <%= Spree.t(:payment_information) %> <%= link_to "(#{Spree.t(:edit)})", checkout_state_path(:payment) unless order.completed? %>
        +
        + <% order.payments.valid.each do |payment| %> + <%= render payment%>
        + <% end %> +
        +
        + +
        + +
        + +
        + + + + + + + + + + + + + + + + + <% order.line_items.each do |item| %> + + + + + + + + <% end %> + + + + + + + + + + + + + + + + <% if order.line_item_adjustments.exists? %> + <% if order.all_adjustments.promotion.eligible.exists? %> + + <% order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> + + + + + <% end %> + + <% end %> + <% end %> + + + <% order.shipments.group_by { |s| s.selected_shipping_rate.name }.each do |name, shipments| %> + + + + + <% end %> + + + <% if order.all_adjustments.tax.exists? %> + + <% order.all_adjustments.tax.group_by(&:label).each do |label, adjustments| %> + + + + + <% end %> + + <% end %> + + + <% order.adjustments.eligible.each do |adjustment| %> + <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> + + + + + <% end %> + + +
        <%= Spree.t(:item) %><%= Spree.t(:price) %><%= Spree.t(:qty) %><%= Spree.t(:total) %>
        + <% if item.variant.images.length == 0 %> + <%= link_to small_image(item.variant.product), item.variant.product %> + <% else %> + <%= link_to image_tag(item.variant.images.first.attachment.url(:small)), item.variant.product %> + <% end %> + +

        <%= item.variant.product.name %>

        + <%= truncated_product_description(item.variant.product) %> + <%= "(" + item.variant.options_text + ")" unless item.variant.option_values.empty? %> +
        <%= item.single_money.to_html %><%= item.quantity %><%= item.display_amount.to_html %>
        <%= Spree.t(:order_total) %>:<%= order.display_total.to_html %>
        <%= Spree.t(:subtotal) %>:<%= order.display_item_total.to_html %>
        <%= Spree.t(:promotion) %>: <%= label %><%= Spree::Money.new(adjustments.sum(&:amount), currency: order.currency) %>
        <%= Spree.t(:shipping) %>: <%= name %><%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: order.currency).to_html %>
        <%= Spree.t(:tax) %>: <%= label %><%= Spree::Money.new(adjustments.sum(&:amount), currency: order.currency) %>
        <%= adjustment.label %><%= adjustment.display_amount.to_html %>
        diff --git a/frontend/app/views/spree/shared/_products.html.erb b/frontend/app/views/spree/shared/_products.html.erb new file mode 100644 index 00000000000..9fa0f7b06ae --- /dev/null +++ b/frontend/app/views/spree/shared/_products.html.erb @@ -0,0 +1,46 @@ +<% + paginated_products = @searcher.retrieve_products if params.key?(:keywords) + paginated_products ||= products +%> + +<% content_for :head do %> + <% if paginated_products.respond_to?(:num_pages) %> + <%= rel_next_prev_link_tags paginated_products %> + <% end %> +<% end %> + +
        + <% if products.empty? %> +
        + <%= Spree.t(:no_products_found) %> +
        + <% elsif params.key?(:keywords) %> +
        +
        <%= Spree.t(:search_results, :keywords => h(params[:keywords])) %>
        +
        + <% end %> +
        + +<% if products.any? %> +
          + <% products.each do |product| %> + <% url = spree.product_path(product, :taxon_id => @taxon.try(:id)) %> +
        • "classes") %>" data-hook="products_list_item" itemscope itemtype="http://schema.org/Product"> + <% cache(@taxon.present? ? [I18n.locale, current_currency, @taxon, product] : [I18n.locale, current_currency, product]) do %> +
          + <%= link_to small_image(product, :itemprop => "image"), url, :itemprop => 'url' %> +
          + <%= link_to truncate(product.name, :length => 50), url, :class => 'info', :itemprop => "name", :title => product.name %> + + <%= display_price(product) %> + + <% end %> +
        • + <% end %> + <% reset_cycle("classes") %> +
        +<% end %> + +<% if paginated_products.respond_to?(:num_pages) %> + <%= paginate paginated_products %> +<% end %> diff --git a/frontend/app/views/spree/shared/_search.html.erb b/frontend/app/views/spree/shared/_search.html.erb new file mode 100644 index 00000000000..fab80899137 --- /dev/null +++ b/frontend/app/views/spree/shared/_search.html.erb @@ -0,0 +1,11 @@ +<% @taxons = @taxon && @taxon.parent ? @taxon.parent.children : Spree::Taxon.roots %> +<%= form_tag spree.products_path, :method => :get do %> + <% cache(cache_key_for_taxons) do %> + <%= select_tag :taxon, + options_for_select([[Spree.t(:all_departments), '']] + + @taxons.map {|t| [t.name, t.id]}, + @taxon ? @taxon.id : params[:taxon]), 'aria-label' => 'Taxon' %> + <% end %> + <%= search_field_tag :keywords, params[:keywords], :placeholder => Spree.t(:search) %> + <%= submit_tag Spree.t(:search), :name => nil %> +<% end %> diff --git a/frontend/app/views/spree/shared/_shipment_tracking.html.erb b/frontend/app/views/spree/shared/_shipment_tracking.html.erb new file mode 100644 index 00000000000..850b36158a0 --- /dev/null +++ b/frontend/app/views/spree/shared/_shipment_tracking.html.erb @@ -0,0 +1,9 @@ +<% shipments = order.shipments.trackable %> +<% if shipments.any? %> +
        <%= Spree.t(:tracking) %>
        +
        + <% shipments.each do |shipment| %> + <%= link_to_tracking(shipment) %> + <% end %> +
        +<% end %> diff --git a/core/app/views/spree/shared/_sidebar.html.erb b/frontend/app/views/spree/shared/_sidebar.html.erb similarity index 100% rename from core/app/views/spree/shared/_sidebar.html.erb rename to frontend/app/views/spree/shared/_sidebar.html.erb diff --git a/frontend/app/views/spree/shared/_taxonomies.html.erb b/frontend/app/views/spree/shared/_taxonomies.html.erb new file mode 100644 index 00000000000..71e95e2a195 --- /dev/null +++ b/frontend/app/views/spree/shared/_taxonomies.html.erb @@ -0,0 +1,9 @@ +<% max_level = Spree::Config[:max_level_in_taxons_menu] || 1 %> + diff --git a/frontend/app/views/spree/shared/unauthorized.html.erb b/frontend/app/views/spree/shared/unauthorized.html.erb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frontend/app/views/spree/store/cart_link.html.erb b/frontend/app/views/spree/store/cart_link.html.erb new file mode 100644 index 00000000000..8b1f86c54ad --- /dev/null +++ b/frontend/app/views/spree/store/cart_link.html.erb @@ -0,0 +1 @@ +<%= link_to_cart %> \ No newline at end of file diff --git a/frontend/app/views/spree/taxons/_taxon.html.erb b/frontend/app/views/spree/taxons/_taxon.html.erb new file mode 100644 index 00000000000..9c3b94dfef9 --- /dev/null +++ b/frontend/app/views/spree/taxons/_taxon.html.erb @@ -0,0 +1,4 @@ +
        +
        <%= link_to taxon.name, seo_url(taxon), class: 'breadcrumbs' %>
        + <%= render partial: 'spree/shared/products', locals: { products: taxon_preview(taxon), taxon: taxon } %> +
        diff --git a/frontend/app/views/spree/taxons/show.html.erb b/frontend/app/views/spree/taxons/show.html.erb new file mode 100755 index 00000000000..d1c566af884 --- /dev/null +++ b/frontend/app/views/spree/taxons/show.html.erb @@ -0,0 +1,20 @@ +

        <%= @taxon.name %>

        + +<% content_for :sidebar do %> +
        + <%= render partial: 'spree/shared/taxonomies' %> + <%= render partial: 'spree/shared/filters' if @taxon.leaf? %> +
        +<% end %> + +
        + <%= render partial: 'spree/shared/products', locals: { products: @products, taxon: @taxon } %> +
        + +<% unless params[:keywords].present? %> +
        + <% cache [I18n.locale, @taxon] do %> + <%= render partial: 'taxon', collection: @taxon.children %> + <% end %> +
        +<% end %> diff --git a/frontend/config/initializers/assets.rb b/frontend/config/initializers/assets.rb new file mode 100644 index 00000000000..a485f14a866 --- /dev/null +++ b/frontend/config/initializers/assets.rb @@ -0,0 +1 @@ +Rails.application.config.assets.precompile += %w( favicon.ico spree/frontend/cart.png ) diff --git a/frontend/config/initializers/canonical_rails.rb b/frontend/config/initializers/canonical_rails.rb new file mode 100644 index 00000000000..157dccc5acc --- /dev/null +++ b/frontend/config/initializers/canonical_rails.rb @@ -0,0 +1,15 @@ +CanonicalRails.setup do |config| + # http://en.wikipedia.org/wiki/URL_normalization + # Trailing slash represents semantics of a directory, ie a collection view - implying an :index get route; + # otherwise we have to assume semantics of an instance of a resource type, a member view - implying a :show get route + # + # Acts as a whitelist for routes to have trailing slashes + + config.collection_actions# = [:index] + + # Parameter spamming can cause index dilution by creating seemingly different URLs with identical or near-identical content. + # Unless whitelisted, these parameters will be omitted + + config.whitelisted_parameters = [:keywords, :page, :search, :taxon] + +end diff --git a/frontend/config/initializers/check_for_orphaned_preferences.rb b/frontend/config/initializers/check_for_orphaned_preferences.rb new file mode 100644 index 00000000000..6ef43b7a366 --- /dev/null +++ b/frontend/config/initializers/check_for_orphaned_preferences.rb @@ -0,0 +1,9 @@ +begin + required_column_names = ["owner_id", "owner_type", "name"] + if (Spree::Preference.column_names & required_column_names) == required_column_names + ActiveRecord::Base.connection.execute("SELECT owner_id, owner_type, name, value FROM spree_preferences WHERE 'key' IS NULL").each do |pref| + warn "[WARNING] Orphaned preference `#{pref[2]}` with value `#{pref[3]}` for #{pref[1]} with id of: #{pref[0]}, you should reset the preference value manually." + end + end +rescue +end diff --git a/core/config/initializers/deprecation_checker.rb b/frontend/config/initializers/deprecation_checker.rb similarity index 100% rename from core/config/initializers/deprecation_checker.rb rename to frontend/config/initializers/deprecation_checker.rb diff --git a/core/config/initializers/rails_5868.rb b/frontend/config/initializers/rails_5868.rb similarity index 100% rename from core/config/initializers/rails_5868.rb rename to frontend/config/initializers/rails_5868.rb diff --git a/frontend/config/initializers/spree.rb b/frontend/config/initializers/spree.rb new file mode 100644 index 00000000000..31ccc8267bd --- /dev/null +++ b/frontend/config/initializers/spree.rb @@ -0,0 +1,4 @@ +require 'mail' + +# Spree Configuration +SESSION_KEY = '_spree_session_id' diff --git a/frontend/config/routes.rb b/frontend/config/routes.rb new file mode 100644 index 00000000000..10835d7ab46 --- /dev/null +++ b/frontend/config/routes.rb @@ -0,0 +1,37 @@ +Spree::Core::Engine.add_routes do + + root :to => 'home#index' + + resources :products, :only => [:index, :show] + + get '/locale/set', :to => 'locale#set' + + # non-restful checkout stuff + patch '/checkout/update/:state', :to => 'checkout#update', :as => :update_checkout + get '/checkout/:state', :to => 'checkout#edit', :as => :checkout_state + get '/checkout', :to => 'checkout#edit' , :as => :checkout + + populate_redirect = redirect do |params, request| + request.flash[:error] = Spree.t(:populate_get_error) + request.referer || '/cart' + end + + get '/orders/populate', :to => populate_redirect + get '/orders/:id/token/:token' => 'orders#show', :as => :token_order + + resources :orders, :except => [:index, :new, :create, :destroy] do + post :populate, :on => :collection + end + + get '/cart', :to => 'orders#edit', :as => :cart + patch '/cart', :to => 'orders#update', :as => :update_cart + put '/cart/empty', :to => 'orders#empty', :as => :empty_cart + + # route globbing for pretty nested taxon and product paths + get '/t/*id', :to => 'taxons#show', :as => :nested_taxons + + get '/unauthorized', :to => 'home#unauthorized', :as => :unauthorized + get '/content/cvv', :to => 'content#cvv', :as => :cvv + get '/content/*path', :to => 'content#show', :as => :content + get '/cart_link', :to => 'store#cart_link', :as => :cart_link +end diff --git a/frontend/lib/spree/frontend.rb b/frontend/lib/spree/frontend.rb new file mode 100644 index 00000000000..95a1a2a0338 --- /dev/null +++ b/frontend/lib/spree/frontend.rb @@ -0,0 +1,10 @@ +require 'rails/all' +require 'jquery-rails' +require 'canonical-rails' +require 'deface' + +require 'spree/core' + +require 'spree/responder' +require 'spree/frontend/middleware/seo_assist' +require 'spree/frontend/engine' diff --git a/frontend/lib/spree/frontend/engine.rb b/frontend/lib/spree/frontend/engine.rb new file mode 100644 index 00000000000..c1d734a95d2 --- /dev/null +++ b/frontend/lib/spree/frontend/engine.rb @@ -0,0 +1,19 @@ +module Spree + module Frontend + class Engine < ::Rails::Engine + config.middleware.use "Spree::Frontend::Middleware::SeoAssist" + + # sets the manifests / assets to be precompiled, even when initialize_on_precompile is false + initializer "spree.assets.precompile", :group => :all do |app| + app.config.assets.precompile += %w[ + spree/frontend/all* + jquery.validate/localization/messages_* + ] + end + + initializer "spree.frontend.environment", :before => :load_config_initializers do |app| + Spree::Frontend::Config = Spree::FrontendConfiguration.new + end + end + end +end diff --git a/core/lib/spree/core/middleware/seo_assist.rb b/frontend/lib/spree/frontend/middleware/seo_assist.rb similarity index 98% rename from core/lib/spree/core/middleware/seo_assist.rb rename to frontend/lib/spree/frontend/middleware/seo_assist.rb index 712172bbab6..6795e87db11 100644 --- a/core/lib/spree/core/middleware/seo_assist.rb +++ b/frontend/lib/spree/frontend/middleware/seo_assist.rb @@ -1,6 +1,6 @@ # Make redirects for SEO needs module Spree - module Core + module Frontend module Middleware class SeoAssist def initialize(app) diff --git a/core/lib/spree/core/preference_rescue.rb b/frontend/lib/spree/frontend/preference_rescue.rb similarity index 100% rename from core/lib/spree/core/preference_rescue.rb rename to frontend/lib/spree/frontend/preference_rescue.rb diff --git a/frontend/lib/spree_frontend.rb b/frontend/lib/spree_frontend.rb new file mode 100644 index 00000000000..46fe1ff18a0 --- /dev/null +++ b/frontend/lib/spree_frontend.rb @@ -0,0 +1 @@ +require 'spree/frontend' diff --git a/core/lib/tasks/rake_util.rb b/frontend/lib/tasks/rake_util.rb similarity index 100% rename from core/lib/tasks/rake_util.rb rename to frontend/lib/tasks/rake_util.rb diff --git a/core/lib/tasks/taxon.rake b/frontend/lib/tasks/taxon.rake similarity index 78% rename from core/lib/tasks/taxon.rake rename to frontend/lib/tasks/taxon.rake index fe7626283e2..6eee42c8b5b 100644 --- a/core/lib/tasks/taxon.rake +++ b/frontend/lib/tasks/taxon.rake @@ -1,7 +1,7 @@ namespace :spree do desc "Resets all taxon permalinks" task :reset_taxon_permalinks => :environment do - Taxon.where(:parent_id => nil).each {|taxon| redo_permalinks(taxon) } + Spree::Taxon.where(:parent_id => nil).each {|taxon| redo_permalinks(taxon) } end def redo_permalinks(taxon) diff --git a/frontend/script/rails b/frontend/script/rails new file mode 100755 index 00000000000..1a685aba06e --- /dev/null +++ b/frontend/script/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +ENGINE_ROOT = File.expand_path('../..', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/spree/frontend/engine', __FILE__) + +require 'rails/all' +require 'rails/engine/commands' + diff --git a/frontend/spec/controllers/controller_extension_spec.rb b/frontend/spec/controllers/controller_extension_spec.rb new file mode 100644 index 00000000000..d77d90b2bc3 --- /dev/null +++ b/frontend/spec/controllers/controller_extension_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +# This test tests the functionality within +# spree/core/controller_helpers/respond_with.rb +# Rather than duck-punching the existing controllers, let's define a custom one: +class Spree::CustomController < Spree::BaseController + def index + respond_with(Spree::Address.new) do |format| + format.html { render :text => "neutral" } + end + end + + def create + # Just need a model with validations + # Address is good enough, so let's go with that + address = Spree::Address.new(params[:address]) + respond_with(address) + end +end + +describe Spree::CustomController, :type => :controller do + after do + Spree::CustomController.clear_overrides! + end + + context "extension testing" do + context "index" do + context "specify symbol for handler instead of Proc" do + before do + Spree::CustomController.class_eval do + respond_override({:index => {:html => {:success => :success_method}}}) + + private + + def success_method + render :text => 'success!!!' + end + end + end + describe "GET" do + it "has value success" do + spree_get :index + expect(response).to be_success + assert (response.body =~ /success!!!/) + end + end + end + + context "render" do + before do + Spree::CustomController.instance_eval do + respond_override({:index => {:html => {:success => lambda { render(:text => 'success!!!') }}}}) + respond_override({:index => {:html => {:failure => lambda { render(:text => 'failure!!!') }}}}) + end + end + describe "GET" do + it "has value success" do + spree_get :index + expect(response).to be_success + assert (response.body =~ /success!!!/) + end + end + end + + context "redirect" do + before do + Spree::CustomController.instance_eval do + respond_override({:index => {:html => {:success => lambda { redirect_to('/cart') }}}}) + respond_override({:index => {:html => {:failure => lambda { render(:text => 'failure!!!') }}}}) + end + end + describe "GET" do + it "has value success" do + spree_get :index + expect(response).to be_redirect + end + end + end + + context "validation error" do + before do + Spree::CustomController.instance_eval do + respond_to :html + respond_override({:create => {:html => {:success => lambda { render(:text => 'success!!!') }}}}) + respond_override({:create => {:html => {:failure => lambda { render(:text => 'failure!!!') }}}}) + end + end + + describe "POST" do + it "has value success" do + spree_post :create + expect(response).to be_success + assert (response.body =~ /success!/) + end + end + end + + context 'A different controllers respond_override. Regression test for #1301' do + before do + Spree::CheckoutController.instance_eval do + respond_override({:index => {:html => {:success => lambda { render(:text => 'success!!!') }}}}) + end + end + describe "POST" do + it "should not effect the wrong controller" do + spree_get :index + assert (response.body =~ /neutral/) + end + end + end + + end + end + +end diff --git a/frontend/spec/controllers/controller_helpers_spec.rb b/frontend/spec/controllers/controller_helpers_spec.rb new file mode 100644 index 00000000000..250a2ea25c3 --- /dev/null +++ b/frontend/spec/controllers/controller_helpers_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +# In this file, we want to test that the controller helpers function correctly +# So we need to use one of the controllers inside Spree. +# ProductsController is good. +describe Spree::ProductsController, :type => :controller do + + before do + I18n.enforce_available_locales = false + expect(I18n).to receive(:available_locales).and_return([:en, :de]) + Spree::Frontend::Config[:locale] = :de + end + + after do + Spree::Frontend::Config[:locale] = :en + I18n.locale = :en + I18n.enforce_available_locales = true + end + + # Regression test for #1184 + it "sets the default locale based off Spree::Frontend::Config[:locale]" do + expect(I18n.locale).to eq(:en) + spree_get :index + expect(I18n.locale).to eq(:de) + end +end diff --git a/frontend/spec/controllers/spree/checkout_controller_spec.rb b/frontend/spec/controllers/spree/checkout_controller_spec.rb new file mode 100644 index 00000000000..04295ce8356 --- /dev/null +++ b/frontend/spec/controllers/spree/checkout_controller_spec.rb @@ -0,0 +1,424 @@ +require 'spec_helper' + +describe Spree::CheckoutController, :type => :controller do + let(:token) { 'some_token' } + let(:user) { stub_model(Spree::LegacyUser) } + let(:order) { FactoryGirl.create(:order_with_totals) } + + let(:address_params) do + address = FactoryGirl.build(:address) + address.attributes.except("created_at", "updated_at") + end + + before do + allow(controller).to receive_messages try_spree_current_user: user + allow(controller).to receive_messages spree_current_user: user + allow(controller).to receive_messages current_order: order + end + + context "#edit" do + it 'should check if the user is authorized for :edit' do + expect(controller).to receive(:authorize!).with(:edit, order, token) + request.cookie_jar.signed[:guest_token] = token + spree_get :edit, { state: 'address' } + end + + it "should redirect to the cart path unless checkout_allowed?" do + allow(order).to receive_messages :checkout_allowed? => false + spree_get :edit, { :state => "delivery" } + expect(response).to redirect_to(spree.cart_path) + end + + it "should redirect to the cart path if current_order is nil" do + allow(controller).to receive(:current_order).and_return(nil) + spree_get :edit, { :state => "delivery" } + expect(response).to redirect_to(spree.cart_path) + end + + it "should redirect to cart if order is completed" do + allow(order).to receive_messages(:completed? => true) + spree_get :edit, { :state => "address" } + expect(response).to redirect_to(spree.cart_path) + end + + # Regression test for #2280 + it "should redirect to current step trying to access a future step" do + order.update_column(:state, "address") + spree_get :edit, { :state => "delivery" } + expect(response).to redirect_to spree.checkout_state_path("address") + end + + context "when entering the checkout" do + before do + # The first step for checkout controller is address + # Transitioning into this state first is required + order.update_column(:state, "address") + end + + it "should associate the order with a user" do + order.update_column :user_id, nil + expect(order).to receive(:associate_user!).with(user) + spree_get :edit, {}, order_id: 1 + end + end + end + + context "#update" do + it 'should check if the user is authorized for :edit' do + expect(controller).to receive(:authorize!).with(:edit, order, token) + request.cookie_jar.signed[:guest_token] = token + spree_post :update, { state: 'address' } + end + + context "save successful" do + def spree_post_address + spree_post :update, { + :state => "address", + :order => { + :bill_address_attributes => address_params, + :use_billing => true + } + } + end + + before do + # Must have *a* shipping method and a payment method so updating from address works + allow(order).to receive_messages :available_shipping_methods => [stub_model(Spree::ShippingMethod)] + allow(order).to receive_messages :available_payment_methods => [stub_model(Spree::PaymentMethod)] + allow(order).to receive_messages :ensure_available_shipping_rates => true + order.line_items << FactoryGirl.create(:line_item) + end + + context "with the order in the cart state" do + before do + order.update_column(:state, "cart") + allow(order).to receive_messages :user => user + end + + it "should assign order" do + spree_post :update, {:state => "address"} + expect(assigns[:order]).not_to be_nil + end + + it "should advance the state" do + spree_post_address + expect(order.reload.state).to eq("delivery") + end + + it "should redirect the next state" do + spree_post_address + expect(response).to redirect_to spree.checkout_state_path("delivery") + end + + context "current_user respond to save address method" do + it "calls persist order address on user" do + expect(user).to receive(:persist_order_address) + spree_post :update, { + :state => "address", + :order => { + :bill_address_attributes => address_params, + :use_billing => true + }, + :save_user_address => "1" + } + end + end + + context "current_user doesnt respond to persist_order_address" do + it "doesnt raise any error" do + expect { + spree_post :update, { + :state => "address", + :order => { + :bill_address_attributes => address_params, + :use_billing => true + }, + :save_user_address => "1" + } + }.to_not raise_error + end + end + end + + context "with the order in the address state" do + before do + order.update_columns(ship_address_id: create(:address).id, state: "address") + allow(order).to receive_messages user: user + end + + context "with a billing and shipping address" do + before do + @expected_bill_address_id = order.bill_address.id + @expected_ship_address_id = order.ship_address.id + + spree_post :update, { + :state => "address", + :order => { + :bill_address_attributes => order.bill_address.attributes.except("created_at", "updated_at"), + :ship_address_attributes => order.ship_address.attributes.except("created_at", "updated_at"), + :use_billing => false + } + } + + order.reload + end + + it "updates the same billing and shipping address" do + expect(order.bill_address.id).to eq(@expected_bill_address_id) + expect(order.ship_address.id).to eq(@expected_ship_address_id) + end + end + end + + context "when in the confirm state" do + before do + allow(order).to receive_messages :confirmation_required? => true + order.update_column(:state, "confirm") + allow(order).to receive_messages :user => user + # An order requires a payment to reach the complete state + # This is because payment_required? is true on the order + create(:payment, :amount => order.total, :order => order) + order.payments.reload + end + + # This inadvertently is a regression test for #2694 + it "should redirect to the order view" do + spree_post :update, {:state => "confirm"} + expect(response).to redirect_to spree.order_path(order) + end + + it "should populate the flash message" do + spree_post :update, {:state => "confirm"} + expect(flash.notice).to eq(Spree.t(:order_processed_successfully)) + end + + it "should remove completed order from current_order" do + spree_post :update, {:state => "confirm"}, {:order_id => "foofah"} + expect(assigns(:current_order)).to be_nil + expect(assigns(:order)).to eql controller.current_order + end + end + + # Regression test for #4190 + context "state_lock_version" do + let(:post_params) { + { + state: "address", + order: { + bill_address_attributes: order.bill_address.attributes.except("created_at", "updated_at"), + state_lock_version: 0, + use_billing: true + } + } + } + + context "correct" do + it "should properly update and increment version" do + spree_post :update, post_params + expect(order.state_lock_version).to eq 1 + end + end + + context "incorrect" do + before do + order.update_columns(state_lock_version: 1, state: "address") + end + + it "order should receieve ensure_valid_order_version callback" do + expect_any_instance_of(described_class).to receive(:ensure_valid_state_lock_version) + spree_post :update, post_params + end + + it "order should receieve with_lock message" do + expect(order).to receive(:with_lock) + spree_post :update, post_params + end + + it "redirects back to current state" do + spree_post :update, post_params + expect(response).to redirect_to spree.checkout_state_path('address') + expect(flash[:error]).to eq "The order has already been updated." + end + end + end + end + + context "save unsuccessful" do + before do + allow(order).to receive_messages :user => user + allow(order).to receive_messages :update_attributes => false + end + + it "should not assign order" do + spree_post :update, {:state => "address"} + expect(assigns[:order]).not_to be_nil + end + + it "should not change the order state" do + spree_post :update, { :state => 'address' } + end + + it "should render the edit template" do + spree_post :update, { :state => 'address' } + expect(response).to render_template :edit + end + end + + context "when current_order is nil" do + before { allow(controller).to receive_messages :current_order => nil } + + it "should not change the state if order is completed" do + expect(order).not_to receive(:update_attribute) + spree_post :update, {:state => "confirm"} + end + + it "should redirect to the cart_path" do + spree_post :update, {:state => "confirm"} + expect(response).to redirect_to spree.cart_path + end + end + + context "Spree::Core::GatewayError" do + before do + allow(order).to receive_messages :user => user + allow(order).to receive(:update_attributes).and_raise(Spree::Core::GatewayError.new("Invalid something or other.")) + spree_post :update, {:state => "address"} + end + + it "should render the edit template and display exception message" do + expect(response).to render_template :edit + expect(flash.now[:error]).to eq(Spree.t(:spree_gateway_error_flash_for_checkout)) + expect(assigns(:order).errors[:base]).to include("Invalid something or other.") + end + end + + context "fails to transition from address" do + let(:order) do + FactoryGirl.create(:order_with_line_items).tap do |order| + order.next! + expect(order.state).to eq('address') + end + end + + before do + allow(controller).to receive_messages :current_order => order + allow(controller).to receive_messages :check_authorization => true + end + + context "when the country is not a shippable country" do + before do + order.ship_address.tap do |address| + # A different country which is not included in the list of shippable countries + address.country = FactoryGirl.create(:country, :name => "Australia") + address.state_name = 'Victoria' + address.save + end + end + + it "due to no available shipping rates for any of the shipments" do + expect(order.shipments.count).to eq(1) + order.shipments.first.shipping_rates.delete_all + spree_put :update, :order => {} + expect(flash[:error]).to eq(Spree.t(:items_cannot_be_shipped)) + expect(response).to redirect_to(spree.checkout_state_path('address')) + end + end + + context "when the order is invalid" do + before do + allow(order).to receive_messages :update_attributes => true, :next => nil + order.errors.add :base, 'Base error' + order.errors.add :adjustments, 'error' + end + + it "due to the order having errors" do + spree_put :update, :order => {} + expect(flash[:error]).to eq("Base error\nAdjustments error") + expect(response).to redirect_to(spree.checkout_state_path('address')) + end + end + end + + context "fails to transition from payment to complete" do + let(:order) do + FactoryGirl.create(:order_with_line_items).tap do |order| + until order.state == 'payment' + order.next! + end + # So that the confirmation step is skipped and we get straight to the action. + payment_method = FactoryGirl.create(:simple_credit_card_payment_method) + payment = FactoryGirl.create(:payment, :payment_method => payment_method) + order.payments << payment + end + end + + before do + allow(controller).to receive_messages :current_order => order + allow(controller).to receive_messages :check_authorization => true + end + + it "when GatewayError is raised" do + allow_any_instance_of(Spree::Payment).to receive(:process!).and_raise(Spree::Core::GatewayError.new(Spree.t(:payment_processing_failed))) + spree_put :update, :order => {} + expect(flash[:error]).to eq(Spree.t(:payment_processing_failed)) + end + end + end + + context "When last inventory item has been purchased" do + let(:product) { mock_model(Spree::Product, :name => "Amazing Object") } + let(:variant) { mock_model(Spree::Variant) } + let(:line_item) { mock_model Spree::LineItem, :insufficient_stock? => true, :amount => 0 } + let(:order) { create(:order) } + + before do + allow(order).to receive_messages(:line_items => [line_item], :state => "payment") + + configure_spree_preferences do |config| + config.track_inventory_levels = true + end + end + + context "and back orders are not allowed" do + before do + spree_post :update, { :state => "payment" } + end + + it "should redirect to cart" do + expect(response).to redirect_to spree.cart_path + end + + it "should set flash message for no inventory" do + expect(flash[:error]).to eq(Spree.t(:inventory_error_flash_for_insufficient_quantity , :names => "'#{product.name}'" )) + end + end + end + + context "order doesn't have a delivery step" do + before do + allow(order).to receive_messages(:checkout_steps => ["cart", "address", "payment"]) + allow(order).to receive_messages state: "address" + allow(controller).to receive_messages :check_authorization => true + end + + it "doesn't set shipping address on the order" do + expect(order).to_not receive(:ship_address=) + spree_post :update + end + + it "doesn't remove unshippable items before payment" do + expect { + spree_post :update, { :state => "payment" } + }.to_not change { order.line_items } + end + end + + it "does remove unshippable items before payment" do + allow(order).to receive_messages :payment_required? => true + allow(controller).to receive_messages :check_authorization => true + + expect { + spree_post :update, { :state => "payment" } + }.to change { order.reload.line_items.length } + end +end diff --git a/frontend/spec/controllers/spree/checkout_controller_with_views_spec.rb b/frontend/spec/controllers/spree/checkout_controller_with_views_spec.rb new file mode 100644 index 00000000000..56122a442bd --- /dev/null +++ b/frontend/spec/controllers/spree/checkout_controller_with_views_spec.rb @@ -0,0 +1,36 @@ +# encoding: utf-8 +require 'spec_helper' + +# This spec is useful for when we just want to make sure a view is rendering correctly +# Walking through the entire checkout process is rather tedious, don't you think? +describe Spree::CheckoutController, type: :controller do + render_views + let(:token) { 'some_token' } + let(:user) { stub_model(Spree::LegacyUser) } + + before do + allow(controller).to receive_messages try_spree_current_user: user + end + + # Regression test for #3246 + context "when using GBP" do + before do + Spree::Config[:currency] = "GBP" + end + + context "when order is in delivery" do + before do + # Using a let block won't acknowledge the currency setting + # Therefore we just do it like this... + order = OrderWalkthrough.up_to(:address) + allow(controller).to receive_messages current_order: order + end + + it "displays rate cost in correct currency" do + spree_get :edit + html = Nokogiri::HTML(response.body) + expect(html.css('.rate-cost').text).to eq "£10.00" + end + end + end +end diff --git a/frontend/spec/controllers/spree/content_controller_spec.rb b/frontend/spec/controllers/spree/content_controller_spec.rb new file mode 100644 index 00000000000..3f50e45ff10 --- /dev/null +++ b/frontend/spec/controllers/spree/content_controller_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +describe Spree::ContentController, :type => :controller do + it "should not display a local file" do + spree_get :show, :path => "../../Gemfile" + expect(response.response_code).to eq(404) + end + + it "should display CVV page" do + spree_get :cvv + expect(response.response_code).to eq(200) + end +end diff --git a/frontend/spec/controllers/spree/current_order_tracking_spec.rb b/frontend/spec/controllers/spree/current_order_tracking_spec.rb new file mode 100644 index 00000000000..3ec4a8a1993 --- /dev/null +++ b/frontend/spec/controllers/spree/current_order_tracking_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'current order tracking', :type => :controller do + let(:user) { create(:user) } + + controller(Spree::StoreController) do + def index + render :nothing => true + end + end + + let(:order) { FactoryGirl.create(:order) } + + it 'automatically tracks who the order was created by & IP address' do + allow(controller).to receive_messages(:try_spree_current_user => user) + get :index + expect(controller.current_order(create_order_if_necessary: true).created_by).to eq controller.try_spree_current_user + expect(controller.current_order.last_ip_address).to eq "0.0.0.0" + end + + context "current order creation" do + before { allow(controller).to receive_messages(:try_spree_current_user => user) } + + it "doesn't create a new order out of the blue" do + expect { + spree_get :index + }.not_to change { Spree::Order.count } + end + end +end + +describe Spree::OrdersController, :type => :controller do + let(:user) { create(:user) } + + before { allow(controller).to receive_messages(:try_spree_current_user => user) } + + describe Spree::OrdersController do + it "doesn't create a new order out of the blue" do + expect { + spree_get :edit + }.not_to change { Spree::Order.count } + end + end +end diff --git a/frontend/spec/controllers/spree/home_controller_spec.rb b/frontend/spec/controllers/spree/home_controller_spec.rb new file mode 100644 index 00000000000..7df8acc7922 --- /dev/null +++ b/frontend/spec/controllers/spree/home_controller_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Spree::HomeController, :type => :controller do + it "provides current user to the searcher class" do + user = mock_model(Spree.user_class, :last_incomplete_spree_order => nil, :spree_api_key => 'fake') + allow(controller).to receive_messages :try_spree_current_user => user + expect_any_instance_of(Spree::Config.searcher_class).to receive(:current_user=).with(user) + spree_get :index + expect(response.status).to eq(200) + end + + context "layout" do + it "renders default layout" do + spree_get :index + expect(response).to render_template(layout: 'spree/layouts/spree_application') + end + + context "different layout specified in config" do + before { Spree::Config.layout = 'layouts/application' } + + it "renders specified layout" do + spree_get :index + expect(response).to render_template(layout: 'layouts/application') + end + end + end +end diff --git a/frontend/spec/controllers/spree/orders_controller_ability_spec.rb b/frontend/spec/controllers/spree/orders_controller_ability_spec.rb new file mode 100644 index 00000000000..57335959f63 --- /dev/null +++ b/frontend/spec/controllers/spree/orders_controller_ability_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +module Spree + describe OrdersController, :type => :controller do + ORDER_TOKEN = 'ORDER_TOKEN' + + let(:user) { create(:user) } + let(:guest_user) { create(:user) } + let(:order) { Spree::Order.create } + + it 'should understand order routes with token' do + expect(spree.token_order_path('R123456', 'ABCDEF')).to eq('/orders/R123456/token/ABCDEF') + end + + context 'when an order exists in the cookies.signed' do + let(:token) { 'some_token' } + let(:specified_order) { create(:order) } + + before do + allow(controller).to receive_messages :current_order => order + allow(controller).to receive_messages :spree_current_user => user + end + + context '#populate' do + it 'should check if user is authorized for :edit' do + expect(controller).to receive(:authorize!).with(:edit, order, token) + spree_post :populate, :token => token + end + it "should check against the specified order" do + expect(controller).to receive(:authorize!).with(:edit, specified_order, token) + spree_post :populate, :id => specified_order.number, :token => token + end + end + + context '#edit' do + it 'should check if user is authorized for :edit' do + expect(controller).to receive(:authorize!).with(:edit, order, token) + spree_get :edit, :token => token + end + it "should check against the specified order" do + expect(controller).to receive(:authorize!).with(:edit, specified_order, token) + spree_get :edit, :id => specified_order.number, :token => token + end + end + + context '#update' do + it 'should check if user is authorized for :edit' do + allow(order).to receive :update_attributes + expect(controller).to receive(:authorize!).with(:edit, order, token) + spree_post :update, :order => { :email => "foo@bar.com" }, :token => token + end + it "should check against the specified order" do + allow(order).to receive :update_attributes + expect(controller).to receive(:authorize!).with(:edit, specified_order, token) + spree_post :update, :order => { :email => "foo@bar.com" }, :id => specified_order.number, :token => token + end + end + + context '#empty' do + it 'should check if user is authorized for :edit' do + expect(controller).to receive(:authorize!).with(:edit, order, token) + spree_post :empty, :token => token + end + it "should check against the specified order" do + expect(controller).to receive(:authorize!).with(:edit, specified_order, token) + spree_post :empty, :id => specified_order.number, :token => token + end + end + + context "#show" do + it "should check against the specified order" do + expect(controller).to receive(:authorize!).with(:edit, specified_order, token) + spree_get :show, :id => specified_order.number, :token => token + end + end + end + + context 'when no authenticated user' do + let(:order) { create(:order, :number => 'R123') } + + context '#show' do + context 'when token parameter present' do + it 'always ooverride existing token when passing a new one' do + cookies.signed[:guest_token] = "soo wrong" + spree_get :show, { :id => 'R123', :token => order.guest_token } + expect(cookies.signed[:guest_token]).to eq(order.guest_token) + end + + it 'should store as guest_token in session' do + spree_get :show, {:id => 'R123', :token => order.guest_token } + expect(cookies.signed[:guest_token]).to eq(order.guest_token) + end + end + + context 'when no token present' do + it 'should respond with 404' do + spree_get :show, {:id => 'R123'} + expect(response.code).to eq('404') + end + end + end + end + end +end diff --git a/frontend/spec/controllers/spree/orders_controller_spec.rb b/frontend/spec/controllers/spree/orders_controller_spec.rb new file mode 100644 index 00000000000..f74346930a3 --- /dev/null +++ b/frontend/spec/controllers/spree/orders_controller_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +describe Spree::OrdersController, :type => :controller do + let(:user) { create(:user) } + + context "Order model mock" do + let(:order) do + Spree::Order.create + end + + before do + allow(controller).to receive_messages(:try_spree_current_user => user) + end + + context "#populate" do + it "should create a new order when none specified" do + spree_post :populate, {}, {} + expect(cookies.signed[:guest_token]).not_to be_blank + expect(Spree::Order.find_by_guest_token(cookies.signed[:guest_token])).to be_persisted + end + + context "with Variant" do + let(:populator) { double('OrderPopulator') } + before do + expect(Spree::OrderPopulator).to receive(:new).and_return(populator) + end + + it "should handle population" do + expect(populator).to receive(:populate).with("2", "5", nil).and_return(true) + spree_post :populate, { :order_id => 1, :variant_id => 2, :quantity => 5 } + expect(response).to redirect_to spree.cart_path + end + + it "shows an error when population fails" do + request.env["HTTP_REFERER"] = spree.root_path + expect(populator).to receive(:populate).with("2", "5", nil).and_return(false) + allow(populator).to receive_message_chain(:errors, :full_messages).and_return(["Order population failed"]) + spree_post :populate, { :order_id => 1, :variant_id => 2, :quantity => 5 } + expect(flash[:error]).to eq("Order population failed") + expect(response).to redirect_to(spree.root_path) + end + end + end + + context "#update" do + context "with authorization" do + before do + allow(controller).to receive :check_authorization + allow(controller).to receive_messages current_order: order + end + + it "should render the edit view (on failure)" do + # email validation is only after address state + order.update_column(:state, "delivery") + spree_put :update, { :order => { :email => "" } }, { :order_id => order.id } + expect(response).to render_template :edit + end + + it "should redirect to cart path (on success)" do + allow(order).to receive(:update_attributes).and_return true + spree_put :update, {}, {:order_id => 1} + expect(response).to redirect_to(spree.cart_path) + end + end + end + + context "#empty" do + before do + allow(controller).to receive :check_authorization + end + + it "should destroy line items in the current order" do + allow(controller).to receive(:current_order).and_return(order) + expect(order).to receive(:empty!) + spree_put :empty + expect(response).to redirect_to(spree.cart_path) + end + end + + # Regression test for #2750 + context "#update" do + before do + allow(user).to receive :last_incomplete_spree_order + allow(controller).to receive :set_current_order + end + + it "cannot update a blank order" do + spree_put :update, :order => { :email => "foo" } + expect(flash[:error]).to eq(Spree.t(:order_not_found)) + expect(response).to redirect_to(spree.root_path) + end + end + end + + context "line items quantity is 0" do + let(:order) { Spree::Order.create } + let(:variant) { create(:variant) } + let!(:line_item) { order.contents.add(variant, 1) } + + before do + allow(controller).to receive(:check_authorization) + allow(controller).to receive_messages(:current_order => order) + end + + it "removes line items on update" do + expect(order.line_items.count).to eq 1 + spree_put :update, :order => { line_items_attributes: { "0" => { id: line_item.id, quantity: 0 } } } + expect(order.reload.line_items.count).to eq 0 + end + end +end diff --git a/frontend/spec/controllers/spree/orders_controller_transitions_spec.rb b/frontend/spec/controllers/spree/orders_controller_transitions_spec.rb new file mode 100644 index 00000000000..9645eb77dc5 --- /dev/null +++ b/frontend/spec/controllers/spree/orders_controller_transitions_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +Spree::Order.class_eval do + attr_accessor :did_transition +end + +module Spree + describe OrdersController, :type => :controller do + # Regression test for #2004 + context "with a transition callback on first state" do + let(:order) { Spree::Order.new } + + before do + allow(controller).to receive_messages :current_order => order + expect(controller).to receive(:authorize!).at_least(:once).and_return(true) + + first_state, _ = Spree::Order.checkout_steps.first + Spree::Order.state_machine.after_transition :to => first_state do |order| + order.did_transition = true + end + end + + it "correctly calls the transition callback" do + expect(order.did_transition).to be_nil + order.line_items << FactoryGirl.create(:line_item) + spree_put :update, { :checkout => "checkout" }, { :order_id => 1} + expect(order.did_transition).to be true + end + end + end +end diff --git a/frontend/spec/controllers/spree/products_controller_spec.rb b/frontend/spec/controllers/spree/products_controller_spec.rb new file mode 100644 index 00000000000..09b3852c83f --- /dev/null +++ b/frontend/spec/controllers/spree/products_controller_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Spree::ProductsController, :type => :controller do + let!(:product) { create(:product, :available_on => 1.year.from_now) } + + # Regression test for #1390 + it "allows admins to view non-active products" do + allow(controller).to receive_messages :spree_current_user => mock_model(Spree.user_class, :has_spree_role? => true, :last_incomplete_spree_order => nil, :spree_api_key => 'fake') + spree_get :show, :id => product.to_param + expect(response.status).to eq(200) + end + + it "cannot view non-active products" do + spree_get :show, :id => product.to_param + expect(response.status).to eq(404) + end + + it "should provide the current user to the searcher class" do + user = mock_model(Spree.user_class, :last_incomplete_spree_order => nil, :spree_api_key => 'fake') + allow(controller).to receive_messages :spree_current_user => user + expect_any_instance_of(Spree::Config.searcher_class).to receive(:current_user=).with(user) + spree_get :index + expect(response.status).to eq(200) + end + + # Regression test for #2249 + it "doesn't error when given an invalid referer" do + current_user = mock_model(Spree.user_class, :has_spree_role? => true, :last_incomplete_spree_order => nil, :generate_spree_api_key! => nil) + allow(controller).to receive_messages :spree_current_user => current_user + request.env['HTTP_REFERER'] = "not|a$url" + + # Previously a URI::InvalidURIError exception was being thrown + expect { spree_get :show, :id => product.to_param }.not_to raise_error + end + + # Regression tests for #2308 & Spree::Core::ControllerHelpers::SSL + context "force_ssl enabled" do + context "receive a SSL request" do + before do + request.env['HTTPS'] = 'on' + end + + it "should not redirect to http" do + expect(controller).not_to receive(:redirect_to) + spree_get :index + expect(request.protocol).to eql('https://') + end + end + end + + context "redirect_https_to_http enabled" do + before do + reset_spree_preferences do |config| + config.allow_ssl_in_development_and_test = true + config.redirect_https_to_http = true + end + end + + context "receives a non SSL request" do + it "should not redirect" do + expect(controller).not_to receive(:redirect_to) + spree_get :index + expect(request.protocol).to eql('http://') + end + end + + context "receives a SSL request" do + before do + request.env['HTTPS'] = 'on' + request.path = "/products?foo=bar" + end + + it "should redirect to http" do + spree_get :index + expect(response).to redirect_to("http://#{request.host}/products?foo=bar") + expect(response.status).to eq(301) + end + end + end +end diff --git a/frontend/spec/controllers/spree/taxons_controller_spec.rb b/frontend/spec/controllers/spree/taxons_controller_spec.rb new file mode 100644 index 00000000000..99ca8988bf9 --- /dev/null +++ b/frontend/spec/controllers/spree/taxons_controller_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Spree::TaxonsController, :type => :controller do + it "should provide the current user to the searcher class" do + taxon = create(:taxon, :permalink => "test") + user = mock_model(Spree.user_class, :last_incomplete_spree_order => nil, :spree_api_key => 'fake') + allow(controller).to receive_messages :spree_current_user => user + expect_any_instance_of(Spree::Config.searcher_class).to receive(:current_user=).with(user) + spree_get :show, :id => taxon.permalink + expect(response.status).to eq(200) + end +end diff --git a/frontend/spec/features/address_spec.rb b/frontend/spec/features/address_spec.rb new file mode 100644 index 00000000000..55d462bbc7f --- /dev/null +++ b/frontend/spec/features/address_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe "Address", type: :feature, inaccessible: true do + let!(:product) { create(:product, :name => "RoR Mug") } + let!(:order) { create(:order_with_totals, :state => 'cart') } + + stub_authorization! + + after do + Capybara.ignore_hidden_elements = true + end + + before do + Capybara.ignore_hidden_elements = false + + visit spree.root_path + + click_link "RoR Mug" + click_button "add-to-cart-button" + + address = "order_bill_address_attributes" + @country_css = "#{address}_country_id" + @state_select_css = "##{address}_state_id" + @state_name_css = "##{address}_state_name" + end + + context "country requires state", :js => true, :focus => true do + let!(:canada) { create(:country, :name => "Canada", :states_required => true, :iso => "CA") } + let!(:uk) { create(:country, :name => "United Kingdom", :states_required => true, :iso => "UK") } + + before { Spree::Config[:default_country_id] = uk.id } + + context "but has no state" do + it "shows the state input field" do + click_button "Checkout" + + select canada.name, :from => @country_css + expect(page).to have_selector(@state_select_css, visible: false) + expect(page).to have_selector(@state_name_css, visible: true) + expect(find(@state_name_css)['class']).not_to match(/hidden/) + expect(find(@state_name_css)['class']).to match(/required/) + expect(find(@state_select_css)['class']).not_to match(/required/) + expect(page).not_to have_selector("input#{@state_name_css}[disabled]") + end + end + + context "and has state" do + before { create(:state, :name => "Ontario", :country => canada) } + + it "shows the state collection selection" do + click_button "Checkout" + + select canada.name, :from => @country_css + expect(page).to have_selector(@state_select_css, visible: true) + expect(page).to have_selector(@state_name_css, visible: false) + expect(find(@state_select_css)['class']).to match(/required/) + expect(find(@state_select_css)['class']).not_to match(/hidden/) + expect(find(@state_name_css)['class']).not_to match(/required/) + end + end + + context "user changes to country without states required" do + let!(:france) { create(:country, :name => "France", :states_required => false, :iso => "FRA") } + + it "clears the state name" do + skip "This is failing on the CI server, but not when you run the tests manually... It also does not fail locally on a machine." + click_button "Checkout" + select canada.name, :from => @country_css + page.find(@state_name_css).set("Toscana") + + select france.name, :from => @country_css + expect(page.find(@state_name_css)).to have_content('') + until page.evaluate_script("$.active").to_i == 0 + expect(find(@state_name_css)['class']).not_to match(/hidden/) + expect(find(@state_name_css)['class']).not_to match(/required/) + expect(find(@state_select_css)['class']).not_to match(/required/) + end + end + end + end + + context "country does not require state", :js => true do + let!(:france) { create(:country, :name => "France", :states_required => false, :iso => "FRA") } + + it "shows a disabled state input field" do + click_button "Checkout" + + select france.name, :from => @country_css + expect(page).to have_selector(@state_select_css, visible: false) + expect(page).to have_selector(@state_name_css, visible: false) + end + end +end diff --git a/frontend/spec/features/automatic_promotion_adjustments_spec.rb b/frontend/spec/features/automatic_promotion_adjustments_spec.rb new file mode 100644 index 00000000000..3416046568c --- /dev/null +++ b/frontend/spec/features/automatic_promotion_adjustments_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe "Automatic promotions", :type => :feature, :js => true do + let!(:country) { create(:country, :name => "United States of America", :states_required => true) } + let!(:state) { create(:state, :name => "Alabama", :country => country) } + let!(:zone) { create(:zone) } + let!(:shipping_method) { create(:shipping_method) } + let!(:payment_method) { create(:check_payment_method) } + let!(:product) { create(:product, :name => "RoR Mug", :price => 20) } + + let!(:promotion) do + promotion = Spree::Promotion.create!(:name => "$10 off when you spend more than $100") + + calculator = Spree::Calculator::FlatRate.new + calculator.preferred_amount = 10 + + rule = Spree::Promotion::Rules::ItemTotal.create + rule.preferred_amount_min = 100 + rule.save + + promotion.rules << rule + + action = Spree::Promotion::Actions::CreateAdjustment.create + action.calculator = calculator + action.save + + promotion.actions << action + end + + context "on the cart page" do + + before do + visit spree.root_path + click_link product.name + click_button "add-to-cart-button" + end + + it "automatically applies the promotion once the order crosses the threshold" do + fill_in "order_line_items_attributes_0_quantity", :with => 10 + click_button "Update" + expect(page).to have_content("Promotion ($10 off when you spend more than $100) -$10.00") + fill_in "order_line_items_attributes_0_quantity", :with => 1 + click_button "Update" + expect(page).not_to have_content("Promotion ($10 off when you spend more than $100) -$10.00") + end + end +end diff --git a/frontend/spec/features/caching/products_spec.rb b/frontend/spec/features/caching/products_spec.rb new file mode 100644 index 00000000000..39375be72f5 --- /dev/null +++ b/frontend/spec/features/caching/products_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe 'products', :type => :feature, :caching => true do + let!(:product) { create(:product) } + let!(:product2) { create(:product) } + let!(:taxonomy) { create(:taxonomy) } + let!(:taxon) { create(:taxon, :taxonomy => taxonomy) } + + before do + product2.update_column(:updated_at, 1.day.ago) + # warm up the cache + visit spree.root_path + assert_written_to_cache("views/en/USD/spree/products/all--#{product.updated_at.utc.to_s(:number)}") + assert_written_to_cache("views/en/USD/spree/products/#{product.id}-#{product.updated_at.utc.to_s(:number)}") + assert_written_to_cache("views/en/spree/taxonomies/#{taxonomy.id}") + assert_written_to_cache("views/en/taxons/#{taxon.updated_at.utc.to_i}") + + clear_cache_events + end + + it "reads from cache upon a second viewing" do + visit spree.root_path + expect(cache_writes.count).to eq(0) + end + + it "busts the cache when a product is updated" do + product.update_column(:updated_at, 1.day.from_now) + visit spree.root_path + assert_written_to_cache("views/en/USD/spree/products/all--#{product.updated_at.utc.to_s(:number)}") + assert_written_to_cache("views/en/USD/spree/products/#{product.id}-#{product.updated_at.utc.to_s(:number)}") + expect(cache_writes.count).to eq(2) + end + + it "busts the cache when all products are deleted" do + product.destroy + product2.destroy + visit spree.root_path + assert_written_to_cache("views/en/USD/spree/products/all--#{Date.today.to_s(:number)}-0") + expect(cache_writes.count).to eq(1) + end + + it "busts the cache when the newest product is deleted" do + product.destroy + visit spree.root_path + assert_written_to_cache("views/en/USD/spree/products/all--#{product2.updated_at.utc.to_s(:number)}") + expect(cache_writes.count).to eq(1) + end + + it "busts the cache when an older product is deleted" do + product2.destroy + visit spree.root_path + assert_written_to_cache("views/en/USD/spree/products/all--#{product.updated_at.utc.to_s(:number)}") + expect(cache_writes.count).to eq(1) + end +end diff --git a/frontend/spec/features/caching/taxons_spec.rb b/frontend/spec/features/caching/taxons_spec.rb new file mode 100644 index 00000000000..442f30f27f6 --- /dev/null +++ b/frontend/spec/features/caching/taxons_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe 'taxons', :type => :feature, :caching => true do + let!(:taxonomy) { create(:taxonomy) } + let!(:taxon) { create(:taxon, :taxonomy => taxonomy) } + + before do + # warm up the cache + visit spree.root_path + assert_written_to_cache("views/en/spree/taxonomies/#{taxonomy.id}") + assert_written_to_cache("views/en/taxons/#{taxon.updated_at.utc.to_i}") + + clear_cache_events + end + + it "busts the cache when max_level_in_taxons_menu conf changes" do + Spree::Config[:max_level_in_taxons_menu] = 5 + visit spree.root_path + assert_written_to_cache("views/en/spree/taxonomies/#{taxonomy.id}") + expect(cache_writes.count).to eq(1) + end +end diff --git a/frontend/spec/features/cart_spec.rb b/frontend/spec/features/cart_spec.rb new file mode 100644 index 00000000000..37bb799fa89 --- /dev/null +++ b/frontend/spec/features/cart_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe "Cart", type: :feature, inaccessible: true do + it "shows cart icon on non-cart pages" do + visit spree.root_path + expect(page).to have_selector("li#link-to-cart a", :visible => true) + end + + it "prevents double clicking the remove button on cart", :js => true do + @product = create(:product, :name => "RoR Mug") + + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + + # prevent form submit to verify button is disabled + page.execute_script("$('#update-cart').submit(function(){return false;})") + + expect(page).not_to have_selector('button#update-button[disabled]') + page.find(:css, '.delete img').click + expect(page).to have_selector('button#update-button[disabled]') + end + + # Regression test for #2006 + it "does not error out with a 404 when GET'ing to /orders/populate" do + visit '/orders/populate' + within(".error") do + expect(page).to have_content(Spree.t(:populate_get_error)) + end + end + + it 'allows you to remove an item from the cart', :js => true do + create(:product, :name => "RoR Mug") + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + within("#line_items") do + click_link "delete_line_item_1" + end + expect(page).not_to have_content("Line items quantity must be an integer") + expect(page).not_to have_content("RoR Mug") + expect(page).to have_content("Your cart is empty") + + within "#link-to-cart" do + expect(page).to have_content("EMPTY") + end + end + + it 'allows you to empty the cart', js: true do + create(:product, :name => "RoR Mug") + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + + expect(page).to have_content("RoR Mug") + click_on "Empty Cart" + expect(page).to have_content("Your cart is empty") + + within "#link-to-cart" do + expect(page).to have_content("EMPTY") + end + end + + # regression for #2276 + context "product contains variants but no option values" do + let(:variant) { create(:variant) } + let(:product) { variant.product } + + before { variant.option_values.destroy_all } + + it "still adds product to cart", inaccessible: true do + visit spree.product_path(product) + click_button "add-to-cart-button" + + visit spree.cart_path + expect(page).to have_content(product.name) + end + end + it "should have a surrounding element with data-hook='cart_container'" do + visit spree.cart_path + expect(page).to have_selector("div[data-hook='cart_container']") + end +end diff --git a/frontend/spec/features/checkout_spec.rb b/frontend/spec/features/checkout_spec.rb new file mode 100644 index 00000000000..7317ede09c1 --- /dev/null +++ b/frontend/spec/features/checkout_spec.rb @@ -0,0 +1,508 @@ +require 'spec_helper' + +describe "Checkout", type: :feature, inaccessible: true do + + include_context 'checkout setup' + + context "visitor makes checkout as guest without registration" do + before(:each) do + stock_location.stock_items.update_all(count_on_hand: 1) + end + + context "defaults to use billing address" do + before do + add_mug_to_cart + Spree::Order.last.update_column(:email, "test@example.com") + click_button "Checkout" + end + + it "should default checkbox to checked", inaccessible: true do + expect(find('input#order_use_billing')).to be_checked + end + + it "should remain checked when used and visitor steps back to address step", :js => true do + fill_in_address + expect(find('input#order_use_billing')).to be_checked + end + end + + # Regression test for #4079 + context "persists state when on address page" do + before do + add_mug_to_cart + click_button "Checkout" + end + + specify do + expect(Spree::Order.count).to eq(1) + expect(Spree::Order.last.state).to eq("address") + end + end + + # Regression test for #1596 + context "full checkout" do + before do + shipping_method.calculator.update!(preferred_amount: 10) + mug.shipping_category = shipping_method.shipping_categories.first + mug.save! + end + + it "does not break the per-item shipping method calculator", :js => true do + add_mug_to_cart + click_button "Checkout" + + fill_in "order_email", :with => "test@example.com" + fill_in_address + + click_button "Save and Continue" + expect(page).not_to have_content("undefined method `promotion'") + click_button "Save and Continue" + expect(page).to have_content("Shipping total: $10.00") + end + end + + # Regression test for #4306 + context "free shipping" do + before do + add_mug_to_cart + click_button "Checkout" + end + + it "should not show 'Free Shipping' when there are no shipments" do + within("#checkout-summary") do + expect(page).to_not have_content('Free Shipping') + end + end + end + + # Regression test for #4190 + it "updates state_lock_version on form submission", js: true do + add_mug_to_cart + click_button "Checkout" + + expect(find('input#order_state_lock_version', visible: false).value).to eq "0" + + fill_in "order_email", with: "test@example.com" + fill_in_address + click_button "Save and Continue" + + expect(find('input#order_state_lock_version', visible: false).value).to eq "1" + end + end + + # Regression test for #2694 and #4117 + context "doesn't allow bad credit card numbers" do + before(:each) do + order = OrderWalkthrough.up_to(:delivery) + allow(order).to receive_messages :confirmation_required? => true + allow(order).to receive_messages(:available_payment_methods => [ create(:credit_card_payment_method, :environment => 'test') ]) + + user = create(:user) + order.user = user + order.update! + + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:current_order => order) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:try_spree_current_user => user) + end + + it "redirects to payment page", inaccessible: true do + visit spree.checkout_state_path(:delivery) + click_button "Save and Continue" + choose "Credit Card" + fill_in "Card Number", :with => '123' + fill_in "card_expiry", :with => '04 / 20' + fill_in "Card Code", :with => '123' + click_button "Save and Continue" + click_button "Place Order" + expect(page).to have_content("Bogus Gateway: Forced failure") + expect(page.current_url).to include("/checkout/payment") + end + end + + #regression test for #3945 + context "when Spree::Config[:always_include_confirm_step] is true" do + before do + Spree::Config[:always_include_confirm_step] = true + end + + it "displays confirmation step", :js => true do + add_mug_to_cart + click_button "Checkout" + + fill_in "order_email", :with => "test@example.com" + fill_in_address + + click_button "Save and Continue" + click_button "Save and Continue" + click_button "Save and Continue" + + continue_button = find(".continue") + expect(continue_button.value).to eq("Place Order") + end + end + + context "and likes to double click buttons" do + let!(:user) { create(:user) } + + let!(:order) do + order = OrderWalkthrough.up_to(:delivery) + allow(order).to receive_messages :confirmation_required? => true + + order.reload + order.user = user + order.update! + order + end + + before(:each) do + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:current_order => order) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:try_spree_current_user => user) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:skip_state_validation? => true) + end + + it "prevents double clicking the payment button on checkout", :js => true do + visit spree.checkout_state_path(:payment) + + # prevent form submit to verify button is disabled + page.execute_script("$('#checkout_form_payment').submit(function(){return false;})") + + expect(page).not_to have_selector('input.button[disabled]') + click_button "Save and Continue" + expect(page).to have_selector('input.button[disabled]') + end + + it "prevents double clicking the confirm button on checkout", :js => true do + order.payments << create(:payment) + visit spree.checkout_state_path(:confirm) + + # prevent form submit to verify button is disabled + page.execute_script("$('#checkout_form_confirm').submit(function(){return false;})") + + expect(page).not_to have_selector('input.button[disabled]') + click_button "Place Order" + expect(page).to have_selector('input.button[disabled]') + end + end + + context "when several payment methods are available" do + let(:credit_cart_payment) {create(:credit_card_payment_method, :environment => 'test') } + let(:check_payment) {create(:check_payment_method, :environment => 'test') } + + after do + Capybara.ignore_hidden_elements = true + end + + before do + Capybara.ignore_hidden_elements = false + order = OrderWalkthrough.up_to(:delivery) + allow(order).to receive_messages(:available_payment_methods => [check_payment,credit_cart_payment]) + order.user = create(:user) + order.update! + + allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: order.user) + + visit spree.checkout_state_path(:payment) + end + + it "the first payment method should be selected", :js => true do + payment_method_css = "#order_payments_attributes__payment_method_id_" + expect(find("#{payment_method_css}#{check_payment.id}")).to be_checked + expect(find("#{payment_method_css}#{credit_cart_payment.id}")).not_to be_checked + end + + it "the fields for the other payment methods should be hidden", :js => true do + payment_method_css = "#payment_method_" + expect(find("#{payment_method_css}#{check_payment.id}")).to be_visible + expect(find("#{payment_method_css}#{credit_cart_payment.id}")).not_to be_visible + end + end + + context "user has payment sources", js: true do + let(:bogus) { create(:credit_card_payment_method) } + let(:user) { create(:user) } + + let!(:credit_card) do + create(:credit_card, user_id: user.id, payment_method: bogus, gateway_customer_profile_id: "BGS-WEFWF") + end + + before do + order = OrderWalkthrough.up_to(:delivery) + allow(order).to receive_messages(:available_payment_methods => [bogus]) + + allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user) + allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user) + + visit spree.checkout_state_path(:payment) + end + + it "selects first source available and customer moves on" do + expect(find "#use_existing_card_yes").to be_checked + + expect { + click_on "Save and Continue" + }.not_to change { Spree::CreditCard.count } + + click_on "Place Order" + expect(current_path).to eql(spree.order_path(Spree::Order.last)) + end + + it "allows user to enter a new source" do + choose "use_existing_card_no" + + fill_in "Name on card", :with => 'Spree Commerce' + fill_in "Card Number", :with => '4111111111111111' + fill_in "card_expiry", :with => '04 / 20' + fill_in "Card Code", :with => '123' + + expect { + click_on "Save and Continue" + }.to change { Spree::CreditCard.count }.by 1 + + click_on "Place Order" + expect(current_path).to eql(spree.order_path(Spree::Order.last)) + end + end + + # regression for #2921 + context "goes back from payment to add another item", js: true do + let!(:bag) { create(:product, :name => "RoR Bag") } + + it "transit nicely through checkout steps again" do + add_mug_to_cart + click_on "Checkout" + fill_in "order_email", :with => "test@example.com" + fill_in_address + click_on "Save and Continue" + click_on "Save and Continue" + expect(current_path).to eql(spree.checkout_state_path("payment")) + + visit spree.root_path + click_link bag.name + click_button "add-to-cart-button" + + click_on "Checkout" + click_on "Save and Continue" + click_on "Save and Continue" + click_on "Save and Continue" + + expect(current_path).to eql(spree.order_path(Spree::Order.last)) + end + end + + context "from payment step customer goes back to cart", js: true do + before do + add_mug_to_cart + click_on "Checkout" + fill_in "order_email", :with => "test@example.com" + fill_in_address + click_on "Save and Continue" + click_on "Save and Continue" + expect(current_path).to eql(spree.checkout_state_path("payment")) + end + + context "and updates line item quantity and try to reach payment page" do + before do + visit spree.cart_path + within ".cart-item-quantity" do + fill_in first("input")["name"], with: 3 + end + + click_on "Update" + end + + it "redirects user back to address step" do + visit spree.checkout_state_path("payment") + expect(current_path).to eql(spree.checkout_state_path("address")) + end + + it "updates shipments properly through step address -> delivery transitions" do + visit spree.checkout_state_path("payment") + click_on "Save and Continue" + click_on "Save and Continue" + + expect(Spree::InventoryUnit.count).to eq 3 + end + end + + context "and adds new product to cart and try to reach payment page" do + let!(:bag) { create(:product, :name => "RoR Bag") } + + before do + visit spree.root_path + click_link bag.name + click_button "add-to-cart-button" + end + + it "redirects user back to address step" do + visit spree.checkout_state_path("payment") + expect(current_path).to eql(spree.checkout_state_path("address")) + end + + it "updates shipments properly through step address -> delivery transitions" do + visit spree.checkout_state_path("payment") + click_on "Save and Continue" + click_on "Save and Continue" + + expect(Spree::InventoryUnit.count).to eq 2 + end + end + end + + context "in coupon promotion, submits coupon along with payment", js: true do + let!(:promotion) { Spree::Promotion.create(name: "Huhuhu", code: "huhu") } + let!(:calculator) { Spree::Calculator::FlatPercentItemTotal.create(preferred_flat_percent: "10") } + let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator) } + + before do + promotion.actions << action + + add_mug_to_cart + click_on "Checkout" + + fill_in "order_email", :with => "test@example.com" + fill_in_address + click_on "Save and Continue" + + click_on "Save and Continue" + expect(current_path).to eql(spree.checkout_state_path("payment")) + end + + it "makes sure payment reflects order total with discounts" do + fill_in "Coupon Code", with: promotion.code + click_on "Save and Continue" + + expect(page).to have_content(promotion.name) + expect(Spree::Payment.first.amount.to_f).to eq Spree::Order.last.total.to_f + end + + context "invalid coupon" do + it "doesnt create a payment record" do + fill_in "Coupon Code", with: 'invalid' + click_on "Save and Continue" + + expect(Spree::Payment.count).to eq 0 + expect(page).to have_content(Spree.t(:coupon_code_not_found)) + end + end + + context "doesn't fill in coupon code input" do + it "advances just fine" do + click_on "Save and Continue" + expect(current_path).to eql(spree.order_path(Spree::Order.last)) + end + end + end + + context "order has only payment step" do + before do + create(:credit_card_payment_method) + @old_checkout_flow = Spree::Order.checkout_flow + Spree::Order.class_eval do + checkout_flow do + go_to_state :payment + go_to_state :confirm + end + end + + allow_any_instance_of(Spree::Order).to receive_messages email: "spree@commerce.com" + + add_mug_to_cart + click_on "Checkout" + end + + after do + Spree::Order.checkout_flow(&@old_checkout_flow) + end + + it "goes right payment step and place order just fine" do + expect(current_path).to eq spree.checkout_state_path('payment') + + choose "Credit Card" + fill_in "Name on card", :with => 'Spree Commerce' + fill_in "Card Number", :with => '4111111111111111' + fill_in "card_expiry", :with => '04 / 20' + fill_in "Card Code", :with => '123' + click_button "Save and Continue" + + expect(current_path).to eq spree.checkout_state_path('confirm') + click_button "Place Order" + end + end + + + context "save my address" do + before do + stock_location.stock_items.update_all(count_on_hand: 1) + add_mug_to_cart + end + + context 'as a guest' do + before do + Spree::Order.last.update_column(:email, "test@example.com") + click_button "Checkout" + end + + it 'should not be displayed' do + expect(page).to_not have_css("[data-hook=save_user_address]") + end + end + + context 'as a User' do + before do + user = create(:user) + Spree::Order.last.update_column :user_id, user.id + allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user) + click_button "Checkout" + end + + it 'should be displayed' do + expect(page).to have_css("[data-hook=save_user_address]") + end + end + end + + context "when order is completed" do + let!(:user) { create(:user) } + let!(:order) { OrderWalkthrough.up_to(:delivery) } + + before(:each) do + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:current_order => order) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:try_spree_current_user => user) + allow_any_instance_of(Spree::OrdersController).to receive_messages(:try_spree_current_user => user) + + visit spree.checkout_state_path(:delivery) + click_button "Save and Continue" + click_button "Save and Continue" + end + + it "displays a thank you message" do + expect(page).to have_content(Spree.t(:thank_you_for_your_order)) + end + + it "does not display a thank you message on that order future visits" do + visit spree.order_path(order) + expect(page).to_not have_content(Spree.t(:thank_you_for_your_order)) + end + end + + def fill_in_address + address = "order_bill_address_attributes" + fill_in "#{address}_firstname", with: "Ryan" + fill_in "#{address}_lastname", with: "Bigg" + fill_in "#{address}_address1", with: "143 Swan Street" + fill_in "#{address}_city", with: "Richmond" + select "United States of America", from: "#{address}_country_id" + select "Alabama", from: "#{address}_state_id" + fill_in "#{address}_zipcode", with: "12345" + fill_in "#{address}_phone", with: "(555) 555-5555" + end + + def add_mug_to_cart + visit spree.root_path + click_link mug.name + click_button "add-to-cart-button" + end +end diff --git a/frontend/spec/features/checkout_unshippable_spec.rb b/frontend/spec/features/checkout_unshippable_spec.rb new file mode 100644 index 00000000000..fda17c21009 --- /dev/null +++ b/frontend/spec/features/checkout_unshippable_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe "checkout with unshippable items", type: :feature, inaccessible: true do + let!(:stock_location) { create(:stock_location) } + let(:order) { OrderWalkthrough.up_to(:address) } + + before do + OrderWalkthrough.add_line_item!(order) + line_item = order.line_items.last + stock_item = stock_location.stock_item(line_item.variant) + stock_item.adjust_count_on_hand -999 + stock_item.backorderable = false + stock_item.save! + + user = create(:user) + order.user = user + order.update! + + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:current_order => order) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:try_spree_current_user => user) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:skip_state_validation? => true) + allow_any_instance_of(Spree::CheckoutController).to receive_messages(:ensure_sufficient_stock_lines => true) + end + + it 'displays and removes' do + visit spree.checkout_state_path(:delivery) + expect(page).to have_content('Unshippable Items') + + click_button "Save and Continue" + + order.reload + expect(order.line_items.count).to eq 1 + end +end + diff --git a/frontend/spec/features/coupon_code_spec.rb b/frontend/spec/features/coupon_code_spec.rb new file mode 100644 index 00000000000..eaf5f114046 --- /dev/null +++ b/frontend/spec/features/coupon_code_spec.rb @@ -0,0 +1,225 @@ +require 'spec_helper' + +describe "Coupon code promotions", type: :feature, js: true do + let!(:country) { create(:country, :name => "United States of America", :states_required => true) } + let!(:state) { create(:state, :name => "Alabama", :country => country) } + let!(:zone) { create(:zone) } + let!(:shipping_method) { create(:shipping_method) } + let!(:payment_method) { create(:check_payment_method) } + let!(:product) { create(:product, :name => "RoR Mug", :price => 20) } + + context "visitor makes checkout as guest without registration" do + def create_basic_coupon_promotion(code) + promotion = Spree::Promotion.create!(:name => code.titleize, + :code => code, + :starts_at => 1.day.ago, + :expires_at => 1.day.from_now) + + calculator = Spree::Calculator::FlatRate.new + calculator.preferred_amount = 10 + + action = Spree::Promotion::Actions::CreateItemAdjustments.new + action.calculator = calculator + action.promotion = promotion + action.save + + promotion.reload # so that promotion.actions is available + end + + let!(:promotion) { create_basic_coupon_promotion("onetwo") } + + # OrdersController + context "on the payment page" do + before do + + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + click_button "Checkout" + fill_in "order_email", :with => "spree@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Smith" + fill_in "Street Address", :with => "1 John Street" + fill_in "City", :with => "City of John" + fill_in "Zip", :with => "01337" + select country.name, :from => "Country" + select state.name, :from => "order[bill_address_attributes][state_id]" + fill_in "Phone", :with => "555-555-5555" + + # To shipping method screen + click_button "Save and Continue" + # To payment screen + click_button "Save and Continue" + end + + it "informs about an invalid coupon code" do + fill_in "order_coupon_code", :with => "coupon_codes_rule_man" + click_button "Save and Continue" + expect(page).to have_content(Spree.t(:coupon_code_not_found)) + end + + it "can enter an invalid coupon code, then a real one" do + fill_in "order_coupon_code", :with => "coupon_codes_rule_man" + click_button "Save and Continue" + expect(page).to have_content(Spree.t(:coupon_code_not_found)) + fill_in "order_coupon_code", :with => "onetwo" + click_button "Save and Continue" + expect(page).to have_content("Promotion (Onetwo) -$10.00") + end + + context "with a promotion" do + it "applies a promotion to an order" do + fill_in "order_coupon_code", :with => "onetwo" + click_button "Save and Continue" + expect(page).to have_content("Promotion (Onetwo) -$10.00") + end + end + end + + # CheckoutController + context "on the cart page" do + + before do + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + end + + it "can enter a coupon code and receives success notification" do + fill_in "order_coupon_code", :with => "onetwo" + click_button "Update" + expect(page).to have_content(Spree.t(:coupon_code_applied)) + end + + it "can enter a promotion code with both upper and lower case letters" do + fill_in "order_coupon_code", :with => "ONETwO" + click_button "Update" + expect(page).to have_content(Spree.t(:coupon_code_applied)) + end + + it "informs the user about a coupon code which has exceeded its usage" do + promotion.update_column(:usage_limit, 5) + allow_any_instance_of(promotion.class).to receive_messages(:credits_count => 10) + + fill_in "order_coupon_code", :with => "onetwo" + click_button "Update" + expect(page).to have_content(Spree.t(:coupon_code_max_usage)) + end + + context "informs the user if the coupon code is not eligible" do + before do + rule = Spree::Promotion::Rules::ItemTotal.new + rule.promotion = promotion + rule.preferred_amount_min = 100 + rule.save + end + + specify do + visit spree.cart_path + + fill_in "order_coupon_code", :with => "onetwo" + click_button "Update" + expect(page).to have_content(Spree.t(:item_total_less_than_or_equal, scope: [:eligibility_errors, :messages], amount: "$100.00")) + end + end + + it "informs the user if the coupon is expired" do + promotion.expires_at = Date.today.beginning_of_week + promotion.starts_at = Date.today.beginning_of_week.advance(:day => 3) + promotion.save! + fill_in "order_coupon_code", :with => "onetwo" + click_button "Update" + expect(page).to have_content(Spree.t(:coupon_code_expired)) + end + + context "calculates the correct amount of money saved with flat percent promotions" do + before do + calculator = Spree::Calculator::FlatPercentItemTotal.new + calculator.preferred_flat_percent = 20 + promotion.actions.first.calculator = calculator + promotion.save + + create(:product, :name => "Spree Mug", :price => 10) + end + + specify do + visit spree.root_path + click_link "Spree Mug" + click_button "add-to-cart-button" + + visit spree.cart_path + fill_in "order_coupon_code", :with => "onetwo" + click_button "Update" + + fill_in "order_line_items_attributes_0_quantity", :with => 2 + fill_in "order_line_items_attributes_1_quantity", :with => 2 + click_button "Update" + + + within '#cart_adjustments' do + # 20% of $40 = 8 + # 20% of $20 = 4 + # Therefore: promotion discount amount is $12. + expect(page).to have_content("Promotion (Onetwo) -$12.00") + end + + within '.cart-total' do + expect(page).to have_content("$48.00") + end + end + end + + context "calculates the correct amount of money saved with flat 100% promotions on the whole order" do + before do + calculator = Spree::Calculator::FlatPercentItemTotal.new + calculator.preferred_flat_percent = 100 + + action = Spree::Promotion::Actions::CreateAdjustment.new + action.calculator = calculator + action.promotion = promotion + action.save + + promotion.promotion_actions = [action] + promotion.save + + create(:product, name: "Spree Mug", price: 10) + end + + specify do + visit spree.root_path + click_link "Spree Mug" + click_button "add-to-cart-button" + + visit spree.cart_path + + within '.cart-total' do + expect(page).to have_content("$30.00") + end + + fill_in "order_coupon_code", with: "onetwo" + click_button "Update" + + within '#cart_adjustments' do + expect(page).to have_content("Promotion (Onetwo) -$30.00") + end + + within '.cart-total' do + expect(page).to have_content("$0.00") + end + + fill_in "order_line_items_attributes_0_quantity", with: 2 + fill_in "order_line_items_attributes_1_quantity", with: 2 + click_button "Update" + + within '#cart_adjustments' do + expect(page).to have_content("Promotion (Onetwo) -$60.00") + end + + within '.cart-total' do + expect(page).to have_content("$0.00") + end + end + end + end + end +end diff --git a/frontend/spec/features/currency_spec.rb b/frontend/spec/features/currency_spec.rb new file mode 100644 index 00000000000..e907eea47dc --- /dev/null +++ b/frontend/spec/features/currency_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe "Switching currencies in backend", :type => :feature do + before do + create(:base_product, :name => "RoR Mug") + end + + # Regression test for #2340 + it "does not cause current_order to become nil", inaccessible: true do + visit spree.root_path + click_link "RoR Mug" + click_button "Add To Cart" + # Now that we have an order... + Spree::Config[:currency] = "AUD" + expect { visit spree.root_path }.not_to raise_error + end + +end diff --git a/frontend/spec/features/free_item_spec.rb b/frontend/spec/features/free_item_spec.rb new file mode 100644 index 00000000000..8c64ad4b314 --- /dev/null +++ b/frontend/spec/features/free_item_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +# Gigantic regression test for #2191 +describe "Free shipping promotions", :type => :feature, :js => true do + let!(:country) { create(:country, :name => "United States of America", :states_required => true) } + let!(:state) { create(:state, :name => "Alabama", :country => country) } + let!(:zone) { create(:zone) } + let!(:shipping_method) do + sm = create(:shipping_method) + sm.calculator.preferred_amount = 10 + sm.calculator.save + sm + end + + let!(:payment_method) { create(:check_payment_method) } + let!(:product) { create(:product, :name => "RoR Mug", :price => 20) } + let!(:free_product) { create(:product, :name => "RoR Shirt", :price => 20) } + let!(:promotion) do + promotion = Spree::Promotion.create!(:name => "Free Shirt!", + :starts_at => 1.day.ago, + :expires_at => 1.day.from_now, + :code => "freeshirt") + + action_1 = Spree::Promotion::Actions::CreateLineItems.new + action_1.promotion_action_line_items.build( + :variant => free_product.master, + :quantity => 1 + ) + action_1.promotion = promotion + action_1.save + + action_2 = Spree::Promotion::Actions::CreateAdjustment.new + action_2.calculator = Spree::Calculator::FlatRate.new + action_2.calculator.preferred_amount = 20 + action_2.promotion = promotion + + action_2.save + + promotion.reload # so that promotion.actions is available + end + + context "promotion with free line item" do + before do + + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + fill_in "order_coupon_code", :with => "freeshirt" + click_button "Update" + + all("a.delete").first.click # Delete the first line item + expect(page).not_to have_content("RoR Mug") + expect(page).to have_content("RoR Shirt") + expect(page).to have_content("Adjustment: Promotion (Free Shirt!) -$20.00") + expect(page).to have_content("Total $0.00") + + click_button "Checkout" + fill_in "order_email", :with => "spree@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Smith" + fill_in "Street Address", :with => "1 John Street" + fill_in "City", :with => "City of John" + fill_in "Zip", :with => "01337" + select country.name, :from => "Country" + select state.name, :from => "order[bill_address_attributes][state_id]" + fill_in "Phone", :with => "555-555-5555" + + # To shipping method screen + click_button "Save and Continue" + # To payment screen + click_button "Save and Continue" + end + + # The actual regression test for #2191 + it "does not skip the payment step" do + # The bug is that it skips the payment step because the shipment cost has not been set for the order. + # Therefore we are checking here that it's *definitely* on the payment step and hasn't jumped to complete. + expect(page.current_url).to match(/checkout\/payment/) + + within("#checkout-summary") do + expect(page).to have_content("Shipping total: $10.00") + expect(page).to have_content("Promotion (Free Shirt!): -$20.00") + expect(page).to have_content("Order Total: $10.00") + end + end + end +end diff --git a/frontend/spec/features/free_shipping_promotions_spec.rb b/frontend/spec/features/free_shipping_promotions_spec.rb new file mode 100644 index 00000000000..dac377a25c0 --- /dev/null +++ b/frontend/spec/features/free_shipping_promotions_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe "Free shipping promotions", :type => :feature, :js => true do + let!(:country) { create(:country, :name => "United States of America", :states_required => true) } + let!(:state) { create(:state, :name => "Alabama", :country => country) } + let!(:zone) { create(:zone) } + let!(:shipping_method) do + sm = create(:shipping_method) + sm.calculator.preferred_amount = 10 + sm.calculator.save + sm + end + + let!(:payment_method) { create(:check_payment_method) } + let!(:product) { create(:product, :name => "RoR Mug", :price => 20) } + let!(:promotion) do + promotion = Spree::Promotion.create!(:name => "Free Shipping", + :starts_at => 1.day.ago, + :expires_at => 1.day.from_now) + + action = Spree::Promotion::Actions::FreeShipping.new + action.promotion = promotion + action.save + + promotion.reload # so that promotion.actions is available + end + + context "free shipping promotion automatically applied" do + before do + + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + click_button "Checkout" + fill_in "order_email", :with => "spree@example.com" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Smith" + fill_in "Street Address", :with => "1 John Street" + fill_in "City", :with => "City of John" + fill_in "Zip", :with => "01337" + select country.name, :from => "Country" + select state.name, :from => "order[bill_address_attributes][state_id]" + fill_in "Phone", :with => "555-555-5555" + + # To shipping method screen + click_button "Save and Continue" + # To payment screen + click_button "Save and Continue" + end + + # Regression test for #4428 + it "applies the free shipping promotion" do + within("#checkout-summary") do + expect(page).to have_content("Shipping total: $10.00") + expect(page).to have_content("Promotion (Free Shipping): -$10.00") + end + end + end +end diff --git a/frontend/spec/features/locale_spec.rb b/frontend/spec/features/locale_spec.rb new file mode 100644 index 00000000000..52f34ce6397 --- /dev/null +++ b/frontend/spec/features/locale_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe 'setting locale', :type => :feature do + def with_locale(locale) + I18n.locale = locale + Spree::Frontend::Config[:locale] = locale + yield + I18n.locale = I18n.default_locale + Spree::Frontend::Config[:locale] = 'en' + end + + context 'shopping cart link and page' do + before do + I18n.backend.store_translations(:fr, + :spree => { + :cart => 'Panier', + :shopping_cart => 'Panier' + }) + end + + it 'should be in french' do + with_locale('fr') do + visit spree.root_path + click_link 'Panier' + expect(page).to have_content('Panier') + end + end + end + + context 'checkout form validation messages' do + include_context 'checkout setup' + + let(:error_messages) do + { + 'en' => 'This field is required.', + 'fr' => 'Ce champ est obligatoire.', + 'de' => 'Dieses Feld ist ein Pflichtfeld.', + } + end + + def check_error_text(text) + %w(firstname lastname address1 city).each do |attr| + expect(find(".field#b#{attr} label.error").text).to eq(text) + end + end + + it 'shows translated jquery.validate error messages', js: true do + visit spree.root_path + click_link mug.name + click_button 'add-to-cart-button' + error_messages.each do |locale, message| + with_locale(locale) do + visit '/checkout/address' + find('.form-buttons input[type=submit]').click + check_error_text message + end + end + end + end +end diff --git a/frontend/spec/features/order_spec.rb b/frontend/spec/features/order_spec.rb new file mode 100644 index 00000000000..780bf1657c3 --- /dev/null +++ b/frontend/spec/features/order_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe 'orders', :type => :feature do + let(:order) { OrderWalkthrough.up_to(:complete) } + let(:user) { create(:user) } + + before do + order.update_attribute(:user_id, user.id) + order.shipments.destroy_all + allow_any_instance_of(Spree::OrdersController).to receive_messages(:try_spree_current_user => user) + end + + it "can visit an order" do + # Regression test for current_user call on orders/show + expect { visit spree.order_path(order) }.not_to raise_error + end + + it "should display line item price" do + # Regression test for #2772 + line_item = order.line_items.first + line_item.target_shipment = create(:shipment) + line_item.price = 19.00 + line_item.save! + + visit spree.order_path(order) + + # Tests view spree/shared/_order_details + within 'td.price' do + expect(page).to have_content "19.00" + end + end + + it "should have credit card info if paid with credit card" do + create(:payment, :order => order) + visit spree.order_path(order) + within '.payment-info' do + expect(page).to have_content "Ending in 1111" + end + end + + it "should have payment method name visible if not paid with credit card" do + create(:check_payment, :order => order) + visit spree.order_path(order) + within '.payment-info' do + expect(page).to have_content "Check" + end + end + + # Regression test for #2282 + context "can support a credit card with blank information" do + before do + credit_card = create(:credit_card) + credit_card.update_column(:cc_type, '') + payment = order.payments.first + payment.source = credit_card + payment.save! + end + + specify do + visit spree.order_path(order) + within '.payment-info' do + expect { find("img") }.to raise_error(Capybara::ElementNotFound) + end + end + end + + it "should return the correct title when displaying a completed order" do + visit spree.order_path(order) + + within '#order_summary' do + expect(page).to have_content("#{Spree.t(:order)} #{order.number}") + end + end +end diff --git a/frontend/spec/features/page_promotions_spec.rb b/frontend/spec/features/page_promotions_spec.rb new file mode 100644 index 00000000000..8f6f209967b --- /dev/null +++ b/frontend/spec/features/page_promotions_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'page promotions', :type => :feature do + let!(:product) { create(:product, :name => "RoR Mug", :price => 20) } + before do + promotion = Spree::Promotion.create!(:name => "$10 off", + :path => 'test', + :starts_at => 1.day.ago, + :expires_at => 1.day.from_now) + + calculator = Spree::Calculator::FlatRate.new + calculator.preferred_amount = 10 + + action = Spree::Promotion::Actions::CreateItemAdjustments.create(:calculator => calculator) + promotion.actions << action + + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + end + + it "automatically applies a page promotion upon visiting" do + expect(page).not_to have_content("Promotion ($10 off) -$10.00") + visit '/content/test' + visit '/cart' + expect(page).to have_content("Promotion ($10 off) -$10.00") + expect(page).to have_content("Subtotal (1 item) $20.00") + end + + it "does not activate an adjustment for a path that doesn't have a promotion" do + expect(page).not_to have_content("Promotion ($10 off) -$10.00") + visit '/content/cvv' + visit '/cart' + expect(page).not_to have_content("Promotion ($10 off) -$10.00") + end +end \ No newline at end of file diff --git a/frontend/spec/features/products_spec.rb b/frontend/spec/features/products_spec.rb new file mode 100644 index 00000000000..6f4e1bf3641 --- /dev/null +++ b/frontend/spec/features/products_spec.rb @@ -0,0 +1,270 @@ +# encoding: utf-8 +require 'spec_helper' + +describe "Visiting Products", type: :feature, inaccessible: true do + include_context "custom products" + + let(:store_name) do + ((first_store = Spree::Store.first) && first_store.name).to_s + end + + before(:each) do + visit spree.root_path + end + + it "should be able to show the shopping cart after adding a product to it" do + click_link "Ruby on Rails Ringer T-Shirt" + expect(page).to have_content("$19.99") + + click_button 'add-to-cart-button' + expect(page).to have_content("Shopping Cart") + end + + describe 'meta tags and title' do + let(:jersey) { Spree::Product.find_by_name('Ruby on Rails Baseball Jersey') } + let(:metas) { { :meta_description => 'Brand new Ruby on Rails Jersey', :meta_title => 'Ruby on Rails Baseball Jersey Buy High Quality Geek Apparel', :meta_keywords => 'ror, jersey, ruby' } } + + it 'should return the correct title when displaying a single product' do + click_link jersey.name + expect(page).to have_title('Ruby on Rails Baseball Jersey - ' + store_name) + within('div#product-description') do + within('h1.product-title') do + expect(page).to have_content('Ruby on Rails Baseball Jersey') + end + end + end + + it 'displays metas' do + jersey.update_attributes metas + click_link jersey.name + expect(page).to have_meta(:description, 'Brand new Ruby on Rails Jersey') + expect(page).to have_meta(:keywords, 'ror, jersey, ruby') + end + + it 'displays title if set' do + jersey.update_attributes metas + click_link jersey.name + expect(page).to have_title('Ruby on Rails Baseball Jersey Buy High Quality Geek Apparel') + end + + it "doesn't use meta_title as heading on page" do + jersey.update_attributes metas + click_link jersey.name + within("h1") do + expect(page).to have_content(jersey.name) + expect(page).not_to have_content(jersey.meta_title) + end + end + + it 'uses product name in title when meta_title set to empty string' do + jersey.update_attributes meta_title: '' + click_link jersey.name + expect(page).to have_title('Ruby on Rails Baseball Jersey - ' + store_name) + end + end + + context "using Russian Rubles as a currency" do + before do + Spree::Config[:currency] = "RUB" + end + + let!(:product) do + product = Spree::Product.find_by_name("Ruby on Rails Ringer T-Shirt") + product.price = 19.99 + product.tap(&:save) + end + + # Regression tests for #2737 + context "uses руб as the currency symbol" do + it "on products page" do + visit spree.root_path + within("#product_#{product.id}") do + within(".price") do + expect(page).to have_content("₽19.99") + end + end + end + + it "on product page" do + visit spree.product_path(product) + within(".price") do + expect(page).to have_content("₽19.99") + end + end + + it "when adding a product to the cart", :js => true do + visit spree.product_path(product) + click_button "Add To Cart" + click_link "Home" + within(".cart-info") do + expect(page).to have_content("₽19.99") + end + end + + it "when on the 'address' state of the cart" do + visit spree.product_path(product) + click_button "Add To Cart" + click_button "Checkout" + within("tr[data-hook=item_total]") do + expect(page).to have_content("₽19.99") + end + end + end + end + + it "should be able to search for a product" do + fill_in "keywords", :with => "shirt" + click_button "Search" + + expect(page.all('ul.product-listing li').size).to eq(1) + end + + context "a product with variants" do + let(:product) { Spree::Product.find_by_name("Ruby on Rails Baseball Jersey") } + let(:option_value) { create(:option_value) } + let!(:variant) { product.variants.create!(:price => 5.59) } + + before do + Spree::Config[:display_currency] = true + # Need to have two images to trigger the error + image = File.open(File.expand_path('../../fixtures/thinking-cat.jpg', __FILE__)) + product.images.create!(:attachment => image) + product.images.create!(:attachment => image) + + product.option_types << option_value.option_type + variant.option_values << option_value + end + + it "should be displayed" do + expect { click_link product.name }.to_not raise_error + end + + it "displays price of first variant listed", js: true do + click_link product.name + within("#product-price") do + expect(page).to have_content variant.price + expect(page).not_to have_content Spree.t(:out_of_stock) + end + end + + it "doesn't display out of stock for master product" do + product.master.stock_items.update_all count_on_hand: 0, backorderable: false + + click_link product.name + within("#product-price") do + expect(page).not_to have_content Spree.t(:out_of_stock) + end + end + + # Regression test for #4342 + it "does not fail when display_currency is true" do + Spree::Config[:display_currency] = true + click_link product.name + within("#cart-form") do + find('input[type=radio]') + end + end + end + + context "a product with variants, images only for the variants" do + let(:product) { Spree::Product.find_by_name("Ruby on Rails Baseball Jersey") } + + before do + image = File.open(File.expand_path('../../fixtures/thinking-cat.jpg', __FILE__)) + v1 = product.variants.create!(price: 9.99) + v2 = product.variants.create!(price: 10.99) + v1.images.create!(attachment: image) + v2.images.create!(attachment: image) + end + + it "should not display no image available" do + visit spree.root_path + expect(page).to have_xpath("//img[contains(@src,'thinking-cat')]") + end + end + + it "should be able to hide products without price" do + expect(page.all('ul.product-listing li').size).to eq(9) + Spree::Config.show_products_without_price = false + Spree::Config.currency = "CAN" + visit spree.root_path + expect(page.all('ul.product-listing li').size).to eq(0) + end + + + it "should be able to display products priced under 10 dollars" do + within(:css, '#taxonomies') { click_link "Ruby on Rails" } + check "Price_Range_Under_$10.00" + within(:css, '#sidebar_products_search') { click_button "Search" } + expect(page).to have_content("No products found") + end + + it "should be able to display products priced between 15 and 18 dollars" do + within(:css, '#taxonomies') { click_link "Ruby on Rails" } + check "Price_Range_$15.00_-_$18.00" + within(:css, '#sidebar_products_search') { click_button "Search" } + + expect(page.all('ul.product-listing li').size).to eq(3) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + expect(tmp.sort!).to eq(["Ruby on Rails Mug", "Ruby on Rails Stein", "Ruby on Rails Tote"]) + end + + it "should be able to display products priced between 15 and 18 dollars across multiple pages" do + Spree::Config.products_per_page = 2 + within(:css, '#taxonomies') { click_link "Ruby on Rails" } + check "Price_Range_$15.00_-_$18.00" + within(:css, '#sidebar_products_search') { click_button "Search" } + + expect(page.all('ul.product-listing li').size).to eq(2) + products = page.all('ul.product-listing li a[itemprop=name]') + expect(products.count).to eq(2) + + find('nav.pagination .next a').click + products = page.all('ul.product-listing li a[itemprop=name]') + expect(products.count).to eq(1) + end + + it "should be able to display products priced 18 dollars and above" do + within(:css, '#taxonomies') { click_link "Ruby on Rails" } + check "Price_Range_$18.00_-_$20.00" + check "Price_Range_$20.00_or_over" + within(:css, '#sidebar_products_search') { click_button "Search" } + + expect(page.all('ul.product-listing li').size).to eq(4) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + expect(tmp.sort!).to eq(["Ruby on Rails Bag", + "Ruby on Rails Baseball Jersey", + "Ruby on Rails Jr. Spaghetti", + "Ruby on Rails Ringer T-Shirt"]) + end + + it "should be able to put a product without a description in the cart" do + product = FactoryGirl.create(:base_product, :description => nil, :name => 'Sample', :price => '19.99') + visit spree.product_path(product) + expect(page).to have_content "This product has no description" + click_button 'add-to-cart-button' + expect(page).to have_content "This product has no description" + end + + it "shouldn't be able to put a product without a current price in the cart" do + product = FactoryGirl.create(:base_product, :description => nil, :name => 'Sample', :price => '19.99') + Spree::Config.currency = "CAN" + Spree::Config.show_products_without_price = true + visit spree.product_path(product) + expect(page).to have_content "This product is not available in the selected currency." + expect(page).not_to have_content "add-to-cart-button" + end + + it "should return the correct title when displaying a single product" do + product = Spree::Product.find_by_name("Ruby on Rails Baseball Jersey") + click_link product.name + + within("div#product-description") do + within("h1.product-title") do + expect(page).to have_content("Ruby on Rails Baseball Jersey") + end + end + end +end diff --git a/frontend/spec/features/taxons_spec.rb b/frontend/spec/features/taxons_spec.rb new file mode 100644 index 00000000000..7441947a4d4 --- /dev/null +++ b/frontend/spec/features/taxons_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe "viewing products", type: :feature, inaccessible: true do + let!(:taxonomy) { create(:taxonomy, :name => "Category") } + let!(:super_clothing) { taxonomy.root.children.create(:name => "Super Clothing") } + let!(:t_shirts) { super_clothing.children.create(:name => "T-Shirts") } + let!(:xxl) { t_shirts.children.create(:name => "XXL") } + let!(:product) do + product = create(:product, :name => "Superman T-Shirt") + product.taxons << t_shirts + end + let(:metas) { { :meta_description => 'Brand new Ruby on Rails TShirts', :meta_title => "Ruby On Rails TShirt", :meta_keywords => 'ror, tshirt, ruby' } } + let(:store_name) do + ((first_store = Spree::Store.first) && first_store.name).to_s + end + + # Regression test for #1796 + it "can see a taxon's products, even if that taxon has child taxons" do + visit '/t/category/super-clothing/t-shirts' + expect(page).to have_content("Superman T-Shirt") + end + + it "shouldn't show nested taxons with a search" do + visit '/t/category/super-clothing?keywords=shirt' + expect(page).to have_content("Superman T-Shirt") + expect(page).not_to have_selector("div[data-hook='taxon_children']") + end + + describe 'meta tags and title' do + it 'displays metas' do + t_shirts.update_attributes metas + visit '/t/category/super-clothing/t-shirts' + expect(page).to have_meta(:description, 'Brand new Ruby on Rails TShirts') + expect(page).to have_meta(:keywords, 'ror, tshirt, ruby') + end + + it 'display title if set' do + t_shirts.update_attributes metas + visit '/t/category/super-clothing/t-shirts' + expect(page).to have_title("Ruby On Rails TShirt") + end + + it 'displays title from taxon root and taxon name' do + visit '/t/category/super-clothing/t-shirts' + expect(page).to have_title('Category - T-Shirts - ' + store_name) + end + + # Regression test for #2814 + it "doesn't use meta_title as heading on page" do + t_shirts.update_attributes metas + visit '/t/category/super-clothing/t-shirts' + within("h1.taxon-title") do + expect(page).to have_content(t_shirts.name) + end + end + + it 'uses taxon name in title when meta_title set to empty string' do + t_shirts.update_attributes meta_title: '' + visit '/t/category/super-clothing/t-shirts' + expect(page).to have_title('Category - T-Shirts - ' + store_name) + end + end + + context "taxon pages" do + include_context "custom products" + before do + visit spree.root_path + end + + it "should be able to visit brand Ruby on Rails" do + within(:css, '#taxonomies') { click_link "Ruby on Rails" } + + expect(page.all('ul.product-listing li').size).to eq(7) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + array = ["Ruby on Rails Bag", + "Ruby on Rails Baseball Jersey", + "Ruby on Rails Jr. Spaghetti", + "Ruby on Rails Mug", + "Ruby on Rails Ringer T-Shirt", + "Ruby on Rails Stein", + "Ruby on Rails Tote"] + expect(tmp.sort!).to eq(array) + end + + it "should be able to visit brand Ruby" do + within(:css, '#taxonomies') { click_link "Ruby" } + + expect(page.all('ul.product-listing li').size).to eq(1) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + expect(tmp.sort!).to eq(["Ruby Baseball Jersey"]) + end + + it "should be able to visit brand Apache" do + within(:css, '#taxonomies') { click_link "Apache" } + + expect(page.all('ul.product-listing li').size).to eq(1) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + expect(tmp.sort!).to eq(["Apache Baseball Jersey"]) + end + + it "should be able to visit category Clothing" do + click_link "Clothing" + + expect(page.all('ul.product-listing li').size).to eq(5) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + expect(tmp.sort!).to eq(["Apache Baseball Jersey", + "Ruby Baseball Jersey", + "Ruby on Rails Baseball Jersey", + "Ruby on Rails Jr. Spaghetti", + "Ruby on Rails Ringer T-Shirt"]) + end + + it "should be able to visit category Mugs" do + click_link "Mugs" + + expect(page.all('ul.product-listing li').size).to eq(2) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + expect(tmp.sort!).to eq(["Ruby on Rails Mug", "Ruby on Rails Stein"]) + end + + it "should be able to visit category Bags" do + click_link "Bags" + + expect(page.all('ul.product-listing li').size).to eq(2) + tmp = page.all('ul.product-listing li a').map(&:text).flatten.compact + tmp.delete("") + expect(tmp.sort!).to eq(["Ruby on Rails Bag", "Ruby on Rails Tote"]) + end + end +end diff --git a/frontend/spec/features/template_rendering_spec.rb b/frontend/spec/features/template_rendering_spec.rb new file mode 100644 index 00000000000..ae0ff97b7f3 --- /dev/null +++ b/frontend/spec/features/template_rendering_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe "Template rendering", :type => :feature do + + after do + Capybara.ignore_hidden_elements = true + end + + before do + Capybara.ignore_hidden_elements = false + end + + it 'layout should have canonical tag referencing site url' do + Spree::Store.create!(code: 'spree', name: 'My Spree Store', url: 'spreestore.example.com', mail_from_address: 'test@example.com') + + visit spree.root_path + expect(find('link[rel=canonical]')[:href]).to eql('http://spreestore.example.com/') + end +end diff --git a/frontend/spec/fixtures/thinking-cat.jpg b/frontend/spec/fixtures/thinking-cat.jpg new file mode 100644 index 00000000000..7e8524d367b Binary files /dev/null and b/frontend/spec/fixtures/thinking-cat.jpg differ diff --git a/frontend/spec/helpers/base_helper_spec.rb b/frontend/spec/helpers/base_helper_spec.rb new file mode 100644 index 00000000000..98f024f8c8f --- /dev/null +++ b/frontend/spec/helpers/base_helper_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +module Spree + describe BaseHelper, :type => :helper do + # Regression test for #2759 + it "nested_taxons_path works with a Taxon object" do + taxon = create(:taxon, :name => "iphone") + expect(spree.nested_taxons_path(taxon)).to eq("/t/iphone") + end + end +end diff --git a/frontend/spec/spec_helper.rb b/frontend/spec/spec_helper.rb new file mode 100644 index 00000000000..65f3a2aac59 --- /dev/null +++ b/frontend/spec/spec_helper.rb @@ -0,0 +1,126 @@ +if ENV["COVERAGE"] + # Run Coverage report + require 'simplecov' + SimpleCov.start do + add_group 'Controllers', 'app/controllers' + add_group 'Helpers', 'app/helpers' + add_group 'Mailers', 'app/mailers' + add_group 'Models', 'app/models' + add_group 'Views', 'app/views' + add_group 'Libraries', 'lib' + end +end + +# This file is copied to ~/spec when you run 'ruby script/generate rspec' +# from the project root directory. +ENV["RAILS_ENV"] ||= 'test' + +begin + require File.expand_path("../dummy/config/environment", __FILE__) +rescue LoadError + puts "Could not load dummy application. Please ensure you have run `bundle exec rake test_app`" + exit +end + +require 'rspec/rails' +require 'ffaker' + +# Requires supporting files with custom matchers and macros, etc, +# in ./support/ and its subdirectories. +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} + +require 'database_cleaner' + +if ENV["CHECK_TRANSLATIONS"] + require "spree/testing_support/i18n" +end + +require 'spree/testing_support/authorization_helpers' +require 'spree/testing_support/capybara_ext' +require 'spree/testing_support/factories' +require 'spree/testing_support/preferences' +require 'spree/testing_support/controller_requests' +require 'spree/testing_support/flash' +require 'spree/testing_support/url_helpers' +require 'spree/testing_support/order_walkthrough' +require 'spree/testing_support/caching' + +require 'paperclip/matchers' + +if ENV['WEBDRIVER'] == 'accessible' + require 'capybara/accessible' + Capybara.javascript_driver = :accessible +else + require 'capybara/poltergeist' + Capybara.javascript_driver = :poltergeist +end + +RSpec.configure do |config| + config.color = true + config.infer_spec_type_from_file_location! + config.mock_with :rspec + + config.fixture_path = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures") + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, comment the following line or assign false + # instead of true. + config.use_transactional_fixtures = false + + # A workaround to deal with random failure caused by phantomjs. Turn it on + # by setting ENV['RSPEC_RETRY_COUNT']. Limit it to features tests where + # phantomjs is used. + config.before(:all, :type => :feature) do + if ENV['RSPEC_RETRY_COUNT'] + config.verbose_retry = true # show retry status in spec process + config.default_retry_count = ENV['RSPEC_RETRY_COUNT'].to_i + end + end + + if ENV['WEBDRIVER'] == 'accessible' + config.around(:each, :inaccessible => true) do |example| + Capybara::Accessible.skip_audit { example.run } + end + end + + config.before(:each) do + WebMock.disable! + if RSpec.current_example.metadata[:js] + DatabaseCleaner.strategy = :truncation + else + DatabaseCleaner.strategy = :transaction + end + # TODO: Find out why open_transactions ever gets below 0 + # See issue #3428 + if ActiveRecord::Base.connection.open_transactions < 0 + ActiveRecord::Base.connection.increment_open_transactions + end + DatabaseCleaner.start + reset_spree_preferences + end + + config.after(:each) do + DatabaseCleaner.clean + end + + config.after(:each, :type => :feature) do |example| + missing_translations = page.body.scan(/translation missing: #{I18n.locale}\.(.*?)[\s<\"&]/) + if missing_translations.any? + #binding.pry + puts "Found missing translations: #{missing_translations.inspect}" + puts "In spec: #{example.location}" + end + end + + + config.include FactoryGirl::Syntax::Methods + + config.include Spree::TestingSupport::Preferences + config.include Spree::TestingSupport::UrlHelpers + config.include Spree::TestingSupport::ControllerRequests + config.include Spree::TestingSupport::Flash + + config.include Paperclip::Shoulda::Matchers + + config.fail_fast = ENV['FAIL_FAST'] || false +end diff --git a/frontend/spec/support/shared_contexts/checkout_setup.rb b/frontend/spec/support/shared_contexts/checkout_setup.rb new file mode 100644 index 00000000000..66f2c05d30b --- /dev/null +++ b/frontend/spec/support/shared_contexts/checkout_setup.rb @@ -0,0 +1,9 @@ +shared_context 'checkout setup' do + let!(:country) { create(:country, states_required: true) } + let!(:state) { create(:state, country: country) } + let!(:shipping_method) { create(:shipping_method) } + let!(:stock_location) { create(:stock_location) } + let!(:mug) { create(:product, name: "RoR Mug") } + let!(:payment_method) { create(:check_payment_method) } + let!(:zone) { create(:zone) } +end diff --git a/frontend/spec/support/shared_contexts/custom_products.rb b/frontend/spec/support/shared_contexts/custom_products.rb new file mode 100644 index 00000000000..380f287fcc9 --- /dev/null +++ b/frontend/spec/support/shared_contexts/custom_products.rb @@ -0,0 +1,25 @@ +shared_context "custom products" do + before(:each) do + taxonomy = FactoryGirl.create(:taxonomy, :name => 'Categories') + root = taxonomy.root + clothing_taxon = FactoryGirl.create(:taxon, :name => 'Clothing', :parent_id => root.id) + bags_taxon = FactoryGirl.create(:taxon, :name => 'Bags', :parent_id => root.id) + mugs_taxon = FactoryGirl.create(:taxon, :name => 'Mugs', :parent_id => root.id) + + taxonomy = FactoryGirl.create(:taxonomy, :name => 'Brands') + root = taxonomy.root + apache_taxon = FactoryGirl.create(:taxon, :name => 'Apache', :parent_id => root.id) + rails_taxon = FactoryGirl.create(:taxon, :name => 'Ruby on Rails', :parent_id => root.id) + ruby_taxon = FactoryGirl.create(:taxon, :name => 'Ruby', :parent_id => root.id) + + FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Ringer T-Shirt', :price => '19.99', :taxons => [rails_taxon, clothing_taxon]) + FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Mug', :price => '15.99', :taxons => [rails_taxon, mugs_taxon]) + FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Tote', :price => '15.99', :taxons => [rails_taxon, bags_taxon]) + FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Bag', :price => '22.99', :taxons => [rails_taxon, bags_taxon]) + FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Baseball Jersey', :price => '19.99', :taxons => [rails_taxon, clothing_taxon]) + FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Stein', :price => '16.99', :taxons => [rails_taxon, mugs_taxon]) + FactoryGirl.create(:custom_product, :name => 'Ruby on Rails Jr. Spaghetti', :price => '19.99', :taxons => [rails_taxon, clothing_taxon]) + FactoryGirl.create(:custom_product, :name => 'Ruby Baseball Jersey', :price => '19.99', :taxons => [ruby_taxon, clothing_taxon]) + FactoryGirl.create(:custom_product, :name => 'Apache Baseball Jersey', :price => '19.99', :taxons => [apache_taxon, clothing_taxon]) + end +end diff --git a/frontend/spec/support/shared_contexts/product_prototypes.rb b/frontend/spec/support/shared_contexts/product_prototypes.rb new file mode 100644 index 00000000000..2803f310649 --- /dev/null +++ b/frontend/spec/support/shared_contexts/product_prototypes.rb @@ -0,0 +1,30 @@ +shared_context "product prototype" do + + def build_option_type_with_values(name, values) + ot = FactoryGirl.create(:option_type, :name => name) + values.each do |val| + ot.option_values.create(:name => val.downcase, :presentation => val) + end + ot + end + + let(:product_attributes) do + # FactoryGirl.attributes_for is un-deprecated! + # https://github.com/thoughtbot/factory_girl/issues/274#issuecomment-3592054 + FactoryGirl.attributes_for(:base_product) + end + + let(:prototype) do + size = build_option_type_with_values("size", %w(Small Medium Large)) + FactoryGirl.create(:prototype, :name => "Size", :option_types => [ size ]) + end + + let(:option_values_hash) do + hash = {} + prototype.option_types.each do |i| + hash[i.id.to_s] = i.option_value_ids + end + hash + end + +end diff --git a/frontend/spec/views/spree/checkout/_summary_spec.rb b/frontend/spec/views/spree/checkout/_summary_spec.rb new file mode 100644 index 00000000000..3f9e92e1186 --- /dev/null +++ b/frontend/spec/views/spree/checkout/_summary_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe "spree/checkout/_summary.html.erb", :type => :view do + # Regression spec for #4223 + it "does not use the @order instance variable" do + order = stub_model(Spree::Order) + expect do + render :partial => "spree/checkout/summary", :locals => {:order => order} + end.not_to raise_error + end +end \ No newline at end of file diff --git a/frontend/spree_frontend.gemspec b/frontend/spree_frontend.gemspec new file mode 100644 index 00000000000..9c3cdfa2d30 --- /dev/null +++ b/frontend/spree_frontend.gemspec @@ -0,0 +1,28 @@ +# encoding: UTF-8 +version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip + +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'spree_frontend' + s.version = version + s.summary = 'Frontend e-commerce functionality for the Spree project.' + s.description = 'Required dependency for Spree' + + s.required_ruby_version = '>= 1.9.3' + s.author = 'Sean Schofield' + s.email = 'sean@spreecommerce.com' + s.homepage = 'http://spreecommerce.com' + s.rubyforge_project = 'spree_frontend' + + s.files = Dir['LICENSE', 'README.md', 'app/**/*', 'config/**/*', 'lib/**/*', 'db/**/*', 'vendor/**/*'] + s.require_path = 'lib' + s.requirements << 'none' + + s.add_dependency 'spree_api', version + s.add_dependency 'spree_core', version + + s.add_dependency 'canonical-rails', '~> 0.0.4' + s.add_dependency 'jquery-rails', '~> 3.1.2' + + s.add_development_dependency 'capybara-accessible' +end diff --git a/core/vendor/assets/images/datepicker/cal.gif b/frontend/vendor/assets/images/datepicker/cal.gif similarity index 100% rename from core/vendor/assets/images/datepicker/cal.gif rename to frontend/vendor/assets/images/datepicker/cal.gif diff --git a/frontend/vendor/assets/images/flags/ad.png b/frontend/vendor/assets/images/flags/ad.png new file mode 100755 index 00000000000..385fa1d7ea6 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ad.png differ diff --git a/frontend/vendor/assets/images/flags/ae.png b/frontend/vendor/assets/images/flags/ae.png new file mode 100755 index 00000000000..cf10beb4a37 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ae.png differ diff --git a/frontend/vendor/assets/images/flags/af.png b/frontend/vendor/assets/images/flags/af.png new file mode 100755 index 00000000000..30b77326612 Binary files /dev/null and b/frontend/vendor/assets/images/flags/af.png differ diff --git a/frontend/vendor/assets/images/flags/ag.png b/frontend/vendor/assets/images/flags/ag.png new file mode 100755 index 00000000000..3ca9b1763cc Binary files /dev/null and b/frontend/vendor/assets/images/flags/ag.png differ diff --git a/frontend/vendor/assets/images/flags/ai.png b/frontend/vendor/assets/images/flags/ai.png new file mode 100755 index 00000000000..9bde3ce7099 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ai.png differ diff --git a/frontend/vendor/assets/images/flags/al.png b/frontend/vendor/assets/images/flags/al.png new file mode 100755 index 00000000000..e5f7b0647cf Binary files /dev/null and b/frontend/vendor/assets/images/flags/al.png differ diff --git a/frontend/vendor/assets/images/flags/am.png b/frontend/vendor/assets/images/flags/am.png new file mode 100755 index 00000000000..6d0a22383b3 Binary files /dev/null and b/frontend/vendor/assets/images/flags/am.png differ diff --git a/frontend/vendor/assets/images/flags/an.png b/frontend/vendor/assets/images/flags/an.png new file mode 100755 index 00000000000..ea0a786b82b Binary files /dev/null and b/frontend/vendor/assets/images/flags/an.png differ diff --git a/core/vendor/assets/images/flags/ao.png b/frontend/vendor/assets/images/flags/ao.png similarity index 100% rename from core/vendor/assets/images/flags/ao.png rename to frontend/vendor/assets/images/flags/ao.png diff --git a/frontend/vendor/assets/images/flags/ar.png b/frontend/vendor/assets/images/flags/ar.png new file mode 100755 index 00000000000..9bcd298bcd8 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ar.png differ diff --git a/frontend/vendor/assets/images/flags/as.png b/frontend/vendor/assets/images/flags/as.png new file mode 100755 index 00000000000..221462ad233 Binary files /dev/null and b/frontend/vendor/assets/images/flags/as.png differ diff --git a/frontend/vendor/assets/images/flags/at.png b/frontend/vendor/assets/images/flags/at.png new file mode 100755 index 00000000000..700a656671b Binary files /dev/null and b/frontend/vendor/assets/images/flags/at.png differ diff --git a/frontend/vendor/assets/images/flags/au.png b/frontend/vendor/assets/images/flags/au.png new file mode 100755 index 00000000000..709eac00de2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/au.png differ diff --git a/frontend/vendor/assets/images/flags/aw.png b/frontend/vendor/assets/images/flags/aw.png new file mode 100755 index 00000000000..7b7d01c10fc Binary files /dev/null and b/frontend/vendor/assets/images/flags/aw.png differ diff --git a/frontend/vendor/assets/images/flags/ax.png b/frontend/vendor/assets/images/flags/ax.png new file mode 100755 index 00000000000..eeb0393bbdf Binary files /dev/null and b/frontend/vendor/assets/images/flags/ax.png differ diff --git a/frontend/vendor/assets/images/flags/az.png b/frontend/vendor/assets/images/flags/az.png new file mode 100755 index 00000000000..6cd850a992f Binary files /dev/null and b/frontend/vendor/assets/images/flags/az.png differ diff --git a/frontend/vendor/assets/images/flags/ba.png b/frontend/vendor/assets/images/flags/ba.png new file mode 100755 index 00000000000..c996c301cbf Binary files /dev/null and b/frontend/vendor/assets/images/flags/ba.png differ diff --git a/frontend/vendor/assets/images/flags/bb.png b/frontend/vendor/assets/images/flags/bb.png new file mode 100755 index 00000000000..a2347377564 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bb.png differ diff --git a/frontend/vendor/assets/images/flags/bd.png b/frontend/vendor/assets/images/flags/bd.png new file mode 100755 index 00000000000..685325475ad Binary files /dev/null and b/frontend/vendor/assets/images/flags/bd.png differ diff --git a/frontend/vendor/assets/images/flags/be.png b/frontend/vendor/assets/images/flags/be.png new file mode 100755 index 00000000000..12c10f46e0a Binary files /dev/null and b/frontend/vendor/assets/images/flags/be.png differ diff --git a/frontend/vendor/assets/images/flags/bf.png b/frontend/vendor/assets/images/flags/bf.png new file mode 100755 index 00000000000..22b42bcc983 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bf.png differ diff --git a/frontend/vendor/assets/images/flags/bg.png b/frontend/vendor/assets/images/flags/bg.png new file mode 100755 index 00000000000..80242c2b6ce Binary files /dev/null and b/frontend/vendor/assets/images/flags/bg.png differ diff --git a/frontend/vendor/assets/images/flags/bh.png b/frontend/vendor/assets/images/flags/bh.png new file mode 100755 index 00000000000..e31d7529ff2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bh.png differ diff --git a/frontend/vendor/assets/images/flags/bi.png b/frontend/vendor/assets/images/flags/bi.png new file mode 100755 index 00000000000..efbbec661d2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bi.png differ diff --git a/frontend/vendor/assets/images/flags/bj.png b/frontend/vendor/assets/images/flags/bj.png new file mode 100755 index 00000000000..53d4eb1e99c Binary files /dev/null and b/frontend/vendor/assets/images/flags/bj.png differ diff --git a/frontend/vendor/assets/images/flags/bm.png b/frontend/vendor/assets/images/flags/bm.png new file mode 100755 index 00000000000..0ba86b159cb Binary files /dev/null and b/frontend/vendor/assets/images/flags/bm.png differ diff --git a/frontend/vendor/assets/images/flags/bn.png b/frontend/vendor/assets/images/flags/bn.png new file mode 100755 index 00000000000..8598740f896 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bn.png differ diff --git a/frontend/vendor/assets/images/flags/bo.png b/frontend/vendor/assets/images/flags/bo.png new file mode 100755 index 00000000000..5d8fbd81248 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bo.png differ diff --git a/frontend/vendor/assets/images/flags/br.png b/frontend/vendor/assets/images/flags/br.png new file mode 100755 index 00000000000..4949701d3f5 Binary files /dev/null and b/frontend/vendor/assets/images/flags/br.png differ diff --git a/frontend/vendor/assets/images/flags/bs.png b/frontend/vendor/assets/images/flags/bs.png new file mode 100755 index 00000000000..7262df73c22 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bs.png differ diff --git a/frontend/vendor/assets/images/flags/bt.png b/frontend/vendor/assets/images/flags/bt.png new file mode 100755 index 00000000000..0abb9130c7d Binary files /dev/null and b/frontend/vendor/assets/images/flags/bt.png differ diff --git a/frontend/vendor/assets/images/flags/bv.png b/frontend/vendor/assets/images/flags/bv.png new file mode 100755 index 00000000000..74264c5866b Binary files /dev/null and b/frontend/vendor/assets/images/flags/bv.png differ diff --git a/frontend/vendor/assets/images/flags/bw.png b/frontend/vendor/assets/images/flags/bw.png new file mode 100755 index 00000000000..186fc90f861 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bw.png differ diff --git a/frontend/vendor/assets/images/flags/by.png b/frontend/vendor/assets/images/flags/by.png new file mode 100755 index 00000000000..81c5086ca61 Binary files /dev/null and b/frontend/vendor/assets/images/flags/by.png differ diff --git a/frontend/vendor/assets/images/flags/bz.png b/frontend/vendor/assets/images/flags/bz.png new file mode 100755 index 00000000000..9e9545aa3f2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/bz.png differ diff --git a/frontend/vendor/assets/images/flags/ca.png b/frontend/vendor/assets/images/flags/ca.png new file mode 100755 index 00000000000..33a3b60c7b1 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ca.png differ diff --git a/frontend/vendor/assets/images/flags/catalonia.png b/frontend/vendor/assets/images/flags/catalonia.png new file mode 100644 index 00000000000..82c81b7f847 Binary files /dev/null and b/frontend/vendor/assets/images/flags/catalonia.png differ diff --git a/frontend/vendor/assets/images/flags/cc.png b/frontend/vendor/assets/images/flags/cc.png new file mode 100755 index 00000000000..dba88833035 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cc.png differ diff --git a/frontend/vendor/assets/images/flags/cd.png b/frontend/vendor/assets/images/flags/cd.png new file mode 100644 index 00000000000..20c9ecef46c Binary files /dev/null and b/frontend/vendor/assets/images/flags/cd.png differ diff --git a/frontend/vendor/assets/images/flags/cf.png b/frontend/vendor/assets/images/flags/cf.png new file mode 100755 index 00000000000..91459f8b5ac Binary files /dev/null and b/frontend/vendor/assets/images/flags/cf.png differ diff --git a/frontend/vendor/assets/images/flags/cg.png b/frontend/vendor/assets/images/flags/cg.png new file mode 100755 index 00000000000..0811ccce06e Binary files /dev/null and b/frontend/vendor/assets/images/flags/cg.png differ diff --git a/frontend/vendor/assets/images/flags/ch.png b/frontend/vendor/assets/images/flags/ch.png new file mode 100755 index 00000000000..54a86b539fe Binary files /dev/null and b/frontend/vendor/assets/images/flags/ch.png differ diff --git a/frontend/vendor/assets/images/flags/ci.png b/frontend/vendor/assets/images/flags/ci.png new file mode 100755 index 00000000000..55acf14ddf9 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ci.png differ diff --git a/frontend/vendor/assets/images/flags/ck.png b/frontend/vendor/assets/images/flags/ck.png new file mode 100755 index 00000000000..b14a3ab64eb Binary files /dev/null and b/frontend/vendor/assets/images/flags/ck.png differ diff --git a/frontend/vendor/assets/images/flags/cl.png b/frontend/vendor/assets/images/flags/cl.png new file mode 100755 index 00000000000..0b85ae07a24 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cl.png differ diff --git a/frontend/vendor/assets/images/flags/cm.png b/frontend/vendor/assets/images/flags/cm.png new file mode 100755 index 00000000000..65d3827a46d Binary files /dev/null and b/frontend/vendor/assets/images/flags/cm.png differ diff --git a/frontend/vendor/assets/images/flags/cn.png b/frontend/vendor/assets/images/flags/cn.png new file mode 100755 index 00000000000..fac1cb30e57 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cn.png differ diff --git a/frontend/vendor/assets/images/flags/co.png b/frontend/vendor/assets/images/flags/co.png new file mode 100755 index 00000000000..eeb10fd86a7 Binary files /dev/null and b/frontend/vendor/assets/images/flags/co.png differ diff --git a/frontend/vendor/assets/images/flags/cr.png b/frontend/vendor/assets/images/flags/cr.png new file mode 100755 index 00000000000..e3f483f7580 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cr.png differ diff --git a/frontend/vendor/assets/images/flags/cs.png b/frontend/vendor/assets/images/flags/cs.png new file mode 100755 index 00000000000..b20a4a19213 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cs.png differ diff --git a/frontend/vendor/assets/images/flags/cu.png b/frontend/vendor/assets/images/flags/cu.png new file mode 100755 index 00000000000..e101491e976 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cu.png differ diff --git a/frontend/vendor/assets/images/flags/cv.png b/frontend/vendor/assets/images/flags/cv.png new file mode 100755 index 00000000000..7a1f79755d6 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cv.png differ diff --git a/frontend/vendor/assets/images/flags/cx.png b/frontend/vendor/assets/images/flags/cx.png new file mode 100755 index 00000000000..8acecd6ec6e Binary files /dev/null and b/frontend/vendor/assets/images/flags/cx.png differ diff --git a/frontend/vendor/assets/images/flags/cy.png b/frontend/vendor/assets/images/flags/cy.png new file mode 100755 index 00000000000..e025ab0e070 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cy.png differ diff --git a/frontend/vendor/assets/images/flags/cz.png b/frontend/vendor/assets/images/flags/cz.png new file mode 100755 index 00000000000..69ea6759783 Binary files /dev/null and b/frontend/vendor/assets/images/flags/cz.png differ diff --git a/frontend/vendor/assets/images/flags/de.png b/frontend/vendor/assets/images/flags/de.png new file mode 100755 index 00000000000..2df162b4af4 Binary files /dev/null and b/frontend/vendor/assets/images/flags/de.png differ diff --git a/frontend/vendor/assets/images/flags/dj.png b/frontend/vendor/assets/images/flags/dj.png new file mode 100755 index 00000000000..3fe7b82cf6e Binary files /dev/null and b/frontend/vendor/assets/images/flags/dj.png differ diff --git a/frontend/vendor/assets/images/flags/dk.png b/frontend/vendor/assets/images/flags/dk.png new file mode 100755 index 00000000000..622cebd2ce6 Binary files /dev/null and b/frontend/vendor/assets/images/flags/dk.png differ diff --git a/frontend/vendor/assets/images/flags/dm.png b/frontend/vendor/assets/images/flags/dm.png new file mode 100755 index 00000000000..c49785d5ba9 Binary files /dev/null and b/frontend/vendor/assets/images/flags/dm.png differ diff --git a/frontend/vendor/assets/images/flags/do.png b/frontend/vendor/assets/images/flags/do.png new file mode 100755 index 00000000000..13ae2605d15 Binary files /dev/null and b/frontend/vendor/assets/images/flags/do.png differ diff --git a/frontend/vendor/assets/images/flags/dz.png b/frontend/vendor/assets/images/flags/dz.png new file mode 100755 index 00000000000..144fe154e23 Binary files /dev/null and b/frontend/vendor/assets/images/flags/dz.png differ diff --git a/frontend/vendor/assets/images/flags/ec.png b/frontend/vendor/assets/images/flags/ec.png new file mode 100755 index 00000000000..3b5383ba23a Binary files /dev/null and b/frontend/vendor/assets/images/flags/ec.png differ diff --git a/frontend/vendor/assets/images/flags/ee.png b/frontend/vendor/assets/images/flags/ee.png new file mode 100755 index 00000000000..beec7abef45 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ee.png differ diff --git a/frontend/vendor/assets/images/flags/eg.png b/frontend/vendor/assets/images/flags/eg.png new file mode 100755 index 00000000000..493d5c4ec92 Binary files /dev/null and b/frontend/vendor/assets/images/flags/eg.png differ diff --git a/frontend/vendor/assets/images/flags/eh.png b/frontend/vendor/assets/images/flags/eh.png new file mode 100755 index 00000000000..89fe31a0a96 Binary files /dev/null and b/frontend/vendor/assets/images/flags/eh.png differ diff --git a/frontend/vendor/assets/images/flags/england.png b/frontend/vendor/assets/images/flags/england.png new file mode 100755 index 00000000000..89914eb0802 Binary files /dev/null and b/frontend/vendor/assets/images/flags/england.png differ diff --git a/frontend/vendor/assets/images/flags/er.png b/frontend/vendor/assets/images/flags/er.png new file mode 100755 index 00000000000..2dfe8106b59 Binary files /dev/null and b/frontend/vendor/assets/images/flags/er.png differ diff --git a/frontend/vendor/assets/images/flags/es.png b/frontend/vendor/assets/images/flags/es.png new file mode 100755 index 00000000000..cfd22a445ca Binary files /dev/null and b/frontend/vendor/assets/images/flags/es.png differ diff --git a/frontend/vendor/assets/images/flags/et.png b/frontend/vendor/assets/images/flags/et.png new file mode 100755 index 00000000000..11f79b002a3 Binary files /dev/null and b/frontend/vendor/assets/images/flags/et.png differ diff --git a/frontend/vendor/assets/images/flags/europeanunion.png b/frontend/vendor/assets/images/flags/europeanunion.png new file mode 100644 index 00000000000..8f5a3eb31c1 Binary files /dev/null and b/frontend/vendor/assets/images/flags/europeanunion.png differ diff --git a/frontend/vendor/assets/images/flags/fam.png b/frontend/vendor/assets/images/flags/fam.png new file mode 100755 index 00000000000..4eb9993cbb9 Binary files /dev/null and b/frontend/vendor/assets/images/flags/fam.png differ diff --git a/frontend/vendor/assets/images/flags/fi.png b/frontend/vendor/assets/images/flags/fi.png new file mode 100755 index 00000000000..ec7348e48f4 Binary files /dev/null and b/frontend/vendor/assets/images/flags/fi.png differ diff --git a/frontend/vendor/assets/images/flags/fj.png b/frontend/vendor/assets/images/flags/fj.png new file mode 100755 index 00000000000..903f680324e Binary files /dev/null and b/frontend/vendor/assets/images/flags/fj.png differ diff --git a/frontend/vendor/assets/images/flags/fk.png b/frontend/vendor/assets/images/flags/fk.png new file mode 100755 index 00000000000..5adec9e3e21 Binary files /dev/null and b/frontend/vendor/assets/images/flags/fk.png differ diff --git a/frontend/vendor/assets/images/flags/fm.png b/frontend/vendor/assets/images/flags/fm.png new file mode 100755 index 00000000000..2e2a7a96f06 Binary files /dev/null and b/frontend/vendor/assets/images/flags/fm.png differ diff --git a/frontend/vendor/assets/images/flags/fo.png b/frontend/vendor/assets/images/flags/fo.png new file mode 100755 index 00000000000..39d2040bc6b Binary files /dev/null and b/frontend/vendor/assets/images/flags/fo.png differ diff --git a/frontend/vendor/assets/images/flags/fr.png b/frontend/vendor/assets/images/flags/fr.png new file mode 100755 index 00000000000..6507c0d4308 Binary files /dev/null and b/frontend/vendor/assets/images/flags/fr.png differ diff --git a/frontend/vendor/assets/images/flags/ga.png b/frontend/vendor/assets/images/flags/ga.png new file mode 100755 index 00000000000..b6ca79e3bcd Binary files /dev/null and b/frontend/vendor/assets/images/flags/ga.png differ diff --git a/frontend/vendor/assets/images/flags/gb.png b/frontend/vendor/assets/images/flags/gb.png new file mode 100644 index 00000000000..4576e0d64ec Binary files /dev/null and b/frontend/vendor/assets/images/flags/gb.png differ diff --git a/frontend/vendor/assets/images/flags/gd.png b/frontend/vendor/assets/images/flags/gd.png new file mode 100755 index 00000000000..41c85f48fd6 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gd.png differ diff --git a/frontend/vendor/assets/images/flags/ge.png b/frontend/vendor/assets/images/flags/ge.png new file mode 100755 index 00000000000..8caf6e84138 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ge.png differ diff --git a/frontend/vendor/assets/images/flags/gf.png b/frontend/vendor/assets/images/flags/gf.png new file mode 100755 index 00000000000..6507c0d4308 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gf.png differ diff --git a/frontend/vendor/assets/images/flags/gh.png b/frontend/vendor/assets/images/flags/gh.png new file mode 100755 index 00000000000..5a04cd4b577 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gh.png differ diff --git a/frontend/vendor/assets/images/flags/gi.png b/frontend/vendor/assets/images/flags/gi.png new file mode 100755 index 00000000000..d0e93656b3d Binary files /dev/null and b/frontend/vendor/assets/images/flags/gi.png differ diff --git a/frontend/vendor/assets/images/flags/gl.png b/frontend/vendor/assets/images/flags/gl.png new file mode 100755 index 00000000000..43102a8ca48 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gl.png differ diff --git a/frontend/vendor/assets/images/flags/gm.png b/frontend/vendor/assets/images/flags/gm.png new file mode 100755 index 00000000000..31ccb49e3a2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gm.png differ diff --git a/frontend/vendor/assets/images/flags/gn.png b/frontend/vendor/assets/images/flags/gn.png new file mode 100755 index 00000000000..3efeaa534a8 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gn.png differ diff --git a/frontend/vendor/assets/images/flags/gp.png b/frontend/vendor/assets/images/flags/gp.png new file mode 100755 index 00000000000..160dae80c0b Binary files /dev/null and b/frontend/vendor/assets/images/flags/gp.png differ diff --git a/frontend/vendor/assets/images/flags/gq.png b/frontend/vendor/assets/images/flags/gq.png new file mode 100755 index 00000000000..89e595efe10 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gq.png differ diff --git a/frontend/vendor/assets/images/flags/gr.png b/frontend/vendor/assets/images/flags/gr.png new file mode 100755 index 00000000000..82b50b57757 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gr.png differ diff --git a/frontend/vendor/assets/images/flags/gs.png b/frontend/vendor/assets/images/flags/gs.png new file mode 100755 index 00000000000..e37521f63be Binary files /dev/null and b/frontend/vendor/assets/images/flags/gs.png differ diff --git a/frontend/vendor/assets/images/flags/gt.png b/frontend/vendor/assets/images/flags/gt.png new file mode 100755 index 00000000000..5acb39f198c Binary files /dev/null and b/frontend/vendor/assets/images/flags/gt.png differ diff --git a/frontend/vendor/assets/images/flags/gu.png b/frontend/vendor/assets/images/flags/gu.png new file mode 100755 index 00000000000..668170fd260 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gu.png differ diff --git a/frontend/vendor/assets/images/flags/gw.png b/frontend/vendor/assets/images/flags/gw.png new file mode 100755 index 00000000000..2ad970b20ba Binary files /dev/null and b/frontend/vendor/assets/images/flags/gw.png differ diff --git a/frontend/vendor/assets/images/flags/gy.png b/frontend/vendor/assets/images/flags/gy.png new file mode 100755 index 00000000000..e0979cb5b50 Binary files /dev/null and b/frontend/vendor/assets/images/flags/gy.png differ diff --git a/frontend/vendor/assets/images/flags/hk.png b/frontend/vendor/assets/images/flags/hk.png new file mode 100755 index 00000000000..40bd54954e7 Binary files /dev/null and b/frontend/vendor/assets/images/flags/hk.png differ diff --git a/frontend/vendor/assets/images/flags/hm.png b/frontend/vendor/assets/images/flags/hm.png new file mode 100755 index 00000000000..709eac00de2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/hm.png differ diff --git a/frontend/vendor/assets/images/flags/hn.png b/frontend/vendor/assets/images/flags/hn.png new file mode 100755 index 00000000000..d29296df9ca Binary files /dev/null and b/frontend/vendor/assets/images/flags/hn.png differ diff --git a/frontend/vendor/assets/images/flags/hr.png b/frontend/vendor/assets/images/flags/hr.png new file mode 100755 index 00000000000..20c66c3a60b Binary files /dev/null and b/frontend/vendor/assets/images/flags/hr.png differ diff --git a/frontend/vendor/assets/images/flags/ht.png b/frontend/vendor/assets/images/flags/ht.png new file mode 100755 index 00000000000..d8e284c2cd1 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ht.png differ diff --git a/frontend/vendor/assets/images/flags/hu.png b/frontend/vendor/assets/images/flags/hu.png new file mode 100755 index 00000000000..33648a40b66 Binary files /dev/null and b/frontend/vendor/assets/images/flags/hu.png differ diff --git a/frontend/vendor/assets/images/flags/id.png b/frontend/vendor/assets/images/flags/id.png new file mode 100755 index 00000000000..0dc3600afde Binary files /dev/null and b/frontend/vendor/assets/images/flags/id.png differ diff --git a/frontend/vendor/assets/images/flags/ie.png b/frontend/vendor/assets/images/flags/ie.png new file mode 100755 index 00000000000..292e9a512f2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ie.png differ diff --git a/frontend/vendor/assets/images/flags/il.png b/frontend/vendor/assets/images/flags/il.png new file mode 100755 index 00000000000..96f6ceb2da0 Binary files /dev/null and b/frontend/vendor/assets/images/flags/il.png differ diff --git a/frontend/vendor/assets/images/flags/in.png b/frontend/vendor/assets/images/flags/in.png new file mode 100755 index 00000000000..01d691ba1c1 Binary files /dev/null and b/frontend/vendor/assets/images/flags/in.png differ diff --git a/frontend/vendor/assets/images/flags/io.png b/frontend/vendor/assets/images/flags/io.png new file mode 100755 index 00000000000..d87ed5f9321 Binary files /dev/null and b/frontend/vendor/assets/images/flags/io.png differ diff --git a/frontend/vendor/assets/images/flags/iq.png b/frontend/vendor/assets/images/flags/iq.png new file mode 100755 index 00000000000..4eb6b878bdf Binary files /dev/null and b/frontend/vendor/assets/images/flags/iq.png differ diff --git a/frontend/vendor/assets/images/flags/ir.png b/frontend/vendor/assets/images/flags/ir.png new file mode 100755 index 00000000000..96755068f8b Binary files /dev/null and b/frontend/vendor/assets/images/flags/ir.png differ diff --git a/frontend/vendor/assets/images/flags/is.png b/frontend/vendor/assets/images/flags/is.png new file mode 100755 index 00000000000..e1eb17685fc Binary files /dev/null and b/frontend/vendor/assets/images/flags/is.png differ diff --git a/frontend/vendor/assets/images/flags/it.png b/frontend/vendor/assets/images/flags/it.png new file mode 100755 index 00000000000..c4c491d6729 Binary files /dev/null and b/frontend/vendor/assets/images/flags/it.png differ diff --git a/frontend/vendor/assets/images/flags/ja.png b/frontend/vendor/assets/images/flags/ja.png new file mode 100755 index 00000000000..c3c7946f013 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ja.png differ diff --git a/frontend/vendor/assets/images/flags/jm.png b/frontend/vendor/assets/images/flags/jm.png new file mode 100755 index 00000000000..f4012fa0dc6 Binary files /dev/null and b/frontend/vendor/assets/images/flags/jm.png differ diff --git a/frontend/vendor/assets/images/flags/jo.png b/frontend/vendor/assets/images/flags/jo.png new file mode 100755 index 00000000000..3482c831283 Binary files /dev/null and b/frontend/vendor/assets/images/flags/jo.png differ diff --git a/frontend/vendor/assets/images/flags/ke.png b/frontend/vendor/assets/images/flags/ke.png new file mode 100755 index 00000000000..5842ce3c421 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ke.png differ diff --git a/frontend/vendor/assets/images/flags/kg.png b/frontend/vendor/assets/images/flags/kg.png new file mode 100755 index 00000000000..766bc898166 Binary files /dev/null and b/frontend/vendor/assets/images/flags/kg.png differ diff --git a/frontend/vendor/assets/images/flags/kh.png b/frontend/vendor/assets/images/flags/kh.png new file mode 100755 index 00000000000..c2d9aea1ce5 Binary files /dev/null and b/frontend/vendor/assets/images/flags/kh.png differ diff --git a/frontend/vendor/assets/images/flags/ki.png b/frontend/vendor/assets/images/flags/ki.png new file mode 100755 index 00000000000..196746e08e0 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ki.png differ diff --git a/frontend/vendor/assets/images/flags/km.png b/frontend/vendor/assets/images/flags/km.png new file mode 100755 index 00000000000..29f5137ef0c Binary files /dev/null and b/frontend/vendor/assets/images/flags/km.png differ diff --git a/frontend/vendor/assets/images/flags/kn.png b/frontend/vendor/assets/images/flags/kn.png new file mode 100755 index 00000000000..4460ff60534 Binary files /dev/null and b/frontend/vendor/assets/images/flags/kn.png differ diff --git a/frontend/vendor/assets/images/flags/kp.png b/frontend/vendor/assets/images/flags/kp.png new file mode 100755 index 00000000000..dc59c72020f Binary files /dev/null and b/frontend/vendor/assets/images/flags/kp.png differ diff --git a/frontend/vendor/assets/images/flags/kr.png b/frontend/vendor/assets/images/flags/kr.png new file mode 100755 index 00000000000..09ad2c39b54 Binary files /dev/null and b/frontend/vendor/assets/images/flags/kr.png differ diff --git a/frontend/vendor/assets/images/flags/kw.png b/frontend/vendor/assets/images/flags/kw.png new file mode 100755 index 00000000000..2742d890f3d Binary files /dev/null and b/frontend/vendor/assets/images/flags/kw.png differ diff --git a/frontend/vendor/assets/images/flags/ky.png b/frontend/vendor/assets/images/flags/ky.png new file mode 100755 index 00000000000..b813d08985e Binary files /dev/null and b/frontend/vendor/assets/images/flags/ky.png differ diff --git a/frontend/vendor/assets/images/flags/kz.png b/frontend/vendor/assets/images/flags/kz.png new file mode 100755 index 00000000000..b25bd067e75 Binary files /dev/null and b/frontend/vendor/assets/images/flags/kz.png differ diff --git a/frontend/vendor/assets/images/flags/la.png b/frontend/vendor/assets/images/flags/la.png new file mode 100755 index 00000000000..834de2dad3c Binary files /dev/null and b/frontend/vendor/assets/images/flags/la.png differ diff --git a/frontend/vendor/assets/images/flags/lb.png b/frontend/vendor/assets/images/flags/lb.png new file mode 100755 index 00000000000..5ecb9e8d1a5 Binary files /dev/null and b/frontend/vendor/assets/images/flags/lb.png differ diff --git a/frontend/vendor/assets/images/flags/lc.png b/frontend/vendor/assets/images/flags/lc.png new file mode 100644 index 00000000000..2c9870761b5 Binary files /dev/null and b/frontend/vendor/assets/images/flags/lc.png differ diff --git a/frontend/vendor/assets/images/flags/li.png b/frontend/vendor/assets/images/flags/li.png new file mode 100755 index 00000000000..0d7ad9717cc Binary files /dev/null and b/frontend/vendor/assets/images/flags/li.png differ diff --git a/frontend/vendor/assets/images/flags/lk.png b/frontend/vendor/assets/images/flags/lk.png new file mode 100755 index 00000000000..019a29b3e37 Binary files /dev/null and b/frontend/vendor/assets/images/flags/lk.png differ diff --git a/frontend/vendor/assets/images/flags/lr.png b/frontend/vendor/assets/images/flags/lr.png new file mode 100755 index 00000000000..62c336f2799 Binary files /dev/null and b/frontend/vendor/assets/images/flags/lr.png differ diff --git a/frontend/vendor/assets/images/flags/ls.png b/frontend/vendor/assets/images/flags/ls.png new file mode 100755 index 00000000000..50ba6fb831d Binary files /dev/null and b/frontend/vendor/assets/images/flags/ls.png differ diff --git a/frontend/vendor/assets/images/flags/lt.png b/frontend/vendor/assets/images/flags/lt.png new file mode 100755 index 00000000000..ace453f7f2d Binary files /dev/null and b/frontend/vendor/assets/images/flags/lt.png differ diff --git a/frontend/vendor/assets/images/flags/lu.png b/frontend/vendor/assets/images/flags/lu.png new file mode 100755 index 00000000000..4a9e3d5d9a3 Binary files /dev/null and b/frontend/vendor/assets/images/flags/lu.png differ diff --git a/frontend/vendor/assets/images/flags/lv.png b/frontend/vendor/assets/images/flags/lv.png new file mode 100755 index 00000000000..d9236f966bd Binary files /dev/null and b/frontend/vendor/assets/images/flags/lv.png differ diff --git a/frontend/vendor/assets/images/flags/ly.png b/frontend/vendor/assets/images/flags/ly.png new file mode 100755 index 00000000000..445d8d694c2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ly.png differ diff --git a/frontend/vendor/assets/images/flags/ma.png b/frontend/vendor/assets/images/flags/ma.png new file mode 100755 index 00000000000..44f2961c1be Binary files /dev/null and b/frontend/vendor/assets/images/flags/ma.png differ diff --git a/frontend/vendor/assets/images/flags/mc.png b/frontend/vendor/assets/images/flags/mc.png new file mode 100755 index 00000000000..9c034d4bcc2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mc.png differ diff --git a/frontend/vendor/assets/images/flags/md.png b/frontend/vendor/assets/images/flags/md.png new file mode 100755 index 00000000000..80b6bf63af9 Binary files /dev/null and b/frontend/vendor/assets/images/flags/md.png differ diff --git a/frontend/vendor/assets/images/flags/me.png b/frontend/vendor/assets/images/flags/me.png new file mode 100644 index 00000000000..0a3ee99b217 Binary files /dev/null and b/frontend/vendor/assets/images/flags/me.png differ diff --git a/frontend/vendor/assets/images/flags/mg.png b/frontend/vendor/assets/images/flags/mg.png new file mode 100755 index 00000000000..b8740f880c7 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mg.png differ diff --git a/frontend/vendor/assets/images/flags/mh.png b/frontend/vendor/assets/images/flags/mh.png new file mode 100755 index 00000000000..a2cb2d528fb Binary files /dev/null and b/frontend/vendor/assets/images/flags/mh.png differ diff --git a/frontend/vendor/assets/images/flags/mk.png b/frontend/vendor/assets/images/flags/mk.png new file mode 100755 index 00000000000..f9810d0c706 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mk.png differ diff --git a/frontend/vendor/assets/images/flags/ml.png b/frontend/vendor/assets/images/flags/ml.png new file mode 100755 index 00000000000..a28459d13f1 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ml.png differ diff --git a/frontend/vendor/assets/images/flags/mm.png b/frontend/vendor/assets/images/flags/mm.png new file mode 100755 index 00000000000..106421e06d6 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mm.png differ diff --git a/frontend/vendor/assets/images/flags/mn.png b/frontend/vendor/assets/images/flags/mn.png new file mode 100755 index 00000000000..cc6c412171f Binary files /dev/null and b/frontend/vendor/assets/images/flags/mn.png differ diff --git a/frontend/vendor/assets/images/flags/mo.png b/frontend/vendor/assets/images/flags/mo.png new file mode 100755 index 00000000000..fc49f581ee4 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mo.png differ diff --git a/frontend/vendor/assets/images/flags/mp.png b/frontend/vendor/assets/images/flags/mp.png new file mode 100755 index 00000000000..f66195ed90b Binary files /dev/null and b/frontend/vendor/assets/images/flags/mp.png differ diff --git a/frontend/vendor/assets/images/flags/mq.png b/frontend/vendor/assets/images/flags/mq.png new file mode 100755 index 00000000000..3743546d6d5 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mq.png differ diff --git a/frontend/vendor/assets/images/flags/mr.png b/frontend/vendor/assets/images/flags/mr.png new file mode 100755 index 00000000000..6700d1ce92b Binary files /dev/null and b/frontend/vendor/assets/images/flags/mr.png differ diff --git a/frontend/vendor/assets/images/flags/ms.png b/frontend/vendor/assets/images/flags/ms.png new file mode 100755 index 00000000000..ecda6cd36be Binary files /dev/null and b/frontend/vendor/assets/images/flags/ms.png differ diff --git a/frontend/vendor/assets/images/flags/mt.png b/frontend/vendor/assets/images/flags/mt.png new file mode 100755 index 00000000000..20ad5c078f9 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mt.png differ diff --git a/frontend/vendor/assets/images/flags/mu.png b/frontend/vendor/assets/images/flags/mu.png new file mode 100755 index 00000000000..9fcd7256544 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mu.png differ diff --git a/frontend/vendor/assets/images/flags/mv.png b/frontend/vendor/assets/images/flags/mv.png new file mode 100755 index 00000000000..f03f90ac658 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mv.png differ diff --git a/frontend/vendor/assets/images/flags/mw.png b/frontend/vendor/assets/images/flags/mw.png new file mode 100755 index 00000000000..1ee6485edd3 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mw.png differ diff --git a/frontend/vendor/assets/images/flags/mx.png b/frontend/vendor/assets/images/flags/mx.png new file mode 100755 index 00000000000..d1350447374 Binary files /dev/null and b/frontend/vendor/assets/images/flags/mx.png differ diff --git a/frontend/vendor/assets/images/flags/my.png b/frontend/vendor/assets/images/flags/my.png new file mode 100755 index 00000000000..d47867be7a0 Binary files /dev/null and b/frontend/vendor/assets/images/flags/my.png differ diff --git a/frontend/vendor/assets/images/flags/mz.png b/frontend/vendor/assets/images/flags/mz.png new file mode 100755 index 00000000000..464017e923c Binary files /dev/null and b/frontend/vendor/assets/images/flags/mz.png differ diff --git a/frontend/vendor/assets/images/flags/na.png b/frontend/vendor/assets/images/flags/na.png new file mode 100755 index 00000000000..300a80223aa Binary files /dev/null and b/frontend/vendor/assets/images/flags/na.png differ diff --git a/frontend/vendor/assets/images/flags/nc.png b/frontend/vendor/assets/images/flags/nc.png new file mode 100755 index 00000000000..fc6437b08b9 Binary files /dev/null and b/frontend/vendor/assets/images/flags/nc.png differ diff --git a/frontend/vendor/assets/images/flags/ne.png b/frontend/vendor/assets/images/flags/ne.png new file mode 100755 index 00000000000..d1aeebcf2c4 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ne.png differ diff --git a/frontend/vendor/assets/images/flags/nf.png b/frontend/vendor/assets/images/flags/nf.png new file mode 100755 index 00000000000..b00b331fe31 Binary files /dev/null and b/frontend/vendor/assets/images/flags/nf.png differ diff --git a/frontend/vendor/assets/images/flags/ng.png b/frontend/vendor/assets/images/flags/ng.png new file mode 100755 index 00000000000..6dcc973ed58 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ng.png differ diff --git a/frontend/vendor/assets/images/flags/ni.png b/frontend/vendor/assets/images/flags/ni.png new file mode 100755 index 00000000000..7b3cdd31aef Binary files /dev/null and b/frontend/vendor/assets/images/flags/ni.png differ diff --git a/frontend/vendor/assets/images/flags/nl.png b/frontend/vendor/assets/images/flags/nl.png new file mode 100755 index 00000000000..ad5243eb4a8 Binary files /dev/null and b/frontend/vendor/assets/images/flags/nl.png differ diff --git a/frontend/vendor/assets/images/flags/no.png b/frontend/vendor/assets/images/flags/no.png new file mode 100755 index 00000000000..74264c5866b Binary files /dev/null and b/frontend/vendor/assets/images/flags/no.png differ diff --git a/frontend/vendor/assets/images/flags/np.png b/frontend/vendor/assets/images/flags/np.png new file mode 100755 index 00000000000..42fa5f0d737 Binary files /dev/null and b/frontend/vendor/assets/images/flags/np.png differ diff --git a/frontend/vendor/assets/images/flags/nr.png b/frontend/vendor/assets/images/flags/nr.png new file mode 100755 index 00000000000..211e42bb19f Binary files /dev/null and b/frontend/vendor/assets/images/flags/nr.png differ diff --git a/frontend/vendor/assets/images/flags/nu.png b/frontend/vendor/assets/images/flags/nu.png new file mode 100755 index 00000000000..b9f7f988f47 Binary files /dev/null and b/frontend/vendor/assets/images/flags/nu.png differ diff --git a/frontend/vendor/assets/images/flags/nz.png b/frontend/vendor/assets/images/flags/nz.png new file mode 100755 index 00000000000..be935de8728 Binary files /dev/null and b/frontend/vendor/assets/images/flags/nz.png differ diff --git a/frontend/vendor/assets/images/flags/om.png b/frontend/vendor/assets/images/flags/om.png new file mode 100755 index 00000000000..ec95c5aeb9f Binary files /dev/null and b/frontend/vendor/assets/images/flags/om.png differ diff --git a/frontend/vendor/assets/images/flags/pa.png b/frontend/vendor/assets/images/flags/pa.png new file mode 100755 index 00000000000..3397cf1de40 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pa.png differ diff --git a/frontend/vendor/assets/images/flags/pe.png b/frontend/vendor/assets/images/flags/pe.png new file mode 100755 index 00000000000..069f5ba8985 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pe.png differ diff --git a/frontend/vendor/assets/images/flags/pf.png b/frontend/vendor/assets/images/flags/pf.png new file mode 100755 index 00000000000..4891c4e97ff Binary files /dev/null and b/frontend/vendor/assets/images/flags/pf.png differ diff --git a/frontend/vendor/assets/images/flags/pg.png b/frontend/vendor/assets/images/flags/pg.png new file mode 100755 index 00000000000..d447975ce44 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pg.png differ diff --git a/frontend/vendor/assets/images/flags/ph.png b/frontend/vendor/assets/images/flags/ph.png new file mode 100755 index 00000000000..703dd735a59 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ph.png differ diff --git a/frontend/vendor/assets/images/flags/pk.png b/frontend/vendor/assets/images/flags/pk.png new file mode 100755 index 00000000000..f20f3f6e2e5 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pk.png differ diff --git a/frontend/vendor/assets/images/flags/pl.png b/frontend/vendor/assets/images/flags/pl.png new file mode 100755 index 00000000000..6b5c1a2f749 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pl.png differ diff --git a/frontend/vendor/assets/images/flags/pm.png b/frontend/vendor/assets/images/flags/pm.png new file mode 100755 index 00000000000..4aa799ba7b2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pm.png differ diff --git a/frontend/vendor/assets/images/flags/pn.png b/frontend/vendor/assets/images/flags/pn.png new file mode 100755 index 00000000000..6f62a64d650 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pn.png differ diff --git a/frontend/vendor/assets/images/flags/pr.png b/frontend/vendor/assets/images/flags/pr.png new file mode 100755 index 00000000000..2b3e50af423 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pr.png differ diff --git a/frontend/vendor/assets/images/flags/ps.png b/frontend/vendor/assets/images/flags/ps.png new file mode 100755 index 00000000000..dd1d7821ec5 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ps.png differ diff --git a/frontend/vendor/assets/images/flags/pt.png b/frontend/vendor/assets/images/flags/pt.png new file mode 100755 index 00000000000..50ba8aa2f4d Binary files /dev/null and b/frontend/vendor/assets/images/flags/pt.png differ diff --git a/frontend/vendor/assets/images/flags/pw.png b/frontend/vendor/assets/images/flags/pw.png new file mode 100755 index 00000000000..71fd0a48ca2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/pw.png differ diff --git a/frontend/vendor/assets/images/flags/py.png b/frontend/vendor/assets/images/flags/py.png new file mode 100755 index 00000000000..6f205901c95 Binary files /dev/null and b/frontend/vendor/assets/images/flags/py.png differ diff --git a/frontend/vendor/assets/images/flags/qa.png b/frontend/vendor/assets/images/flags/qa.png new file mode 100755 index 00000000000..aed9b8c6792 Binary files /dev/null and b/frontend/vendor/assets/images/flags/qa.png differ diff --git a/frontend/vendor/assets/images/flags/re.png b/frontend/vendor/assets/images/flags/re.png new file mode 100755 index 00000000000..6507c0d4308 Binary files /dev/null and b/frontend/vendor/assets/images/flags/re.png differ diff --git a/frontend/vendor/assets/images/flags/ro.png b/frontend/vendor/assets/images/flags/ro.png new file mode 100755 index 00000000000..2535bc81f2f Binary files /dev/null and b/frontend/vendor/assets/images/flags/ro.png differ diff --git a/frontend/vendor/assets/images/flags/rs.png b/frontend/vendor/assets/images/flags/rs.png new file mode 100644 index 00000000000..f1d179630d4 Binary files /dev/null and b/frontend/vendor/assets/images/flags/rs.png differ diff --git a/frontend/vendor/assets/images/flags/ru.png b/frontend/vendor/assets/images/flags/ru.png new file mode 100755 index 00000000000..8ab1e677984 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ru.png differ diff --git a/frontend/vendor/assets/images/flags/rw.png b/frontend/vendor/assets/images/flags/rw.png new file mode 100755 index 00000000000..7cd51a9c37e Binary files /dev/null and b/frontend/vendor/assets/images/flags/rw.png differ diff --git a/frontend/vendor/assets/images/flags/sa.png b/frontend/vendor/assets/images/flags/sa.png new file mode 100755 index 00000000000..bff6542314d Binary files /dev/null and b/frontend/vendor/assets/images/flags/sa.png differ diff --git a/frontend/vendor/assets/images/flags/sb.png b/frontend/vendor/assets/images/flags/sb.png new file mode 100755 index 00000000000..8af1fb9d2fe Binary files /dev/null and b/frontend/vendor/assets/images/flags/sb.png differ diff --git a/frontend/vendor/assets/images/flags/sc.png b/frontend/vendor/assets/images/flags/sc.png new file mode 100755 index 00000000000..fec2961dbb3 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sc.png differ diff --git a/frontend/vendor/assets/images/flags/scotland.png b/frontend/vendor/assets/images/flags/scotland.png new file mode 100755 index 00000000000..bfabc4ff339 Binary files /dev/null and b/frontend/vendor/assets/images/flags/scotland.png differ diff --git a/frontend/vendor/assets/images/flags/sd.png b/frontend/vendor/assets/images/flags/sd.png new file mode 100755 index 00000000000..eba0a2079b1 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sd.png differ diff --git a/frontend/vendor/assets/images/flags/se.png b/frontend/vendor/assets/images/flags/se.png new file mode 100755 index 00000000000..2a74e78cd06 Binary files /dev/null and b/frontend/vendor/assets/images/flags/se.png differ diff --git a/frontend/vendor/assets/images/flags/sg.png b/frontend/vendor/assets/images/flags/sg.png new file mode 100755 index 00000000000..84172cae186 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sg.png differ diff --git a/frontend/vendor/assets/images/flags/sh.png b/frontend/vendor/assets/images/flags/sh.png new file mode 100755 index 00000000000..c7d1a73962b Binary files /dev/null and b/frontend/vendor/assets/images/flags/sh.png differ diff --git a/frontend/vendor/assets/images/flags/si.png b/frontend/vendor/assets/images/flags/si.png new file mode 100755 index 00000000000..266b9df29d3 Binary files /dev/null and b/frontend/vendor/assets/images/flags/si.png differ diff --git a/frontend/vendor/assets/images/flags/sj.png b/frontend/vendor/assets/images/flags/sj.png new file mode 100755 index 00000000000..74264c5866b Binary files /dev/null and b/frontend/vendor/assets/images/flags/sj.png differ diff --git a/frontend/vendor/assets/images/flags/sk.png b/frontend/vendor/assets/images/flags/sk.png new file mode 100755 index 00000000000..53d286a6e1f Binary files /dev/null and b/frontend/vendor/assets/images/flags/sk.png differ diff --git a/frontend/vendor/assets/images/flags/sl.png b/frontend/vendor/assets/images/flags/sl.png new file mode 100755 index 00000000000..c59445bc58f Binary files /dev/null and b/frontend/vendor/assets/images/flags/sl.png differ diff --git a/frontend/vendor/assets/images/flags/sm.png b/frontend/vendor/assets/images/flags/sm.png new file mode 100755 index 00000000000..ae1886c90e1 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sm.png differ diff --git a/frontend/vendor/assets/images/flags/sn.png b/frontend/vendor/assets/images/flags/sn.png new file mode 100755 index 00000000000..89961cb89b2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sn.png differ diff --git a/frontend/vendor/assets/images/flags/so.png b/frontend/vendor/assets/images/flags/so.png new file mode 100755 index 00000000000..2cb5b27216f Binary files /dev/null and b/frontend/vendor/assets/images/flags/so.png differ diff --git a/frontend/vendor/assets/images/flags/sr.png b/frontend/vendor/assets/images/flags/sr.png new file mode 100755 index 00000000000..764f5446943 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sr.png differ diff --git a/frontend/vendor/assets/images/flags/st.png b/frontend/vendor/assets/images/flags/st.png new file mode 100755 index 00000000000..dd62dce7fff Binary files /dev/null and b/frontend/vendor/assets/images/flags/st.png differ diff --git a/frontend/vendor/assets/images/flags/sv.png b/frontend/vendor/assets/images/flags/sv.png new file mode 100755 index 00000000000..eea4e5b2810 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sv.png differ diff --git a/frontend/vendor/assets/images/flags/sy.png b/frontend/vendor/assets/images/flags/sy.png new file mode 100755 index 00000000000..df88ef51d71 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sy.png differ diff --git a/frontend/vendor/assets/images/flags/sz.png b/frontend/vendor/assets/images/flags/sz.png new file mode 100755 index 00000000000..cb8b587d8b8 Binary files /dev/null and b/frontend/vendor/assets/images/flags/sz.png differ diff --git a/frontend/vendor/assets/images/flags/tc.png b/frontend/vendor/assets/images/flags/tc.png new file mode 100755 index 00000000000..0c96ec08c85 Binary files /dev/null and b/frontend/vendor/assets/images/flags/tc.png differ diff --git a/frontend/vendor/assets/images/flags/td.png b/frontend/vendor/assets/images/flags/td.png new file mode 100755 index 00000000000..6e2da097056 Binary files /dev/null and b/frontend/vendor/assets/images/flags/td.png differ diff --git a/frontend/vendor/assets/images/flags/tf.png b/frontend/vendor/assets/images/flags/tf.png new file mode 100755 index 00000000000..3ed6bc60add Binary files /dev/null and b/frontend/vendor/assets/images/flags/tf.png differ diff --git a/frontend/vendor/assets/images/flags/tg.png b/frontend/vendor/assets/images/flags/tg.png new file mode 100755 index 00000000000..2ab18d070af Binary files /dev/null and b/frontend/vendor/assets/images/flags/tg.png differ diff --git a/frontend/vendor/assets/images/flags/th.png b/frontend/vendor/assets/images/flags/th.png new file mode 100755 index 00000000000..8a7f438c04f Binary files /dev/null and b/frontend/vendor/assets/images/flags/th.png differ diff --git a/frontend/vendor/assets/images/flags/tj.png b/frontend/vendor/assets/images/flags/tj.png new file mode 100755 index 00000000000..d106640ee20 Binary files /dev/null and b/frontend/vendor/assets/images/flags/tj.png differ diff --git a/frontend/vendor/assets/images/flags/tk.png b/frontend/vendor/assets/images/flags/tk.png new file mode 100755 index 00000000000..0440c43b42c Binary files /dev/null and b/frontend/vendor/assets/images/flags/tk.png differ diff --git a/frontend/vendor/assets/images/flags/tl.png b/frontend/vendor/assets/images/flags/tl.png new file mode 100755 index 00000000000..0fa1b59794f Binary files /dev/null and b/frontend/vendor/assets/images/flags/tl.png differ diff --git a/frontend/vendor/assets/images/flags/tm.png b/frontend/vendor/assets/images/flags/tm.png new file mode 100755 index 00000000000..b6076f7dfc3 Binary files /dev/null and b/frontend/vendor/assets/images/flags/tm.png differ diff --git a/frontend/vendor/assets/images/flags/tn.png b/frontend/vendor/assets/images/flags/tn.png new file mode 100755 index 00000000000..33d830d5027 Binary files /dev/null and b/frontend/vendor/assets/images/flags/tn.png differ diff --git a/frontend/vendor/assets/images/flags/to.png b/frontend/vendor/assets/images/flags/to.png new file mode 100755 index 00000000000..c4688f05a69 Binary files /dev/null and b/frontend/vendor/assets/images/flags/to.png differ diff --git a/frontend/vendor/assets/images/flags/tr.png b/frontend/vendor/assets/images/flags/tr.png new file mode 100755 index 00000000000..3698f405fac Binary files /dev/null and b/frontend/vendor/assets/images/flags/tr.png differ diff --git a/frontend/vendor/assets/images/flags/tt.png b/frontend/vendor/assets/images/flags/tt.png new file mode 100755 index 00000000000..4f36edb09fd Binary files /dev/null and b/frontend/vendor/assets/images/flags/tt.png differ diff --git a/frontend/vendor/assets/images/flags/tv.png b/frontend/vendor/assets/images/flags/tv.png new file mode 100755 index 00000000000..d24d4e72cd8 Binary files /dev/null and b/frontend/vendor/assets/images/flags/tv.png differ diff --git a/frontend/vendor/assets/images/flags/tw.png b/frontend/vendor/assets/images/flags/tw.png new file mode 100755 index 00000000000..432b92c0623 Binary files /dev/null and b/frontend/vendor/assets/images/flags/tw.png differ diff --git a/frontend/vendor/assets/images/flags/tz.png b/frontend/vendor/assets/images/flags/tz.png new file mode 100755 index 00000000000..eaf2d3796f9 Binary files /dev/null and b/frontend/vendor/assets/images/flags/tz.png differ diff --git a/frontend/vendor/assets/images/flags/ua.png b/frontend/vendor/assets/images/flags/ua.png new file mode 100755 index 00000000000..ddda0b21abf Binary files /dev/null and b/frontend/vendor/assets/images/flags/ua.png differ diff --git a/frontend/vendor/assets/images/flags/ug.png b/frontend/vendor/assets/images/flags/ug.png new file mode 100755 index 00000000000..7a776155920 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ug.png differ diff --git a/frontend/vendor/assets/images/flags/um.png b/frontend/vendor/assets/images/flags/um.png new file mode 100755 index 00000000000..9ff9ecf851f Binary files /dev/null and b/frontend/vendor/assets/images/flags/um.png differ diff --git a/frontend/vendor/assets/images/flags/us.png b/frontend/vendor/assets/images/flags/us.png new file mode 100755 index 00000000000..b358c9a944f Binary files /dev/null and b/frontend/vendor/assets/images/flags/us.png differ diff --git a/frontend/vendor/assets/images/flags/uy.png b/frontend/vendor/assets/images/flags/uy.png new file mode 100755 index 00000000000..6730b961fc7 Binary files /dev/null and b/frontend/vendor/assets/images/flags/uy.png differ diff --git a/frontend/vendor/assets/images/flags/uz.png b/frontend/vendor/assets/images/flags/uz.png new file mode 100755 index 00000000000..c3ba2ddcf26 Binary files /dev/null and b/frontend/vendor/assets/images/flags/uz.png differ diff --git a/frontend/vendor/assets/images/flags/va.png b/frontend/vendor/assets/images/flags/va.png new file mode 100755 index 00000000000..c9be43d2665 Binary files /dev/null and b/frontend/vendor/assets/images/flags/va.png differ diff --git a/frontend/vendor/assets/images/flags/vc.png b/frontend/vendor/assets/images/flags/vc.png new file mode 100755 index 00000000000..a7935eee689 Binary files /dev/null and b/frontend/vendor/assets/images/flags/vc.png differ diff --git a/frontend/vendor/assets/images/flags/ve.png b/frontend/vendor/assets/images/flags/ve.png new file mode 100755 index 00000000000..43eb422e114 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ve.png differ diff --git a/frontend/vendor/assets/images/flags/vg.png b/frontend/vendor/assets/images/flags/vg.png new file mode 100755 index 00000000000..63afb343ca2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/vg.png differ diff --git a/frontend/vendor/assets/images/flags/vi.png b/frontend/vendor/assets/images/flags/vi.png new file mode 100755 index 00000000000..812028c82f0 Binary files /dev/null and b/frontend/vendor/assets/images/flags/vi.png differ diff --git a/frontend/vendor/assets/images/flags/vn.png b/frontend/vendor/assets/images/flags/vn.png new file mode 100755 index 00000000000..a2cd8a151a2 Binary files /dev/null and b/frontend/vendor/assets/images/flags/vn.png differ diff --git a/frontend/vendor/assets/images/flags/vu.png b/frontend/vendor/assets/images/flags/vu.png new file mode 100755 index 00000000000..e7972bc01d7 Binary files /dev/null and b/frontend/vendor/assets/images/flags/vu.png differ diff --git a/frontend/vendor/assets/images/flags/wales.png b/frontend/vendor/assets/images/flags/wales.png new file mode 100755 index 00000000000..1b32fae1805 Binary files /dev/null and b/frontend/vendor/assets/images/flags/wales.png differ diff --git a/frontend/vendor/assets/images/flags/wf.png b/frontend/vendor/assets/images/flags/wf.png new file mode 100755 index 00000000000..d9d33e18b01 Binary files /dev/null and b/frontend/vendor/assets/images/flags/wf.png differ diff --git a/frontend/vendor/assets/images/flags/ws.png b/frontend/vendor/assets/images/flags/ws.png new file mode 100755 index 00000000000..8b40fdd2245 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ws.png differ diff --git a/frontend/vendor/assets/images/flags/ye.png b/frontend/vendor/assets/images/flags/ye.png new file mode 100755 index 00000000000..6437e6f9184 Binary files /dev/null and b/frontend/vendor/assets/images/flags/ye.png differ diff --git a/frontend/vendor/assets/images/flags/yt.png b/frontend/vendor/assets/images/flags/yt.png new file mode 100755 index 00000000000..ca9bacbbf7b Binary files /dev/null and b/frontend/vendor/assets/images/flags/yt.png differ diff --git a/frontend/vendor/assets/images/flags/za.png b/frontend/vendor/assets/images/flags/za.png new file mode 100755 index 00000000000..5200bf97616 Binary files /dev/null and b/frontend/vendor/assets/images/flags/za.png differ diff --git a/frontend/vendor/assets/images/flags/zm.png b/frontend/vendor/assets/images/flags/zm.png new file mode 100755 index 00000000000..5145ab55474 Binary files /dev/null and b/frontend/vendor/assets/images/flags/zm.png differ diff --git a/frontend/vendor/assets/images/flags/zw.png b/frontend/vendor/assets/images/flags/zw.png new file mode 100755 index 00000000000..3d2d4b65913 Binary files /dev/null and b/frontend/vendor/assets/images/flags/zw.png differ diff --git a/core/vendor/assets/images/jquery.formalize/button.png b/frontend/vendor/assets/images/jquery.formalize/button.png similarity index 100% rename from core/vendor/assets/images/jquery.formalize/button.png rename to frontend/vendor/assets/images/jquery.formalize/button.png diff --git a/core/vendor/assets/images/jquery.formalize/select_arrow.gif b/frontend/vendor/assets/images/jquery.formalize/select_arrow.gif similarity index 100% rename from core/vendor/assets/images/jquery.formalize/select_arrow.gif rename to frontend/vendor/assets/images/jquery.formalize/select_arrow.gif diff --git a/core/vendor/assets/javascripts/jquery.formalize.min.js b/frontend/vendor/assets/javascripts/jquery.formalize.min.js similarity index 100% rename from core/vendor/assets/javascripts/jquery.formalize.min.js rename to frontend/vendor/assets/javascripts/jquery.formalize.min.js diff --git a/frontend/vendor/assets/javascripts/jquery.validate/additional-methods.min.js b/frontend/vendor/assets/javascripts/jquery.validate/additional-methods.min.js new file mode 100644 index 00000000000..2e3fa4dff15 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/additional-methods.min.js @@ -0,0 +1,12 @@ +/*! + * jQuery Validation Plugin 1.11.1 + * + * http://bassistance.de/jquery-plugins/jquery-plugin-validation/ + * http://docs.jquery.com/Plugins/Validation + * + * Copyright 2013 Jörn Zaefferer + * Released under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ + +!function(){function stripHtml(value){return value.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'"_+=\/\-]*/g,"")}jQuery.validator.addMethod("maxWords",function(value,element,params){return this.optional(element)||stripHtml(value).match(/\b\w+\b/g).length<=params},jQuery.validator.format("Please enter {0} words or less."));jQuery.validator.addMethod("minWords",function(value,element,params){return this.optional(element)||stripHtml(value).match(/\b\w+\b/g).length>=params},jQuery.validator.format("Please enter at least {0} words."));jQuery.validator.addMethod("rangeWords",function(value,element,params){var valueStripped=stripHtml(value);var regex=/\b\w+\b/g;return this.optional(element)||valueStripped.match(regex).length>=params[0]&&valueStripped.match(regex).length<=params[1]},jQuery.validator.format("Please enter between {0} and {1} words."))}();jQuery.validator.addMethod("letterswithbasicpunc",function(value,element){return this.optional(element)||/^[a-z\-.,()'"\s]+$/i.test(value)},"Letters or punctuation only please");jQuery.validator.addMethod("alphanumeric",function(value,element){return this.optional(element)||/^\w+$/i.test(value)},"Letters, numbers, and underscores only please");jQuery.validator.addMethod("lettersonly",function(value,element){return this.optional(element)||/^[a-z]+$/i.test(value)},"Letters only please");jQuery.validator.addMethod("nowhitespace",function(value,element){return this.optional(element)||/^\S+$/i.test(value)},"No white space please");jQuery.validator.addMethod("ziprange",function(value,element){return this.optional(element)||/^90[2-5]\d\{2\}-\d{4}$/.test(value)},"Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx");jQuery.validator.addMethod("zipcodeUS",function(value,element){return this.optional(element)||/\d{5}-\d{4}$|^\d{5}$/.test(value)},"The specified US ZIP Code is invalid");jQuery.validator.addMethod("integer",function(value,element){return this.optional(element)||/^-?\d+$/.test(value)},"A positive or negative non-decimal number please");jQuery.validator.addMethod("vinUS",function(v){if(v.length!==17){return false}var i,n,d,f,cd,cdv;var LL=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"];var VL=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9];var FL=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2];var rs=0;for(i=0;i<17;i++){f=FL[i];d=v.slice(i,i+1);if(i===8){cdv=d}if(!isNaN(d)){d*=f}else{for(n=0;n9&&phone_number.match(/^(\+?1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/)},"Please specify a valid phone number");jQuery.validator.addMethod("phoneUK",function(phone_number,element){phone_number=phone_number.replace(/\(|\)|\s+|-/g,"");return this.optional(element)||phone_number.length>9&&phone_number.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number");jQuery.validator.addMethod("mobileUK",function(phone_number,element){phone_number=phone_number.replace(/\(|\)|\s+|-/g,"");return this.optional(element)||phone_number.length>9&&phone_number.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[45789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number");jQuery.validator.addMethod("phonesUK",function(phone_number,element){phone_number=phone_number.replace(/\(|\)|\s+|-/g,"");return this.optional(element)||phone_number.length>9&&phone_number.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[45789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number");jQuery.validator.addMethod("postcodeUK",function(value,element){return this.optional(element)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(value)},"Please specify a valid UK postcode");jQuery.validator.addMethod("strippedminlength",function(value,element,param){return jQuery(value).text().length>=param},jQuery.validator.format("Please enter at least {0} characters"));jQuery.validator.addMethod("email2",function(value,element,param){return this.optional(element)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value)},jQuery.validator.messages.email);jQuery.validator.addMethod("url2",function(value,element,param){return this.optional(element)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value)},jQuery.validator.messages.url);jQuery.validator.addMethod("creditcardtypes",function(value,element,param){if(/[^0-9\-]+/.test(value)){return false}value=value.replace(/\D/g,"");var validTypes=0;if(param.mastercard){validTypes|=1}if(param.visa){validTypes|=2}if(param.amex){validTypes|=4}if(param.dinersclub){validTypes|=8}if(param.enroute){validTypes|=16}if(param.discover){validTypes|=32}if(param.jcb){validTypes|=64}if(param.unknown){validTypes|=128}if(param.all){validTypes=1|2|4|8|16|32|64|128}if(validTypes&1&&/^(5[12345])/.test(value)){return value.length===16}if(validTypes&2&&/^(4)/.test(value)){return value.length===16}if(validTypes&4&&/^(3[47])/.test(value)){return value.length===15}if(validTypes&8&&/^(3(0[012345]|[68]))/.test(value)){return value.length===14}if(validTypes&16&&/^(2(014|149))/.test(value)){return value.length===15}if(validTypes&32&&/^(6011)/.test(value)){return value.length===16}if(validTypes&64&&/^(3)/.test(value)){return value.length===16}if(validTypes&64&&/^(2131|1800)/.test(value)){return value.length===15}if(validTypes&128){return true}return false},"Please enter a valid credit card number.");jQuery.validator.addMethod("ipv4",function(value,element,param){return this.optional(element)||/^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i.test(value)},"Please enter a valid IP v4 address.");jQuery.validator.addMethod("ipv6",function(value,element,param){return this.optional(element)||/^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test(value)},"Please enter a valid IP v6 address.");jQuery.validator.addMethod("pattern",function(value,element,param){if(this.optional(element)){return true}if(typeof param==="string"){param=new RegExp("^(?:"+param+")$")}return param.test(value)},"Invalid format.");jQuery.validator.addMethod("require_from_group",function(value,element,options){var validator=this;var selector=options[1];var validOrNot=$(selector,element.form).filter(function(){return validator.elementValue(this)}).length>=options[0];if(!$(element).data("being_validated")){var fields=$(selector,element.form);fields.data("being_validated",true);fields.valid();fields.data("being_validated",false)}return validOrNot},jQuery.format("Please fill at least {0} of these fields."));jQuery.validator.addMethod("skip_or_fill_minimum",function(value,element,options){var validator=this,numberRequired=options[0],selector=options[1];var numberFilled=$(selector,element.form).filter(function(){return validator.elementValue(this)}).length;var valid=numberFilled>=numberRequired||numberFilled===0;if(!$(element).data("being_validated")){var fields=$(selector,element.form);fields.data("being_validated",true);fields.valid();fields.data("being_validated",false)}return valid},jQuery.format("Please either skip these fields or fill at least {0} of them."));jQuery.validator.addMethod("accept",function(value,element,param){var typeParam=typeof param==="string"?param.replace(/\s/g,"").replace(/,/g,"|"):"image/*",optionalValue=this.optional(element),i,file;if(optionalValue){return optionalValue}if($(element).attr("type")==="file"){typeParam=typeParam.replace(/\*/g,".*");if(element.files&&element.files.length){for(i=0;i").attr("name",validator.submitButton.name).val($(validator.submitButton).val()).appendTo(validator.currentForm)}validator.settings.submitHandler.call(validator,validator.currentForm,event);if(validator.submitButton){hidden.remove()}return false}return true}if(validator.cancelSubmit){validator.cancelSubmit=false;return handle()}if(validator.form()){if(validator.pendingRequest){validator.formSubmitted=true;return false}return handle()}else{validator.focusInvalid();return false}})}return validator},valid:function(){if($(this[0]).is("form")){return this.validate().form()}else{var valid=true;var validator=$(this[0].form).validate();this.each(function(){valid=valid&&validator.element(this)});return valid}},removeAttrs:function(attributes){var result={},$element=this;$.each(attributes.split(/\s/),function(index,value){result[value]=$element.attr(value);$element.removeAttr(value)});return result},rules:function(command,argument){var element=this[0];if(command){var settings=$.data(element.form,"validator").settings;var staticRules=settings.rules;var existingRules=$.validator.staticRules(element);switch(command){case"add":$.extend(existingRules,$.validator.normalizeRule(argument));delete existingRules.messages;staticRules[element.name]=existingRules;if(argument.messages){settings.messages[element.name]=$.extend(settings.messages[element.name],argument.messages)}break;case"remove":if(!argument){delete staticRules[element.name];return existingRules}var filtered={};$.each(argument.split(/\s/),function(index,method){filtered[method]=existingRules[method];delete existingRules[method]});return filtered}}var data=$.validator.normalizeRules($.extend({},$.validator.classRules(element),$.validator.attributeRules(element),$.validator.dataRules(element),$.validator.staticRules(element)),element);if(data.required){var param=data.required;delete data.required;data=$.extend({required:param},data)}return data}});$.extend($.expr[":"],{blank:function(a){return!$.trim(""+$(a).val())},filled:function(a){return!!$.trim(""+$(a).val())},unchecked:function(a){return!$(a).prop("checked")}});$.validator=function(options,form){this.settings=$.extend(true,{},$.validator.defaults,options);this.currentForm=form;this.init()};$.validator.format=function(source,params){if(arguments.length===1){return function(){var args=$.makeArray(arguments);args.unshift(source);return $.validator.format.apply(this,args)}}if(arguments.length>2&¶ms.constructor!==Array){params=$.makeArray(arguments).slice(1)}if(params.constructor!==Array){params=[params]}$.each(params,function(i,n){source=source.replace(new RegExp("\\{"+i+"\\}","g"),function(){return n})});return source};$.extend($.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",validClass:"valid",errorElement:"label",focusInvalid:true,errorContainer:$([]),errorLabelContainer:$([]),onsubmit:true,ignore:":hidden",ignoreTitle:false,onfocusin:function(element,event){this.lastActive=element;if(this.settings.focusCleanup&&!this.blockFocusCleanup){if(this.settings.unhighlight){this.settings.unhighlight.call(this,element,this.settings.errorClass,this.settings.validClass)}this.addWrapper(this.errorsFor(element)).hide()}},onfocusout:function(element,event){if(!this.checkable(element)&&(element.name in this.submitted||!this.optional(element))){this.element(element)}},onkeyup:function(element,event){if(event.which===9&&this.elementValue(element)===""){return}else if(element.name in this.submitted||element===this.lastElement){this.element(element)}},onclick:function(element,event){if(element.name in this.submitted){this.element(element)}else if(element.parentNode.name in this.submitted){this.element(element.parentNode)}},highlight:function(element,errorClass,validClass){if(element.type==="radio"){this.findByName(element.name).addClass(errorClass).removeClass(validClass)}else{$(element).addClass(errorClass).removeClass(validClass)}},unhighlight:function(element,errorClass,validClass){if(element.type==="radio"){this.findByName(element.name).removeClass(errorClass).addClass(validClass)}else{$(element).removeClass(errorClass).addClass(validClass)}}},setDefaults:function(settings){$.extend($.validator.defaults,settings)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",maxlength:$.validator.format("Please enter no more than {0} characters."),minlength:$.validator.format("Please enter at least {0} characters."),rangelength:$.validator.format("Please enter a value between {0} and {1} characters long."),range:$.validator.format("Please enter a value between {0} and {1}."),max:$.validator.format("Please enter a value less than or equal to {0}."),min:$.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:false,prototype:{init:function(){this.labelContainer=$(this.settings.errorLabelContainer);this.errorContext=this.labelContainer.length&&this.labelContainer||$(this.currentForm);this.containers=$(this.settings.errorContainer).add(this.settings.errorLabelContainer);this.submitted={};this.valueCache={};this.pendingRequest=0;this.pending={};this.invalid={};this.reset();var groups=this.groups={};$.each(this.settings.groups,function(key,value){if(typeof value==="string"){value=value.split(/\s/)}$.each(value,function(index,name){groups[name]=key})});var rules=this.settings.rules;$.each(rules,function(key,value){rules[key]=$.validator.normalizeRule(value)});function delegate(event){var validator=$.data(this[0].form,"validator"),eventType="on"+event.type.replace(/^validate/,"");if(validator.settings[eventType]){validator.settings[eventType].call(validator,this[0],event)}}$(this.currentForm).validateDelegate(":text, [type='password'], [type='file'], select, textarea, "+"[type='number'], [type='search'] ,[type='tel'], [type='url'], "+"[type='email'], [type='datetime'], [type='date'], [type='month'], "+"[type='week'], [type='time'], [type='datetime-local'], "+"[type='range'], [type='color'] ","focusin focusout keyup",delegate).validateDelegate("[type='radio'], [type='checkbox'], select, option","click",delegate);if(this.settings.invalidHandler){$(this.currentForm).bind("invalid-form.validate",this.settings.invalidHandler)}},form:function(){this.checkForm();$.extend(this.submitted,this.errorMap);this.invalid=$.extend({},this.errorMap);if(!this.valid()){$(this.currentForm).triggerHandler("invalid-form",[this])}this.showErrors();return this.valid()},checkForm:function(){this.prepareForm();for(var i=0,elements=this.currentElements=this.elements();elements[i];i++){this.check(elements[i])}return this.valid()},element:function(element){element=this.validationTargetFor(this.clean(element));this.lastElement=element;this.prepareElement(element);this.currentElements=$(element);var result=this.check(element)!==false;if(result){delete this.invalid[element.name]}else{this.invalid[element.name]=true}if(!this.numberOfInvalids()){this.toHide=this.toHide.add(this.containers)}this.showErrors();return result},showErrors:function(errors){if(errors){$.extend(this.errorMap,errors);this.errorList=[];for(var name in errors){this.errorList.push({message:errors[name],element:this.findByName(name)[0]})}this.successList=$.grep(this.successList,function(element){return!(element.name in errors)})}if(this.settings.showErrors){this.settings.showErrors.call(this,this.errorMap,this.errorList)}else{this.defaultShowErrors()}},resetForm:function(){if($.fn.resetForm){$(this.currentForm).resetForm()}this.submitted={};this.lastElement=null;this.prepareForm();this.hideErrors();this.elements().removeClass(this.settings.errorClass).removeData("previousValue")},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(obj){var count=0;for(var i in obj){count++}return count},hideErrors:function(){this.addWrapper(this.toHide).hide()},valid:function(){return this.size()===0},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid){try{$(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(e){}}},findLastActive:function(){var lastActive=this.lastActive;return lastActive&&$.grep(this.errorList,function(n){return n.element.name===lastActive.name}).length===1&&lastActive},elements:function(){var validator=this,rulesCache={};return $(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, [disabled]").not(this.settings.ignore).filter(function(){if(!this.name&&validator.settings.debug&&window.console){console.error("%o has no name assigned",this)}if(this.name in rulesCache||!validator.objectLength($(this).rules())){return false}rulesCache[this.name]=true;return true})},clean:function(selector){return $(selector)[0]},errors:function(){var errorClass=this.settings.errorClass.replace(" ",".");return $(this.settings.errorElement+"."+errorClass,this.errorContext)},reset:function(){this.successList=[];this.errorList=[];this.errorMap={};this.toShow=$([]);this.toHide=$([]);this.currentElements=$([])},prepareForm:function(){this.reset();this.toHide=this.errors().add(this.containers)},prepareElement:function(element){this.reset();this.toHide=this.errorsFor(element)},elementValue:function(element){var type=$(element).attr("type"),val=$(element).val();if(type==="radio"||type==="checkbox"){return $("input[name='"+$(element).attr("name")+"']:checked").val()}if(typeof val==="string"){return val.replace(/\r/g,"")}return val},check:function(element){element=this.validationTargetFor(this.clean(element));var rules=$(element).rules();var dependencyMismatch=false;var val=this.elementValue(element);var result;for(var method in rules){var rule={method:method,parameters:rules[method]};try{result=$.validator.methods[method].call(this,val,element,rule.parameters);if(result==="dependency-mismatch"){dependencyMismatch=true;continue}dependencyMismatch=false;if(result==="pending"){this.toHide=this.toHide.not(this.errorsFor(element));return}if(!result){this.formatAndAdd(element,rule);return false}}catch(e){if(this.settings.debug&&window.console){console.log("Exception occurred when checking element "+element.id+", check the '"+rule.method+"' method.",e)}throw e}}if(dependencyMismatch){return}if(this.objectLength(rules)){this.successList.push(element)}return true},customDataMessage:function(element,method){return $(element).data("msg-"+method.toLowerCase())||element.attributes&&$(element).attr("data-msg-"+method.toLowerCase())},customMessage:function(name,method){var m=this.settings.messages[name];return m&&(m.constructor===String?m:m[method])},findDefined:function(){for(var i=0;iWarning: No message defined for "+element.name+"")},formatAndAdd:function(element,rule){var message=this.defaultMessage(element,rule.method),theregex=/\$?\{(\d+)\}/g;if(typeof message==="function"){message=message.call(this,rule.parameters,element)}else if(theregex.test(message)){message=$.validator.format(message.replace(theregex,"{$1}"),rule.parameters)}this.errorList.push({message:message,element:element});this.errorMap[element.name]=message;this.submitted[element.name]=message},addWrapper:function(toToggle){if(this.settings.wrapper){toToggle=toToggle.add(toToggle.parent(this.settings.wrapper))}return toToggle},defaultShowErrors:function(){var i,elements;for(i=0;this.errorList[i];i++){var error=this.errorList[i];if(this.settings.highlight){this.settings.highlight.call(this,error.element,this.settings.errorClass,this.settings.validClass)}this.showLabel(error.element,error.message)}if(this.errorList.length){this.toShow=this.toShow.add(this.containers)}if(this.settings.success){for(i=0;this.successList[i];i++){this.showLabel(this.successList[i])}}if(this.settings.unhighlight){for(i=0,elements=this.validElements();elements[i];i++){this.settings.unhighlight.call(this,elements[i],this.settings.errorClass,this.settings.validClass)}}this.toHide=this.toHide.not(this.toShow);this.hideErrors();this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return $(this.errorList).map(function(){return this.element})},showLabel:function(element,message){var label=this.errorsFor(element);if(label.length){label.removeClass(this.settings.validClass).addClass(this.settings.errorClass);label.html(message)}else{label=$("<"+this.settings.errorElement+">").attr("for",this.idOrName(element)).addClass(this.settings.errorClass).html(message||"");if(this.settings.wrapper){label=label.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()}if(!this.labelContainer.append(label).length){if(this.settings.errorPlacement){this.settings.errorPlacement(label,$(element))}else{label.insertAfter(element)}}}if(!message&&this.settings.success){label.text("");if(typeof this.settings.success==="string"){label.addClass(this.settings.success)}else{this.settings.success(label,element)}}this.toShow=this.toShow.add(label)},errorsFor:function(element){var name=this.idOrName(element);return this.errors().filter(function(){return $(this).attr("for")===name})},idOrName:function(element){return this.groups[element.name]||(this.checkable(element)?element.name:element.id||element.name)},validationTargetFor:function(element){if(this.checkable(element)){element=this.findByName(element.name).not(this.settings.ignore)[0]}return element},checkable:function(element){return/radio|checkbox/i.test(element.type)},findByName:function(name){return $(this.currentForm).find("[name='"+name+"']")},getLength:function(value,element){switch(element.nodeName.toLowerCase()){case"select":return $("option:selected",element).length;case"input":if(this.checkable(element)){return this.findByName(element.name).filter(":checked").length}}return value.length},depend:function(param,element){return this.dependTypes[typeof param]?this.dependTypes[typeof param](param,element):true},dependTypes:{"boolean":function(param,element){return param},string:function(param,element){return!!$(param,element.form).length},"function":function(param,element){return param(element)}},optional:function(element){var val=this.elementValue(element);return!$.validator.methods.required.call(this,val,element)&&"dependency-mismatch"},startRequest:function(element){if(!this.pending[element.name]){this.pendingRequest++;this.pending[element.name]=true}},stopRequest:function(element,valid){this.pendingRequest--;if(this.pendingRequest<0){this.pendingRequest=0}delete this.pending[element.name];if(valid&&this.pendingRequest===0&&this.formSubmitted&&this.form()){$(this.currentForm).submit();this.formSubmitted=false}else if(!valid&&this.pendingRequest===0&&this.formSubmitted){$(this.currentForm).triggerHandler("invalid-form",[this]);this.formSubmitted=false}},previousValue:function(element){return $.data(element,"previousValue")||$.data(element,"previousValue",{old:null,valid:true,message:this.defaultMessage(element,"remote")})}},classRuleSettings:{required:{required:true},email:{email:true},url:{url:true},date:{date:true},dateISO:{dateISO:true},number:{number:true},digits:{digits:true},creditcard:{creditcard:true}},addClassRules:function(className,rules){if(className.constructor===String){this.classRuleSettings[className]=rules}else{$.extend(this.classRuleSettings,className)}},classRules:function(element){var rules={};var classes=$(element).attr("class");if(classes){$.each(classes.split(" "),function(){if(this in $.validator.classRuleSettings){$.extend(rules,$.validator.classRuleSettings[this])}})}return rules},attributeRules:function(element){var rules={};var $element=$(element);var type=$element[0].getAttribute("type");for(var method in $.validator.methods){var value;if(method==="required"){value=$element.get(0).getAttribute(method);if(value===""){value=true}value=!!value}else{value=$element.attr(method)}if(/min|max/.test(method)&&(type===null||/number|range|text/.test(type))){value=Number(value)}if(value){rules[method]=value}else if(type===method&&type!=="range"){rules[method]=true}}if(rules.maxlength&&/-1|2147483647|524288/.test(rules.maxlength)){delete rules.maxlength}return rules},dataRules:function(element){var method,value,rules={},$element=$(element);for(method in $.validator.methods){value=$element.data("rule-"+method.toLowerCase());if(value!==undefined){rules[method]=value}}return rules},staticRules:function(element){var rules={};var validator=$.data(element.form,"validator");if(validator.settings.rules){rules=$.validator.normalizeRule(validator.settings.rules[element.name])||{}}return rules},normalizeRules:function(rules,element){$.each(rules,function(prop,val){if(val===false){delete rules[prop];return}if(val.param||val.depends){var keepRule=true;switch(typeof val.depends){case"string":keepRule=!!$(val.depends,element.form).length;break;case"function":keepRule=val.depends.call(element,element);break}if(keepRule){rules[prop]=val.param!==undefined?val.param:true}else{delete rules[prop]}}});$.each(rules,function(rule,parameter){rules[rule]=$.isFunction(parameter)?parameter(element):parameter});$.each(["minlength","maxlength"],function(){if(rules[this]){rules[this]=Number(rules[this])}});$.each(["rangelength","range"],function(){var parts;if(rules[this]){if($.isArray(rules[this])){rules[this]=[Number(rules[this][0]),Number(rules[this][1])]}else if(typeof rules[this]==="string"){parts=rules[this].split(/[\s,]+/);rules[this]=[Number(parts[0]),Number(parts[1])]}}});if($.validator.autoCreateRanges){if(rules.min&&rules.max){rules.range=[rules.min,rules.max];delete rules.min;delete rules.max}if(rules.minlength&&rules.maxlength){rules.rangelength=[rules.minlength,rules.maxlength];delete rules.minlength;delete rules.maxlength}}return rules},normalizeRule:function(data){if(typeof data==="string"){var transformed={};$.each(data.split(/\s/),function(){transformed[this]=true});data=transformed}return data},addMethod:function(name,method,message){$.validator.methods[name]=method;$.validator.messages[name]=message!==undefined?message:$.validator.messages[name];if(method.length<3){$.validator.addClassRules(name,$.validator.normalizeRule(name))}},methods:{required:function(value,element,param){if(!this.depend(param,element)){return"dependency-mismatch"}if(element.nodeName.toLowerCase()==="select"){var val=$(element).val();return val&&val.length>0}if(this.checkable(element)){return this.getLength(value,element)>0}return $.trim(value).length>0},email:function(value,element){return this.optional(element)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value)},url:function(value,element){return this.optional(element)||/^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value)},date:function(value,element){return this.optional(element)||!/Invalid|NaN/.test(new Date(value).toString())},dateISO:function(value,element){return this.optional(element)||/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value)},number:function(value,element){return this.optional(element)||/^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)},digits:function(value,element){return this.optional(element)||/^\d+$/.test(value)},creditcard:function(value,element){if(this.optional(element)){return"dependency-mismatch"}if(/[^0-9 \-]+/.test(value)){return false}var nCheck=0,nDigit=0,bEven=false;value=value.replace(/\D/g,"");for(var n=value.length-1;n>=0;n--){var cDigit=value.charAt(n);nDigit=parseInt(cDigit,10);if(bEven){if((nDigit*=2)>9){nDigit-=9}}nCheck+=nDigit;bEven=!bEven}return nCheck%10===0},minlength:function(value,element,param){var length=$.isArray(value)?value.length:this.getLength($.trim(value),element);return this.optional(element)||length>=param},maxlength:function(value,element,param){var length=$.isArray(value)?value.length:this.getLength($.trim(value),element);return this.optional(element)||length<=param},rangelength:function(value,element,param){var length=$.isArray(value)?value.length:this.getLength($.trim(value),element);return this.optional(element)||length>=param[0]&&length<=param[1]},min:function(value,element,param){return this.optional(element)||value>=param},max:function(value,element,param){return this.optional(element)||value<=param},range:function(value,element,param){return this.optional(element)||value>=param[0]&&value<=param[1]},equalTo:function(value,element,param){var target=$(param);if(this.settings.onfocusout){target.unbind(".validate-equalTo").bind("blur.validate-equalTo",function(){$(element).valid()})}return value===target.val()},remote:function(value,element,param){if(this.optional(element)){return"dependency-mismatch"}var previous=this.previousValue(element);if(!this.settings.messages[element.name]){this.settings.messages[element.name]={}}previous.originalMessage=this.settings.messages[element.name].remote;this.settings.messages[element.name].remote=previous.message;param=typeof param==="string"&&{url:param}||param;if(previous.old===value){return previous.valid}previous.old=value;var validator=this;this.startRequest(element);var data={};data[element.name]=value;$.ajax($.extend(true,{url:param,mode:"abort",port:"validate"+element.name,dataType:"json",data:data,success:function(response){validator.settings.messages[element.name].remote=previous.originalMessage;var valid=response===true||response==="true";if(valid){var submitted=validator.formSubmitted;validator.prepareElement(element);validator.formSubmitted=submitted;validator.successList.push(element);delete validator.invalid[element.name];validator.showErrors()}else{var errors={};var message=response||validator.defaultMessage(element,"remote");errors[element.name]=previous.message=$.isFunction(message)?message(value):message;validator.invalid[element.name]=true;validator.showErrors(errors)}previous.valid=valid;validator.stopRequest(element,valid)}},param));return"pending"}}});$.format=$.validator.format}(jQuery);!function($){var pendingRequests={};if($.ajaxPrefilter){$.ajaxPrefilter(function(settings,_,xhr){var port=settings.port;if(settings.mode==="abort"){if(pendingRequests[port]){pendingRequests[port].abort()}pendingRequests[port]=xhr}})}else{var ajax=$.ajax;$.ajax=function(settings){var mode=("mode"in settings?settings:$.ajaxSettings).mode,port=("port"in settings?settings:$.ajaxSettings).port;if(mode==="abort"){if(pendingRequests[port]){pendingRequests[port].abort()}pendingRequests[port]=ajax.apply(this,arguments);return pendingRequests[port]}return ajax.apply(this,arguments)}}}(jQuery);!function($){$.extend($.fn,{validateDelegate:function(delegate,type,handler){return this.bind(type,function(event){var target=$(event.target);if(target.is(delegate)){return handler.apply(target,arguments)}})}})}(jQuery); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ar.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ar.js new file mode 100644 index 00000000000..6df9495f53e --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ar.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: AR (Arabic; العربية) + */ +(function ($) { + $.extend($.validator.messages, { + required: "هذا الحقل إلزامي", + remote: "يرجى تصحيح هذا الحقل للمتابعة", + email: "رجاء إدخال عنوان بريد إلكتروني صحيح", + url: "رجاء إدخال عنوان موقع إلكتروني صحيح", + date: "رجاء إدخال تاريخ صحيح", + dateISO: "رجاء إدخال تاريخ صحيح (ISO)", + number: "رجاء إدخال عدد بطريقة صحيحة", + digits: "رجاء إدخال أرقام فقط", + creditcard: "رجاء إدخال رقم بطاقة ائتمان صحيح", + equalTo: "رجاء إدخال نفس القيمة", + accept: "رجاء إدخال ملف بامتداد موافق عليه", + maxlength: $.validator.format("الحد الأقصى لعدد الحروف هو {0}"), + minlength: $.validator.format("الحد الأدنى لعدد الحروف هو {0}"), + rangelength: $.validator.format("عدد الحروف يجب أن يكون بين {0} و {1}"), + range: $.validator.format("رجاء إدخال عدد قيمته بين {0} و {1}"), + max: $.validator.format("رجاء إدخال عدد أقل من أو يساوي (0}"), + min: $.validator.format("رجاء إدخال عدد أكبر من أو يساوي (0}") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_bg.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_bg.js new file mode 100644 index 00000000000..10ba1d32295 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_bg.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: BG (Bulgarian; български език) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Полето е задължително.", + remote: "Моля, въведете правилната стойност.", + email: "Моля, въведете валиден email.", + url: "Моля, въведете валидно URL.", + date: "Моля, въведете валидна дата.", + dateISO: "Моля, въведете валидна дата (ISO).", + number: "Моля, въведете валиден номер.", + digits: "Моля, въведете само цифри", + creditcard: "Моля, въведете валиден номер на кредитна карта.", + equalTo: "Моля, въведете същата стойност отново.", + accept: "Моля, въведете стойност с валидно разширение.", + maxlength: $.validator.format("Моля, въведете повече от {0} символа."), + minlength: $.validator.format("Моля, въведете поне {0} символа."), + rangelength: $.validator.format("Моля, въведете стойност с дължина между {0} и {1} символа."), + range: $.validator.format("Моля, въведете стойност между {0} и {1}."), + max: $.validator.format("Моля, въведете стойност по-малка или равна на {0}."), + min: $.validator.format("Моля, въведете стойност по-голяма или равна на {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ca.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ca.js new file mode 100644 index 00000000000..940c37f5475 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ca.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: CA (Catalan; català) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Aquest camp és obligatori.", + remote: "Si us plau, omple aquest camp.", + email: "Si us plau, escriu una adreça de correu-e vàlida", + url: "Si us plau, escriu una URL vàlida.", + date: "Si us plau, escriu una data vàlida.", + dateISO: "Si us plau, escriu una data (ISO) vàlida.", + number: "Si us plau, escriu un número enter vàlid.", + digits: "Si us plau, escriu només dígits.", + creditcard: "Si us plau, escriu un número de tarjeta vàlid.", + equalTo: "Si us plau, escriu el maateix valor de nou.", + accept: "Si us plau, escriu un valor amb una extensió acceptada.", + maxlength: $.validator.format("Si us plau, no escriguis més de {0} caracters."), + minlength: $.validator.format("Si us plau, no escriguis menys de {0} caracters."), + rangelength: $.validator.format("Si us plau, escriu un valor entre {0} i {1} caracters."), + range: $.validator.format("Si us plau, escriu un valor entre {0} i {1}."), + max: $.validator.format("Si us plau, escriu un valor menor o igual a {0}."), + min: $.validator.format("Si us plau, escriu un valor major o igual a {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_cs.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_cs.js new file mode 100644 index 00000000000..43cc3ad1b04 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_cs.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: CS (Czech; čeština, český jazyk) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Tento údaj je povinný.", + remote: "Prosím, opravte tento údaj.", + email: "Prosím, zadejte platný e-mail.", + url: "Prosím, zadejte platné URL.", + date: "Prosím, zadejte platné datum.", + dateISO: "Prosím, zadejte platné datum (ISO).", + number: "Prosím, zadejte číslo.", + digits: "Prosím, zadávejte pouze číslice.", + creditcard: "Prosím, zadejte číslo kreditní karty.", + equalTo: "Prosím, zadejte znovu stejnou hodnotu.", + accept: "Prosím, zadejte soubor se správnou příponou.", + maxlength: $.validator.format("Prosím, zadejte nejvíce {0} znaků."), + minlength: $.validator.format("Prosím, zadejte nejméně {0} znaků."), + rangelength: $.validator.format("Prosím, zadejte od {0} do {1} znaků."), + range: $.validator.format("Prosím, zadejte hodnotu od {0} do {1}."), + max: $.validator.format("Prosím, zadejte hodnotu menší nebo rovnu {0}."), + min: $.validator.format("Prosím, zadejte hodnotu větší nebo rovnu {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_da.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_da.js new file mode 100644 index 00000000000..bcceb202e19 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_da.js @@ -0,0 +1,22 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: DA (Danish; dansk) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Dette felt er påkrævet.", + maxlength: $.validator.format("Indtast højst {0} tegn."), + minlength: $.validator.format("Indtast mindst {0} tegn."), + rangelength: $.validator.format("Indtast mindst {0} og højst {1} tegn."), + email: "Indtast en gyldig email-adresse.", + url: "Indtast en gyldig URL.", + date: "Indtast en gyldig dato.", + number: "Indtast et tal.", + digits: "Indtast kun cifre.", + equalTo: "Indtast den samme værdi igen.", + range: $.validator.format("Angiv en værdi mellem {0} og {1}."), + max: $.validator.format("Angiv en værdi der højst er {0}."), + min: $.validator.format("Angiv en værdi der mindst er {0}."), + creditcard: "Indtast et gyldigt kreditkortnummer." + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_de.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_de.js new file mode 100644 index 00000000000..073853e97b3 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_de.js @@ -0,0 +1,22 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: DE (German, Deutsch) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Dieses Feld ist ein Pflichtfeld.", + maxlength: $.validator.format("Geben Sie bitte maximal {0} Zeichen ein."), + minlength: $.validator.format("Geben Sie bitte mindestens {0} Zeichen ein."), + rangelength: $.validator.format("Geben Sie bitte mindestens {0} und maximal {1} Zeichen ein."), + email: "Geben Sie bitte eine gültige E-Mail Adresse ein.", + url: "Geben Sie bitte eine gültige URL ein.", + date: "Bitte geben Sie ein gültiges Datum ein.", + number: "Geben Sie bitte eine Nummer ein.", + digits: "Geben Sie bitte nur Ziffern ein.", + equalTo: "Bitte denselben Wert wiederholen.", + range: $.validator.format("Geben Sie bitte einen Wert zwischen {0} und {1} ein."), + max: $.validator.format("Geben Sie bitte einen Wert kleiner oder gleich {0} ein."), + min: $.validator.format("Geben Sie bitte einen Wert größer oder gleich {0} ein."), + creditcard: "Geben Sie bitte eine gültige Kreditkarten-Nummer ein." + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_el.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_el.js new file mode 100644 index 00000000000..6cd5a1de4ef --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_el.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: EL (Greek; ελληνικά) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Αυτό το πεδίο είναι υποχρεωτικό.", + remote: "Παρακαλώ διορθώστε αυτό το πεδίο.", + email: "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email.", + url: "Παρακαλώ εισάγετε ένα έγκυρο URL.", + date: "Παρακαλώ εισάγετε μια έγκυρη ημερομηνία.", + dateISO: "Παρακαλώ εισάγετε μια έγκυρη ημερομηνία (ISO).", + number: "Παρακαλώ εισάγετε έναν έγκυρο αριθμό.", + digits: "Παρακαλώ εισάγετε μόνο αριθμητικά ψηφία.", + creditcard: "Παρακαλώ εισάγετε έναν έγκυρο αριθμό πιστωτικής κάρτας.", + equalTo: "Παρακαλώ εισάγετε την ίδια τιμή ξανά.", + accept: "Παρακαλώ εισάγετε μια τιμή με έγκυρη επέκταση αρχείου.", + maxlength: $.validator.format("Παρακαλώ εισάγετε μέχρι και {0} χαρακτήρες."), + minlength: $.validator.format("Παρακαλώ εισάγετε τουλάχιστον {0} χαρακτήρες."), + rangelength: $.validator.format("Παρακαλώ εισάγετε μια τιμή με μήκος μεταξύ {0} και {1} χαρακτήρων."), + range: $.validator.format("Παρακαλώ εισάγετε μια τιμή μεταξύ {0} και {1}."), + max: $.validator.format("Παρακαλώ εισάγετε μια τιμή μικρότερη ή ίση του {0}."), + min: $.validator.format("Παρακαλώ εισάγετε μια τιμή μεγαλύτερη ή ίση του {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_es.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_es.js new file mode 100644 index 00000000000..3a30eee8f35 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_es.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: ES (Spanish; Español) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Este campo es obligatorio.", + remote: "Por favor, rellena este campo.", + email: "Por favor, escribe una dirección de correo válida", + url: "Por favor, escribe una URL válida.", + date: "Por favor, escribe una fecha válida.", + dateISO: "Por favor, escribe una fecha (ISO) válida.", + number: "Por favor, escribe un número entero válido.", + digits: "Por favor, escribe sólo dígitos.", + creditcard: "Por favor, escribe un número de tarjeta válido.", + equalTo: "Por favor, escribe el mismo valor de nuevo.", + accept: "Por favor, escribe un valor con una extensión aceptada.", + maxlength: $.validator.format("Por favor, no escribas más de {0} caracteres."), + minlength: $.validator.format("Por favor, no escribas menos de {0} caracteres."), + rangelength: $.validator.format("Por favor, escribe un valor entre {0} y {1} caracteres."), + range: $.validator.format("Por favor, escribe un valor entre {0} y {1}."), + max: $.validator.format("Por favor, escribe un valor menor o igual a {0}."), + min: $.validator.format("Por favor, escribe un valor mayor o igual a {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_et.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_et.js new file mode 100644 index 00000000000..aaa26777f57 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_et.js @@ -0,0 +1,23 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: ET (Estonian; eesti, eesti keel) + */ +(function ($) { + $.extend($.validator.messages, { + required: "See väli peab olema täidetud.", + maxlength: $.validator.format("Palun sisestage vähem kui {0} tähemärki."), + minlength: $.validator.format("Palun sisestage vähemalt {0} tähemärki."), + rangelength: $.validator.format("Palun sisestage väärtus vahemikus {0} kuni {1} tähemärki."), + email: "Palun sisestage korrektne e-maili aadress.", + url: "Palun sisestage korrektne URL.", + date: "Palun sisestage korrektne kuupäev.", + dateISO: "Palun sisestage korrektne kuupäev (YYYY-MM-DD).", + number: "Palun sisestage korrektne number.", + digits: "Palun sisestage ainult numbreid.", + equalTo: "Palun sisestage sama väärtus uuesti.", + range: $.validator.format("Palun sisestage väärtus vahemikus {0} kuni {1}."), + max: $.validator.format("Palun sisestage väärtus, mis on väiksem või võrdne arvuga {0}."), + min: $.validator.format("Palun sisestage väärtus, mis on suurem või võrdne arvuga {0}."), + creditcard: "Palun sisestage korrektne krediitkaardi number." + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_eu.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_eu.js new file mode 100644 index 00000000000..8f02f1b229e --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_eu.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: EU (Basque; euskara, euskera) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Eremu hau beharrezkoa da.", + remote: "Mesedez, bete eremu hau.", + email: "Mesedez, idatzi baliozko posta helbide bat.", + url: "Mesedez, idatzi baliozko URL bat.", + date: "Mesedez, idatzi baliozko data bat.", + dateISO: "Mesedez, idatzi baliozko (ISO) data bat.", + number: "Mesedez, idatzi baliozko zenbaki oso bat.", + digits: "Mesedez, idatzi digituak soilik.", + creditcard: "Mesedez, idatzi baliozko txartel zenbaki bat.", + equalTo: "Mesedez, idatzi berdina berriro ere.", + accept: "Mesedez, idatzi onartutako luzapena duen balio bat.", + maxlength: $.validator.format("Mesedez, ez idatzi {0} karaktere baino gehiago."), + minlength: $.validator.format("Mesedez, ez idatzi {0} karaktere baino gutxiago."), + rangelength: $.validator.format("Mesedez, idatzi {0} eta {1} karaktere arteko balio bat."), + range: $.validator.format("Mesedez, idatzi {0} eta {1} arteko balio bat."), + max: $.validator.format("Mesedez, idatzi {0} edo txikiagoa den balio bat."), + min: $.validator.format("Mesedez, idatzi {0} edo handiagoa den balio bat.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fa.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fa.js new file mode 100644 index 00000000000..38163525cb9 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fa.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: FA (Persian; فارسی) + */ +(function ($) { + $.extend($.validator.messages, { + required: "تکمیل این فیلد اجباری است.", + remote: "لطفا این فیلد را تصحیح کنید.", + email: ".لطفا یک ایمیل صحیح وارد کنید", + url: "لطفا آدرس صحیح وارد کنید.", + date: "لطفا یک تاریخ صحیح وارد کنید", + dateISO: "لطفا تاریخ صحیح وارد کنید (ISO).", + number: "لطفا عدد صحیح وارد کنید.", + digits: "لطفا تنها رقم وارد کنید", + creditcard: "لطفا کریدیت کارت صحیح وارد کنید.", + equalTo: "لطفا مقدار برابری وارد کنید", + accept: "لطفا مقداری وارد کنید که ", + maxlength: $.validator.format("لطفا بیشتر از {0} حرف وارد نکنید."), + minlength: $.validator.format("لطفا کمتر از {0} حرف وارد نکنید."), + rangelength: $.validator.format("لطفا مقداری بین {0} تا {1} حرف وارد کنید."), + range: $.validator.format("لطفا مقداری بین {0} تا {1} حرف وارد کنید."), + max: $.validator.format("لطفا مقداری کمتر از {0} حرف وارد کنید."), + min: $.validator.format("لطفا مقداری بیشتر از {0} حرف وارد کنید.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fi.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fi.js new file mode 100644 index 00000000000..a1fc03c5b98 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fi.js @@ -0,0 +1,23 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: FI (Finnish; suomi, suomen kieli) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Tämä kenttä on pakollinen.", + email: "Syötä oikea sähköpostiosoite.", + url: "Syötä oikea URL osoite.", + date: "Syötä oike päivämäärä.", + dateISO: "Syötä oike päivämäärä (VVVV-MM-DD).", + number: "Syötä numero.", + creditcard: "Syötä voimassa oleva luottokorttinumero.", + digits: "Syötä pelkästään numeroita.", + equalTo: "Syötä sama arvo uudestaan.", + maxlength: $.validator.format("Voit syöttää enintään {0} merkkiä."), + minlength: $.validator.format("Vähintään {0} merkkiä."), + rangelength: $.validator.format("Syötä vähintään {0} ja enintään {1} merkkiä."), + range: $.validator.format("Syötä arvo {0} ja {1} väliltä."), + max: $.validator.format("Syötä arvo joka on pienempi tai yhtä suuri kuin {0}."), + min: $.validator.format("Syötä arvo joka on yhtä suuri tai suurempi kuin {0}.") + }); +}(jQuery)); diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fr.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fr.js new file mode 100644 index 00000000000..c976ff460e6 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_fr.js @@ -0,0 +1,47 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: FR (French; français) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Ce champ est obligatoire.", + remote: "Veuillez corriger ce champ.", + email: "Veuillez fournir une adresse électronique valide.", + url: "Veuillez fournir une adresse URL valide.", + date: "Veuillez fournir une date valide.", + dateISO: "Veuillez fournir une date valide (ISO).", + number: "Veuillez fournir un numéro valide.", + digits: "Veuillez fournir seulement des chiffres.", + creditcard: "Veuillez fournir un numéro de carte de crédit valide.", + equalTo: "Veuillez fournir encore la même valeur.", + accept: "Veuillez fournir une valeur avec une extension valide.", + maxlength: $.validator.format("Veuillez fournir au plus {0} caractères."), + minlength: $.validator.format("Veuillez fournir au moins {0} caractères."), + rangelength: $.validator.format("Veuillez fournir une valeur qui contient entre {0} et {1} caractères."), + range: $.validator.format("Veuillez fournir une valeur entre {0} et {1}."), + max: $.validator.format("Veuillez fournir une valeur inférieur ou égal à {0}."), + min: $.validator.format("Veuillez fournir une valeur supérieur ou égal à {0}."), + maxWords: $.validator.format("Veuillez fournir au plus {0} mots."), + minWords: $.validator.format("Veuillez fournir au moins {0} mots."), + rangeWords: $.validator.format("Veuillez fournir entre {0} et {1} mots."), + letterswithbasicpunc: "Veuillez fournir seulement des lettres et des signes de ponctuation.", + alphanumeric: "Veuillez fournir seulement des lettres, nombres, espaces et soulignages", + lettersonly: "Veuillez fournir seulement des lettres.", + nowhitespace: "Veuillez ne pas inscrire d'espaces blancs.", + ziprange: "Veuillez fournir un code postal entre 902xx-xxxx et 905-xx-xxxx.", + integer: "Veuillez fournir un nombre non décimal qui est positif ou négatif.", + vinUS: "Veuillez fournir un numéro d'identification du véhicule (VIN).", + dateITA: "Veuillez fournir une date valide.", + time: "Veuillez fournir une heure valide entre 00:00 et 23:59.", + phoneUS: "Veuillez fournir un numéro de téléphone valide.", + phoneUK: "Veuillez fournir un numéro de téléphone valide.", + mobileUK: "Veuillez fournir un numéro de téléphone mobile valide.", + strippedminlength: $.validator.format("Veuillez fournir au moins {0} caractères."), + email2: "Veuillez fournir une adresse électronique valide.", + url2: "Veuillez fournir une adresse URL valide.", + creditcardtypes: "Veuillez fournir un numéro de carte de crédit valide.", + ipv4: "Veuillez fournir une adresse IP v4 valide.", + ipv6: "Veuillez fournir une adresse IP v6 valide.", + require_from_group: "Veuillez fournir au moins {0} de ces champs." + }); +}(jQuery)); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_ge.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ge.js similarity index 100% rename from core/vendor/assets/javascripts/jquery.validate/localization/messages_ge.js rename to frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ge.js diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_he.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_he.js new file mode 100644 index 00000000000..373feee7fcc --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_he.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: HE (Hebrew; עברית) + */ +(function ($) { + $.extend($.validator.messages, { + required: "השדה הזה הינו שדה חובה", + remote: "נא לתקן שדה זה", + email: "נא למלא כתובת דוא\"ל חוקית", + url: "נא למלא כתובת אינטרנט חוקית", + date: "נא למלא תאריך חוקי", + dateISO: "נא למלא תאריך חוקי (ISO)", + number: "נא למלא מספר", + digits: "נא למלא רק מספרים", + creditcard: "נא למלא מספר כרטיס אשראי חוקי", + equalTo: "נא למלא את אותו ערך שוב", + accept: "נא למלא ערך עם סיומת חוקית", + maxlength: $.validator.format(".נא לא למלא יותר מ- {0} תווים"), + minlength: $.validator.format("נא למלא לפחות {0} תווים"), + rangelength: $.validator.format("נא למלא ערך בין {0} ל- {1} תווים"), + range: $.validator.format("נא למלא ערך בין {0} ל- {1}"), + max: $.validator.format("נא למלא ערך קטן או שווה ל- {0}"), + min: $.validator.format("נא למלא ערך גדול או שווה ל- {0}") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_hr.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_hr.js new file mode 100755 index 00000000000..895ae2dca97 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_hr.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: HR (Croatia; hrvatski jezik) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Ovo polje je obavezno.", + remote: "Ovo polje treba popraviti.", + email: "Unesite ispravnu e-mail adresu.", + url: "Unesite ispravan URL.", + date: "Unesite ispravan datum.", + dateISO: "Unesite ispravan datum (ISO).", + number: "Unesite ispravan broj.", + digits: "Unesite samo brojeve.", + creditcard: "Unesite ispravan broj kreditne kartice.", + equalTo: "Unesite ponovo istu vrijednost.", + accept: "Unesite vrijednost sa ispravnom ekstenzijom.", + maxlength: $.validator.format("Maksimalni broj znakova je {0} ."), + minlength: $.validator.format("Minimalni broj znakova je {0} ."), + rangelength: $.validator.format("Unesite vrijednost između {0} i {1} znakova."), + range: $.validator.format("Unesite vrijednost između {0} i {1}."), + max: $.validator.format("Unesite vrijednost manju ili jednaku {0}."), + min: $.validator.format("Unesite vrijednost veću ili jednaku {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_hu.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_hu.js new file mode 100644 index 00000000000..cd73fc3eefe --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_hu.js @@ -0,0 +1,24 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: HU (Hungarian; Magyar) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Kötelező megadni.", + maxlength: $.validator.format("Legfeljebb {0} karakter hosszú legyen."), + minlength: $.validator.format("Legalább {0} karakter hosszú legyen."), + rangelength: $.validator.format("Legalább {0} és legfeljebb {1} karakter hosszú legyen."), + email: "Érvényes e-mail címnek kell lennie.", + url: "Érvényes URL-nek kell lennie.", + date: "Dátumnak kell lennie.", + number: "Számnak kell lennie.", + digits: "Csak számjegyek lehetnek.", + equalTo: "Meg kell egyeznie a két értéknek.", + range: $.validator.format("{0} és {1} közé kell esnie."), + max: $.validator.format("Nem lehet nagyobb, mint {0}."), + min: $.validator.format("Nem lehet kisebb, mint {0}."), + creditcard: "Érvényes hitelkártyaszámnak kell lennie.", + remote: "Kérem javítsa ki ezt a mezőt.", + dateISO: "Kérem írjon be egy érvényes dátumot (ISO)." + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_it.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_it.js new file mode 100644 index 00000000000..19323b0f560 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_it.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: IT (Italian; Italiano) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Campo obbligatorio.", + remote: "Controlla questo campo.", + email: "Inserisci un indirizzo email valido.", + url: "Inserisci un indirizzo web valido.", + date: "Inserisci una data valida.", + dateISO: "Inserisci una data valida (ISO).", + number: "Inserisci un numero valido.", + digits: "Inserisci solo numeri.", + creditcard: "Inserisci un numero di carta di credito valido.", + equalTo: "Il valore non corrisponde.", + accept: "Inserisci un valore con un'estensione valida.", + maxlength: $.validator.format("Non inserire più di {0} caratteri."), + minlength: $.validator.format("Inserisci almeno {0} caratteri."), + rangelength: $.validator.format("Inserisci un valore compreso tra {0} e {1} caratteri."), + range: $.validator.format("Inserisci un valore compreso tra {0} e {1}."), + max: $.validator.format("Inserisci un valore minore o uguale a {0}."), + min: $.validator.format("Inserisci un valore maggiore o uguale a {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ja.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ja.js new file mode 100644 index 00000000000..cb060c9c129 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ja.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: JA (Japanese; 日本語) + */ +(function ($) { + $.extend($.validator.messages, { + required: "このフィールドは必須です。", + remote: "このフィールドを修正してください。", + email: "有効なEメールアドレスを入力してください。", + url: "有効なURLを入力してください。", + date: "有効な日付を入力してください。", + dateISO: "有効な日付(ISO)を入力してください。", + number: "有効な数字を入力してください。", + digits: "数字のみを入力してください。", + creditcard: "有効なクレジットカード番号を入力してください。", + equalTo: "同じ値をもう一度入力してください。", + accept: "有効な拡張子を含む値を入力してください。", + maxlength: $.format("{0} 文字以内で入力してください。"), + minlength: $.format("{0} 文字以上で入力してください。"), + rangelength: $.format("{0} 文字から {1} 文字までの値を入力してください。"), + range: $.format("{0} から {1} までの値を入力してください。"), + max: $.format("{0} 以下の値を入力してください。"), + min: $.format("{0} 以上の値を入力してください。") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ka.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ka.js new file mode 100644 index 00000000000..319363e4df3 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ka.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: KA (Georgian; ქართული) + */ +(function ($) { + $.extend($.validator.messages, { + required: "ამ ველის შევსება აუცილებელია.", + remote: "გთხოვთ მიუთითოთ სწორი მნიშვნელობა.", + email: "გთხოვთ მიუთითოთ ელ-ფოსტის კორექტული მისამართი.", + url: "გთხოვთ მიუთითოთ კორექტული URL.", + date: "გთხოვთ მიუთითოთ კორექტული თარიღი.", + dateISO: "გთხოვთ მიუთითოთ კორექტული თარიღი ISO ფორმატში.", + number: "გთხოვთ მიუთითოთ ციფრი.", + digits: "გთხოვთ მიუთითოთ მხოლოდ ციფრები.", + creditcard: "გთხოვთ მიუთითოთ საკრედიტო ბარათის კორექტული ნომერი.", + equalTo: "გთხოვთ მიუთითოთ ასეთივე მნიშვნელობა კიდევ ერთხელ.", + accept: "გთხოვთ აირჩიოთ ფაილი კორექტული გაფართოებით.", + maxlength: $.validator.format("დასაშვებია არაუმეტეს {0} სიმბოლო."), + minlength: $.validator.format("აუცილებელია შეიყვანოთ მინიმუმ {0} სიმბოლო."), + rangelength: $.validator.format("ტექსტში სიმბოლოების რაოდენობა უნდა იყოს {0}-დან {1}-მდე."), + range: $.validator.format("გთხოვთ შეიყვანოთ ციფრი {0}-დან {1}-მდე."), + max: $.validator.format("გთხოვთ შეიყვანოთ ციფრი რომელიც ნაკლებია ან უდრის {0}-ს."), + min: $.validator.format("გთხოვთ შეიყვანოთ ციფრი რომელიც მეტია ან უდრის {0}-ს.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_kk.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_kk.js new file mode 100644 index 00000000000..dd9276fb5c1 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_kk.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: KK (Kazakh; қазақ тілі) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Бұл өрісті міндетті түрде толтырыңыз.", + remote: "Дұрыс мағына енгізуіңізді сұраймыз.", + email: "Нақты электронды поштаңызды енгізуіңізді сұраймыз.", + url: "Нақты URL-ды енгізуіңізді сұраймыз.", + date: "Нақты URL-ды енгізуіңізді сұраймыз.", + dateISO: "Нақты ISO форматымен сәйкес датасын енгізуіңізді сұраймыз.", + number: "Күнді енгізуіңізді сұраймыз.", + digits: "Тек қана сандарды енгізуіңізді сұраймыз.", + creditcard: "Несие картасының нөмірін дұрыс енгізуіңізді сұраймыз.", + equalTo: "Осы мәнді қайта енгізуіңізді сұраймыз.", + accept: "Файлдың кеңейтуін дұрыс таңдаңыз.", + maxlength: $.format("Ұзындығы {0} символдан көр болмасын."), + minlength: $.format("Ұзындығы {0} символдан аз болмасын."), + rangelength: $.format("Ұзындығы {0}-{1} дейін мән енгізуіңізді сұраймыз."), + range: $.format("Пожалуйста, введите число от {0} до {1}. - {0} - {1} санын енгізуіңізді сұраймыз."), + max: $.format("{0} аз немесе тең санын енгізуіңіді сұраймыз."), + min: $.format("{0} көп немесе тең санын енгізуіңізді сұраймыз.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ko.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ko.js new file mode 100644 index 00000000000..24f60071252 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ko.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: KO (Korean; 한국어) + */ +(function ($) { + $.extend($.validator.messages, { + required: "필수 항목입니다.", + remote: "항목을 수정하세요.", + email: "유효하지 않은 E-Mail주소입니다.", + url: "유효하지 않은 URL입니다.", + date: "올바른 날짜를 입력하세요.", + dateISO: "올바른 날짜(ISO)를 입력하세요.", + number: "유효한 숫자가 아닙니다.", + digits: "숫자만 입력 가능합니다.", + creditcard: "신용카드 번호가 바르지 않습니다.", + equalTo: "같은 값을 다시 입력하세요.", + extension: "올바른 확장자가 아닙니다.", + maxlength: $.validator.format("{0}자를 넘을 수 없습니다. "), + minlength: $.validator.format("{0}자 이상 입력하세요."), + rangelength: $.validator.format("문자 길이가 {0} 에서 {1} 사이의 값을 입력하세요."), + range: $.validator.format("{0} 에서 {1} 사이의 값을 입력하세요."), + max: $.validator.format("{0} 이하의 값을 입력하세요."), + min: $.validator.format("{0} 이상의 값을 입력하세요.") + }); +}(jQuery)); diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_lt.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_lt.js new file mode 100644 index 00000000000..856aaeb8b1a --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_lt.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: LT (Lithuanian; lietuvių kalba) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Šis laukas yra privalomas.", + remote: "Prašau pataisyti šį lauką.", + email: "Prašau įvesti teisingą elektroninio pašto adresą.", + url: "Prašau įvesti teisingą URL.", + date: "Prašau įvesti teisingą datą.", + dateISO: "Prašau įvesti teisingą datą (ISO).", + number: "Prašau įvesti teisingą skaičių.", + digits: "Prašau naudoti tik skaitmenis.", + creditcard: "Prašau įvesti teisingą kreditinės kortelės numerį.", + equalTo: "Prašau įvestį tą pačią reikšmę dar kartą.", + accept: "Prašau įvesti reikšmę su teisingu plėtiniu.", + maxlength: $.format("Prašau įvesti ne daugiau kaip {0} simbolių."), + minlength: $.format("Prašau įvesti bent {0} simbolius."), + rangelength: $.format("Prašau įvesti reikšmes, kurių ilgis nuo {0} iki {1} simbolių."), + range: $.format("Prašau įvesti reikšmę intervale nuo {0} iki {1}."), + max: $.format("Prašau įvesti reikšmę mažesnę arba lygią {0}."), + min: $.format("Prašau įvesti reikšmę didesnę arba lygią {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_lv.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_lv.js new file mode 100644 index 00000000000..959a9759c2e --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_lv.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: LV (Latvian; latviešu valoda) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Šis lauks ir obligāts.", + remote: "Lūdzu, pārbaudiet šo lauku.", + email: "Lūdzu, ievadiet derīgu e-pasta adresi.", + url: "Lūdzu, ievadiet derīgu URL adresi.", + date: "Lūdzu, ievadiet derīgu datumu.", + dateISO: "Lūdzu, ievadiet derīgu datumu (ISO).", + number: "Lūdzu, ievadiet derīgu numuru.", + digits: "Lūdzu, ievadiet tikai ciparus.", + creditcard: "Lūdzu, ievadiet derīgu kredītkartes numuru.", + equalTo: "Lūdzu, ievadiet to pašu vēlreiz.", + accept: "Lūdzu, ievadiet vērtību ar derīgu paplašinājumu.", + maxlength: $.validator.format("Lūdzu, ievadiet ne vairāk kā {0} rakstzīmes."), + minlength: $.validator.format("Lūdzu, ievadiet vismaz {0} rakstzīmes."), + rangelength: $.validator.format("Lūdzu ievadiet {0} līdz {1} rakstzīmes."), + range: $.validator.format("Lūdzu, ievadiet skaitli no {0} līdz {1}."), + max: $.validator.format("Lūdzu, ievadiet skaitli, kurš ir mazāks vai vienāds ar {0}."), + min: $.validator.format("Lūdzu, ievadiet skaitli, kurš ir lielāks vai vienāds ar {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ms.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ms.js new file mode 100644 index 00000000000..a256d3cec2c --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ms.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: MS (Malay; Melayu) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Medan ini diperlukan.", + remote: "Sila betulkan medan ini.", + email: "Sila masukkan alamat emel yang betul.", + url: "Sila masukkan URL yang betul.", + date: "Sila masukkan tarikh yang betul.", + dateISO: "Sila masukkan tarikh(ISO) yang betul.", + number: "Sila masukkan nombor yang betul.", + digits: "Sila masukkan nilai digit sahaja.", + creditcard: "Sila masukkan nombor kredit kad yang betul.", + equalTo: "Sila masukkan nilai yang sama semula.", + accept: "Sila masukkan nilai yang telah diterima.", + maxlength: $.validator.format("Sila masukkan nilai tidak lebih dari {0} aksara."), + minlength: $.validator.format("Sila masukkan nilai sekurang-kurangnya {0} aksara."), + rangelength: $.validator.format("Sila masukkan panjang nilai antara {0} dan {1} aksara."), + range: $.validator.format("Sila masukkan nilai antara {0} dan {1} aksara."), + max: $.validator.format("Sila masukkan nilai yang kurang atau sama dengan {0}."), + min: $.validator.format("Sila masukkan nilai yang lebih atau sama dengan {0}.") + }); +}(jQuery)); diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_nl.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_nl.js new file mode 100644 index 00000000000..39f335d7a4d --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_nl.js @@ -0,0 +1,35 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: NL (Dutch; Nederlands, Vlaams) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Dit is een verplicht veld.", + remote: "Controleer dit veld.", + email: "Vul hier een geldig e-mailadres in.", + url: "Vul hier een geldige URL in.", + date: "Vul hier een geldige datum in.", + dateISO: "Vul hier een geldige datum in (ISO-formaat).", + number: "Vul hier een geldig getal in.", + digits: "Vul hier alleen getallen in.", + creditcard: "Vul hier een geldig creditcardnummer in.", + equalTo: "Vul hier dezelfde waarde in.", + accept: "Vul hier een waarde in met een geldige extensie.", + maxlength: $.validator.format("Vul hier maximaal {0} tekens in."), + minlength: $.validator.format("Vul hier minimaal {0} tekens in."), + rangelength: $.validator.format("Vul hier een waarde in van minimaal {0} en maximaal {1} tekens."), + range: $.validator.format("Vul hier een waarde in van minimaal {0} en maximaal {1}."), + max: $.validator.format("Vul hier een waarde in kleiner dan of gelijk aan {0}."), + min: $.validator.format("Vul hier een waarde in groter dan of gelijk aan {0}."), + + // for validations in additional-methods.js + iban: "Vul hier een geldig IBAN in.", + dateNL: "Vul hier een geldige datum in.", + phoneNL: "Vul hier een geldig Nederlands telefoonnummer in.", + mobileNL: "Vul hier een geldig Nederlands mobiel telefoonnummer in.", + postalcodeNL: "Vul hier een geldige postcode in.", + bankaccountNL: "Vul hier een geldig bankrekeningnummer in.", + giroaccountNL: "Vul hier een geldig gironummer in.", + bankorgiroaccountNL: "Vul hier een geldig bank- of gironummer in." + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_no.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_no.js new file mode 100644 index 00000000000..9ba6d2f5b5d --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_no.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: NO (Norwegian; Norsk) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Dette feltet er obligatorisk.", + maxlength: $.validator.format("Maksimalt {0} tegn."), + minlength: $.validator.format("Minimum {0} tegn."), + rangelength: $.validator.format("Angi minimum {0} og maksimum {1} tegn."), + email: "Oppgi en gyldig epostadresse.", + url: "Angi en gyldig URL.", + date: "Angi en gyldig dato.", + dateISO: "Angi en gyldig dato (&ARING;&ARING;&ARING;&ARING;-MM-DD).", + dateSE: "Angi en gyldig dato.", + number: "Angi et gyldig nummer.", + numberSE: "Angi et gyldig nummer.", + digits: "Skriv kun tall.", + equalTo: "Skriv samme verdi igjen.", + range: $.validator.format("Angi en verdi mellom {0} og {1}."), + max: $.validator.format("Angi en verdi som er mindre eller lik {0}."), + min: $.validator.format("Angi en verdi som er større eller lik {0}."), + creditcard: "Angi et gyldig kredittkortnummer." + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_pl.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_pl.js new file mode 100644 index 00000000000..fcf2f6df867 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_pl.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: PL (Polish; język polski, polszczyzna) + */ +(function ($) { + $.extend($.validator.messages, { + required: "To pole jest wymagane.", + remote: "Proszę o wypełnienie tego pola.", + email: "Proszę o podanie prawidłowego adresu email.", + url: "Proszę o podanie prawidłowego URL.", + date: "Proszę o podanie prawidłowej daty.", + dateISO: "Proszę o podanie prawidłowej daty (ISO).", + number: "Proszę o podanie prawidłowej liczby.", + digits: "Proszę o podanie samych cyfr.", + creditcard: "Proszę o podanie prawidłowej karty kredytowej.", + equalTo: "Proszę o podanie tej samej wartości ponownie.", + accept: "Proszę o podanie wartości z prawidłowym rozszerzeniem.", + maxlength: $.validator.format("Proszę o podanie nie więcej niż {0} znaków."), + minlength: $.validator.format("Proszę o podanie przynajmniej {0} znaków."), + rangelength: $.validator.format("Proszę o podanie wartości o długości od {0} do {1} znaków."), + range: $.validator.format("Proszę o podanie wartości z przedziału od {0} do {1}."), + max: $.validator.format("Proszę o podanie wartości mniejszej bądź równej {0}."), + min: $.validator.format("Proszę o podanie wartości większej bądź równej {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ptbr.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ptbr.js new file mode 100644 index 00000000000..cb5b9303185 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ptbr.js @@ -0,0 +1,24 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: PT (Portuguese; português) + * Region: BR (Brazil) + */ +(function ($) {$.extend($.validator.messages, { + required: "Este campo é requerido.", + remote: "Por favor, corrija este campo.", + email: "Por favor, forneça um endereço eletrônico válido.", + url: "Por favor, forneça uma URL válida.", + date: "Por favor, forneça uma data válida.", + dateISO: "Por favor, forneça uma data válida (ISO).", + number: "Por favor, forneça um número válido.", + digits: "Por favor, forneça somente dígitos.", + creditcard: "Por favor, forneça um cartão de crédito válido.", + equalTo: "Por favor, forneça o mesmo valor novamente.", + accept: "Por favor, forneça um valor com uma extensão válida.", + maxlength: $.validator.format("Por favor, forneça não mais que {0} caracteres."), + minlength: $.validator.format("Por favor, forneça ao menos {0} caracteres."), + rangelength: $.validator.format("Por favor, forneça um valor entre {0} e {1} caracteres de comprimento."), + range: $.validator.format("Por favor, forneça um valor entre {0} e {1}."), + max: $.validator.format("Por favor, forneça um valor menor ou igual a {0}."), + min: $.validator.format("Por favor, forneça um valor maior ou igual a {0}.") +});}(jQuery)); diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ptpt.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ptpt.js new file mode 100644 index 00000000000..e59f04a1149 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ptpt.js @@ -0,0 +1,24 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: PT (Portuguese; português) + * Region: PT (Portugal) + */ +(function ($) {$.extend($.validator.messages, { + required: "Campo de preenchimento obrigatório.", + remote: "Por favor, corrija este campo.", + email: "Por favor, introduza um endereço eletrónico válido.", + url: "Por favor, introduza um URL válido.", + date: "Por favor, introduza uma data válida.", + dateISO: "Por favor, introduza uma data válida (ISO).", + number: "Por favor, introduza um número válido.", + digits: "Por favor, introduza apenas dígitos.", + creditcard: "Por favor, introduza um número de cartão de crédito válido.", + equalTo: "Por favor, introduza de novo o mesmo valor.", + accept: "Por favor, introduza um ficheiro com uma extensão válida.", + maxlength: $.validator.format("Por favor, não introduza mais do que {0} caracteres."), + minlength: $.validator.format("Por favor, introduza pelo menos {0} caracteres."), + rangelength: $.validator.format("Por favor, introduza entre {0} e {1} caracteres."), + range: $.validator.format("Por favor, introduza um valor entre {0} e {1}."), + max: $.validator.format("Por favor, introduza um valor menor ou igual a {0}."), + min: $.validator.format("Por favor, introduza um valor maior ou igual a {0}.") +});}(jQuery)); diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ro.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ro.js new file mode 100644 index 00000000000..6286f80f767 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ro.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: RO (Romanian, limba română) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Acest câmp este obligatoriu.", + remote: "Te rugăm să completezi acest câmp.", + email: "Te rugăm să introduci o adresă de email validă", + url: "Te rugăm sa introduci o adresă URL validă.", + date: "Te rugăm să introduci o dată corectă.", + dateISO: "Te rugăm să introduci o dată (ISO) corectă.", + number: "Te rugăm să introduci un număr întreg valid.", + digits: "Te rugăm să introduci doar cifre.", + creditcard: "Te rugăm să introduci un numar de carte de credit valid.", + equalTo: "Te rugăm să reintroduci valoarea.", + accept: "Te rugăm să introduci o valoare cu o extensie validă.", + maxlength: $.validator.format("Te rugăm să nu introduci mai mult de {0} caractere."), + minlength: $.validator.format("Te rugăm să introduci cel puțin {0} caractere."), + rangelength: $.validator.format("Te rugăm să introduci o valoare între {0} și {1} caractere."), + range: $.validator.format("Te rugăm să introduci o valoare între {0} și {1}."), + max: $.validator.format("Te rugăm să introduci o valoare egal sau mai mică decât {0}."), + min: $.validator.format("Te rugăm să introduci o valoare egal sau mai mare decât {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ru.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ru.js new file mode 100644 index 00000000000..46dc78168e3 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_ru.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: RU (Russian; русский язык) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Это поле необходимо заполнить.", + remote: "Пожалуйста, введите правильное значение.", + email: "Пожалуйста, введите корректный адрес электронной почты.", + url: "Пожалуйста, введите корректный URL.", + date: "Пожалуйста, введите корректную дату.", + dateISO: "Пожалуйста, введите корректную дату в формате ISO.", + number: "Пожалуйста, введите число.", + digits: "Пожалуйста, вводите только цифры.", + creditcard: "Пожалуйста, введите правильный номер кредитной карты.", + equalTo: "Пожалуйста, введите такое же значение ещё раз.", + accept: "Пожалуйста, выберите файл с правильным расширением.", + maxlength: $.validator.format("Пожалуйста, введите не больше {0} символов."), + minlength: $.validator.format("Пожалуйста, введите не меньше {0} символов."), + rangelength: $.validator.format("Пожалуйста, введите значение длиной от {0} до {1} символов."), + range: $.validator.format("Пожалуйста, введите число от {0} до {1}."), + max: $.validator.format("Пожалуйста, введите число, меньшее или равное {0}."), + min: $.validator.format("Пожалуйста, введите число, большее или равное {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_se.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_se.js similarity index 100% rename from core/vendor/assets/javascripts/jquery.validate/localization/messages_se.js rename to frontend/vendor/assets/javascripts/jquery.validate/localization/messages_se.js diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_si.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_si.js new file mode 100644 index 00000000000..0c280e80db9 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_si.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: SI (Slovenian) + */ +(function ($) { + $.extend($.validator.messages, { + required: "To polje je obvezno.", + remote: "Vpis v tem polju ni v pravi obliki.", + email: "Prosimo, vnesite pravi email naslov.", + url: "Prosimo, vnesite pravi URL.", + date: "Prosimo, vnesite pravi datum.", + dateISO: "Prosimo, vnesite pravi datum (ISO).", + number: "Prosimo, vnesite pravo številko.", + digits: "Prosimo, vnesite samo številke.", + creditcard: "Prosimo, vnesite pravo številko kreditne kartice.", + equalTo: "Prosimo, ponovno vnesite enako vsebino.", + accept: "Prosimo, vnesite vsebino z pravo končnico.", + maxlength: $.validator.format("Prosimo, da ne vnašate več kot {0} znakov."), + minlength: $.validator.format("Prosimo, vnesite vsaj {0} znakov."), + rangelength: $.validator.format("Prosimo, vnesite od {0} do {1} znakov."), + range: $.validator.format("Prosimo, vnesite vrednost med {0} in {1}."), + max: $.validator.format("Prosimo, vnesite vrednost manjšo ali enako {0}."), + min: $.validator.format("Prosimo, vnesite vrednost večjo ali enako {0}.") + }); +}(jQuery)); diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sk.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sk.js new file mode 100644 index 00000000000..bc6340abfcd --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sk.js @@ -0,0 +1,22 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: SK (Slovak; slovenčina, slovenský jazyk) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Povinné zadať.", + maxlength: $.validator.format("Maximálne {0} znakov."), + minlength: $.validator.format("Minimálne {0} znakov."), + rangelength: $.validator.format("Minimálne {0} a Maximálne {0} znakov."), + email: "E-mailová adresa musí byť platná.", + url: "URL musí byť platný.", + date: "Musí byť dátum.", + number: "Musí byť číslo.", + digits: "Môže obsahovať iba číslice.", + equalTo: "Dva hodnoty sa musia rovnať.", + range: $.validator.format("Musí byť medzi {0} a {1}."), + max: $.validator.format("Nemôže byť viac ako{0}."), + min: $.validator.format("Nemôže byť menej ako{0}."), + creditcard: "Číslo platobnej karty musí byť platné." + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sl.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sl.js new file mode 100644 index 00000000000..fa53d6d721c --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sl.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Language: SL (Slovenian; slovenski jezik) + */ +(function ($) { + $.extend($.validator.messages, { + required: "To polje je obvezno.", + remote: "Prosimo popravite to polje.", + email: "Prosimo vnesite veljaven email naslov.", + url: "Prosimo vnesite veljaven URL naslov.", + date: "Prosimo vnesite veljaven datum.", + dateISO: "Prosimo vnesite veljaven ISO datum.", + number: "Prosimo vnesite veljavno število.", + digits: "Prosimo vnesite samo števila.", + creditcard: "Prosimo vnesite veljavno številko kreditne kartice.", + equalTo: "Prosimo ponovno vnesite vrednost.", + accept: "Prosimo vnesite vrednost z veljavno končnico.", + maxlength: $.validator.format("Prosimo vnesite največ {0} znakov."), + minlength: $.validator.format("Prosimo vnesite najmanj {0} znakov."), + rangelength: $.validator.format("Prosimo vnesite najmanj {0} in največ {1} znakov."), + range: $.validator.format("Prosimo vnesite vrednost med {0} in {1}."), + max: $.validator.format("Prosimo vnesite vrednost manjše ali enako {0}."), + min: $.validator.format("Prosimo vnesite vrednost večje ali enako {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sr.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sr.js new file mode 100644 index 00000000000..73b5ec7ae1a --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sr.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: SR (Serbian; српски језик) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Поље је обавезно.", + remote: "Средите ово поље.", + email: "Унесите исправну и-мејл адресу", + url: "Унесите исправан URL.", + date: "Унесите исправан датум.", + dateISO: "Унесите исправан датум (ISO).", + number: "Унесите исправан број.", + digits: "Унесите само цифе.", + creditcard: "Унесите исправан број кредитне картице.", + equalTo: "Унесите исту вредност поново.", + accept: "Унесите вредност са одговарајућом екстензијом.", + maxlength: $.validator.format("Унесите мање од {0}карактера."), + minlength: $.validator.format("Унесите барем {0} карактера."), + rangelength: $.validator.format("Унесите вредност дугачку између {0} и {1} карактера."), + range: $.validator.format("Унесите вредност између {0} и {1}."), + max: $.validator.format("Унесите вредност мању или једнаку {0}."), + min: $.validator.format("Унесите вредност већу или једнаку {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sv.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sv.js new file mode 100644 index 00000000000..26db0913a3e --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_sv.js @@ -0,0 +1,23 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: SV (Swedish; Svenska) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Detta fält är obligatoriskt.", + maxlength: $.validator.format("Du får ange högst {0} tecken."), + minlength: $.validator.format("Du måste ange minst {0} tecken."), + rangelength: $.validator.format("Ange minst {0} och max {1} tecken."), + email: "Ange en korrekt e-postadress.", + url: "Ange en korrekt URL.", + date: "Ange ett korrekt datum.", + dateISO: "Ange ett korrekt datum (ÅÅÅÅ-MM-DD).", + number: "Ange ett korrekt nummer.", + digits: "Ange endast siffror.", + equalTo: "Ange samma värde igen.", + range: $.validator.format("Ange ett värde mellan {0} och {1}."), + max: $.validator.format("Ange ett värde som är mindre eller lika med {0}."), + min: $.validator.format("Ange ett värde som är större eller lika med {0}."), + creditcard: "Ange ett korrekt kreditkortsnummer." + }); +}(jQuery)); diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_th.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_th.js new file mode 100644 index 00000000000..f3b02355674 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_th.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: TH (Thai; ไทย) + */ +(function ($) { + $.extend($.validator.messages, { + required: "โปรดระบุ", + remote: "โปรดแก้ไขให้ถูกต้อง", + email: "โปรดระบุที่อยู่อีเมล์ที่ถูกต้อง", + url: "โปรดระบุ URL ที่ถูกต้อง", + date: "โปรดระบุวันที่ ที่ถูกต้อง", + dateISO: "โปรดระบุวันที่ ที่ถูกต้อง (ระบบ ISO).", + number: "โปรดระบุทศนิยมที่ถูกต้อง", + digits: "โปรดระบุจำนวนเต็มที่ถูกต้อง", + creditcard: "โปรดระบุรหัสบัตรเครดิตที่ถูกต้อง", + equalTo: "โปรดระบุค่าเดิมอีกครั้ง", + accept: "โปรดระบุค่าที่มีส่วนขยายที่ถูกต้อง", + maxlength: $.validator.format("โปรดอย่าระบุค่าที่ยาวกว่า {0} อักขระ"), + minlength: $.validator.format("โปรดอย่าระบุค่าที่สั้นกว่า {0} อักขระ"), + rangelength: $.validator.format("โปรดอย่าระบุค่าความยาวระหว่าง {0} ถึง {1} อักขระ"), + range: $.validator.format("โปรดระบุค่าระหว่าง {0} และ {1}"), + max: $.validator.format("โปรดระบุค่าน้อยกว่าหรือเท่ากับ {0}"), + min: $.validator.format("โปรดระบุค่ามากกว่าหรือเท่ากับ {0}") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_tr.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_tr.js new file mode 100644 index 00000000000..1c412180a21 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_tr.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: TR (Turkish; Türkçe) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Bu alanın doldurulması zorunludur.", + remote: "Lütfen bu alanı düzeltin.", + email: "Lütfen geçerli bir e-posta adresi giriniz.", + url: "Lütfen geçerli bir web adresi (URL) giriniz.", + date: "Lütfen geçerli bir tarih giriniz.", + dateISO: "Lütfen geçerli bir tarih giriniz(ISO formatında)", + number: "Lütfen geçerli bir sayı giriniz.", + digits: "Lütfen sadece sayısal karakterler giriniz.", + creditcard: "Lütfen geçerli bir kredi kartı giriniz.", + equalTo: "Lütfen aynı değeri tekrar giriniz.", + accept: "Lütfen geçerli uzantıya sahip bir değer giriniz.", + maxlength: $.validator.format("Lütfen en fazla {0} karakter uzunluğunda bir değer giriniz."), + minlength: $.validator.format("Lütfen en az {0} karakter uzunluğunda bir değer giriniz."), + rangelength: $.validator.format("Lütfen en az {0} ve en fazla {1} uzunluğunda bir değer giriniz."), + range: $.validator.format("Lütfen {0} ile {1} arasında bir değer giriniz."), + max: $.validator.format("Lütfen {0} değerine eşit ya da daha küçük bir değer giriniz."), + min: $.validator.format("Lütfen {0} değerine eşit ya da daha büyük bir değer giriniz.") + }); +}(jQuery)); \ No newline at end of file diff --git a/core/vendor/assets/javascripts/jquery.validate/localization/messages_tw.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_tw.js similarity index 100% rename from core/vendor/assets/javascripts/jquery.validate/localization/messages_tw.js rename to frontend/vendor/assets/javascripts/jquery.validate/localization/messages_tw.js diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_uk.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_uk.js new file mode 100644 index 00000000000..cdea494b1d7 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_uk.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: UK (Ukrainian; українська мова) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Це поле необхідно заповнити.", + remote: "Будь ласка, введіть правильне значення.", + email: "Будь ласка, введіть коректну адресу електронної пошти.", + url: "Будь ласка, введіть коректний URL.", + date: "Будь ласка, введіть коректну дату.", + dateISO: "Будь ласка, введіть коректну дату у форматі ISO.", + number: "Будь ласка, введіть число.", + digits: "Вводите потрібно лише цифри.", + creditcard: "Будь ласка, введіть правильний номер кредитної карти.", + equalTo: "Будь ласка, введіть таке ж значення ще раз.", + accept: "Будь ласка, виберіть файл з правильним розширенням.", + maxlength: $.validator.format("Будь ласка, введіть не більше {0} символів."), + minlength: $.validator.format("Будь ласка, введіть не менше {0} символів."), + rangelength: $.validator.format("Будь ласка, введіть значення довжиною від {0} до {1} символів."), + range: $.validator.format("Будь ласка, введіть число від {0} до {1}."), + max: $.validator.format("Будь ласка, введіть число, менше або рівно {0}."), + min: $.validator.format("Будь ласка, введіть число, більше або рівно {0}.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_vi.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_vi.js new file mode 100644 index 00000000000..fd5f2b5d5e1 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_vi.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: VI (Vietnamese; Tiếng Việt) + */ +(function ($) { + $.extend($.validator.messages, { + required: "Hãy nhập.", + remote: "Hãy sửa cho đúng.", + email: "Hãy nhập email.", + url: "Hãy nhập URL.", + date: "Hãy nhập ngày.", + dateISO: "Hãy nhập ngày (ISO).", + number: "Hãy nhập số.", + digits: "Hãy nhập chữ số.", + creditcard: "Hãy nhập số thẻ tín dụng.", + equalTo: "Hãy nhập thêm lần nữa.", + accept: "Phần mở rộng không đúng.", + maxlength: $.format("Hãy nhập từ {0} kí tự trở xuống."), + minlength: $.format("Hãy nhập từ {0} kí tự trở lên."), + rangelength: $.format("Hãy nhập từ {0} đến {1} kí tự."), + range: $.format("Hãy nhập từ {0} đến {1}."), + max: $.format("Hãy nhập từ {0} trở xuống."), + min: $.format("Hãy nhập từ {1} trở lên.") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_zh.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_zh.js new file mode 100644 index 00000000000..2c4d5c30843 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_zh.js @@ -0,0 +1,25 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: ZH (Chinese, 中文 (Zhōngwén), 汉语, 漢語) + */ +(function ($) { + $.extend($.validator.messages, { + required: "必选字段", + remote: "请修正该字段", + email: "请输入正确格式的电子邮件", + url: "请输入合法的网址", + date: "请输入合法的日期", + dateISO: "请输入合法的日期 (ISO).", + number: "请输入合法的数字", + digits: "只能输入整数", + creditcard: "请输入合法的信用卡号", + equalTo: "请再次输入相同的值", + accept: "请输入拥有合法后缀名的字符串", + maxlength: $.validator.format("请输入一个长度最多是 {0} 的字符串"), + minlength: $.validator.format("请输入一个长度最少是 {0} 的字符串"), + rangelength: $.validator.format("请输入一个长度介于 {0} 和 {1} 之间的字符串"), + range: $.validator.format("请输入一个介于 {0} 和 {1} 之间的值"), + max: $.validator.format("请输入一个最大为 {0} 的值"), + min: $.validator.format("请输入一个最小为 {0} 的值") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_zhtw.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_zhtw.js new file mode 100644 index 00000000000..ec0a2ffbc55 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/messages_zhtw.js @@ -0,0 +1,26 @@ +/* + * Translated default messages for the jQuery validation plugin. + * Locale: ZH (Chinese; 中文 (Zhōngwén), 汉语, 漢語) + * Region: TW (Taiwan) + */ +(function ($) { + $.extend($.validator.messages, { + required: "必填", + remote: "請修正此欄位", + email: "請輸入正確的電子信箱", + url: "請輸入合法的URL", + date: "請輸入合法的日期", + dateISO: "請輸入合法的日期 (ISO).", + number: "請輸入數字", + digits: "請輸入整數", + creditcard: "請輸入合法的信用卡號碼", + equalTo: "請重複輸入一次", + accept: "請輸入有效的後缀字串", + maxlength: $.validator.format("請輸入長度不大於{0} 的字串"), + minlength: $.validator.format("請輸入長度不小於 {0} 的字串"), + rangelength: $.validator.format("請輸入長度介於 {0} 和 {1} 之間的字串"), + range: $.validator.format("請輸入介於 {0} 和 {1} 之間的數值"), + max: $.validator.format("請輸入不大於 {0} 的數值"), + min: $.validator.format("請輸入不小於 {0} 的數值") + }); +}(jQuery)); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_de.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_de.js new file mode 100644 index 00000000000..3e8ac8437e0 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_de.js @@ -0,0 +1,12 @@ +/* + * Localized default methods for the jQuery validation plugin. + * Locale: DE + */ +jQuery.extend(jQuery.validator.methods, { + date: function(value, element) { + return this.optional(element) || /^\d\d?\.\d\d?\.\d\d\d?\d?$/.test(value); + }, + number: function(value, element) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:\.\d{3})+)(?:,\d+)?$/.test(value); + } +}); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_nl.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_nl.js new file mode 100644 index 00000000000..450041b14e9 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_nl.js @@ -0,0 +1,9 @@ +/* + * Localized default methods for the jQuery validation plugin. + * Locale: NL + */ +jQuery.extend(jQuery.validator.methods, { + date: function(value, element) { + return this.optional(element) || /^\d\d?[\.\/\-]\d\d?[\.\/\-]\d\d\d?\d?$/.test(value); + } +}); \ No newline at end of file diff --git a/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_pt.js b/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_pt.js new file mode 100644 index 00000000000..21879d3bb66 --- /dev/null +++ b/frontend/vendor/assets/javascripts/jquery.validate/localization/methods_pt.js @@ -0,0 +1,9 @@ +/* + * Localized default methods for the jQuery validation plugin. + * Locale: PT_BR + */ +jQuery.extend(jQuery.validator.methods, { + date: function(value, element) { + return this.optional(element) || /^\d\d?\/\d\d?\/\d\d\d?\d?$/.test(value); + } +}); \ No newline at end of file diff --git a/core/vendor/assets/stylesheets/jquery.formalize.css.erb b/frontend/vendor/assets/stylesheets/jquery.formalize.css.erb similarity index 99% rename from core/vendor/assets/stylesheets/jquery.formalize.css.erb rename to frontend/vendor/assets/stylesheets/jquery.formalize.css.erb index 345be3092f9..94c81af9b22 100644 --- a/core/vendor/assets/stylesheets/jquery.formalize.css.erb +++ b/frontend/vendor/assets/stylesheets/jquery.formalize.css.erb @@ -63,7 +63,6 @@ input[type="button"], a.button { -webkit-appearance: none; -webkit-border-radius: 4px; - -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; diff --git a/guides/.gitattributes b/guides/.gitattributes new file mode 100644 index 00000000000..2504c66a836 --- /dev/null +++ b/guides/.gitattributes @@ -0,0 +1 @@ +*.svg eol=lf diff --git a/guides/.gitignore b/guides/.gitignore new file mode 100644 index 00000000000..303d1f12ded --- /dev/null +++ b/guides/.gitignore @@ -0,0 +1,8 @@ +output +tmp +.DS_Store +.bundle +bin +crash.log +.rbenv-version +.sass-cache diff --git a/guides/CNAME b/guides/CNAME new file mode 100644 index 00000000000..535fa863134 --- /dev/null +++ b/guides/CNAME @@ -0,0 +1 @@ +api.spreecommerce.com \ No newline at end of file diff --git a/guides/Dockerfile b/guides/Dockerfile new file mode 100644 index 00000000000..3e4eb70f6ed --- /dev/null +++ b/guides/Dockerfile @@ -0,0 +1,28 @@ +FROM rlister/ruby:2.1.5 +MAINTAINER Ric Lister + +RUN DEBIAN_FRONTEND=noninteractive \ + apt-get update && \ + apt-get install -y \ + nginx-light + +WORKDIR /guides + +## cache the bundle +ADD Gemfile* /guides/ +RUN bundle install --without development test + +## build pages +ADD . /guides +RUN bundle exec nanoc compile + +## install in nginx root +WORKDIR /var/www +RUN rm -rf html && \ + mv /guides/output html && \ + rm -rf /guides && \ + echo "\ndaemon off;" >> /etc/nginx/nginx.conf && \ + echo "\nerror_log /dev/stdout info;" >> /etc/nginx/nginx.conf + +EXPOSE 80 +CMD [ "nginx" ] diff --git a/guides/Gemfile b/guides/Gemfile new file mode 100644 index 00000000000..e68abc2eca0 --- /dev/null +++ b/guides/Gemfile @@ -0,0 +1,25 @@ +source "http://rubygems.org" + +gem 'builder' +gem 'coderay_bash' +gem 'kramdown' +gem 'mime-types' +gem 'nokogiri' +gem 'nanoc', '3.6.3' +gem 'nanoc-toolbox' +gem 'rake' +gem 'thin' +gem "yajl-ruby" +gem 'sass' +gem 'bourbon' +gem 'neat' + +group :development do + gem 'listen' + gem 'rb-fsevent' + gem 'adsf' + gem 'fssm' + gem 'rspec' +end + +gem 'pry' diff --git a/guides/Gemfile.lock b/guides/Gemfile.lock new file mode 100644 index 00000000000..31f2fded89b --- /dev/null +++ b/guides/Gemfile.lock @@ -0,0 +1,98 @@ +GEM + remote: http://rubygems.org/ + specs: + adsf (1.2.0) + rack (>= 1.0.0) + bourbon (4.0.2) + sass (~> 3.3) + thor + builder (3.2.2) + celluloid (0.16.0) + timers (~> 4.0.0) + coderay (1.1.0) + coderay_bash (1.0.6) + coderay (>= 1.0) + colored (1.2) + cri (2.6.1) + colored (~> 1.2) + daemons (1.1.9) + diff-lcs (1.2.5) + eventmachine (1.0.3) + ffi (1.9.5) + fssm (0.2.10) + hitimes (1.2.2) + jsmin (1.0.1) + kramdown (1.4.2) + listen (2.7.11) + celluloid (>= 0.15.2) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + method_source (0.8.2) + mime-types (2.3) + mini_portile (0.6.0) + nanoc (3.6.3) + cri (~> 2.3) + nanoc-toolbox (0.2.0) + jsmin (~> 1.0) + nanoc (~> 3.6) + nokogiri (~> 1.6) + neat (1.6.0) + bourbon (>= 3.1) + sass (>= 3.3) + nokogiri (1.6.3.1) + mini_portile (= 0.6.0) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + rack (1.5.2) + rake (10.3.2) + rb-fsevent (0.9.4) + rb-inotify (0.9.5) + ffi (>= 0.5.0) + rspec (3.1.0) + rspec-core (~> 3.1.0) + rspec-expectations (~> 3.1.0) + rspec-mocks (~> 3.1.0) + rspec-core (3.1.4) + rspec-support (~> 3.1.0) + rspec-expectations (3.1.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.1.0) + rspec-mocks (3.1.2) + rspec-support (~> 3.1.0) + rspec-support (3.1.1) + sass (3.4.5) + slop (3.6.0) + thin (1.6.2) + daemons (>= 1.0.9) + eventmachine (>= 1.0.0) + rack (>= 1.0.0) + thor (0.19.1) + timers (4.0.1) + hitimes + yajl-ruby (1.2.1) + +PLATFORMS + ruby + +DEPENDENCIES + adsf + bourbon + builder + coderay_bash + fssm + kramdown + listen + mime-types + nanoc (= 3.6.3) + nanoc-toolbox + neat + nokogiri + pry + rake + rb-fsevent + rspec + sass + thin + yajl-ruby diff --git a/guides/README.md b/guides/README.md new file mode 100644 index 00000000000..c58134fc7bb --- /dev/null +++ b/guides/README.md @@ -0,0 +1,202 @@ +# api.spreecommerce.com + +This is a Spree API resource built with [nanoc][nanoc]. + +All submissions are welcome. To submit a change, fork this repo, commit your changes, and send us a [pull request](http://help.github.com/send-pull-requests/). + +## Setup + +Ruby 1.9 is required to build the site. + +Get the nanoc gem, plus kramdown for markdown parsing: + + $ bundle install + +You can see the available commands with nanoc: + + $ nanoc -h + +Nanoc has [some nice documentation](http://nanoc.ws/docs/tutorial/) to get you started. Though if you're mainly concerned with editing or adding content, you won't need to know much about nanoc. + +[nanoc]: http://nanoc.stoneship.org/ + +## Audience + +When contributing to the Spree documentation, it's important to make sure you understand your audience, and are speaking to them using appropriate terminology that they will both understand and appreciate. + +### API + +Those reading these guides are likely intermediate- to advanced-level developers. They are likely comfortable with complex technical language and concepts. + +### Developer + +The audience for guides in the /developer directory includes developers from beginners to advanced. They are expected to already be familiar with Ruby and Rails, but may not have as much experience with deployment and integration with external services. + +### Integration + +Consumers of this documentation are developers with intermediate to advanced skills. They should already be well-versed in Ruby and Rails; familiar with the core Spree application; and have knowledge of integrating with external services. + +### Release Notes + +Developers of all degrees of experience are the audience for these documents. + +### User + +Admins and store owners are the ones most likely to be using this documentation. These guides are where developers can send their clients to teach them how to maintain their store and process orders. + +## Styleguide + +Not sure how to structure the docs? Here's what the structure of the +API docs should look like: + + # API title + + ## API endpoint title + + [VERB] /path/to/endpoint.json + + ### Parameters + + name + : description + + ### Input (request json body) + + <%= json :field => "sample value" %> + + ### Response + + <%= headers 200, :pagination => true, 'X-Custom-Header' => "value" %> + <%= json :resource_name %> + +**Note**: We're using [Kramdown Markdown extensions](http://kramdown.rubyforge.org/syntax.html), such as definition lists. + +### Markdown Conventions + +It is helpful to standardize some markdown conventions so readers learn to recognize visual cues as they work their way through the documentation and tutorials. Following are the conventions used for the Spree documentation: + +####Class Names#### + +When referencing the name of a class, it should be capitalized. If you are writing explanatory prose and not a section of code, the class name should be blocked out with tick (`) marks. For example: + + To begin using your custom `User` class, you must first... + +Having the namespace for the class is optional, but should be included when omitting it could cause confusion. + +An instance of a class should be lowercase, normal font: + + You can view all of the orders for a particular user. + +####Buttons, Links, Section Names, Form Elements#### + +These should always reference the correct label and can have their names quoted. Examples: + +* Click the "Filter Results" button to update the results. +* Follow the "Stock Transfers" link. +* Information displayed in the "Purchase Funnel" section gives you information... +* If you check "Receive Stock" while creating a new transfer... + +####States, Attributes, Methods, Events, and Parameters#### +When referring to the state of an object - an order, for example - the state name should be lowercase and set off with tick (`) marks. For example: + + Orders that are in the `address` state do not have valid shipping and billing addresses assigned to them yet. + +This same style is used for attribute names and their settings, method names, event names, parameter names, parameter settings, and data types. + +####Path Names#### +Path names should be set off with tick (`) marks, and should include enough of the directory structure to make it clear which file is being referenced. For example: + + They are defined in `core/app/models/spree/app_configuration.rb`... + +####Adding Emphasis#### +Any text that needs to be emphasized should be in _italics_. + + Only the shipping options in the _shipping_ address are presented. + +####Terminal Blocks#### + +You can specify terminal blocks by setting it off with \`\`\`bash. +In addition, you can differentiate commands you are using from output +returned by using the `$` precursor for input and `=>` precursor for output. + +```bash +$ irb +$ c = "Hello world" +$ c +=> "Hello world" +``` + +####Special Blocks#### + +Certain blocks of text can be wrapped in sets of three characters, which will place them in divs with appropriate CSS classes. They are: + +| *** | Notes. | +| !!! | Warnings. | +| $$$ | TODO's | +| --- | A title bar; especially useful for headings for code samples. | + +### JSON Responses + +We specify the JSON responses in ruby so that we don't have to write +them by hand all over the docs. You can render the JSON for a resource like this: + + <%= json :product %> + +This looks up `Spree::Resources::PRODUCT` in `lib/resources.rb`. + +Some actions return arrays. You can modify the JSON by passing a block: + + <%= json(:issue) { |hash| [hash] } %> + +## Development + +Nanoc compiles the site into static files living in `./output`. It's +smart enough not to try to compile unchanged files: + + $ nanoc compile + Loading site data... + Compiling site... + create [0.03s] output/changes/index.html + create [0.00s] output/CNAME + create [0.02s] output/changes.atom + create [0.01s] output/index.html + create [0.09s] output/addresses/index.html + create [0.01s] output/changelog/index.html + create [0.02s] output/countries/index.html + create [0.03s] output/index.html + create [0.08s] output/order/line_items/index.html + create [0.15s] output/order/payments/index.html + create [0.02s] output/order/shipments/index.html + + Site compiled in 5.81s. + +You can setup whatever you want to view the files. If you have the adsf +gem, however (I hope so, it was in the Gemfile), you can start Webrick: + + $ nanoc view + $ open http://localhost:3000 + +Compilation times got you down? Use `autocompile`! + + $ nanoc autocompile + +This starts a web server too, so there's no need to run `nanoc view`. +One thing: remember to add trailing slashes to all nanoc links! + +## Edge guides + +Set `EDGE_GUIDES=true` in an environment variable in order to generate the "edge" badge. You may need to remove any previously generated output for the change to take effect + + $ EDGE_GUIDES=true nanoc autocompile + +## Deploy + +The guides can no longer be deployed to production from this +repo. Sufficiently-privileged users should see the private repo +[guides_deploy](https://github.com/spree/guides_deploy). + +## TODO + +* Integrate through a simple hurl.it app for live API calls. +* Write a task for verifying JSON Resource examples against the actual + API. diff --git a/guides/Rules b/guides/Rules new file mode 100644 index 00000000000..111668150ec --- /dev/null +++ b/guides/Rules @@ -0,0 +1,251 @@ +#!/usr/bin/env ruby + +# A few helpful tips about the Rules file: +# +# * The order of rules is important: for each item, only the first matching +# rule is applied. +# +# * Item identifiers start and end with a slash (e.g. “/about/” for the file +# “content/about.html”). To select all children, grandchildren, … of an +# item, use the pattern “/about/*/”; “/about/*” will also select the parent, +# because “*” matches zero or more characters. +require 'coderay_bash' + +compile '/static/*' do +end + +compile '/misc/*' do + filter :erb +end + +compile '/' do + filter :pretty_urls + filter :erb + filter :kramdown, :toc_levels => [2] + layout 'default' +end + +compile '/CNAME/' do +end + +compile '/assets/stylesheets/*/_*/' do + # don’t compile partials +end + +route '/assets/stylesheets/*/_*/' do + # don't output partials, so return nil + nil +end + +compile '/assets/stylesheets/*/' do + filter :sass, syntax: :scss +end + +route '/assets/stylesheets/*/' do + item.identifier.chop + '.css' # so that the /assets/style/screen/ item is compiled to /assets/style/screen.css +end + +compile '/assets/*' do +end + +route '/assets/*' do + item.identifier.chop + '.' + item[:extension] +end + +compile '/feed/' do + filter :pretty_urls + filter :erb + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class +end + +compile '/developer/' do + filter :pretty_urls + filter :erb + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'developer' +end + +compile '/developer/*' do + filter :pretty_urls + filter :erb + filter :parse_info_boxes + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :fenced_code_blocks + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'developer/default' +end + +compile '/release_notes/*' do + filter :pretty_urls + filter :erb + filter :parse_info_boxes + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :fenced_code_blocks + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'release_notes/default' +end + +compile '/api/' do + filter :pretty_urls + filter :erb + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :fenced_code_blocks + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'api' +end + +compile '/api/*' do + filter :pretty_urls + filter :erb + filter :parse_info_boxes + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :fenced_code_blocks + filter :parse_info_boxes + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'api/default' +end + +compile '/user/' do + filter :pretty_urls + filter :erb + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :fenced_code_blocks + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'user' +end + +compile '/user/*' do + filter :pretty_urls + filter :erb + filter :parse_info_boxes + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :fenced_code_blocks + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'user/default' +end + +compile '/release_notes/' do + filter :pretty_urls + filter :fenced_code_blocks + filter :erb + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :parse_info_boxes + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'release_notes' +end + +compile '/release_notes/*' do + filter :pretty_urls + filter :fenced_code_blocks + filter :erb + # Converts ``` codeblocks into Kramdown-friendly ~~~ + filter :parse_info_boxes + filter :kramdown, :toc_levels => [2], :coderay_line_numbers => nil, :coderay_css => :class + layout 'release_notes/default' +end + +route '/static/*' do + item.identifier[7..-2] +end + +############################################################### +# Put the misc stuff in root directory after processing +############################################################### +route '/misc/*' do + location = item.identifier.gsub('/misc/', '/') + location.chop + ".txt" +end + +############################################################### +# Flatten the developer guide stuff into single directory +############################################################### +route '/developer/deployment/*/' do + location = item.identifier.gsub('/deployment/', '/') + location.chop + ".html" +end + +route '/developer/source/*/' do + location = item.identifier.gsub('/source/', '/') + location.chop + ".html" +end + +route '/developer/core/*/' do + location = item.identifier.gsub('/core/', '/') + location.chop + ".html" +end + +route '/developer/tutorials/*/' do + location = item.identifier.gsub('/tutorials/', '/') + location.chop + ".html" +end + +route '/developer/advanced/*/' do + location = item.identifier.gsub('/advanced/', '/') + location.chop + ".html" +end + +route '/developer/customization/*/' do + location = item.identifier.gsub('/customization/', '/') + location.chop + ".html" +end + +route '/developer/upgrades/*/' do + location = item.identifier + location.chop + ".html" +end + +############################################################### +# Flatten the user guide stuff into single directory +############################################################### +route '/user/config/*/' do + location = item.identifier.gsub('/config/', '/') + location.chop + ".html" +end + +route '/user/orders/*/' do + location = item.identifier.gsub('/orders/', '/') + location.chop + ".html" +end + +route '/user/products/*/' do + location = item.identifier.gsub('/products/', '/') + location.chop + ".html" +end + +route '/user/payments/*/' do + location = item.identifier.gsub('/payments/', '/') + location.chop + ".html" +end + +route '/user/shipments/*/' do + location = item.identifier.gsub('/shipments/', '/') + location.chop + ".html" +end + +route '/user/inventory/*/' do + location = item.identifier.gsub('/inventory/', '/') + location.chop + ".html" +end + +route '/release_notes/*/' do + location = item.identifier.gsub('/release_notes/', '/release_notes/spree_') + location.chop + ".html" +end + +route '/api/*/' do + item.identifier.chop + ".html" +end + +route '/integration/*/' do + item.identifier.chop + ".html" +end + +route '/user/*/' do + item.identifier.chop + ".html" +end + +route '*' do + item.identifier + 'index.html' +end + +layout '*', :erb diff --git a/guides/TODO b/guides/TODO new file mode 100644 index 00000000000..a8b21f728e3 --- /dev/null +++ b/guides/TODO @@ -0,0 +1 @@ +* Adding a new ActiveMerchant gateway to spree_gateway diff --git a/guides/assets/images/spreeconf-badge.jpg b/guides/assets/images/spreeconf-badge.jpg new file mode 100644 index 00000000000..084fb773b49 Binary files /dev/null and b/guides/assets/images/spreeconf-badge.jpg differ diff --git a/guides/assets/stylesheets/guides.css b/guides/assets/stylesheets/guides.css new file mode 100644 index 00000000000..66b37f33f5a --- /dev/null +++ b/guides/assets/stylesheets/guides.css @@ -0,0 +1,252 @@ +.docs h3 { + padding-top: 10px; } +.docs h4 { + color: #333; + font-size: 18px; + font-weight: normal; + font-family: adelle-1,adelle-2,Helvetica,Arial,sans-serif; + padding-top: 10px; } +.docs h5 { + color: #333; + font-size: 15px; + font-weight: normal; + font-family: adelle-1,adelle-2,Helvetica,Arial,sans-serif; + padding-top: 10px; + text-transform: uppercase; + letter-spacing: 0.02em; } +code { + border: medium none; + margin: 0 0 10px; + font-size: 13px; } + +.code pre{ + margin: 0px; +} + +.code, .note, .info, .warning { + margin: 0 0 12px; + padding-top: 6px; } +.bg, .bg-file { + padding: 20px 35px 10px 30px; + margin: 0 0 0 17px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; } + +.tab { + display: block; + float: left; + margin: -8px 0 0; + min-width: 36px; + height: 36px; } +.code .tab { + background: url("../images/tab-code.png") no-repeat scroll left top #f8f9fa; } +.note .tab { + background: url("../images/tab-notes.png") no-repeat scroll left top #FFF9D8; } +.info .tab { + background: url("../images/tab-pin.png") no-repeat scroll left top #E5CFEC; } +.warning .tab { + background: url("../images/tab-warning.png") no-repeat scroll left top #F9D9D8; } +.code .bg, .bg-file { + background: #f8f9fa; + -webkit-box-shadow: inset 1px 1px 12px 2px #dee0e1; + -moz-box-shadow: inset 1px 1px 12px 2px #dee0e1; + box-shadow: inset 1px 1px 12px 2px #dee0e1; } +.note .bg { + background: #f8f5e4; + -webkit-box-shadow: inset 1px 1px 12px 2px #e8e0b4; + -moz-box-shadow: inset 1px 1px 12px 2px #e8e0b4; + box-shadow: inset 1px 1px 12px 2px #e8e0b4; } +.info .bg { + background: #f9f0fc; + -webkit-box-shadow: inset 1px 1px 12px 2px #ecdaf2; + -moz-box-shadow: inset 1px 1px 12px 2px #ecdaf2; + box-shadow: inset 1px 1px 12px 2px #ecdaf2; } +.warning .bg { + background: #ffeceb; + -webkit-box-shadow: inset 1px 1px 12px 2px #fbd6d6; + -moz-box-shadow: inset 1px 1px 12px 2px #fbd6d6; + box-shadow: inset 1px 1px 12px 2px #fbd6d6; } + +.code p, .note p, .info p, .warning p { + font-size: 14.5px; } +.code p { + color: #232323; } +.note p { + color: #4f4a30; } +.info p { + color: #532362; } +.warning p { + color: #713534; } + +tt { + padding: 3px 5px; + font-size: 13px; + margin: 0 1px; + background: #efefef; } +.bg tt { + font-size: 13px; } +.code tt { + color: #090909; + background-color: #c1c4c5; } +.note tt { + color: #624801; + background-color: #F5E9A8; } +.info tt { + color: #400951; + background-color: #efd0fa; } +.warning tt { + color: #8c0606; + background-color: #fec9c9; } + +.path { + display: inline-block; + font-family: "lucida console", monospace; + background: #CBE1AF; + color: #666; + padding: 6px 14px 2px 8px; + margin: 3px 0 0 36px; + font-size: 13px; } + +ul.nav { + float: right; +} + +.bg-file pre { + margin-top: 20px; +} + +.main-inner ul li { list-style:none; background: url(../images/grey_bullet.gif) no-repeat left 0.5em; padding-left: 1em; margin-left: 0; } + +.syntaxhighlighter { + font-size: 0.80em !important; +} + +#guidesMenu { + float: right; + margin-right: 12px; +} + +#guides { + background-color: #FFFFFF; + border: 2px solid #D2CECE; + color: #3E434C; + font-size: 0.75em; + padding: 4px; + position: absolute; + right: 225px; + top: 200px; + width: 520px; + z-index: 9999; +} + +#guides .L { + width: 240px; + float: left; +} +#guides .R { + width: 240px; + margin-left: 40px; + float: right; +} + +#guides hr { + background-color: #d2cece; +} + +#guides dl { + /*font-size: 140%;*/ + display: inline; +} + +#guides dl dt { + color: #3e434c; + letter-spacing: 0.05em; + font-weight: bold; +} + +#guides dl dd { + text-indent: -10px; + margin-left: 18px; +} +#edge-badge { + position: fixed; + right: 0px; + top: 0px; + z-index: 100; + border: none; +} + +.sidebar.spreeconf.banner .spot-inner { + background-color: white; + box-shadow: none; + text-align: center; + -webkit-box-shadow: 0px 0px 2px rgba(50, 50, 50, 0.75); + -moz-box-shadow: 0px 0px 2px rgba(50, 50, 50, 0.75); + box-shadow: 0px 0px 2px rgba(50, 50, 50, 0.75); +} + +.sidebar.spreeconf.banner .spot-inner .illo img { + margin-top: 0; + margin-left: -15px; +} + +#share-window * { + font-family: 'Bree Serif', serif !important; + color: #247cbd; +} + +#share-window { + position: fixed; + bottom: -450px; + margin-left: 665px; + width: 260px; + height: 390px; + border: 5px solid #247cbd; + border-radius: 5px; + background-color: white; + text-align: center; + padding: 30px 10px 10px; +} + +#share-window .close-widget { + position: absolute; + right: 10px; + top: 5px; + font-size: 20px; + color: #D96657; + text-decoration: none; +} + +#share-window h1, #share-window h4 { + font-weight: normal; + font-family: 'Bree Serif', serif !important; + margin-bottom: 0; +} + +#share-window h1 { + font-size: 28px; + line-height: 35px; +} + +#share-window a.button { + color: #FFF; +} +#share-window p, #share-window p strong { + margin-top: 10px; + color: #D96657; +} + +#share-window h2 { + margin-bottom: 0; +} + +#share-window h3 { + margin-top: 0; + padding-top: 0; +} +#share-window h4 { + font-size: 28px; + padding-top: 0; + margin-bottom: 10px; +} diff --git a/guides/config.yaml b/guides/config.yaml new file mode 100644 index 00000000000..0ec9277a8cb --- /dev/null +++ b/guides/config.yaml @@ -0,0 +1,71 @@ +# A list of file extensions that nanoc will consider to be textual rather than +# binary. If an item with an extension not in this list is found, the file +# will be considered as binary. +text_extensions: [ 'css', 'erb', 'haml', 'htm', 'html', 'js', 'less', 'markdown', 'md', 'php', 'rb', 'sass', 'scss', 'txt', 'xhtml', 'xml', 'atom' ] + +# The path to the directory where all generated files will be written to. This +# can be an absolute path starting with a slash, but it can also be path +# relative to the site directory. +output_dir: output + +# A list of index filenames, i.e. names of files that will be served by a web +# server when a directory is requested. Usually, index files are named +# “index.hml”, but depending on the web server, this may be something else, +# such as “default.htm”. This list is used by nanoc to generate pretty URLs. +index_filenames: [ 'index.html' ] + +# Whether or not to generate a diff of the compiled content when compiling a +# site. The diff will contain the differences between the compiled content +# before and after the last site compilation. +enable_output_diff: false + +# The data sources where nanoc loads its data from. This is an array of +# hashes; each array element represents a single data source. By default, +# there is only a single data source that reads data from the “content/” and +# “layout/” directories in the site directory. +data_sources: + - + # The type is the identifier of the data source. By default, this will be + # `filesystem_unified`. + type: filesystem_unified + encoding: utf-8 + # The path where items should be mounted (comparable to mount points in + # Unix-like systems). This is “/” by default, meaning that items will have + # “/” prefixed to their identifiers. If the items root were “/en/” + # instead, an item at content/about.html would have an identifier of + # “/en/about/” instead of just “/about/”. + items_root: / + + # The path where layouts should be mounted. The layouts root behaves the + # same as the items root, but applies to layouts rather than items. + layouts_root: / + + - + type: static + items_root: /static + +# Configuration for the “watch” command, which watches a site for changes and +# recompiles if necessary. +watcher: + # A list of directories to watch for changes. When editing this, make sure + # that the “output/” and “tmp/” directories are _not_ included in this list, + # because recompiling the site will cause these directories to change, which + # will cause the site to be recompiled, which will cause these directories + # to change, which will cause the site to be recompiled again, and so on. + dirs_to_watch: [ 'content', 'layouts', 'lib', 'static' ] + + # A list of single files to watch for changes. As mentioned above, don’t put + # any files from the “output/” or “tmp/” directories in here. + files_to_watch: [ 'config.yaml', 'Rules' ] + + # When to send notifications (using Growl or notify-send). + notify_on_compilation_success: true + notify_on_compilation_failure: true + +# For the atom feed. +base_url: http://edgeguides.spreecommerce.com + +# Array of [version, released_at] Array tuples. +#api_versions: +# - +# - v1 diff --git a/guides/content/api/addresses.md b/guides/content/api/addresses.md new file mode 100644 index 00000000000..e8a51a86b0e --- /dev/null +++ b/guides/content/api/addresses.md @@ -0,0 +1,40 @@ +--- +title: Address +description: Use the Spree Commerce storefront API to access Address data. +--- + +## Show + +Retrieve details about a particular address: + +```text +GET /api/orders/1/addresses/1``` + +### Response + +<%= headers 200 %> +<%= json(:address) %> + +## Update + +To update an address, make a request like this: + +```text +PUT /api/orders/1/addresses/1?address[firstname]=Ryan``` + +This request will update the `firstname` field for an address to the value of \"Ryan\" + +Valid address fields are: + +* firstname +* lastname +* company +* address1 +* address2 +* city +* zipcode +* phone +* alternative_phone +* country_id +* state_id + diff --git a/guides/content/api/changelog.md b/guides/content/api/changelog.md new file mode 100644 index 00000000000..5852a338a10 --- /dev/null +++ b/guides/content/api/changelog.md @@ -0,0 +1,5 @@ +--- +title: GitHub API Changelog +--- + +The API changelog can now be found on the [homepage](/). Please update your links. diff --git a/guides/content/api/changes/2012-10-24-1-2-1-and-1-3-0-api-changes.md b/guides/content/api/changes/2012-10-24-1-2-1-and-1-3-0-api-changes.md new file mode 100644 index 00000000000..619f1f660e4 --- /dev/null +++ b/guides/content/api/changes/2012-10-24-1-2-1-and-1-3-0-api-changes.md @@ -0,0 +1,42 @@ +--- +kind: change +title: Spree 1.2.1 and 1.3.0 API changes +created_at: 2012-10-24 +author_name: radar +--- + +The API in Spree has had some minor changes in the 1.2.x release, as well as in the 1.3.0 release. They are listed below. Please ensure your API clients are updated accordingly. + +## Standardize variant response + +Previously, variant data was returned like this: + +<%= json [ :variant => { :product_id => 1 }] %> + +But products were returned like this: + +<%= json :products => [ :product => { :id => 1 }] %> + +To standardize these responses, variants will now be returned underneath a `variants` key in the JSON response, like this: + +<%= json :variants => [ :variant => { :product_id => 1 }] %> + +## Standarize payments response + +Previously, payments data was returned like this: + +<%= json [ :payment => { :payment_method_id => 1 } ] %> + +Similar to the previous point, this API will now return payments like this: + +<%= json :payments => [ :payment => { :payment_method_id => 1 } ] %> + +## Deleted variants + +Deleted variants are no longer returned within requests to the variants API endpoint. Admins can view deleted variants by passing along a `show_deleted` parameter which evaluates to a "truthy" value. + +## Minor changes + +* A request to `POST /api/orders` no longer needs to include parameters for a line item. +* Requests to `POST /api/orders` now return status 201, not 200. +* All DELETE responses will now return status 204, not 200. diff --git a/guides/content/api/changes/index.html b/guides/content/api/changes/index.html new file mode 100644 index 00000000000..7ceeabfa731 --- /dev/null +++ b/guides/content/api/changes/index.html @@ -0,0 +1,8 @@ +--- +title: GitHub API Changes +--- + +# API Changes + +<%= render '_changes', :changes => api_changes %> + diff --git a/guides/content/api/checkouts.md b/guides/content/api/checkouts.md new file mode 100644 index 00000000000..ada2d251fe5 --- /dev/null +++ b/guides/content/api/checkouts.md @@ -0,0 +1,488 @@ +--- +title: Checkouts +description: Use the Spree Commerce storefront API to access Checkout data. +--- + +# Checkouts API + +## Introduction + +The checkout API functionality can be used to advance an existing order's state. +Sending a `PUT` request to `/api/checkouts/:number` will advance an order's +state or, failing that, report any errors. + +The following sections will walk through creating a new order and advancing an order from its `cart` state to its `complete` state. + +## Creating a blank order + +To create a new, empty order, make this request: + + POST /api/orders.json + +### Response + +<%= headers 201 %> +
        {
        +  "id": 4,
        +  "number": "R307128032",
        +  "item_total": "0.0",
        +  "total": "0.0",
        +  "ship_total": "0.0",
        +  "state": "cart",
        +  "adjustment_total": "0.0",
        +  "user_id": 1,
        +  "created_at": "2014-07-06T18:52:33.724Z",
        +  "updated_at": "2014-07-06T18:52:33.752Z",
        +  "completed_at": null,
        +  "payment_total": "0.0",
        +  "shipment_state": null,
        +  "payment_state": null,
        +  "email": "spree@example.com",
        +  "special_instructions": null,
        +  "channel": "spree",
        +  "included_tax_total": "0.0",
        +  "additional_tax_total": "0.0",
        +  "display_included_tax_total": "$0.00",
        +  "display_additional_tax_total": "$0.00",
        +  "tax_total": "0.0",
        +  "currency": "USD",
        +  "display_item_total": "$0.00",
        +  "total_quantity": 0,
        +  "display_total": "$0.00",
        +  "display_ship_total": "$0.00",
        +  "display_tax_total": "$0.00",
        +  "token": "n0kZnXjRfjnhZMY5ijhiOA",
        +  "checkout_steps": [
        +    "address",
        +    "delivery",
        +    "complete"
        +  ],
        +  "permissions": {
        +    "can_update": true
        +  },
        +  "bill_address": null,
        +  "ship_address": null,
        +  "line_items": [],
        +  "payments": [],
        +  "shipments": [],
        +  "adjustments": []
        +}
        +
        + +Any time you update the order or move a checkout step you'll get +a response similar as above along with the new associated objects. e.g. addresses, +payments, shipments. + +## Add line items to an order + +Pass line item attributes like this: + +
        {
        +  "line_item": {
        +    "variant_id": 1,
        +    "quantity": 5
        +  }
        +}
        +
        + +to this api endpoint: + + POST /api/orders/:number/line_items.json + +<%= headers 201 %> +
        {
        +  "id": 3,
        +  "quantity": 5,
        +  "price": "15.99",
        +  "variant_id": 1,
        +  "single_display_amount": "$15.99",
        +  "display_amount": "$79.95",
        +  "total": "79.95",
        +  "variant": {
        +    "id": 1,
        +    "name": "Ruby on Rails Tote",
        +    "sku": "ROR-00011",
        +    "price": "15.99",
        +    "weight": "0.0",
        +    "height": null,
        +    "width": null,
        +    "depth": null,
        +    "is_master": true,
        +    "cost_price": "17.0",
        +    "slug": "ruby-on-rails-tote",
        +    "description": "Nihil et itaque adipisci sed ea dolorum.",
        +    "track_inventory": true,
        +    "display_price": "$15.99",
        +    "options_text": "",
        +    "in_stock": true,
        +    "option_values": [],
        +    "images": [
        +      {
        +        "id": 21,
        +        "position": 1,
        +        "attachment_content_type": "image/jpeg",
        +        "attachment_file_name": "ror_tote.jpeg",
        +        "type": "Spree::Image",
        +        "attachment_updated_at": "2014-07-06T18:37:34.534Z",
        +        "attachment_width": 360,
        +        "attachment_height": 360,
        +        "alt": null,
        +        "viewable_type": "Spree::Variant",
        +        "viewable_id": 1,
        +        "mini_url": "/spree/products/21/mini/ror_tote.jpeg?1404671854",
        +        "small_url": "/spree/products/21/small/ror_tote.jpeg?1404671854",
        +        "product_url": "/spree/products/21/product/ror_tote.jpeg?1404671854",
        +        "large_url": "/spree/products/21/large/ror_tote.jpeg?1404671854"
        +      },
        +      {
        +        "id": 22,
        +        "position": 2,
        +        "attachment_content_type": "image/jpeg",
        +        "attachment_file_name": "ror_tote_back.jpeg",
        +        "type": "Spree::Image",
        +        "attachment_updated_at": "2014-07-06T18:37:34.921Z",
        +        "attachment_width": 360,
        +        "attachment_height": 360,
        +        "alt": null,
        +        "viewable_type": "Spree::Variant",
        +        "viewable_id": 1,
        +        "mini_url": "/spree/products/22/mini/ror_tote_back.jpeg?1404671854",
        +        "small_url": "/spree/products/22/small/ror_tote_back.jpeg?1404671854",
        +        "product_url": "/spree/products/22/product/ror_tote_back.jpeg?1404671854",
        +        "large_url": "/spree/products/22/large/ror_tote_back.jpeg?1404671854"
        +      }
        +    ],
        +    "product_id": 1
        +  },
        +  "adjustments": []
        +}
        +
        + +## Updating an order + +To update an order you must be authenticated as the order's user, and perform a request like this: + + PUT /api/orders/:number.json + +If you know the order's token, then you can also update the order: + + PUT /api/orders/:number.json?order_token=abcdef123456 + +Requests performed as a non-admin or non-authorized user will be met with a 401 response from this action. + +## Address + +To transition an order to its next step, make a request like this: + + PUT /api/checkouts/:number/next.json + +If the request is successfull you'll get a 200 response using the same order +template shown when creating the order with the state updated. See example of +failed response below. + +### Failed Response + +<%= headers 422 %> +<%= json(:order_failed_transition) %> + +## Delivery + +To advance to the next state, `delivery`, the order will first need both a shipping and billing address. + +In order to update the addresses, make this request with the necessary parameters: + + PUT /api/checkouts/:number.json + +As an example, here are the required address attributes and how they should be formatted: + +<%= json \ + :order => { + :bill_address_attributes => { + :firstname => 'John', + :lastname => 'Doe', + :address1 => '7735 Old Georgetown Road', + :city => 'Bethesda', + :phone => '3014445002', + :zipcode => '20814', + :state_id => 48, + :country_id => 49 + }, + + :ship_address_attributes => { + :firstname => 'John', + :lastname => 'Doe', + :address1 => '7735 Old Georgetown Road', + :city => 'Bethesda', + :phone => '3014445002', + :zipcode => '20814', + :state_id => 48, + :country_id => 49 + } + } +%> + +### Response + +Once valid address information has been submitted, the shipments and shipping rates +available for this order will be returned inside a `shipments` key inside the order, +as seen below: + +<%= headers 200 %> +
        {
        +  ...
        +  "shipments": [
        +    {
        +      "id": 4,
        +      "tracking": null,
        +      "number": "H22035832422",
        +      "cost": "15.0",
        +      "shipped_at": null,
        +      "state": "pending",
        +      "order_id": "R181010551",
        +      "stock_location_name": "default",
        +      "shipping_rates": [
        +        {
        +          "id": 10,
        +          "name": "UPS Ground (USD)",
        +          "cost": "5.0",
        +          "selected": false,
        +          "shipping_method_id": 1,
        +          "display_cost": "$5.00"
        +        },
        +        {
        +          "id": 11,
        +          "name": "UPS Two Day (USD)",
        +          "cost": "10.0",
        +          "selected": false,
        +          "shipping_method_id": 2,
        +          "display_cost": "$10.00"
        +        },
        +        {
        +          "id": 12,
        +          "name": "UPS One Day (USD)",
        +          "cost": "15.0",
        +          "selected": true,
        +          "shipping_method_id": 3,
        +          "display_cost": "$15.00"
        +        }
        +      ],
        +      "selected_shipping_rate": {
        +        "id": 12,
        +        "name": "UPS One Day (USD)",
        +        "cost": "15.0",
        +        "selected": true,
        +        "shipping_method_id": 3,
        +        "display_cost": "$15.00"
        +      },
        +      "shipping_methods": [
        +        {
        +          "id": 1,
        +          "name": "UPS Ground (USD)",
        +          "zones": [
        +            {
        +              "id": 2,
        +              "name": "North America",
        +              "description": "USA + Canada"
        +            }
        +          ],
        +          "shipping_categories": [
        +            {
        +              "id": 1,
        +              "name": "Default"
        +            }
        +          ]
        +        },
        +        {
        +          "id": 2,
        +          "name": "UPS Two Day (USD)",
        +          "zones": [
        +            {
        +              "id": 2,
        +              "name": "North America",
        +              "description": "USA + Canada"
        +            }
        +          ],
        +          "shipping_categories": [
        +            {
        +              "id": 1,
        +              "name": "Default"
        +            }
        +          ]
        +        },
        +        {
        +          "id": 3,
        +          "name": "UPS One Day (USD)",
        +          "zones": [
        +            {
        +              "id": 2,
        +              "name": "North America",
        +              "description": "USA + Canada"
        +            }
        +          ],
        +          "shipping_categories": [
        +            {
        +              "id": 1,
        +              "name": "Default"
        +            }
        +          ]
        +        }
        +      ],
        +      "manifest": [
        +        {
        +          "quantity": 3,
        +          "states": {
        +            "on_hand": 3
        +          },
        +          "variant_id": 1
        +        }
        +      ]
        +    }
        +  ],
        +  ...
        +
        + +## Payment + +To advance to the next state, `payment`, you will need to select a shipping rate +for each shipment for the order. These were returned when transitioning to the +`delivery` step. If you need want to see them again, make the following request: + + GET /api/orders/:number.json + +Spree will select a shipping rate by default so you can advance to the `payment` +state by making this request: + + PUT /api/checkouts/:number/next.json + +If the order doesn't have an assigned shipping rate, or you want to choose a different +shipping rate make the following request to select one and advance the order's state: + + PUT /api/checkouts/:number.json + +With parameters such as these: + +<%= json ( + { + order: { + shipments_attributes: { + "0" => { + selected_shipping_rate_id: 1, + id: 1 + } + } + } + }) %> + +*** +Please ensure you select a shipping rate for each shipment in the order. In the request +above, the `selected_shipping_rate_id` should be the id of the shipping rate you want to +use and the `id` should be the id of the shipment you are choosing this shipping rate for. +*** + +## Confirm + +To advance to the next state, `confirm`, the order will need to have a payment. +You can create a payment by passing in parameters such as this: + +<%= json \ + :order => { + :payments_attributes => [{ + :payment_method_id => "1" + }] + }, + :payment_source => { + "1" => { + "number" => "4111111111111111", + "month" => "1", + "year" => "2017", + "verification_value" => "123", + "name" => "John Smith" + } + } +%> + +*** +The numbered key in the `payment_source` hash directly corresponds to the +`payment_method_id` attribute within the `payment_attributes` key. +*** + +You can also use an existing card for the order by submitting the credit card +id. See an example request: + +<%= json \ + :order => { + :existing_card => "1" + } +%> + +_Please note that for 2-2-stable checkout api the request body to submit a payment +via api/checkouts is slight different. See example:_ + +<%= json \ + :order => { + :payments_attributes => { + :payment_method_id => "1" + }, + :payment_source => { + "1" => { + "number" => "4111111111111111", + "month" => "1", + "year" => "2017", + "verification_value" => "123", + "name" => "John Smith" + } + } + } +%> + +If the order already has a payment, you can advance it to the `confirm` state by making this request: + + PUT /api/checkouts/:number.json + +For more information on payments, view the [payments documentation](payments). + +### Response + +<%= headers 200 %> +
        {
        +  ...
        +  "state": "confirm",
        +  ...
        +  "payments": [
        +    {
        +      "id": 3,
        +      "source_type": "Spree::CreditCard",
        +      "source_id": 2,
        +      "amount": "65.37",
        +      "display_amount": "$65.37",
        +      "payment_method_id": 1,
        +      "response_code": null,
        +      "state": "checkout",
        +      "avs_response": null,
        +      "created_at": "2014-07-06T19:55:08.308Z",
        +      "updated_at": "2014-07-06T19:55:08.308Z",
        +      "payment_method": {
        +        "id": 1,
        +        "name": "Credit Card",
        +        "environment": "development"
        +      },
        +      "source": {
        +        "id": 2,
        +        "month": "1",
        +        "year": "2017",
        +        "cc_type": null,
        +        "last_digits": "1111",
        +        "name": "John Smith"
        +      }
        +    }
        +  ],
        +  ...
        +
        + +## Complete + +Now the order is ready to be advanced to the final state, `complete`. To accomplish this, make this request: + + PUT /api/checkouts/:number.json + +You should get a 200 response with all the order info. diff --git a/guides/content/api/countries.md b/guides/content/api/countries.md new file mode 100644 index 00000000000..57e9aaf3b91 --- /dev/null +++ b/guides/content/api/countries.md @@ -0,0 +1,73 @@ +--- +title: Countries +description: Use the Spree Commerce storefront API to access Country data. +--- + +## Index + +Retrieve a list of all countries by making this request: + +```text +GET /api/countries``` + +Countries are paginated and can be iterated through by passing along a `page` parameter: + +```text +GET /api/countries?page=2``` + +### Parameters + +page +: The page number of country to display. + +per_page +: The number of countries to return per page + +### Response + +<%= headers 200 %> +<%= json(:country) do |h| +{ :countries => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +To search for a particular country, make a request like this: + +```text +GET /api/countries?q[name_cont]=united``` + +The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:country) do |h| + { :countries => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/countries?q[s]=name%20desc``` + +## Show + +Retrieve details about a particular country: + +```text +GET /api/countries/1``` + +### Response + +<%= headers 200 %> +<%= json(:country) %> + diff --git a/guides/content/api/index.md b/guides/content/api/index.md new file mode 100644 index 00000000000..06c8068071e --- /dev/null +++ b/guides/content/api/index.md @@ -0,0 +1,11 @@ +--- +title: API +--- + +## Spree API Guide + +This site covers the inner working of Spree\'s RESTful API. It assumes a basic understanding of the principles of REST. + +The REST API is designed to give developers a convenient way to access data contained within Spree. With a standard read/write interface to store data, it is now very simple to write third party applications (eg. iPhone) that can talk to your Spree store. It is also possible to build sophisticated middleware applications that can serve as a bridge between Spree and a warehouse or inventory system. + +For a comprehensive list of API functions, start browsing the resources in the above diagram. diff --git a/guides/content/api/line_items.md b/guides/content/api/line_items.md new file mode 100644 index 00000000000..e7fb7aec776 --- /dev/null +++ b/guides/content/api/line_items.md @@ -0,0 +1,43 @@ +--- +title: Line Items +description: Use the Spree Commerce storefront API to access LineItem data. +--- + +# Line Items API + +## Create + +To create a new line item, make a request like this: + + POST /api/orders/R1234567/line_items?line_item[variant_id]=1&line_item[quantity]=1 + +This will create a new line item representing a single item for the variant with the id of 1. + +### Response + +<%= headers 201 %> +<%= json(:line_item) %> + +## Update + +To update the information for a line item, make a request like this: + + PUT /api/orders/R1234567/line_items/1?line_item[variant_id]=1&line_item[quantity]=1 + +This request will update the line item with the ID of 1 for the order, updating the line item's `variant_id` to 1, and its `quantity` 1. + +### Response + +<%= headers 200 %> +<%= json(:line_item) %> + +## Delete + +To delete a line item, make a request like this: + + DELETE /api/orders/R1234567/line_items/1 + +### Response + +<%= headers 204 %> + diff --git a/guides/content/api/orders.md b/guides/content/api/orders.md new file mode 100644 index 00000000000..da757388e46 --- /dev/null +++ b/guides/content/api/orders.md @@ -0,0 +1,151 @@ +--- +title: Orders +description: Use the Spree Commerce storefront API to access Order data. +--- + +## Index + +<%= admin_only %> + +Retrieve a list of orders by making this request: + +```text +GET /api/orders``` + +Orders are paginated and can be iterated through by passing along a `page` parameter: + +```text +GET /api/orders?page=2``` + +### Parameters + +page +: The page number of order to display. + +per_page +: The number of orders to return per page + +### Response + +<%= headers 200 %> +<%= json(:order) do |h| +{ :orders => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +To search for a particular order, make a request like this: + +```text +GET /api/orders?q[email_cont]=bob``` + +The searching API is provided through the Ransack gem which Spree depends on. The `email_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:order) do |h| + { :orders => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/orders?q[s]=number%20desc``` + +It is also possible to sort results using an associated object's field. + +```text +GET /api/orders?q[s]=user_name%20asc``` + +## Show + +To view the details for a single product, make a request using that order\'s number: + +```text +GET /api/orders/R123456789``` + +Orders through the API will only be visible to admins and the users who own +them. If a user attempts to access an order that does not belong to them, they +will be met with an authorization error. + +Users may pass in the order's token in order to be authorized to view an order: + +```text +GET /api/orders/R123456789?order_token=abcdef123456 +``` + +The `order_token` parameter will work for authorizing any action for an order within Spree's API. + +### Successful Response + +<%= headers 200 %> +<%= json :order_show %> + +### Not Found Response + +<%= not_found %> + +### Authorization Failure + +<%= authorization_failure %> + +## Show (delivery) + +When an order is in the "delivery" state, additional shipments information will be returned in the API: + +<%= json(:shipment) do |h| + { :shipments => [h] } +end %> + +## Create + +To create a new order through the API, make this request: + +```text +POST /api/orders``` + +If you wish to create an order with a line item matching to a variant whose ID is \"1\" and quantity is 5, make this request: + +```text +POST /api/orders?order[line_items][0][variant_id]=1&order[line_items][0][quantity]=5``` + +### Successful response + +<%= headers 201 %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + :name => ["can't be blank"], + :price => ["can't be blank"] + } +%> + +## Update Address + +To add address information to an order, please see the [checkout transitions](checkouts#checkout-transitions) section of the Checkouts guide. + +## Empty + +To empty an order\'s cart, make this request: + +```text +PUT /api/orders/R1234567/empty``` + +All line items will be removed from the cart and the order\'s information will +be cleared. Inventory that was previously depleted by this order will be +repleted. diff --git a/guides/content/api/payments.md b/guides/content/api/payments.md new file mode 100644 index 00000000000..9f356151eac --- /dev/null +++ b/guides/content/api/payments.md @@ -0,0 +1,194 @@ +--- +title: Payments +description: Use the Spree Commerce storefront API to access Payment data. +--- + +# Payments API + +## Index + +To see details about an order's payments, make this request: + + GET /api/orders/R1234567/payments + +Payments are paginated and can be iterated through by passing along a `page` parameter: + + GET /api/orders/R1234567/payments?page=2 + +### Parameters + +page +: The page number of payment to display. + +per_page +: The number of payments to return per page + +### Response + +<%= headers 200 %> +<%= json(:payment) do |h| +{ :payments => [h], + :count => 2, + :pages => 2, + :current_page => 1 } +end %> + +## Search + +To search for a particular payment, make a request like this: + + GET /api/orders/R1234567/payments?q[response_code_cont]=123 + +The searching API is provided through the Ransack gem which Spree depends on. The `response_code_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:payment) do |h| +{ :payments => [h], + :count => 2, + :pages => 2, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + + GET /api/payments?q[s]=state%20desc + +It is also possible to sort results using an associated object's field. + + GET /api/payments?q[s]=order_number%20asc + +## New + +In order to create a new payment, you will need to know about the available payment methods and attributes. To find these out, make this request: + + GET /api/orders/R1234567/payments/new + +### Response + +<%= headers 200 %> +<%= json \ + :attributes => + ["id", "source_type", "source_id", "amount", + "payment_method_id", "response_code", "state", + "avs_response", "created_at", "updated_at"], + :payment_methods => [Spree::Resources::PAYMENT_METHOD] %> + +## Create + +To create a new payment, make a request like this: + + POST /api/orders/R1234567/payments?payment[payment_method_id]=1&payment[amount]=10 + +### Response + +<%= headers 201 %> +<%= json(:payment) %> + +## Show + +To get information for a particular payment, make a request like this: + + GET /api/orders/R1234567/payments/1 + +### Response + +<%= headers 200 %> +<%= json(:payment) %> + +## Authorize + +To authorize a payment, make a request like this: + + PUT /api/orders/R1234567/payments/1/authorize + +### Response + +<%= headers 200 %> +<%= json :payment %> + +### Failed Response + +<%= headers 422 %> +<%= json :error => "There was a problem with the payment gateway: [text]" %> + +## Capture + +<%= warning "Capturing a payment is typically done shortly after authorizing the payment. If you are auto-capturing payments, you may be able to use the purchase endpoint instead." %> + +To capture a payment, make a request like this: + + PUT /api/orders/R1234567/payments/1/capture + +### Response + +<%= headers 200 %> +<%= json :payment %> + +### Failed Response + +<%= headers 422 %> +<%= json :error => "There was a problem with the payment gateway: [text]" %> + +## Purchase + +<%= warning "Purchasing a payment is typically done only if you are not authorizing payments before-hand. If you are authorizing payments, then use the authorize and capture endpoints instead." %> + +To make a purchase with a payment, make a request like this: + + PUT /api/orders/R1234567/payments/1/purchase + +### Response + +<%= headers 200 %> +<%= json :payment %> + +### Failed Response + +<%= headers 422 %> +<%= json :error => "There was a problem with the payment gateway: [text]" %> + +## Void + +To void a payment, make a request like this: + + PUT /api/orders/R1234567/payments/1/void + +### Response + +<%= headers 200 %> +<%= json :payment %> + +### Failed Response + +<%= headers 422 %> +<%= json :error => "There was a problem with the payment gateway: [text]" %> + +## Credit + +To credit a payment, make a request like this: + + PUT /api/orders/R1234567/payments/1/credit?amount=10 + +If the payment is over the payment's credit allowed limit, a "Credit Over Limit" response will be returned. + +### Response + +<%= headers 200 %> +<%= json :payment %> + +### Failed Response + +<%= headers 422 %> +<%= json :error => "There was a problem with the payment gateway: [text]" %> + +### Credit Over Limit Response + +<%= headers 422 %> +<%= json :error => "This payment can only be credited up to [amount]. Please specify an amount less than or equal to this number." %> + diff --git a/guides/content/api/product_properties.md b/guides/content/api/product_properties.md new file mode 100644 index 00000000000..0552ec0a32e --- /dev/null +++ b/guides/content/api/product_properties.md @@ -0,0 +1,116 @@ +--- +title: Product Properties +description: Use the Spree Commerce storefront API to access ProductProperty data. +--- + +<%= warning "Requests to this API will only succeed if the user making them has access to the underlying products. If the user is not an admin and the product is not available yet, users will receive a 404 response from this API." %> + +## Index + +List + +Retrieve a list of all product properties for a product by making this request: + + GET /api/products/1/product_properties + +Product properties are paginated and can be iterated through by passing along a `page` parameter: + + GET /api/products/1/product_properties?page=2 + +### Parameters + +page +: The page number of product property to display. + +per_page +: The number of product properties to return per page + +### Response + +<%= headers 200 %> +<%= json(:product_property) do |h| +{ :product_properties => [h], + :count => 10, + :pages => 2, + :current_page => 1 } +end %> + +## Search + +To search for a particular product property, make a request like this: + + GET /api/products/1/product_properties?q[property_name_cont]=bag + +The searching API is provided through the Ransack gem which Spree depends on. The `property_name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:product_property) do |h| + { :product_properties => [h], + :count => 10, + :pages => 2, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + + GET /api/products/1/product_properties?q[s]=property_name%20desc + +## Show + +To get information about a single product property, make a request like this: + + GET /api/products/1/product_properties/1 + +Or you can use a property's name: + + GET /api/products/1/product_properties/size + +### Response + +<%= headers 200 %> +<%= json(:product_property) %> + +## Create + +<%= admin_only %> + +To create a new product property, make a request like this: + + POST /api/products/1/product_properties?product_property[property_name]=size&product_property[value]=10 + +If a property with that name does not already exist, then it will automatically be created. + +### Response + +<%= headers 201 %> +<%= json(:product_property) %> + +## Update + +To update an existing product property, make a request like this: + + PUT /api/products/1/product_properties/size?product_property[value]=10 + +You may also use a property's id if you know it: + + PUT /api/products/1/product_properties/1?product_property[value]=10 + +### Response + +<%= headers 200 %> +<%= json(:product_property) %> + +## Delete + +To delete a product property, make a request like this: + + DELETE /api/products/1/product_properties/size + +<%= headers 204 %> + diff --git a/guides/content/api/products.md b/guides/content/api/products.md new file mode 100644 index 00000000000..e6b974f306c --- /dev/null +++ b/guides/content/api/products.md @@ -0,0 +1,184 @@ +--- +title: Products +description: Use the Spree Commerce storefront API to access Product data. +--- + +## Index + +List products visible to the authenticated user. If the user is not an admin, they will only be able to see products which have an `available_on` date in the past. If the user is an admin, they are able to see all products. + +```text +GET /api/products``` + +Products are paginated and can be iterated through by passing along a `page` parameter: + +```text +GET /api/products?page=2``` + +### Parameters + +show_deleted +: **boolean** - `true` to show deleted products, `false` to hide them. Default: `false`. **Only available to users with an admin role.** + +page +: The page number of products to display. + +per_page +: The number of products to return per page + +### Response + +<%= headers 200 %> +<%= json(:product) do |h| +{ :products => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +To search for a particular product, make a request like this: + +```text +GET /api/products?q[name_cont]=Spree``` + +The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:product) do |h| +{ :products => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/products?q[s]=sku%20asc``` + +It is also possible to sort results using an associated object's field. + +```text +GET /api/products?q[s]=shipping_category_name%20asc``` + +## Show + +To view the details for a single product, make a request using that product\'s permalink: + +```text +GET /api/products/a-product``` + +You may also query by the product\'s id attribute: + +```text +GET /api/products/1``` + +Note that the API will attempt a permalink lookup before an ID lookup. + +### Successful Response + +<%= headers 200 %> +<%= json :product %> + +### Not Found Response + +<%= not_found %> + +## New + +You can learn about the potential attributes (required and non-required) for a product by making this request: + +```text +GET /api/products/new``` + +### Response + +<%= headers 200 %> +<%= json \ + :attributes => [ + :id, :name, :description, :price, :available_on, :permalink, + :count_on_hand, :meta_description, :meta_keywords, :shipping_category_id, :taxon_ids + ], + :required_attributes => [:name, :price, :shipping_category_id] + %> + +## Create + +<%= admin_only %> + +To create a new product through the API, make this request with the necessary parameters: + +```text +POST /api/products``` + +For instance, a request to create a new product called \"Headphones\" with a price of $100 would look like this: + +```text +POST /api/products?product[name]=Headphones&product[price]=100&product[shipping_category_id]=1``` + +### Successful response + +<%= headers 201 %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + :name => ["can't be blank"], + :price => ["can't be blank"], + :shipping_category_id => ["can't be blank"] + } +%> + +## Update + +<%= admin_only %> + +To update a product\'s details, make this request with the necessary parameters: + +```text +PUT /api/products/a-product``` + +For instance, to update a product\'s name, send it through like this: + +```text +PUT /api/products/a-product?product[name]=Headphones``` + +### Successful response + +<%= headers 201 %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + :name => ["can't be blank"], + :price => ["can't be blank"], + :shipping_category_id => ["can't be blank"] + } +%> + +## Delete + +<%= admin_only %> + +To delete a product, make this request: + +```text +DELETE /api/products/a-product``` + +This request, much like a typical product \"deletion\" through the admin interface, will not actually remove the record from the database. It simply sets the `deleted_at` field to the current time on the product, as well as all of that product\'s variants. + +<%= headers 204 %> \ No newline at end of file diff --git a/guides/content/api/return_authorizations.md b/guides/content/api/return_authorizations.md new file mode 100644 index 00000000000..f68a8cfb914 --- /dev/null +++ b/guides/content/api/return_authorizations.md @@ -0,0 +1,121 @@ +--- +title: Return Authorizations +description: Use the Spree Commerce storefront API to access ReturnAuthorization data. +--- + +# Return Authorizations API + +<%= admin_only %> + +## Index + +To list all return authorizations for an order, make a request like this: + + GET /api/orders/R1234567/return_authorizations + +Return authorizations are paginated and can be iterated through by passing along a `page` parameter: + + GET /api/orders/R1234567/return_authorizations?page=2 + +### Parameters + +page +: The page number of return authorization to display. + +per_page +: The number of return authorizations to return per page + +### Response + +<%= headers 200 %> +<%= json(:return_authorization) do |h| +{ :return_authorizations => [h], + :count => 2, + :pages => 1, + :current_page => 1 } +end %> + +## Search + +To search for a particular return authorization, make a request like this: + + GET /api/orders/R1234567/return_authorizations?q[reason_cont]=damage + +The searching API is provided through the Ransack gem which Spree depends on. The `reason_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + + GET /api/orders/R1234567/return_authorizations?q[s]=amount%20asc + +### Response + +<%= headers 200 %> +<%= json(:return_authorization) do |h| + { :return_authorizations => [h], + :count => 1, + :pages => 1, + :current_page => 1 } +end %> + +## Show + +To get information for a single return authorization, make a request like this: + + GET /api/orders/R1234567/return_authorizations/1 + +### Response + +<%= headers 200 %> +<%= json(:return_authorization) %> + +## Create + +<%= admin_only %> + +To create a return authorization, make a request like this: + + POST /api/orders/R1234567/return_authorizations + +For instance, if you want to create a return authorization with a number, make +this request: + + POST /api/orders/R1234567/return_authorizations?return_authorization[number]=123456 + +### Response + +<%= headers 201 %> +<%= json(:return_authorization) %> + +## Update + +<%= admin_only %> + +To update a return authorization, make a request like this: + + PUT /api/orders/R1234567/return_authorizations/1 + +For instance, to update a return authorization's number, make this request: + + PUT /api/orders/R1234567/return_authorizations/1?return_authorization[number]=123456 + +### Response + +<%= headers 200 %> +<%= json(:return_authorization) %> + +## Delete + +<%= admin_only %> + +To delete a return authorization, make a request like this: + + DELETE /api/orders/R1234567/return_authorizations/1 + +### Response + +<%= headers 204 %> + diff --git a/guides/content/api/shipments.md b/guides/content/api/shipments.md new file mode 100644 index 00000000000..39e06275515 --- /dev/null +++ b/guides/content/api/shipments.md @@ -0,0 +1,149 @@ +--- +title: Shipments +description: Use the Spree Commerce storefront API to access Shipment data. +--- + +# Shipments API + +## Mine + +Retrieve a list of the current user's shipments by making this request: + +```text +GET /api/shipments/mine``` + +Shipments are paginated and can be iterated through by passing along a `page` parameter: + +```text +GET /api/shipments/mine?page=2``` + +### Parameters + +page +: The page number of shipments to display. + +per_page +: The number of shipments to return per page. + +### Response + +<%= headers 200 %> +<%= json(:shipment) do |h| +{ count: 25, + current_page: 1, + pages: 5, + shipments: [h] } +end %> + +## Create + +<%= admin_only %> + +The following attributes are required when creating a shipment: + +- order_id +- stock_location_id +- variant_id + +To create a shipment, make a request like this: + +```text +POST /api/shipments?shipment[order_id]=R1234567``` + +The `order_id` is the number of the order to create a shipment for and is provided as part of the URL string as shown above. The shipment will be created at the selected stock location and include the variant selected. + +Assuming in this instance that you want to create a shipment with a stock_location_id of `1` and a variant_id of `10` for order `R1234567`, send through the parameters like this: + +<%= json \ + :order_id => 123456, + :stock_location_id => 1, + :variant_id => 10 + %> + +### Response + +<%= headers 200 %> +<%= json(:shipment) %> + +## Update + +<%= admin_only %> + +To update shipment information, make a request like this: + +```text +PUT /api/shipments/H123456789?shipment[tracking]=TRK9000``` + +### Parameters + +unlock +: When set to `yes`, the shipment's adjustment will be recalculated. + +To update order ship method inspect order/shipments/shipping_rates for available shipping_rate_id values and use following api call: + + PUT /api/shipments/H123456789?shipment[selected_shipping_rate_id]=162&shipment[unlock]=yes + +### Response + +<%= headers 200 %> +<%= json(:shipment) %> + +## Ready + +<%= admin_only %> + +To mark a shipment as ready, make a request like this: + + PUT /api/shipments/H123456789/ready + +You may choose to update shipment attributes with this request as well: + + PUT /api/shipments/H123456789/ready?shipment[number]=1234567 + +### Response + +<%= headers 200 %> +<%= json(:shipment) %> + +## Ship + +<%= admin_only %> + +To mark a shipment as shipped, make a request like this: + + PUT /api/shipments/H123456789/ship + +You may choose to update shipment attributes with this request as well: + + PUT /api/shipments/H123456789/ship?shipment[number]=1234567 + +### Response + +<%= headers 200 %> +<%= json(:shipment) %> + +## Add Variant + +<%= admin_only %> + +To add a variant to a shipment, make a request like this: + + PUT /api/shipments/H123456789/add?variant_id=1&quantity=1 + +### Response + +<%= headers 200 %> +<%= json(:shipment) %> + +## Remove Variant + +<%= admin_only %> + +To remove a variant from a shipment, make a request like this: + + PUT /api/shipments/H123456789/remove?variant_id=1&quantity=1 + +### Response + +<%= headers 200 %> +<%= json(:shipment) %> diff --git a/guides/content/api/states.md b/guides/content/api/states.md new file mode 100644 index 00000000000..4fe959fd16e --- /dev/null +++ b/guides/content/api/states.md @@ -0,0 +1,50 @@ +--- +title: States +description: Use the Spree Commerce storefront API to access State data. +--- + +## Index + +To get a list of states within Spree, make a request like this: + +```text +GET /api/states``` + +States are paginated and can be iterated through by passing along a `page` +parameter: + +```text +GET /api/states?page=2``` + +As well as a `per_page` parameter to control how many results will be returned: + +```text +GET /api/states?per_page=100``` + +You can scope the states by country by passing along a `country_id` parameter +too: + +```text +GET /api/states?country_id=1``` + +### Response + +<%= headers 200 %> +<%= json(:state) do |h| +{ :states => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Show + +To find out about a single state, make a request like this: + +```text +GET /api/states/1``` + +### Response + +<%= headers 200 %> +<%= json(:state) %> diff --git a/guides/content/api/stock_items.md b/guides/content/api/stock_items.md new file mode 100644 index 00000000000..f5ce6dacc10 --- /dev/null +++ b/guides/content/api/stock_items.md @@ -0,0 +1,169 @@ +--- +title: Stock Items +description: Use the Spree Commerce storefront API to access StockItem data. +--- + +## Index + +<%= admin_only %> + +To return a paginated list of all stock items for a stock location, make this request, passing the stock location id you wish to see stock items for: + +```text +GET /api/stock_locations/1/stock_items``` + +### Parameters + +page +: The page number of stock items to display. + +per_page +: The number of stock items to return per page + +### Response + +<%= headers 200 %> +<%= json(:stock_item) do |h| +{ :stock_items => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +<%= admin_only %> + +To search for a particular stock item, make a request like this: + +```text +GET /api/stock_locations/1/stock_items?q[variant_id_eq]=10``` + +The searching API is provided through the Ransack gem which Spree depends on. The `variant_id_eq` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:stock_item) do |h| + { :stock_items => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/stock_locations/1/stock_items?q[s]=variant_id%20asc``` + +## Show + +<%= admin_only %> + +To view the details for a single stock item, make a request using that stock item's id, along with its `stock_location_id`: + +```text +GET /api/stock_locations/1/stock_items/2``` + +### Successful Response + +<%= headers 200 %> +<%= json :stock_item %> + +### Not Found Response + +<%= not_found %> + +## Create + +<%= admin_only %> + +To create a new stock item for a stock location, make this request with the necessary parameters: + +```text +POST /api/stock_locations/1/stock_items``` + +For instance, a request to create a new stock item with a count_on_hand of 10 and a variant_id of 1 would look like this:: + +<%= json \ + :stock_item => { + :count_on_hand => "10", + :variant_id => "1", + :backorderable => "true" + } %> + +### Successful response + +<%= headers 201 %> +<%= json(:stock_item) %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + } +%> + +## Update + +<%= admin_only %> + +Note that using this endpoint, count_on_hand is APPENDED to its current value. + +Sending a request with a negative count_on_hand will subtract the current value. + +To force a value for count_on_hand, include force: true in your request, this will replace the current +value as it's stored in the database. + +To update a stock item's details, make this request with the necessary parameters. + +```text +PUT /api/stock_locations/1/stock_items/2``` + +For instance, to update a stock item's count_on_hand, send it through like this: + +<%= json \ + :stock_item => { + :count_on_hand => "30", + } %> + +Or alternatively with the force attribute to replace the current count_on_hand with a new value: + +<%= json \ + :stock_item => { + :count_on_hand => "30", + :force => true, + } %> + +### Successful response + +<%= headers 201 %> +<%= json(:stock_item) %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + } +%> + +## Delete + +<%= admin_only %> + +To delete a stock item, make this request: + +```text +DELETE /api/stock_locations/1/stock_items/2``` + +### Response + +<%= headers 204 %> diff --git a/guides/content/api/stock_locations.md b/guides/content/api/stock_locations.md new file mode 100644 index 00000000000..a9475a6f2ac --- /dev/null +++ b/guides/content/api/stock_locations.md @@ -0,0 +1,132 @@ +--- +title: Stock Locations +description: Use the Spree Commerce storefront API to access StockLocation data. +--- + +## Index + +<%= admin_only %> + +To get a list of stock locations, make this request: + +```text +GET /api/stock_locations``` + +Stock locations are paginated and can be iterated through by passing along a `page` parameter: + +```text +GET /api/stock_locations?page=2``` + +### Parameters + +page +: The page number of stock location to display. + +per_page +: The number of stock locations to return per page + +### Response + +<%= headers 200 %> +<%= json(:stock_location) do |h| +{ :stock_locations => [h], + :count => 5, + :pages => 1, + :current_page => 1 } +end %> + +## Search + +<%= admin_only %> + +To search for a particular stock location, make a request like this: + +```text +GET /api/stock_locations?q[name_cont]=default``` + +The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:stock_location) do |h| +{ :stock_locations => [h], + :count => 5, + :pages => 1, + :current_page => 1 } +end %> + +## Show + +<%= admin_only %> + +To get information for a single stock location, make this request: + +```text +GET /api/stock_locations/1``` + +### Response + +<%= headers 200 %> +<%= json(:stock_location) %> + +## Create + +<%= admin_only %> + +To create a stock location, make a request like this: + +```text +POST /api/stock_locations``` + +Assuming in this instance that you want to create a stock location with a name of `East Coast`, send through the parameters like this: + +<%= json \ + :stock_location => { + :name => "East Coast", + :action => "true" + } %> + +### Response + +<%= headers 201 %> +<%= json(:stock_location) %> + +## Update + +<%= admin_only %> + +To update a stock location, make a request like this: + +```text +PUT /api/stock_locations/1``` + +To update stock location information, use parameters like this: + +<%= json \ + :stock_location => { + :name => "North Pole", + :action => "false" + } %> + +### Response + +<%= headers 200 %> +<%= json(:stock_location) %> + +## Delete + +<%= admin_only %> + +To delete a stock location, make a request like this: + +```text +DELETE /api/stock_locations/1``` + +This request will also delete any related `stock item` records. + +### Response + +<%= headers 204 %> diff --git a/guides/content/api/stock_movements.md b/guides/content/api/stock_movements.md new file mode 100644 index 00000000000..b9a1b301abe --- /dev/null +++ b/guides/content/api/stock_movements.md @@ -0,0 +1,154 @@ +--- +title: Stock Movements +description: Use the Spree Commerce storefront API to access StockMovement data. +--- + +## Index + +<%= admin_only %> + +To return a paginated list of all stock movements for a stock location, make this request, passing the stock location id you wish to see stock items for: + +```text +GET /api/stock_locations/1/stock_movements``` + +### Parameters + +page +: The page number of stock movements to display. + +per_page +: The number of stock movements to return per page + +### Response + +<%= headers 200 %> +<%= json(:stock_movement) do |h| +{ :stock_movements => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +<%= admin_only %> + +To search for a particular stock movement, make a request like this: + +```text +GET /api/stock_locations/1/stock_movements?q[quantity_eq]=10``` + +The searching API is provided through the Ransack gem which Spree depends on. The `quantity_eq` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:stock_movement) do |h| + { :stock_movements => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/stock_locations/1/stock_movements?q[s]=quantity%20asc``` + +## Show + +<%= admin_only %> + +To view the details for a single stock movement, make a request using that stock movement's id, along with its `stock_location_id`: + +```text +GET /api/stock_locations/1/stock_movements/1``` + +### Successful Response + +<%= headers 200 %> +<%= json :stock_movement %> + +### Not Found Response + +<%= not_found %> + +## Create + +<%= admin_only %> + +To create a new stock movement for a stock location, make this request with the necessary parameters: + +```text +POST /api/stock_locations/1/stock_movements``` + +For instance, a request to create a new stock movement with a quantity of 10, the action set to received, and a stock_item_id of 1 would look like this:: + +<%= json \ + :stock_movement => { + :quantity => "10", + :stock_item_id => "1", + :action => "received" + } %> + +### Successful response + +<%= headers 201 %> +<%= json(:stock_movement) %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + } +%> + +## Update + +<%= admin_only %> + +To update a stock movement's details, make this request with the necessary parameters: + +```text +PUT /api/stock_locations/1/stock_movements/1``` + +For instance, to update a stock movement's quantity, send it through like this: + +<%= json \ + :stock_movement => { + :quantity => "30", + } %> + +### Successful response + +<%= headers 201 %> +<%= json(:stock_movement) %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + } +%> + +## Delete + +<%= admin_only %> + +To delete a stock movement, make this request: + +```text +DELETE /api/stock_locations/1/stock_movement/1``` + +### Response + +<%= headers 204 %> diff --git a/guides/content/api/summary.md b/guides/content/api/summary.md new file mode 100644 index 00000000000..5417f836234 --- /dev/null +++ b/guides/content/api/summary.md @@ -0,0 +1,115 @@ +--- +title: Summary +--- + +## Overview + +Spree currently supports RESTful access to the resources listed in the sidebar +on the right » + +This API was built using the great [Rabl](https://github.com/nesquena/rabl) gem. +Please consult its documentation if you wish to understand how the templates use +it to return data. + +This API conforms to a set of [rules](#rules). + +### JSON Data + +Developers communicate with the Spree API using the [JSON](http://www.json.org) data format. Requests for data are communicated in the standard manner using the HTTP protocol. + +### Making an API Call + +You will need an authentication token to access the API. These keys can be generated on the user edit screen within the admin interface. To make a request to the API, pass a `X-Spree-Token` header along with the request: + +```bash +$ curl --header "X-Spree-Token: YOUR_KEY_HERE" http://example.com/api/products.json``` + + +Alternatively, you may also pass through the token as a parameter in the request if a header just won't suit your purposes (i.e. JavaScript console debugging). + +```bash +$ curl http://example.com/api/products.json?token=YOUR_KEY_HERE``` + +The token allows the request to assume the same level of permissions as the actual user to whom the token belongs. + +### Error Messages + +You may encounter the follow error messages when using the API. + +#### Not Found + +<%= not_found %> + +#### Authorization Failure + +<%= authorization_failure %> + +#### Invalid API Key + +<%= headers 401 %> +<%= json(:error => "Invalid API key ([key]) specified.") %> + +## Rules + +The following are some simple rules that all Spree API endpoints comply with. + +1. All successful requests for the API will return a status of 200. +2. Successful create and update requests will result in a status of 201 and 200 respectively. +3. Both create and update requests will return Spree\'s representation of the data upon success. +4. If a create or update request fails, a status code of 422 will be returned, with a hash containing an \"error\" key, and an \"errors\" key. The errors value will contain all ActiveRecord validation errors encountered when saving this record. +5. Delete requests will return status of 200, and no content. +6. Requests that list collections, such as /api/products will return a limited result set back. +7. Requests that list collections can be paginated through by passing a page parameter that is a number greater than 0. +8. If a resource can not be found, the API will return a status of 404. +9. Unauthorized requests will be met with a 401 response. + +## Customizing Responses + +If you wish to customize the responses from the API, you can do this in one of +two ways: overriding the template, or providing a custom template. + +### Overriding template + +Overriding a template for the API should be done if you want to *always* provide +a custom response for an API endpoint. Template loading in Rails will attempt to +look up a template within your application's view paths first. If it isn't +available there, then it will fallback to looking within the other engine's view +paths, eventually finding its way to the API engine. + +You can use this to your advantage and define a view template within your +application that exists at the same path as a template within the API engine. +For instance, if you place a template in your application at +`app/views/spree/api/products/show.v1.rabl`, it will take precedence over the +template within the API engine. + +This is the method we would recommend to *completely* override an API response. + +### Custom template + +If you don't want to always override the response for an API controller, you can +customize it in another way by creating an alternative template to use for some +API responses. + +To do this, create a template under the view directory of your targetted +resource. For instance, if you wanted to customize a response for one of the +actions within the `ProductsController` of the API, you would place the template +at `app/views/spree/api/products`. The template must be given a unique name that +won't conflict with any other templates; you could call it `small_show` for +instance. + +If you were to take this route, the new template file's path would be +`app/views/spree/api/products/small_show.v1.rabl`. The `v1` part of the filename +indicates that its a response for version 1 of the API, and the `rabl` on the +end is the markup language used. + +To use this new template for your API response, simply pass the `template` +parameter along with the request: +`http://example.com/api/products/1?template=small_show`. The API component of +Spree will then detect this parameter, find the template, and then use this to +render the response. + +*** +Due to [the way this implemented](https://github.com/spree/spree/blob/v2.3.1/api/lib/spree/api/responders/rabl_template.rb#L5-L18) +you need to ensure the action rendering in your custom template explicitly +calls `respond_with` +*** diff --git a/guides/content/api/taxonomies.md b/guides/content/api/taxonomies.md new file mode 100644 index 00000000000..7c8ffe84acf --- /dev/null +++ b/guides/content/api/taxonomies.md @@ -0,0 +1,203 @@ +--- +title: Taxonomies +description: Use the Spree Commerce storefront API to access Taxonomy data. +--- + +## Index + +To get a list of all the taxonomies, including their root nodes and the +immediate children for the root node, make a request like this: + +```text +GET /api/taxonomies``` + +### Parameters + +page +: The page number of taxonomy to display. + +per_page +: The number of taxonomies to return per page + +### Response + +<%= headers 200 %> +<%= json(:taxonomy) do |h| +{ :taxonomies => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +To search for a particular taxonomy, make a request like this: + +```text +GET /api/taxonomies?q[name_cont]=brand``` + +The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:taxonomy) do |h| + { :taxonomies => [h], + :count => 5, + :pages => 2, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/taxonomies?q[s]=name%20asc``` + +It is also possible to sort results using an associated object's field. + +```text +GET /api/taxonomies?q[s]=root_name%20desc``` + +## Show + +To get information for a single taxonomy, including its root node and the immediate children of the root node, make a request like this: + +```text +GET /api/taxonomies/1``` + +### Response + +<%= headers 200 %> +<%= json(:taxonomy) %> + +## Create + +<%= admin_only %> + +To create a taxonomy, make a request like this: + +```text +POST /api/taxonomies``` + +For instance, if you want to create a taxonomy with the name \"Brands\", make +this request: + +```text +POST /api/taxonomies?taxonomy[name]=Brand``` + +If you\'re creating a taxonomy without a root taxon, a root taxon will automatically be +created for you with the same name as the taxon. + +### Response + +<%= headers 201 %> +<%= json(:new_taxonomy) %> + +## Update + +<%= admin_only %> + +To update a taxonomy, make a request like this: + +```text +PUT /api/taxonomies/1``` + +For instance, to update a taxonomy\'s name, make this request: + +```text +PUT /api/taxonomies/1?taxonomy[name]=Brand``` + +### Response + +<%= headers 200 %> +<%= json(:taxonomy) %> + +## Delete + +<%= admin_only %> + +To delete a taxonomy, make a request like this: + +```text +DELETE /api/taxonomies/1``` + +### Response + +<%= headers 204 %> + +## List taxons + +To get a list for all taxons underneath the root taxon for a taxonomy (and their immediate children) for a taxonomy, make this request: + + GET /api/taxonomies/1/taxons + +### Response + +<%= headers 200 %> +<%= json(:taxon_with_children) { |h| [h] } %> + +## A single taxon + +To see information about a taxon and its immediate children, make a request +like this: + + GET /api/taxonomies/1/taxons/1 + +### Response + +<%= headers 200 %> +<%= json(:taxon_with_children) %> + + +## Taxon Create + +<%= admin_only %> + +To create a taxon, make a request like this: + + POST /api/taxonomies/1/taxons + +To create a new taxon with the name "Brands", make this request: + + POST /api/taxonomies/1/taxons?taxon[name]=Brands + +### Response + +<%= headers 201 %> +<%= json(:taxon_without_children) %> + + +## Taxon Update + +<%= admin_only %> + +To update a taxon, make a request like this: + + PUT /api/taxonomies/1/taxons/1 + +For example, to update the taxon's name to "Brand", make this request: + + PUT /api/taxonomies/1/taxons/1?taxon[name]=Brand + +### Response + +<%= headers 200 %> +<%= json(:taxon_with_children) %> + +## Taxon Delete + +<%= admin_only %> + +To delete a taxon, make a request like this: + + DELETE /api/taxonomies/1/taxons/1 + +<%= warning "This will cause all child taxons to be deleted as well." %> + +### Response + +<%= headers 204 %> diff --git a/guides/content/api/users.md b/guides/content/api/users.md new file mode 100644 index 00000000000..1623fbd1f90 --- /dev/null +++ b/guides/content/api/users.md @@ -0,0 +1,116 @@ +--- +title: Users +description: Use the Spree Commerce storefront API to access User data. +--- + +List users visible to the authenticated user. If the user is not an admin, +they will only be able to see their own user, unless they have custom +permissions to see other users. If the user is an admin then they can see all +users. + +```text +GET /api/users``` + +Users are paginated and can be iterated through by passing along a `page` +parameter: + +```text +GET /api/users?page=2``` + +### Response + +<%= headers 200 %> +<%= json(:user) do |h| + { :users => [h], :count => 25, :pages => 5, :current_page => 1 } +end %> + +## A single user + +To view the details for a single user, make a request using that user\'s +id: + +```text +GET /api/users/1``` + +### Successful Response + +<%= headers 200 %> <%= json :user %> + +### Not Found Response + +<%= not_found %> + +## Pre-creation of a user + +You can learn about the potential attributes (required and non-required) for a +user by making this request: + +```text GET /api/users/new``` + +### Response + +<%= headers 200 %> +<%= json :attributes => ["", ""], :required_attributes => [] %> + +## Creating a new new + +<%= admin_only %> + +To create a new user through the API, make this request with the necessary +parameters: + +```text +POST /api/users``` + +For instance, a request to create a new user with the email +\"spree@example.com\" and password \"password\" would look like this: + +```text +POST /api/users?user[email]=spree@example.com&user[password]=password``` + +### Successful response + +<%= headers 201 %> + +### Failed response + +<%= headers 422 %> +<%= json :error => "Invalid resource. Please fix errors and try again.", + :errors => { :email => ["can't be blank"] } %> + +## Updating a user + +<%= admin_only %> + +To update a user\'s details, make this request with the necessary parameters: + +```text +PUT /api/users/1``` + +For instance, to update a user\'s password, send it through like this: + +```text PUT /api/users/1?user[password]=password``` + +### Successful response + +<%= headers 201 %> + +### Failed response + +<%= headers 422 %> +<%= json :error => "Invalid resource. Please fix errors and try again.", + :errors => { :email => ["can't be blank"] } %> + +## Deleting a user + +<%= admin_only %> + +To delete a user, make this request: + +```text +DELETE /api/users/1``` + +### Response + +<%= headers 204 %> + diff --git a/guides/content/api/variants.md b/guides/content/api/variants.md new file mode 100644 index 00000000000..d666aa3dc2f --- /dev/null +++ b/guides/content/api/variants.md @@ -0,0 +1,193 @@ +--- +title: Variants +description: Use the Spree Commerce storefront API to access Variant data. +--- + +## Index + +To return a paginated list of all variants within the store, make this request: + +```text +GET /api/variants``` + +You can limit this to showing the variants for a particular product by passing through a product's permalink: + +```text +GET /api/products/ruby-on-rails-tote/variants``` + +or + +```text +GET /api/variants?product_id=ruby-on-rails-tote``` + +### Parameters + +show_deleted +: **boolean** - `true` to show deleted variants, `false` to hide them. Default: `false`. **Only available to users with an admin role.** + +page +: The page number of variants to display. + +per_page +: The number of variants to return per page + +### Response + +<%= headers 200 %> +<%= json(:variant) do |h| +{ :variants => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +To search for a particular variant, make a request like this: + +```text +GET /api/variants?q[sku_cont]=foo``` + +You can limit this to showing the variants for a particular product by passing through a product id: + +```text +GET /api/products/ruby-on-rails-tote/variants?q[sku_cont]=foo``` + +or + +```text +GET /api/variants?product_id=ruby-on-rails-tote&q[sku_cont]=foo``` + + +The searching API is provided through the Ransack gem which Spree depends on. The `sku_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:variant) do |h| + { :variants => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/variants?q[s]=price%20asc``` + +It is also possible to sort results using an associated object's field. + +```text +GET /api/variants?q[s]=product_name%20asc``` + +## Show + +To view the details for a single variant, make a request using that variant\'s id, along with the product's permalink as its `product_id`: + +```text +GET /api/products/ruby-on-rails-tote/variants/1``` + +Or: + +```text +GET /api/variants/1?product_id=ruby-on-rails-tote``` + +### Successful Response + +<%= headers 200 %> +<%= json :variant %> + +### Not Found Response + +<%= not_found %> + +## New + +You can learn about the potential attributes (required and non-required) for a variant by making this request: + +```text +GET /api/products/ruby-on-rails-tote/variants/new``` + +### Response + +<%= headers 200 %> +<%= json \ + :attributes => [ + :id, :name, :count_on_hand, :sku, :price, :weight, :height, + :width, :depth, :is_master, :cost_price, :permalink + ], + :required_attributes => [] + %> + +## Create + +<%= admin_only %> + +To create a new variant for a product, make this request with the necessary parameters: + +```text +POST /api/products/ruby-on-rails-tote/variants``` + +For instance, a request to create a new variant with a SKU of 12345 and a price of 19.99 would look like this:: + +```text +POST /api/products/ruby-on-rails-tote/variants/?variant[sku]=12345&variant[price]=19.99``` + +### Successful response + +<%= headers 201 %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + } +%> + +## Update + +<%= admin_only %> + +To update a variant\'s details, make this request with the necessary parameters: + +```text +PUT /api/products/ruby-on-rails-tote/variants/2``` + +For instance, to update a variant\'s SKU, send it through like this: + +```text +PUT /api/products/ruby-on-rails-tote/variants/2?variant[sku]=12345``` + +### Successful response + +<%= headers 201 %> + +### Failed response + +<%= headers 422 %> +<%= json \ + :error => "Invalid resource. Please fix errors and try again.", + :errors => { + } +%> + +## Delete + +<%= admin_only %> + +To delete a variant, make this request: + +```text +DELETE /api/products/ruby-on-rails-tote/variants/2``` + +This request, much like a typical variant \"deletion\" through the admin interface, will not actually remove the record from the database. It simply sets the `deleted_at` field to the current time on the variant. + +<%= headers 204 %> + diff --git a/guides/content/api/zones.md b/guides/content/api/zones.md new file mode 100644 index 00000000000..d74d703bc65 --- /dev/null +++ b/guides/content/api/zones.md @@ -0,0 +1,145 @@ +--- +title: Zones +description: Use the Spree Commerce storefront API to access Zone data. +--- + +## Index + +To get a list of zones, make this request: + +```text +GET /api/zones``` + +Zones are paginated and can be iterated through by passing along a `page` parameter: + +```text +GET /api/zones?page=2``` + +### Parameters + +page +: The page number of zone to display. + +per_page +: The number of zones to return per page + +### Response + +<%= headers 200 %> +<%= json(:zone) do |h| +{ :zones => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +## Search + +To search for a particular zone, make a request like this: + +```text +GET /api/zones?q[name_cont]=north``` + +The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching). + +The search results are paginated. + +### Response + +<%= headers 200 %> +<%= json(:zone) do |h| + { :zones => [h], + :count => 25, + :pages => 5, + :current_page => 1 } +end %> + +### Sorting results + +Results can be returned in a specific order by specifying which field to sort by when making a request. + +```text +GET /api/zones?q[s]=name%20desc``` + +## Show + +To get information for a single zone, make this request: + +```text +GET /api/zones/1``` + +### Response + +<%= headers 200 %> +<%= json(:zone) %> + +## Create + +<%= admin_only %> + +To create a zone, make a request like this: + +```text +POST /api/zones``` + +Assuming in this instance that you want to create a zone containing +a zone member which is a `Spree::Country` record with the `id` attribute of 1, send through the parameters like this: + +<%= json \ + :zone => { + :name => "North Pole", + :zone_members => [ + { + :zoneable_type => "Spree::Country", + :zoneable_id => 1 + } + ] + } %> + +### Response + +<%= headers 201 %> +<%= json(:zone) %> + +## Update + +<%= admin_only %> + +To update a zone, make a request like this: + +```text +PUT /api/zones/1``` + +To update zone and zone member information, use parameters like this: + +<%= json \ + :id => 1, + :zone => { + :name => "North Pole", + :zone_members => [ + { + :zoneable_type => "Spree::Country", + :zoneable_id => 1 + } + ] + } %> + +### Response + +<%= headers 200 %> +<%= json(:zone) %> + +## Delete + +<%= admin_only %> + +To delete a zone, make a request like this: + +```text +DELETE /api/zones/1``` + +This request will also delete any related `zone_member` records. + +### Response + +<%= headers 204 %> diff --git a/guides/content/assets/javascripts/api-objects.js b/guides/content/assets/javascripts/api-objects.js new file mode 100644 index 00000000000..513f03bcd09 --- /dev/null +++ b/guides/content/assets/javascripts/api-objects.js @@ -0,0 +1,189 @@ +window.onload = function() { + + // Polygon drawing + function polygon(x, y, size, sides, rotate) { + var self = this; + + self.centrePoint = [x,y]; + self.size = size; + self.sides = sides; + self.rotated = rotate; + self.sizeMultiplier = 50; + self.points = []; + + for (i = 0; i < sides; i++) { + self.points.push([( + x + + (self.size * self.sizeMultiplier) * + (rotate ? + Math.sin(2 * 3.14159265 * i / sides) : + Math.cos(2 * 3.14159265 * i / sides) + ) + ), + ( + y + + (self.size * self.sizeMultiplier) * + (rotate ? + Math.cos(2 * 3.14159265 * i / sides) : + Math.sin(2 * 3.14159265 * i / sides) + ) + ) + ]); + } + + self.svgString = 'M' + self.points.join(' ') + ' L Z'; + } + + // Canvas + var canvas = new Raphael(document.getElementById('api-objects'), 960, 400); + + // Hexagon attrinutes on hover out & default state + var h_attr_out = { + stroke: "#9FBBEA", + "stroke-width": "2", + fill: "white", + "fill-opacity": 0.2 + } + + // Hexagon attrinutes on hover in state + var h_attr_in = { + fill: "#78AD2F", + "fill-opacity": '1', + stroke: '#fff' + } + + // Text attrinutes on hover out & default state + var t_attr_out = { + fill: "white", + "font-size": "13px", + "font-family": "Source Code Pro", + "fill-opacity": 0.5 + } + + // Text attrinutes on hover in state + var t_attr_in = { + "fill-opacity": 1 + } + + // Text on hover in animation + var t_animate_in = function(this_object, hexagon_object) { + hexagon_object.animate(h_attr_in, 200); + this_object.animate(t_attr_in, 200); + } + + // Text on hover out animation + var t_animate_out = function(this_object, hexagon_object) { + hexagon_object.animate(h_attr_out, 200); + this_object.animate(t_attr_out, 200); + } + + // Hexagon on hover in animation + var h_animate_in = function(this_object, text_object) { + this_object.animate(h_attr_in, 200); + text_object.animate(t_attr_in, 200); + } + + // Hexagon on hover out animation + var h_animate_out = function(this_object, text_object) { + this_object.animate(h_attr_out, 200); + text_object.animate(t_attr_out, 200); + } + + // API Hexagon object + function h_object (id, pos_x, pos_y, href, t_object_id) { + var self = this; + self.id = id; + self.pos_x = pos_x; + self.pos_y = pos_y; + self.href = href; + self.t_object_id = t_object_id; + + var path = canvas.path( + new polygon(self.pos_x, self.pos_y, 1.05, 6, 90).svgString + ); + + path.id = self.id; + + path.data("pos_x", self.pos_x); + path.data("pos_y", self.pos_y); + + path.attr(h_attr_out); + path.attr("href", self.href); + + path.hover(function(){ + h_animate_in(path, canvas.getById(self.t_object_id)) + }, function(){ + h_animate_out(path, canvas.getById(self.t_object_id)) + }) + } + + // API Hexagon Text object + function t_object (id, text, h_object_id) { + var self = this; + self.id = id; + self.text = text; + self.h_object_id = h_object_id; + self.pos_x = canvas.getById(self.h_object_id).data("pos_x"); + self.pos_y = canvas.getById(self.h_object_id).data("pos_y"); + + var path = canvas.text(self.pos_x, self.pos_y, self.text) + + path.id = self.id; + + path.data("pos_x", self.pos_x); + path.data("pos_y", self.pos_y); + + path.attr(t_attr_out) + path.attr("href", canvas.getById(self.h_object_id).attr("href")) + + path.hover(function(){ + t_animate_in(path, canvas.getById(self.h_object_id)); + }, function(){ + t_animate_out(path, canvas.getById(self.h_object_id)); + }) + } + + // Creatring api objects on canvas + var line_items = new h_object("h_line_items", 57, 74, "line_items.html", "t_line_items") + var line_items_text = new t_object("t_line_items", "LINE ITEMS", "h_line_items") + + var return_auth = new h_object("h_return_auth", 171, 74, "return_authorizations.html", "t_return_auth") + var return_auth_text = new t_object("t_return_auth", "RETURN\nAUTHORI...", "h_return_auth") + + var orders = new h_object("h_orders", 57, 201, "orders.html", "t_orders") + var orders_text = new t_object("t_orders", "ORDERS", "h_orders") + + var payments = new h_object("h_payments", 171, 200, "payments.html", "t_payments") + var payments_text = new t_object("t_payments", "PAYMENTS", "h_payments") + + var shipments = new h_object("h_shipments", 57, 327, "shipments.html", "t_shipments") + var shipments_text = new t_object("t_shipments", "SHIPMENTS", "h_shipments") + + var product_properties = new h_object("h_product_properties", 678, 200, "product_properties.html", "t_product_properties") + var product_properties_text = new t_object("t_product_properties", "PRODUCT\nPROPERTIES", "h_product_properties") + + var variants = new h_object("h_variants", 792, 74, "variants.html", "t_variants") + var variants_text = new t_object("t_variants", "VARIANTS", "h_variants") + + var images = new h_object("h_images", 906, 74, "#", "t_images") + var images_text = new t_object("t_images", "IMAGES", "h_images") + + var products = new h_object("h_products", 792, 200, "products.html", "t_products") + var products_text = new t_object("t_products", "PRODUCTS", "h_products") + + var taxons = new h_object("h_taxons", 792, 327, "#", "t_taxons") + var taxons_text = new t_object("t_taxons", "TAXONS", "h_taxons") + + var taxonomies = new h_object("h_taxonomies", 906, 327, "taxonomies.html", "t_taxonomies") + var taxonomies_text = new t_object("t_taxonomies", "TAXONOMIES", "h_taxonomies") + + var zones = new h_object("h_zones", 366, 327, "zones.html", "t_zones") + var zones_text = new t_object("t_zones", "ZONES", "h_zones") + + var countries = new h_object("h_countries", 477, 327, "countries.html", "t_countries") + var countries_text = new t_object("t_countries", "COUNTRIES", "h_countries") + + var addresses = new h_object("h_addresses", 590, 327, "addresses.html", "t_addresses") + var addresses_text = new t_object("t_addresses", "ADDRESSES", "h_addresses") + +} \ No newline at end of file diff --git a/core/vendor/assets/javascripts/css_browser_selector_dev.js b/guides/content/assets/javascripts/css_browser_selector_dev.js similarity index 100% rename from core/vendor/assets/javascripts/css_browser_selector_dev.js rename to guides/content/assets/javascripts/css_browser_selector_dev.js diff --git a/guides/content/assets/javascripts/documentation.js b/guides/content/assets/javascripts/documentation.js new file mode 100644 index 00000000000..65d857e2e66 --- /dev/null +++ b/guides/content/assets/javascripts/documentation.js @@ -0,0 +1,85 @@ +// Init sidebar +$(function() { + + // Add anchor links to headers + $("#content").find('h2').each(function(){ + $(this).prepend(" ") + }); + $("#content").find('h3').each(function(){ + $(this).prepend(" ") + }); + $("#content").find('h4').each(function(){ + $(this).prepend(" ") + }); + + // Sidebar menu + var sidebar_menu = $("#sidebar-menu") + + sidebar_menu.find('h3 a.js-expand-btn').click(function(e){ + e.preventDefault(); + var icon = $(this).find('i'); + + if(icon.attr('class') == 'icon-right-open'){ + icon.removeClass('icon-right-open').addClass('icon-down-open'); + sidebar_menu.find('a.active').removeClass('active'); + icon.parent().next().addClass('active'); + icon.parent().parent().next().stop().slideDown(); + } + else{ + icon.removeClass('icon-down-open').addClass('icon-right-open'); + sidebar_menu.find('a.active').removeClass('active'); + icon.parent().parent().next().stop().slideUp(); + } + }); + + sidebar_menu.find('.js-guides li i').on('click', function(){ + if($(this).parent().find('ul:hidden').length > 0){ + $(this).removeClass('icon-right-dir').addClass('icon-down-dir'); + $(this).parent().find('ul').stop().slideDown(); + $(this).parent().removeClass('closed').addClass('opened'); + } + else if($(this).parent().find('ul:visible')) { + $(this).removeClass('icon-down-dir').addClass('icon-right-dir'); + $(this).parent().find('ul').stop().slideUp(); + $(this).parent().removeClass('opened').addClass('closed'); + } + }); + + var current_url = window.location.pathname.split('/')[2]; + var active_menu = sidebar_menu.find('a[href="'+current_url+'"]') + + active_menu.addClass('active-open'); + if(active_menu.parent().next().attr('class') == 'js-guides'){ + active_menu.parent().next().show(); + } + else { + active_menu.parent().parent().show() + } + + // TOC + var current_url = window.location.pathname.split('/')[2]; + var active = sidebar_menu.find('a[href="'+current_url+'"]'); + var toc = active.parent().find('.toc'); + active.parent().addClass('current'); + // if(active.prev().hasClass('icon-dot')){ + // active.prev().removeClass('icon-dot').addClass('icon-down-dir'); + // } + toc.toc({ + 'container': '#content', + 'anchorName': function(i, heading, prefix) { //custom function for anchor name + return $(heading).attr('id'); + }, + 'smoothScrolling': false + }); + + // $('#sidebar-menu').waypoint('sticky', { + // handler: function(){ + + // }, + // offset: -45 + // }); + + // Automatically open sidebar menu depending on section page belongs to + var current_section = $('meta[name=section]').attr('content'); + $('.toggle-' + current_section + '-menu i').click(); +}); diff --git a/guides/content/assets/javascripts/jquery.toc.js b/guides/content/assets/javascripts/jquery.toc.js new file mode 100644 index 00000000000..9fe50553dfc --- /dev/null +++ b/guides/content/assets/javascripts/jquery.toc.js @@ -0,0 +1,107 @@ +/*! + * toc - jQuery Table of Contents Plugin + * v0.1.2 + * http://projects.jga.me/toc/ + * copyright Greg Allen 2013 + * MIT License +*/ +(function($) { +$.fn.toc = function(options) { + var self = this; + var opts = $.extend({}, jQuery.fn.toc.defaults, options); + + var container = $(opts.container); + var headings = $(opts.selectors, container); + var headingOffsets = []; + var activeClassName = opts.prefix+'-active'; + + var scrollTo = function(e) { + if (opts.smoothScrolling) { + e.preventDefault(); + var elScrollTo = $(e.target).attr('href'); + var $el = $(elScrollTo); + + $('body,html').animate({ scrollTop: $el.offset().top }, 400, 'swing', function() { + location.hash = elScrollTo; + }); + } + $('li', self).removeClass(activeClassName); + $(e.target).parent().addClass(activeClassName); + }; + + //highlight on scroll + var timeout; + var highlightOnScroll = function(e) { + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(function() { + var top = $(window).scrollTop(), + highlighted; + for (var i = 0, c = headingOffsets.length; i < c; i++) { + if (headingOffsets[i] >= top) { + $('li', self).removeClass(activeClassName); + highlighted = $('li:eq('+(i-1)+')', self).addClass(activeClassName); + opts.onHighlight(highlighted); + break; + } + } + }, 50); + }; + if (opts.highlightOnScroll) { + $(window).bind('scroll', highlightOnScroll); + highlightOnScroll(); + } + + return this.each(function() { + //build TOC + var el = $(this); + var ul = $('
          '); + headings.each(function(i, heading) { + var $h = $(heading); + headingOffsets.push($h.offset().top - opts.highlightOffset); + + //add anchor + var anchor = $('').attr('id', opts.anchorName(i, heading, opts.prefix)).insertBefore($h); + + //build TOC item + var a = $('') + .text(opts.headerText(i, heading, $h)) + .attr('href', '#' + opts.anchorName(i, heading, opts.prefix)) + .bind('click', function(e) { + scrollTo(e); + el.trigger('selected', $(this).attr('href')); + }); + + var li = $('
        • ') + .addClass(opts.itemClass(i, heading, $h, opts.prefix)) + .append(a); + + ul.append(li); + }); + el.html(ul); + }); +}; + + +jQuery.fn.toc.defaults = { + container: 'body', + selectors: 'h1,h2,h3', + smoothScrolling: true, + prefix: 'toc', + onHighlight: function() {}, + highlightOnScroll: true, + highlightOffset: 100, + anchorName: function(i, heading, prefix) { + return prefix+i; + }, + headerText: function(i, heading, $heading) { + return $heading.text(); + }, + itemClass: function(i, heading, $heading, prefix) { + return prefix + '-' + $heading[0].tagName.toLowerCase(); + } + +}; + +})(jQuery); diff --git a/guides/content/assets/javascripts/raphael-min.js b/guides/content/assets/javascripts/raphael-min.js new file mode 100644 index 00000000000..d30dbad858f --- /dev/null +++ b/guides/content/assets/javascripts/raphael-min.js @@ -0,0 +1,10 @@ +// ┌────────────────────────────────────────────────────────────────────┐ \\ +// │ Raphaël 2.1.0 - JavaScript Vector Library │ \\ +// ├────────────────────────────────────────────────────────────────────┤ \\ +// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ +// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\ +// ├────────────────────────────────────────────────────────────────────┤ \\ +// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\ +// └────────────────────────────────────────────────────────────────────┘ \\ + +(function(a){var b="0.3.4",c="hasOwnProperty",d=/[\.\/]/,e="*",f=function(){},g=function(a,b){return a-b},h,i,j={n:{}},k=function(a,b){var c=j,d=i,e=Array.prototype.slice.call(arguments,2),f=k.listeners(a),l=0,m=!1,n,o=[],p={},q=[],r=h,s=[];h=a,i=0;for(var t=0,u=f.length;tf*b.top){e=b.percents[y],p=b.percents[y-1]||0,t=t/b.top*(e-p),o=b.percents[y+1],j=b.anim[e];break}f&&d.attr(b.anim[b.percents[y]])}if(!!j){if(!k){for(var A in j)if(j[g](A))if(U[g](A)||d.paper.customAttributes[g](A)){u[A]=d.attr(A),u[A]==null&&(u[A]=T[A]),v[A]=j[A];switch(U[A]){case C:w[A]=(v[A]-u[A])/t;break;case"colour":u[A]=a.getRGB(u[A]);var B=a.getRGB(v[A]);w[A]={r:(B.r-u[A].r)/t,g:(B.g-u[A].g)/t,b:(B.b-u[A].b)/t};break;case"path":var D=bR(u[A],v[A]),E=D[1];u[A]=D[0],w[A]=[];for(y=0,z=u[A].length;yd)return d;while(cf?c=e:d=e,e=(d-c)/2+c}return e}function n(a,b){var c=o(a,b);return((l*c+k)*c+j)*c}function m(a){return((i*a+h)*a+g)*a}var g=3*b,h=3*(d-b)-g,i=1-g-h,j=3*c,k=3*(e-c)-j,l=1-j-k;return n(a,1/(200*f))}function cq(){return this.x+q+this.y+q+this.width+" × "+this.height}function cp(){return this.x+q+this.y}function cb(a,b,c,d,e,f){a!=null?(this.a=+a,this.b=+b,this.c=+c,this.d=+d,this.e=+e,this.f=+f):(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0)}function bH(b,c,d){b=a._path2curve(b),c=a._path2curve(c);var e,f,g,h,i,j,k,l,m,n,o=d?0:[];for(var p=0,q=b.length;p=0&&y<=1&&A>=0&&A<=1&&(d?n++:n.push({x:x.x,y:x.y,t1:y,t2:A}))}}return n}function bF(a,b){return bG(a,b,1)}function bE(a,b){return bG(a,b)}function bD(a,b,c,d,e,f,g,h){if(!(x(a,c)x(e,g)||x(b,d)x(f,h))){var i=(a*d-b*c)*(e-g)-(a-c)*(e*h-f*g),j=(a*d-b*c)*(f-h)-(b-d)*(e*h-f*g),k=(a-c)*(f-h)-(b-d)*(e-g);if(!k)return;var l=i/k,m=j/k,n=+l.toFixed(2),o=+m.toFixed(2);if(n<+y(a,c).toFixed(2)||n>+x(a,c).toFixed(2)||n<+y(e,g).toFixed(2)||n>+x(e,g).toFixed(2)||o<+y(b,d).toFixed(2)||o>+x(b,d).toFixed(2)||o<+y(f,h).toFixed(2)||o>+x(f,h).toFixed(2))return;return{x:l,y:m}}}function bC(a,b,c,d,e,f,g,h,i){if(!(i<0||bB(a,b,c,d,e,f,g,h)n)k/=2,l+=(m1?1:i<0?0:i;var j=i/2,k=12,l=[-0.1252,.1252,-0.3678,.3678,-0.5873,.5873,-0.7699,.7699,-0.9041,.9041,-0.9816,.9816],m=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],n=0;for(var o=0;od;d+=2){var f=[{x:+a[d-2],y:+a[d-1]},{x:+a[d],y:+a[d+1]},{x:+a[d+2],y:+a[d+3]},{x:+a[d+4],y:+a[d+5]}];b?d?e-4==d?f[3]={x:+a[0],y:+a[1]}:e-2==d&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[e-2],y:+a[e-1]}:e-4==d?f[3]=f[2]:d||(f[0]={x:+a[d],y:+a[d+1]}),c.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return c}function bx(){return this.hex}function bv(a,b,c){function d(){var e=Array.prototype.slice.call(arguments,0),f=e.join("␀"),h=d.cache=d.cache||{},i=d.count=d.count||[];if(h[g](f)){bu(i,f);return c?c(h[f]):h[f]}i.length>=1e3&&delete h[i.shift()],i.push(f),h[f]=a[m](b,e);return c?c(h[f]):h[f]}return d}function bu(a,b){for(var c=0,d=a.length;c',bl=bk.firstChild,bl.style.behavior="url(#default#VML)";if(!bl||typeof bl.adj!="object")return a.type=p;bk=null}a.svg=!(a.vml=a.type=="VML"),a._Paper=j,a.fn=k=j.prototype=a.prototype,a._id=0,a._oid=0,a.is=function(a,b){b=v.call(b);if(b=="finite")return!M[g](+a);if(b=="array")return a instanceof Array;return b=="null"&&a===null||b==typeof a&&a!==null||b=="object"&&a===Object(a)||b=="array"&&Array.isArray&&Array.isArray(a)||H.call(a).slice(8,-1).toLowerCase()==b},a.angle=function(b,c,d,e,f,g){if(f==null){var h=b-d,i=c-e;if(!h&&!i)return 0;return(180+w.atan2(-i,-h)*180/B+360)%360}return a.angle(b,c,f,g)-a.angle(d,e,f,g)},a.rad=function(a){return a%360*B/180},a.deg=function(a){return a*180/B%360},a.snapTo=function(b,c,d){d=a.is(d,"finite")?d:10;if(a.is(b,E)){var e=b.length;while(e--)if(z(b[e]-c)<=d)return b[e]}else{b=+b;var f=c%b;if(fb-d)return c-f+b}return c};var bn=a.createUUID=function(a,b){return function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(a,b).toUpperCase()}}(/[xy]/g,function(a){var b=w.random()*16|0,c=a=="x"?b:b&3|8;return c.toString(16)});a.setWindow=function(b){eve("raphael.setWindow",a,h.win,b),h.win=b,h.doc=h.win.document,a._engine.initWin&&a._engine.initWin(h.win)};var bo=function(b){if(a.vml){var c=/^\s+|\s+$/g,d;try{var e=new ActiveXObject("htmlfile");e.write(""),e.close(),d=e.body}catch(f){d=createPopup().document.body}var g=d.createTextRange();bo=bv(function(a){try{d.style.color=r(a).replace(c,p);var b=g.queryCommandValue("ForeColor");b=(b&255)<<16|b&65280|(b&16711680)>>>16;return"#"+("000000"+b.toString(16)).slice(-6)}catch(e){return"none"}})}else{var i=h.doc.createElement("i");i.title="Raphaël Colour Picker",i.style.display="none",h.doc.body.appendChild(i),bo=bv(function(a){i.style.color=a;return h.doc.defaultView.getComputedStyle(i,p).getPropertyValue("color")})}return bo(b)},bp=function(){return"hsb("+[this.h,this.s,this.b]+")"},bq=function(){return"hsl("+[this.h,this.s,this.l]+")"},br=function(){return this.hex},bs=function(b,c,d){c==null&&a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b&&(d=b.b,c=b.g,b=b.r);if(c==null&&a.is(b,D)){var e=a.getRGB(b);b=e.r,c=e.g,d=e.b}if(b>1||c>1||d>1)b/=255,c/=255,d/=255;return[b,c,d]},bt=function(b,c,d,e){b*=255,c*=255,d*=255;var f={r:b,g:c,b:d,hex:a.rgb(b,c,d),toString:br};a.is(e,"finite")&&(f.opacity=e);return f};a.color=function(b){var c;a.is(b,"object")&&"h"in b&&"s"in b&&"b"in b?(c=a.hsb2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):a.is(b,"object")&&"h"in b&&"s"in b&&"l"in b?(c=a.hsl2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):(a.is(b,"string")&&(b=a.getRGB(b)),a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b?(c=a.rgb2hsl(b),b.h=c.h,b.s=c.s,b.l=c.l,c=a.rgb2hsb(b),b.v=c.b):(b={hex:"none"},b.r=b.g=b.b=b.h=b.s=b.v=b.l=-1)),b.toString=br;return b},a.hsb2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"b"in a&&(c=a.b,b=a.s,a=a.h,d=a.o),a*=360;var e,f,g,h,i;a=a%360/60,i=c*b,h=i*(1-z(a%2-1)),e=f=g=c-i,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return bt(e,f,g,d)},a.hsl2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"l"in a&&(c=a.l,b=a.s,a=a.h);if(a>1||b>1||c>1)a/=360,b/=100,c/=100;a*=360;var e,f,g,h,i;a=a%360/60,i=2*b*(c<.5?c:1-c),h=i*(1-z(a%2-1)),e=f=g=c-i/2,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return bt(e,f,g,d)},a.rgb2hsb=function(a,b,c){c=bs(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g;f=x(a,b,c),g=f-y(a,b,c),d=g==0?null:f==a?(b-c)/g:f==b?(c-a)/g+2:(a-b)/g+4,d=(d+360)%6*60/360,e=g==0?0:g/f;return{h:d,s:e,b:f,toString:bp}},a.rgb2hsl=function(a,b,c){c=bs(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g,h,i;g=x(a,b,c),h=y(a,b,c),i=g-h,d=i==0?null:g==a?(b-c)/i:g==b?(c-a)/i+2:(a-b)/i+4,d=(d+360)%6*60/360,f=(g+h)/2,e=i==0?0:f<.5?i/(2*f):i/(2-2*f);return{h:d,s:e,l:f,toString:bq}},a._path2string=function(){return this.join(",").replace(Y,"$1")};var bw=a._preload=function(a,b){var c=h.doc.createElement("img");c.style.cssText="position:absolute;left:-9999em;top:-9999em",c.onload=function(){b.call(this),this.onload=null,h.doc.body.removeChild(this)},c.onerror=function(){h.doc.body.removeChild(this)},h.doc.body.appendChild(c),c.src=a};a.getRGB=bv(function(b){if(!b||!!((b=r(b)).indexOf("-")+1))return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bx};if(b=="none")return{r:-1,g:-1,b:-1,hex:"none",toString:bx};!X[g](b.toLowerCase().substring(0,2))&&b.charAt()!="#"&&(b=bo(b));var c,d,e,f,h,i,j,k=b.match(L);if(k){k[2]&&(f=R(k[2].substring(5),16),e=R(k[2].substring(3,5),16),d=R(k[2].substring(1,3),16)),k[3]&&(f=R((i=k[3].charAt(3))+i,16),e=R((i=k[3].charAt(2))+i,16),d=R((i=k[3].charAt(1))+i,16)),k[4]&&(j=k[4][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),k[1].toLowerCase().slice(0,4)=="rgba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100));if(k[5]){j=k[5][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsb2rgb(d,e,f,h)}if(k[6]){j=k[6][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsla"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsl2rgb(d,e,f,h)}k={r:d,g:e,b:f,toString:bx},k.hex="#"+(16777216|f|e<<8|d<<16).toString(16).slice(1),a.is(h,"finite")&&(k.opacity=h);return k}return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bx}},a),a.hsb=bv(function(b,c,d){return a.hsb2rgb(b,c,d).hex}),a.hsl=bv(function(b,c,d){return a.hsl2rgb(b,c,d).hex}),a.rgb=bv(function(a,b,c){return"#"+(16777216|c|b<<8|a<<16).toString(16).slice(1)}),a.getColor=function(a){var b=this.getColor.start=this.getColor.start||{h:0,s:1,b:a||.75},c=this.hsb2rgb(b.h,b.s,b.b);b.h+=.075,b.h>1&&(b.h=0,b.s-=.2,b.s<=0&&(this.getColor.start={h:0,s:1,b:b.b}));return c.hex},a.getColor.reset=function(){delete this.start},a.parsePathString=function(b){if(!b)return null;var c=bz(b);if(c.arr)return bJ(c.arr);var d={a:7,c:6,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,z:0},e=[];a.is(b,E)&&a.is(b[0],E)&&(e=bJ(b)),e.length||r(b).replace(Z,function(a,b,c){var f=[],g=b.toLowerCase();c.replace(_,function(a,b){b&&f.push(+b)}),g=="m"&&f.length>2&&(e.push([b][n](f.splice(0,2))),g="l",b=b=="m"?"l":"L");if(g=="r")e.push([b][n](f));else while(f.length>=d[g]){e.push([b][n](f.splice(0,d[g])));if(!d[g])break}}),e.toString=a._path2string,c.arr=bJ(e);return e},a.parseTransformString=bv(function(b){if(!b)return null;var c={r:3,s:4,t:2,m:6},d=[];a.is(b,E)&&a.is(b[0],E)&&(d=bJ(b)),d.length||r(b).replace($,function(a,b,c){var e=[],f=v.call(b);c.replace(_,function(a,b){b&&e.push(+b)}),d.push([b][n](e))}),d.toString=a._path2string;return d});var bz=function(a){var b=bz.ps=bz.ps||{};b[a]?b[a].sleep=100:b[a]={sleep:100},setTimeout(function(){for(var c in b)b[g](c)&&c!=a&&(b[c].sleep--,!b[c].sleep&&delete b[c])});return b[a]};a.findDotsAtSegment=function(a,b,c,d,e,f,g,h,i){var j=1-i,k=A(j,3),l=A(j,2),m=i*i,n=m*i,o=k*a+l*3*i*c+j*3*i*i*e+n*g,p=k*b+l*3*i*d+j*3*i*i*f+n*h,q=a+2*i*(c-a)+m*(e-2*c+a),r=b+2*i*(d-b)+m*(f-2*d+b),s=c+2*i*(e-c)+m*(g-2*e+c),t=d+2*i*(f-d)+m*(h-2*f+d),u=j*a+i*c,v=j*b+i*d,x=j*e+i*g,y=j*f+i*h,z=90-w.atan2(q-s,r-t)*180/B;(q>s||r=a.x&&b<=a.x2&&c>=a.y&&c<=a.y2},a.isBBoxIntersect=function(b,c){var d=a.isPointInsideBBox;return d(c,b.x,b.y)||d(c,b.x2,b.y)||d(c,b.x,b.y2)||d(c,b.x2,b.y2)||d(b,c.x,c.y)||d(b,c.x2,c.y)||d(b,c.x,c.y2)||d(b,c.x2,c.y2)||(b.xc.x||c.xb.x)&&(b.yc.y||c.yb.y)},a.pathIntersection=function(a,b){return bH(a,b)},a.pathIntersectionNumber=function(a,b){return bH(a,b,1)},a.isPointInsidePath=function(b,c,d){var e=a.pathBBox(b);return a.isPointInsideBBox(e,c,d)&&bH(b,[["M",c,d],["H",e.x2+10]],1)%2==1},a._removedFactory=function(a){return function(){eve("raphael.log",null,"Raphaël: you are calling to method “"+a+"” of removed object",a)}};var bI=a.pathBBox=function(a){var b=bz(a);if(b.bbox)return b.bbox;if(!a)return{x:0,y:0,width:0,height:0,x2:0,y2:0};a=bR(a);var c=0,d=0,e=[],f=[],g;for(var h=0,i=a.length;h1&&(v=w.sqrt(v),c=v*c,d=v*d);var x=c*c,y=d*d,A=(f==g?-1:1)*w.sqrt(z((x*y-x*u*u-y*t*t)/(x*u*u+y*t*t))),C=A*c*u/d+(a+h)/2,D=A*-d*t/c+(b+i)/2,E=w.asin(((b-D)/d).toFixed(9)),F=w.asin(((i-D)/d).toFixed(9));E=aF&&(E=E-B*2),!g&&F>E&&(F=F-B*2)}else E=j[0],F=j[1],C=j[2],D=j[3];var G=F-E;if(z(G)>k){var H=F,I=h,J=i;F=E+k*(g&&F>E?1:-1),h=C+c*w.cos(F),i=D+d*w.sin(F),m=bO(h,i,c,d,e,0,g,I,J,[F,H,C,D])}G=F-E;var K=w.cos(E),L=w.sin(E),M=w.cos(F),N=w.sin(F),O=w.tan(G/4),P=4/3*c*O,Q=4/3*d*O,R=[a,b],S=[a+P*L,b-Q*K],T=[h+P*N,i-Q*M],U=[h,i];S[0]=2*R[0]-S[0],S[1]=2*R[1]-S[1];if(j)return[S,T,U][n](m);m=[S,T,U][n](m).join()[s](",");var V=[];for(var W=0,X=m.length;W"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bP(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bP(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y)),i=f-2*d+b-(h-2*f+d),j=2*(d-b)-2*(f-d),k=b-d,l=(-j+w.sqrt(j*j-4*i*k))/2/i,n=(-j-w.sqrt(j*j-4*i*k))/2/i,z(l)>"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bP(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bP(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y));return{min:{x:y[m](0,p),y:y[m](0,o)},max:{x:x[m](0,p),y:x[m](0,o)}}}),bR=a._path2curve=bv(function(a,b){var c=!b&&bz(a);if(!b&&c.curve)return bJ(c.curve);var d=bL(a),e=b&&bL(b),f={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},g={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},h=function(a,b){var c,d;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null);switch(a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"][n](bO[m](0,[b.x,b.y][n](a.slice(1))));break;case"S":c=b.x+(b.x-(b.bx||b.x)),d=b.y+(b.y-(b.by||b.y)),a=["C",c,d][n](a.slice(1));break;case"T":b.qx=b.x+(b.x-(b.qx||b.x)),b.qy=b.y+(b.y-(b.qy||b.y)),a=["C"][n](bN(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"][n](bN(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"][n](bM(b.x,b.y,a[1],a[2]));break;case"H":a=["C"][n](bM(b.x,b.y,a[1],b.y));break;case"V":a=["C"][n](bM(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"][n](bM(b.x,b.y,b.X,b.Y))}return a},i=function(a,b){if(a[b].length>7){a[b].shift();var c=a[b];while(c.length)a.splice(b++,0,["C"][n](c.splice(0,6)));a.splice(b,1),l=x(d.length,e&&e.length||0)}},j=function(a,b,c,f,g){a&&b&&a[g][0]=="M"&&b[g][0]!="M"&&(b.splice(g,0,["M",f.x,f.y]),c.bx=0,c.by=0,c.x=a[g][1],c.y=a[g][2],l=x(d.length,e&&e.length||0))};for(var k=0,l=x(d.length,e&&e.length||0);ke){if(c&&!l.start){m=cs(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n),k+=["C"+m.start.x,m.start.y,m.m.x,m.m.y,m.x,m.y];if(f)return k;l.start=k,k=["M"+m.x,m.y+"C"+m.n.x,m.n.y,m.end.x,m.end.y,i[5],i[6]].join(),n+=j,g=+i[5],h=+i[6];continue}if(!b&&!c){m=cs(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);return{x:m.x,y:m.y,alpha:m.alpha}}}n+=j,g=+i[5],h=+i[6]}k+=i.shift()+i}l.end=k,m=b?n:c?l:a.findDotsAtSegment(g,h,i[0],i[1],i[2],i[3],i[4],i[5],1),m.alpha&&(m={x:m.x,y:m.y,alpha:m.alpha});return m}},cu=ct(1),cv=ct(),cw=ct(0,1);a.getTotalLength=cu,a.getPointAtLength=cv,a.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return cw(a,b).end;var d=cw(a,c,1);return b?cw(d,b).end:d},cl.getTotalLength=function(){if(this.type=="path"){if(this.node.getTotalLength)return this.node.getTotalLength();return cu(this.attrs.path)}},cl.getPointAtLength=function(a){if(this.type=="path")return cv(this.attrs.path,a)},cl.getSubpath=function(b,c){if(this.type=="path")return a.getSubpath(this.attrs.path,b,c)};var cx=a.easing_formulas={linear:function(a){return a},"<":function(a){return A(a,1.7)},">":function(a){return A(a,.48)},"<>":function(a){var b=.48-a/1.04,c=w.sqrt(.1734+b*b),d=c-b,e=A(z(d),1/3)*(d<0?-1:1),f=-c-b,g=A(z(f),1/3)*(f<0?-1:1),h=e+g+.5;return(1-h)*3*h*h+h*h*h},backIn:function(a){var b=1.70158;return a*a*((b+1)*a-b)},backOut:function(a){a=a-1;var b=1.70158;return a*a*((b+1)*a+b)+1},elastic:function(a){if(a==!!a)return a;return A(2,-10*a)*w.sin((a-.075)*2*B/.3)+1},bounce:function(a){var b=7.5625,c=2.75,d;a<1/c?d=b*a*a:a<2/c?(a-=1.5/c,d=b*a*a+.75):a<2.5/c?(a-=2.25/c,d=b*a*a+.9375):(a-=2.625/c,d=b*a*a+.984375);return d}};cx.easeIn=cx["ease-in"]=cx["<"],cx.easeOut=cx["ease-out"]=cx[">"],cx.easeInOut=cx["ease-in-out"]=cx["<>"],cx["back-in"]=cx.backIn,cx["back-out"]=cx.backOut;var cy=[],cz=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){setTimeout(a,16)},cA=function(){var b=+(new Date),c=0;for(;c1&&!d.next){for(s in k)k[g](s)&&(r[s]=d.totalOrigin[s]);d.el.attr(r),cE(d.anim,d.el,d.anim.percents[0],null,d.totalOrigin,d.repeat-1)}d.next&&!d.stop&&cE(d.anim,d.el,d.next,null,d.totalOrigin,d.repeat)}}a.svg&&m&&m.paper&&m.paper.safari(),cy.length&&cz(cA)},cB=function(a){return a>255?255:a<0?0:a};cl.animateWith=function(b,c,d,e,f,g){var h=this;if(h.removed){g&&g.call(h);return h}var i=d instanceof cD?d:a.animation(d,e,f,g),j,k;cE(i,h,i.percents[0],null,h.attr());for(var l=0,m=cy.length;l.5)*2-1;i(m-.5,2)+i(n-.5,2)>.25&&(n=f.sqrt(.25-i(m-.5,2))*e+.5)&&n!=.5&&(n=n.toFixed(5)-1e-5*e)}return l}),e=e.split(/\s*\-\s*/);if(j=="linear"){var t=e.shift();t=-d(t);if(isNaN(t))return null;var u=[0,0,f.cos(a.rad(t)),f.sin(a.rad(t))],v=1/(g(h(u[2]),h(u[3]))||1);u[2]*=v,u[3]*=v,u[2]<0&&(u[0]=-u[2],u[2]=0),u[3]<0&&(u[1]=-u[3],u[3]=0)}var w=a._parseDots(e);if(!w)return null;k=k.replace(/[\(\)\s,\xb0#]/g,"_"),b.gradient&&k!=b.gradient.id&&(p.defs.removeChild(b.gradient),delete b.gradient);if(!b.gradient){s=q(j+"Gradient",{id:k}),b.gradient=s,q(s,j=="radial"?{fx:m,fy:n}:{x1:u[0],y1:u[1],x2:u[2],y2:u[3],gradientTransform:b.matrix.invert()}),p.defs.appendChild(s);for(var x=0,y=w.length;x1?G.opacity/100:G.opacity});case"stroke":G=a.getRGB(p),i.setAttribute(o,G.hex),o=="stroke"&&G[b]("opacity")&&q(i,{"stroke-opacity":G.opacity>1?G.opacity/100:G.opacity}),o=="stroke"&&d._.arrows&&("startString"in d._.arrows&&t(d,d._.arrows.startString),"endString"in d._.arrows&&t(d,d._.arrows.endString,1));break;case"gradient":(d.type=="circle"||d.type=="ellipse"||c(p).charAt()!="r")&&r(d,p);break;case"opacity":k.gradient&&!k[b]("stroke-opacity")&&q(i,{"stroke-opacity":p>1?p/100:p});case"fill-opacity":if(k.gradient){H=a._g.doc.getElementById(i.getAttribute("fill").replace(/^url\(#|\)$/g,l)),H&&(I=H.getElementsByTagName("stop"),q(I[I.length-1],{"stop-opacity":p}));break};default:o=="font-size"&&(p=e(p,10)+"px");var J=o.replace(/(\-.)/g,function(a){return a.substring(1).toUpperCase()});i.style[J]=p,d._.dirty=1,i.setAttribute(o,p)}}y(d,f),i.style.visibility=m},x=1.2,y=function(d,f){if(d.type=="text"&&!!(f[b]("text")||f[b]("font")||f[b]("font-size")||f[b]("x")||f[b]("y"))){var g=d.attrs,h=d.node,i=h.firstChild?e(a._g.doc.defaultView.getComputedStyle(h.firstChild,l).getPropertyValue("font-size"),10):10;if(f[b]("text")){g.text=f.text;while(h.firstChild)h.removeChild(h.firstChild);var j=c(f.text).split("\n"),k=[],m;for(var n=0,o=j.length;n"));var $=X.getBoundingClientRect();t.W=m.w=($.right-$.left)/Y,t.H=m.h=($.bottom-$.top)/Y,t.X=m.x,t.Y=m.y+t.H/2,("x"in i||"y"in i)&&(t.path.v=a.format("m{0},{1}l{2},{1}",f(m.x*u),f(m.y*u),f(m.x*u)+1));var _=["x","y","text","font","font-family","font-weight","font-style","font-size"];for(var ba=0,bb=_.length;ba.25&&(c=e.sqrt(.25-i(b-.5,2))*((c>.5)*2-1)+.5),m=b+n+c);return o}),f=f.split(/\s*\-\s*/);if(l=="linear"){var p=f.shift();p=-d(p);if(isNaN(p))return null}var q=a._parseDots(f);if(!q)return null;b=b.shape||b.node;if(q.length){b.removeChild(g),g.on=!0,g.method="none",g.color=q[0].color,g.color2=q[q.length-1].color;var r=[];for(var s=0,t=q.length;s')}}catch(c){F=function(a){return b.createElement("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">')}}},a._engine.initWin(a._g.win),a._engine.create=function(){var b=a._getContainer.apply(0,arguments),c=b.container,d=b.height,e,f=b.width,g=b.x,h=b.y;if(!c)throw new Error("VML container not found.");var i=new a._Paper,j=i.canvas=a._g.doc.createElement("div"),k=j.style;g=g||0,h=h||0,f=f||512,d=d||342,i.width=f,i.height=d,f==+f&&(f+="px"),d==+d&&(d+="px"),i.coordsize=u*1e3+n+u*1e3,i.coordorigin="0 0",i.span=a._g.doc.createElement("span"),i.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;",j.appendChild(i.span),k.cssText=a.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden",f,d),c==1?(a._g.doc.body.appendChild(j),k.left=g+"px",k.top=h+"px",k.position="absolute"):c.firstChild?c.insertBefore(j,c.firstChild):c.appendChild(j),i.renderfix=function(){};return i},a.prototype.clear=function(){a.eve("raphael.clear",this),this.canvas.innerHTML=o,this.span=a._g.doc.createElement("span"),this.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;",this.canvas.appendChild(this.span),this.bottom=this.top=null},a.prototype.remove=function(){a.eve("raphael.remove",this),this.canvas.parentNode.removeChild(this.canvas);for(var b in this)this[b]=typeof this[b]=="function"?a._removedFactory(b):null;return!0};var G=a.st;for(var H in E)E[b](H)&&!G[b](H)&&(G[H]=function(a){return function(){var b=arguments;return this.forEach(function(c){c[a].apply(c,b)})}}(H))}(window.Raphael) \ No newline at end of file diff --git a/guides/content/assets/javascripts/svgeezy.js b/guides/content/assets/javascripts/svgeezy.js new file mode 100644 index 00000000000..a41ab4e5db3 --- /dev/null +++ b/guides/content/assets/javascripts/svgeezy.js @@ -0,0 +1,59 @@ +/* + * SVGeezy.js 1.0 + * + * Copyright 2012, Ben Howdle http://twostepmedia.co.uk + * Released under the WTFPL license + * http://sam.zoy.org/wtfpl/ + * + * Date: Sun Aug 26 20:38 2012 GMT + */ + +/* + //call like so, pass in a class name that you don't want it to check and a filetype to replace .svg with + svgeezy.init('nocheck', 'png'); +*/ + +var svgeezy = function() { + + return { + + init: function(avoid, filetype) { + this.avoid = avoid || false; + this.filetype = filetype || 'png'; + this.svgSupport = this.supportsSvg(); + if(!this.svgSupport) { + this.images = document.getElementsByTagName('img'); + this.imgL = this.images.length; + this.fallbacks(); + } + }, + + fallbacks: function() { + while(this.imgL--) { + if(!this.hasClass(this.images[this.imgL], this.avoid) || !this.avoid) { + var src = this.images[this.imgL].getAttribute('src'); + if(src === null) { + continue; + } + if(this.getFileExt(src) == 'svg') { + var newSrc = src.replace('.svg', '.' + this.filetype); + this.images[this.imgL].setAttribute('src', newSrc); + } + } + } + }, + + getFileExt: function(src) { + return src.split('.').pop(); + }, + + hasClass: function(element, cls) { + return(' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1; + }, + + supportsSvg: function() { + return document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1"); + } + }; + + }(); \ No newline at end of file diff --git a/guides/content/assets/stylesheets/components/_code.scss b/guides/content/assets/stylesheets/components/_code.scss new file mode 100644 index 00000000000..ed69b30e028 --- /dev/null +++ b/guides/content/assets/stylesheets/components/_code.scss @@ -0,0 +1,142 @@ +pre, xmp, plaintext, listing, code { + font-family: $font-code !important; + font-size: $font-size; + background-color: #eee; + padding: 2px 10px; + border-radius: 3px; + color: $c-gray; + -webkit-font-smoothing: subpixel-antialiased; +} + +pre.highlight code { + font-size: $font-size; +} + +.CodeRay { + font-family: $font-code, Monaco, "Courier New", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace; + color: #000; +} + +.CodeRay pre { + margin: 0px; +} + +div.CodeRay { } +span.CodeRay { white-space: pre; border: 0px; padding: 2px } + +table.CodeRay { border-collapse: collapse; width: 100%; padding: 2px } +table.CodeRay td { + padding: 1em 0.5em; + vertical-align: top; +} + +.CodeRay .line-numbers, .CodeRay .no { + color: #AAA; + text-align: right; +} + +.CodeRay .line-numbers a { + color: #AAA; +} + +.CodeRay .line-numbers tt { font-weight: bold } +.CodeRay .line-numbers .highlighted { color: red } +.CodeRay .line { display: block; float: left; width: 100%; } +.CodeRay span.line-numbers { padding: 0px 10px 0 0 } +.CodeRay .code { width: 100% } + +ol.CodeRay { font-size: $font-size } +ol.CodeRay li { white-space: pre } + +.CodeRay .code pre { overflow: auto } +.CodeRay .debug { color:white ! important; background:blue ! important; } + +.CodeRay .annotation { color:#007 } +.CodeRay .attribute-name { color:#f08 } +.CodeRay .attribute-value { color:#700 } +.CodeRay .binary { color:#509; font-weight:bold } +.CodeRay .comment { color:#998; font-style: italic;} +.CodeRay .char { color:#04D } +.CodeRay .char .content { color:#04D } +.CodeRay .char .delimiter { color:#039 } +.CodeRay .class { color:#458; font-weight:bold } +.CodeRay .complex { color:#A08; font-weight:bold } +.CodeRay .constant { color:teal; } +.CodeRay .color { color:#0A0 } +.CodeRay .class-variable { color:#369 } +.CodeRay .decorator { color:#B0B; } +.CodeRay .definition { color:#099; font-weight:bold } +.CodeRay .directive { color:#088; font-weight:bold } +.CodeRay .delimiter { color:black } +.CodeRay .doc { color:#970 } +.CodeRay .doctype { color:#34b } +.CodeRay .doc-string { color:#D42; font-weight:bold } +.CodeRay .escape { color:#666; font-weight:bold } +.CodeRay .entity { color:#800; font-weight:bold } +.CodeRay .error { color:#F00; background-color:#FAA } +.CodeRay .exception { color:#C00; font-weight:bold } +.CodeRay .filename { color:#099; } +.CodeRay .function { color:#900; font-weight:bold } +.CodeRay .global-variable { color:teal; font-weight:bold } +.CodeRay .hex { color:#058; font-weight:bold } +.CodeRay .integer { color:#099; } +.CodeRay .include { color:#B44; font-weight:bold } +.CodeRay .inline { color: black } +.CodeRay .inline .inline { background: #ccc } +.CodeRay .inline .inline .inline { background: #bbb } +.CodeRay .inline .inline-delimiter { color: #D14; } +.CodeRay .inline-delimiter { color: #D14; } +.CodeRay .important { color:#f00; } +.CodeRay .interpreted { color:#B2B; font-weight:bold } +.CodeRay .instance-variable { color:teal } +.CodeRay .label { color:#970; font-weight:bold } +.CodeRay .local-variable { color:#963 } +.CodeRay .octal { color:#40E; font-weight:bold } +.CodeRay .operator { } +.CodeRay .predefined-constant { font-weight:bold } +.CodeRay .predefined { color:#369; font-weight:bold } +.CodeRay .preprocessor { color:#579; } +.CodeRay .pseudo-class { color:#00C; font-weight:bold } +.CodeRay .predefined-type { color:#074; font-weight:bold } +.CodeRay .reserved, .keyword { color:#000; font-weight:bold } + +.CodeRay .key { color: #808; } +.CodeRay .key .delimiter { color: #606; } +.CodeRay .key .char { color: #80f; } +.CodeRay .value { color: #088; } + +.CodeRay .regexp { background-color:#fff0ff } +.CodeRay .regexp .content { color:#808 } +.CodeRay .regexp .delimiter { color:#404 } +.CodeRay .regexp .modifier { color:#C2C } +.CodeRay .regexp .function { color:#404; font-weight: bold } + +.CodeRay .string { color: #D20; } +.CodeRay .string .string { } +.CodeRay .string .string .string { background-color:#ffd0d0 } +.CodeRay .string .content { color: #D14; } +.CodeRay .string .char { color: #D14; } +.CodeRay .string .delimiter { color: #D14; } + +.CodeRay .shell { color:#D14 } +.CodeRay .shell .content { } +.CodeRay .shell .delimiter { color:#D14 } + +.CodeRay .symbol { color:#990073 } +.CodeRay .symbol .content { color:#A60 } +.CodeRay .symbol .delimiter { color:#630 } + +.CodeRay .tag { color:#070 } +.CodeRay .tag-special { color:#D70; font-weight:bold } +.CodeRay .type { color:#339; font-weight:bold } +.CodeRay .variable { color:#036 } + +.CodeRay .insert { background: #afa; } +.CodeRay .delete { background: #faa; } +.CodeRay .change { color: #aaf; background: #007; } +.CodeRay .head { color: #f8f; background: #505 } + +.CodeRay .insert .insert { color: #080; font-weight:bold } +.CodeRay .delete .delete { color: #800; font-weight:bold } +.CodeRay .change .change { color: #66f; } +.CodeRay .head .head { color: #f4f; } diff --git a/guides/content/assets/stylesheets/components/_edge_badge.scss b/guides/content/assets/stylesheets/components/_edge_badge.scss new file mode 100644 index 00000000000..eaf547fee94 --- /dev/null +++ b/guides/content/assets/stylesheets/components/_edge_badge.scss @@ -0,0 +1,7 @@ +#edge-badge { + position: fixed; + right: 0px; + top: 0px; + z-index: 100; + border: none; +} diff --git a/guides/content/assets/stylesheets/components/_icons.scss b/guides/content/assets/stylesheets/components/_icons.scss new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/guides/content/assets/stylesheets/components/_icons.scss @@ -0,0 +1 @@ + diff --git a/guides/content/assets/stylesheets/components/_info_boxes.scss b/guides/content/assets/stylesheets/components/_info_boxes.scss new file mode 100644 index 00000000000..fdf9e4156c1 --- /dev/null +++ b/guides/content/assets/stylesheets/components/_info_boxes.scss @@ -0,0 +1,97 @@ +.warning, .info, .note, .github { + padding: 10px 20px; + border-radius: 3px; + border-width: 1px; + border-style: solid; + font-weight: 600; + margin: 10px 0; + position: relative; + + &:before { + font-family: icons !important; + font-weight: normal; + position: absolute; + font-size: 17px; + top: -5px; + left: -5px; + background-color: white; + width: 25px; + text-align: center; + height: 25px; + border-radius: 15px; + border-width: 1px; + border-style: solid; + line-height: 25px; + } + + a { + color: $c-light-blue; + border-bottom: 1px dashed lighten($c-light-blue, 30); + + &:hover { + color: darken($c-light-blue, 5); + border-color: lighten($c-light-blue, 25); + } + } + + code { + background-color: $c-light-blue; + color: white; + padding: 2px 10px 4px; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} + +.warning { + @extend .icon-attention; + background-color: lighten($c-red, 35); + color: $c-red; + + &, &:before { + border-color: lighten($c-red, 30); + } + + &:before { + line-height: 23px; + } +} + +.info { + @extend .icon-info; + background-color: lighten($c-green, 35); + color: darken($c-green, 20); + + &, &:before { + border-color: lighten($c-green, 30); + } +} + +.note { + @extend .icon-doc-text; + background-color: lighten($c-yellow, 35); + color: darken($c-yellow, 20); + + &, &:before { + border-color: lighten($c-yellow, 30); + } +} + +.github { + @extend .icon-github; + background-color: #F3F3F3; + color: #333; + + a { + color: #4183C4; + + &:hover { + color: lighten(#4183C4, 10); + } + } + + &, &:before { + border-color: #E5E5E5; + } +} diff --git a/guides/content/assets/stylesheets/components/_navigation.scss b/guides/content/assets/stylesheets/components/_navigation.scss new file mode 100644 index 00000000000..ca1d309a56d --- /dev/null +++ b/guides/content/assets/stylesheets/components/_navigation.scss @@ -0,0 +1,114 @@ +// Sidebar +#sidebar { + #sidebar-menu { + > ul { + margin-left: 0; + padding-left: 0; + -webkit-padding-start: 0px; + } + + li.js-topic { + margin-bottom: 15px; + } + + li.current > a, li.toc-active > a { + color: $c-green; + } + + li.current { + position: relative; + + > a { + display: inline-block; + width: 200px; + } + + > i.icon-dot { + display: inline-block; + float: left; + width: 20px; + } + + .toc ul { + display: block; + } + + .toc { + padding-left: 10px; + border-left: 2px solid $c-border; + margin-left: 7px; + margin-top: -12px; + padding-top: 15px; + + > ul > li { + position: relative; + + > a { + &:before { + content: ''; + position: absolute; + height: 2px; + width: 6px; + background-color: $c-border; + margin-left: -10px; + top: 50%; + margin-top: -1px; + } + } + } + + a { + font-weight: 400; + } + + .toc-h2 a { + font-weight: 600 + } + .toc-h3 { + margin-left: 10px; + font-style: italic; + border-bottom: 1px dashed $c-border; + + a:before { + display: none; + } + } + } + + &.closed { + .toc { + border-color: transparent; + } + } + } + + h3 { + margin: 0; + line-height: 40px; + } + + a { + font-weight: 600; + padding-left: 0 !important; + } + + &.stuck { + position: fixed; + top: 0; + margin-top: 0px; + } + } +} + +#main-menu { + > ul { + margin-left: 0; + padding-left: 0; + + li { + a { + padding: 0 15px + } + } + } +} diff --git a/guides/content/assets/stylesheets/global/_placeholders.scss b/guides/content/assets/stylesheets/global/_placeholders.scss new file mode 100644 index 00000000000..628dad74e93 --- /dev/null +++ b/guides/content/assets/stylesheets/global/_placeholders.scss @@ -0,0 +1,5 @@ +%ul-inline { + list-style: none; + margin: 0; + padding: 0; +} diff --git a/guides/content/assets/stylesheets/global/_variables.scss b/guides/content/assets/stylesheets/global/_variables.scss new file mode 100644 index 00000000000..d0e55cc3d53 --- /dev/null +++ b/guides/content/assets/stylesheets/global/_variables.scss @@ -0,0 +1,31 @@ +// Colors +$c-green: #8dba53; +$c-blue: #1072ba; +$c-light-blue: #1ca8e2; +$c-orange: #ff8400; +$c-gray: #959595; +$c-gray-blue: #6487a3; +$c-red: #e23753; +$c-yellow: #ffc334; + +$c-border: #DAE8F9; + +$c-twitter: #009FD4; +$c-google: #E4532E; +$c-facebook: #325B9A; +$c-github: #2C84C7; + +// Typography +$font-size: 16px; +$line-height: 1.6; +$font-base: 'Open Sans'; +$font-alt: 'Bariol'; +$font-code: 'Inconsolata'; + +// Change the grid settings +$column: 90px; +$gutter: 30px; +$grid-columns: 12; +$max-width: 960px; + +$visual-grid: false; diff --git a/guides/content/assets/stylesheets/guides.scss b/guides/content/assets/stylesheets/guides.scss new file mode 100644 index 00000000000..fcff6630764 --- /dev/null +++ b/guides/content/assets/stylesheets/guides.scss @@ -0,0 +1,20 @@ +@import 'global/variables'; +@import 'vendor/bourbon/bourbon'; +@import 'vendor/neat/neat'; +@import 'vendor/icons'; +@import 'vendor/icons/spree_index_header'; + +@import 'global/placeholders'; + +@import 'components/code'; +@import 'components/info_boxes'; +@import 'components/edge_badge'; +@import 'components/navigation'; + +@import 'shared/layout'; +@import 'shared/typography'; +@import 'shared/tables'; +@import 'shared/base'; + +@import 'sections/index'; +@import 'sections/home'; diff --git a/guides/content/assets/stylesheets/sections/_home.scss b/guides/content/assets/stylesheets/sections/_home.scss new file mode 100644 index 00000000000..85d4d40e8ea --- /dev/null +++ b/guides/content/assets/stylesheets/sections/_home.scss @@ -0,0 +1,5 @@ +body.home { + #subheader { + height: 470px; + } +} diff --git a/guides/content/assets/stylesheets/sections/_index.scss b/guides/content/assets/stylesheets/sections/_index.scss new file mode 100644 index 00000000000..ce50c2ba357 --- /dev/null +++ b/guides/content/assets/stylesheets/sections/_index.scss @@ -0,0 +1,66 @@ +body.index { + #subheader { + // background-image: none; + // background-color: lighten($c-border, 8); + border-top: 1px solid $c-border; + + h1 { + border: none; + text-transform: uppercase; + } + + h1.huge-title { + margin-top: em(10); + margin-bottom: em(20); + text-align: center; + font-size: em(40); + letter-spacing: em(5); + word-spacing: em(20); + color: white; + } + + ul.sections { + @extend %ul-inline; + @include outer-container; + + margin-bottom: em(40); + + li { + @include span-columns(2 of 10); + @include omega(5n); + text-align: center; + + a { + @include transition (all 0.3s ease-in); + color: white; + + h1 { + font-size: em(20); + color: white; + } + + i { + font-size: 170px; + display: block; + margin-bottom: em(5); + color: white; + + &, &:before { + font-family: 'icomoon' !important; + opacity: 1; + } + + &:hover { + color: darken($c-blue, 10); + } + } + } + } + } + } + + #content, #main-footer { + margin: 0; + padding: 0; + } +} diff --git a/guides/content/assets/stylesheets/shared/_base.scss b/guides/content/assets/stylesheets/shared/_base.scss new file mode 100644 index 00000000000..83ba593f267 --- /dev/null +++ b/guides/content/assets/stylesheets/shared/_base.scss @@ -0,0 +1,35 @@ +// Base +img { + max-width: 100%; +} + +#main { + position: relative; +} + +// Footer +#main-footer { + * { + line-height: 24px; + } + + .block-title { + font-size: 14px; + } + + .social-icons { + margin-top: 25px; + + i { + line-height: 27px; + } + } + + a { + font-weight: 600; + } +} + +.logo img { + margin-top: 30px; +} \ No newline at end of file diff --git a/guides/content/assets/stylesheets/shared/_forms.scss b/guides/content/assets/stylesheets/shared/_forms.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/content/assets/stylesheets/shared/_layout.scss b/guides/content/assets/stylesheets/shared/_layout.scss new file mode 100644 index 00000000000..3b412277a72 --- /dev/null +++ b/guides/content/assets/stylesheets/shared/_layout.scss @@ -0,0 +1,3 @@ +.footer-top { + padding: 30px 0; +} diff --git a/guides/content/assets/stylesheets/shared/_tables.scss b/guides/content/assets/stylesheets/shared/_tables.scss new file mode 100644 index 00000000000..f6cc0f50f47 --- /dev/null +++ b/guides/content/assets/stylesheets/shared/_tables.scss @@ -0,0 +1,29 @@ +table { + width: 100%; + border: 1px solid $c-border; + border-collapse: collapse; + + thead { + th { + text-align: center; + text-transform: uppercase; + font-size: em(14); + font-weight: 600; + background-color: lighten($c-border, 5); + } + } + + tbody { + tr { + td { + + } + } + } + + td, th { + padding: 10px; + border-bottom: 1px solid $c-border; + border-right: 1px solid $c-border; + } +} diff --git a/guides/content/assets/stylesheets/shared/_typography.scss b/guides/content/assets/stylesheets/shared/_typography.scss new file mode 100644 index 00000000000..2b399d4b475 --- /dev/null +++ b/guides/content/assets/stylesheets/shared/_typography.scss @@ -0,0 +1,24 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:400italic,600italic,400,600); +@import url(//fonts.googleapis.com/css?family=Inconsolata); +@include font-face(Bariol, '../../shared/fonts/bariol_regular-webfont'); + +body { + font-size: $font-size; + line-height: $line-height; + font-family: $font-base; + -webkit-font-smoothing: antialiased; +} + +strong, b { + font-weight: 600; +} + +h4 { + font-size: $font-size + 2; +} + +h1, h2, h3, h4 { + a i { + color: $c-light-blue; + } +} diff --git a/guides/content/assets/stylesheets/vendor/_icons.scss b/guides/content/assets/stylesheets/vendor/_icons.scss new file mode 100644 index 00000000000..0e898da1ab7 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/_icons.scss @@ -0,0 +1,314 @@ +@charset "UTF-8"; + +@font-face { + font-family: 'icons'; + src: url("../../shared/fonts/entypo.eot"); + src: url("../../shared/fonts/entypo.eot?#iefix") format('embedded-opentype'), url("../../shared/fonts/entypo.woff") format('woff'), url("../../shared/fonts/entypo.ttf") format('truetype'), url("../../shared/fonts/entypo.svg#entypo") format('svg'); + font-weight: normal; + font-style: normal; +} +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: 'icons'; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: 0.2em; + text-align: center; + opacity: 0.8; +/* Uncomment for 3D effect */ +/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +/* fix buttons height */ + line-height: 1em; +/* you can be more comfortable with increased icons size */ +/* font-size: 120%; */ +} + + +.icon-plus:before { content: '\e816'; } /* '' */ +.icon-minus:before { content: '\e819'; } /* '' */ +.icon-info:before { content: '\e81e'; } /* '' */ +.icon-left-thin:before { content: '\e88c'; } /* '' */ +.icon-up-thin:before { content: '\e88e'; } /* '' */ +.icon-right-thin:before { content: '\e88d'; } /* '' */ +.icon-down-thin:before { content: '\e88b'; } /* '' */ +.icon-level-up:before { content: '\e893'; } /* '' */ +.icon-level-down:before { content: '\e892'; } /* '' */ +.icon-switch:before { content: '\e896'; } /* '' */ +.icon-infinity:before { content: '\e8c2'; } /* '' */ +.icon-plus-squared:before { content: '\e818'; } /* '' */ +.icon-minus-squared:before { content: '\e81b'; } /* '' */ +.icon-home:before { content: '\e821'; } /* '' */ +.icon-keyboard:before { content: '\e83a'; } /* '' */ +.icon-erase:before { content: '\e8c3'; } /* '' */ +.icon-pause:before { content: '\e899'; } /* '' */ +.icon-fast-forward:before { content: '\e89d'; } /* '' */ +.icon-fast-backward:before { content: '\e89e'; } /* '' */ +.icon-to-end:before { content: '\e89b'; } /* '' */ +.icon-to-start:before { content: '\e89c'; } /* '' */ +.icon-hourglass:before { content: '\e863'; } /* '' */ +.icon-stop:before { content: '\e898'; } /* '' */ +.icon-up-dir:before { content: '\e886'; } /* '' */ +.icon-play:before { content: '\e897'; } /* '' */ +.icon-right-dir:before { content: '\e885'; } /* '' */ +.icon-down-dir:before { content: '\e883'; } /* '' */ +.icon-left-dir:before { content: '\e884'; } /* '' */ +.icon-adjust:before { content: '\e867'; } /* '' */ +.icon-cloud:before { content: '\e8b2'; } /* '' */ +.icon-star:before { content: '\e808'; } /* '' */ +.icon-star-empty:before { content: '\e809'; } /* '' */ +.icon-cup:before { content: '\e846'; } /* '' */ +.icon-menu:before { content: '\e811'; } /* '' */ +.icon-moon:before { content: '\e8b5'; } /* '' */ +.icon-heart-empty:before { content: '\e807'; } /* '' */ +.icon-heart:before { content: '\e806'; } /* '' */ +.icon-note:before { content: '\e800'; } /* '' */ +.icon-note-beamed:before { content: '\e801'; } /* '' */ +.icon-layout:before { content: '\e810'; } /* '' */ +.icon-flag:before { content: '\e82a'; } /* '' */ +.icon-tools:before { content: '\e856'; } /* '' */ +.icon-cog:before { content: '\e855'; } /* '' */ +.icon-attention:before { content: '\e83e'; } /* '' */ +.icon-flash:before { content: '\e8b4'; } /* '' */ +.icon-record:before { content: '\e89a'; } /* '' */ +.icon-cloud-thunder:before { content: '\e8b3'; } /* '' */ +.icon-tape:before { content: '\e8c8'; } /* '' */ +.icon-flight:before { content: '\e8b6'; } /* '' */ +.icon-mail:before { content: '\e805'; } /* '' */ +.icon-pencil:before { content: '\e836'; } /* '' */ +.icon-feather:before { content: '\e837'; } /* '' */ +.icon-check:before { content: '\e812'; } /* '' */ +.icon-cancel:before { content: '\e813'; } /* '' */ +.icon-cancel-circled:before { content: '\e814'; } /* '' */ +.icon-cancel-squared:before { content: '\e815'; } /* '' */ +.icon-help:before { content: '\e81c'; } /* '' */ +.icon-quote:before { content: '\e833'; } /* '' */ +.icon-plus-circled:before { content: '\e817'; } /* '' */ +.icon-minus-circled:before { content: '\e81a'; } /* '' */ +.icon-right:before { content: '\e881'; } /* '' */ +.icon-direction:before { content: '\e844'; } /* '' */ +.icon-forward:before { content: '\e832'; } /* '' */ +.icon-ccw:before { content: '\e88f'; } /* '' */ +.icon-cw:before { content: '\e890'; } /* '' */ +.icon-left:before { content: '\e880'; } /* '' */ +.icon-up:before { content: '\e882'; } /* '' */ +.icon-down:before { content: '\e87f'; } /* '' */ +.icon-list-add:before { content: '\e8a6'; } /* '' */ +.icon-list:before { content: '\e8a5'; } /* '' */ +.icon-left-bold:before { content: '\e888'; } /* '' */ +.icon-right-bold:before { content: '\e889'; } /* '' */ +.icon-up-bold:before { content: '\e88a'; } /* '' */ +.icon-down-bold:before { content: '\e887'; } /* '' */ +.icon-user-add:before { content: '\e80c'; } /* '' */ +.icon-help-circled:before { content: '\e81d'; } /* '' */ +.icon-info-circled:before { content: '\e81f'; } /* '' */ +.icon-eye:before { content: '\e826'; } /* '' */ +.icon-tag:before { content: '\e827'; } /* '' */ +.icon-upload-cloud:before { content: '\e82f'; } /* '' */ +.icon-reply:before { content: '\e830'; } /* '' */ +.icon-reply-all:before { content: '\e831'; } /* '' */ +.icon-code:before { content: '\e834'; } /* '' */ +.icon-export:before { content: '\e835'; } /* '' */ +.icon-print:before { content: '\e838'; } /* '' */ +.icon-retweet:before { content: '\e839'; } /* '' */ +.icon-comment:before { content: '\e83b'; } /* '' */ +.icon-chat:before { content: '\e83c'; } /* '' */ +.icon-vcard:before { content: '\e840'; } /* '' */ +.icon-address:before { content: '\e841'; } /* '' */ +.icon-location:before { content: '\e842'; } /* '' */ +.icon-map:before { content: '\e843'; } /* '' */ +.icon-compass:before { content: '\e845'; } /* '' */ +.icon-trash:before { content: '\e847'; } /* '' */ +.icon-doc:before { content: '\e848'; } /* '' */ +.icon-doc-text-inv:before { content: '\e84c'; } /* '' */ +.icon-docs:before { content: '\e849'; } /* '' */ +.icon-doc-landscape:before { content: '\e84a'; } /* '' */ +.icon-archive:before { content: '\e851'; } /* '' */ +.icon-rss:before { content: '\e853'; } /* '' */ +.icon-share:before { content: '\e857'; } /* '' */ +.icon-basket:before { content: '\e859'; } /* '' */ +.icon-shareable:before { content: '\e858'; } /* '' */ +.icon-login:before { content: '\e85c'; } /* '' */ +.icon-logout:before { content: '\e85d'; } /* '' */ +.icon-volume:before { content: '\e861'; } /* '' */ +.icon-resize-full:before { content: '\e869'; } /* '' */ +.icon-resize-small:before { content: '\e86a'; } /* '' */ +.icon-popup:before { content: '\e86b'; } /* '' */ +.icon-publish:before { content: '\e86c'; } /* '' */ +.icon-window:before { content: '\e86d'; } /* '' */ +.icon-arrow-combo:before { content: '\e86e'; } /* '' */ +.icon-chart-pie:before { content: '\e8c4'; } /* '' */ +.icon-language:before { content: '\e8ca'; } /* '' */ +.icon-air:before { content: '\e8ce'; } /* '' */ +.icon-database:before { content: '\e8d3'; } /* '' */ +.icon-drive:before { content: '\e8d4'; } /* '' */ +.icon-bucket:before { content: '\e8d5'; } /* '' */ +.icon-thermometer:before { content: '\e8d6'; } /* '' */ +.icon-down-circled:before { content: '\e86f'; } /* '' */ +.icon-left-circled:before { content: '\e870'; } /* '' */ +.icon-right-circled:before { content: '\e871'; } /* '' */ +.icon-up-circled:before { content: '\e872'; } /* '' */ +.icon-down-open:before { content: '\e873'; } /* '' */ +.icon-left-open:before { content: '\e874'; } /* '' */ +.icon-right-open:before { content: '\e875'; } /* '' */ +.icon-up-open:before { content: '\e876'; } /* '' */ +.icon-down-open-mini:before { content: '\e877'; } /* '' */ +.icon-left-open-mini:before { content: '\e878'; } /* '' */ +.icon-right-open-mini:before { content: '\e879'; } /* '' */ +.icon-up-open-mini:before { content: '\e87a'; } /* '' */ +.icon-down-open-big:before { content: '\e87b'; } /* '' */ +.icon-left-open-big:before { content: '\e87c'; } /* '' */ +.icon-right-open-big:before { content: '\e87d'; } /* '' */ +.icon-up-open-big:before { content: '\e87e'; } /* '' */ +.icon-progress-0:before { content: '\e89f'; } /* '' */ +.icon-progress-1:before { content: '\e8a0'; } /* '' */ +.icon-progress-2:before { content: '\e8a1'; } /* '' */ +.icon-progress-3:before { content: '\e8a2'; } /* '' */ +.icon-back-in-time:before { content: '\e8aa'; } /* '' */ +.icon-network:before { content: '\e8ad'; } /* '' */ +.icon-inbox:before { content: '\e8af'; } /* '' */ +.icon-install:before { content: '\e8b0'; } /* '' */ +.icon-lifebuoy:before { content: '\e8b9'; } /* '' */ +.icon-mouse:before { content: '\e8ba'; } /* '' */ +.icon-dot:before { content: '\e8bd'; } /* '' */ +.icon-dot-2:before { content: '\e8be'; } /* '' */ +.icon-dot-3:before { content: '\e8bf'; } /* '' */ +.icon-suitcase:before { content: '\e8bc'; } /* '' */ +.icon-flow-cascade:before { content: '\e8d8'; } /* '' */ +.icon-flow-branch:before { content: '\e8d9'; } /* '' */ +.icon-flow-tree:before { content: '\e8da'; } /* '' */ +.icon-flow-line:before { content: '\e8db'; } /* '' */ +.icon-flow-parallel:before { content: '\e8dc'; } /* '' */ +.icon-brush:before { content: '\e8c0'; } /* '' */ +.icon-paper-plane:before { content: '\e8b7'; } /* '' */ +.icon-magnet:before { content: '\e8c1'; } /* '' */ +.icon-gauge:before { content: '\e8de'; } /* '' */ +.icon-traffic-cone:before { content: '\e8df'; } /* '' */ +.icon-cc:before { content: '\e8e0'; } /* '' */ +.icon-cc-by:before { content: '\e8e1'; } /* '' */ +.icon-cc-nc:before { content: '\e8e2'; } /* '' */ +.icon-cc-nc-eu:before { content: '\e8e3'; } /* '' */ +.icon-cc-nc-jp:before { content: '\e8e4'; } /* '' */ +.icon-cc-sa:before { content: '\e8e5'; } /* '' */ +.icon-cc-nd:before { content: '\e8e6'; } /* '' */ +.icon-cc-pd:before { content: '\e8e7'; } /* '' */ +.icon-cc-zero:before { content: '\e8e8'; } /* '' */ +.icon-cc-share:before { content: '\e8e9'; } /* '' */ +.icon-cc-remix:before { content: '\e8ea'; } /* '' */ +.icon-github:before { content: '\e8eb'; } /* '' */ +.icon-github-circled:before { content: '\e8ec'; } /* '' */ +.icon-flickr:before { content: '\e8ed'; } /* '' */ +.icon-flickr-circled:before { content: '\e8ee'; } /* '' */ +.icon-vimeo-1:before { content: '\e8ef'; } /* '' */ +.icon-vimeo-circled:before { content: '\e8f0'; } /* '' */ +.icon-twitter:before { content: '\e8f1'; } /* '' */ +.icon-twitter-circled:before { content: '\e8f2'; } /* '' */ +.icon-facebook:before { content: '\e8f3'; } /* '' */ +.icon-facebook-circled:before { content: '\e8f4'; } /* '' */ +.icon-facebook-squared:before { content: '\e8f5'; } /* '' */ +.icon-gplus:before { content: '\e8f6'; } /* '' */ +.icon-gplus-circled:before { content: '\e8f7'; } /* '' */ +.icon-pinterest:before { content: '\e8f8'; } /* '' */ +.icon-pinterest-circled:before { content: '\e8f9'; } /* '' */ +.icon-tumblr:before { content: '\e8fa'; } /* '' */ +.icon-tumblr-circled:before { content: '\e8fb'; } /* '' */ +.icon-linkedin:before { content: '\e8fc'; } /* '' */ +.icon-linkedin-circled:before { content: '\e8fd'; } /* '' */ +.icon-dribbble:before { content: '\e8fe'; } /* '' */ +.icon-dribbble-circled:before { content: '\e8ff'; } /* '' */ +.icon-stumbleupon:before { content: '\e900'; } /* '' */ +.icon-stumbleupon-circled:before { content: '\e901'; } /* '' */ +.icon-lastfm:before { content: '\e902'; } /* '' */ +.icon-lastfm-circled:before { content: '\e903'; } /* '' */ +.icon-rdio:before { content: '\e904'; } /* '' */ +.icon-rdio-circled:before { content: '\e905'; } /* '' */ +.icon-spotify:before { content: '\e906'; } /* '' */ +.icon-spotify-circled:before { content: '\e907'; } /* '' */ +.icon-qq:before { content: '\e908'; } /* '' */ +.icon-instagram:before { content: '\e909'; } /* '' */ +.icon-dropbox:before { content: '\e90a'; } /* '' */ +.icon-evernote:before { content: '\e90b'; } /* '' */ +.icon-flattr:before { content: '\e90c'; } /* '' */ +.icon-skype:before { content: '\e90d'; } /* '' */ +.icon-skype-circled:before { content: '\e90e'; } /* '' */ +.icon-renren:before { content: '\e90f'; } /* '' */ +.icon-sina-weibo:before { content: '\e910'; } /* '' */ +.icon-paypal:before { content: '\e911'; } /* '' */ +.icon-picasa:before { content: '\e912'; } /* '' */ +.icon-soundcloud:before { content: '\e913'; } /* '' */ +.icon-mixi:before { content: '\e914'; } /* '' */ +.icon-behance:before { content: '\e915'; } /* '' */ +.icon-google-circles:before { content: '\e916'; } /* '' */ +.icon-vkontakte:before { content: '\e917'; } /* '' */ +.icon-smashing:before { content: '\e918'; } /* '' */ +.icon-db-shape:before { content: '\e91a'; } /* '' */ +.icon-sweden:before { content: '\e919'; } /* '' */ +.icon-logo-db:before { content: '\e91b'; } /* '' */ +.icon-picture:before { content: '\e80e'; } /* '' */ +.icon-globe:before { content: '\e8b1'; } /* '' */ +.icon-leaf:before { content: '\e8b8'; } /* '' */ +.icon-graduation-cap:before { content: '\e8c9'; } /* '' */ +.icon-mic:before { content: '\e85e'; } /* '' */ +.icon-palette:before { content: '\e8a4'; } /* '' */ +.icon-ticket:before { content: '\e8cb'; } /* '' */ +.icon-video:before { content: '\e80d'; } /* '' */ +.icon-target:before { content: '\e8a3'; } /* '' */ +.icon-music:before { content: '\e802'; } /* '' */ +.icon-trophy:before { content: '\e8a8'; } /* '' */ +.icon-thumbs-up:before { content: '\e82b'; } /* '' */ +.icon-thumbs-down:before { content: '\e82c'; } /* '' */ +.icon-bag:before { content: '\e85a'; } /* '' */ +.icon-user:before { content: '\e80a'; } /* '' */ +.icon-users:before { content: '\e80b'; } /* '' */ +.icon-lamp:before { content: '\e864'; } /* '' */ +.icon-alert:before { content: '\e83f'; } /* '' */ +.icon-water:before { content: '\e8cc'; } /* '' */ +.icon-droplet:before { content: '\e8cd'; } /* '' */ +.icon-credit-card:before { content: '\e8cf'; } /* '' */ +.icon-monitor:before { content: '\e8ab'; } /* '' */ +.icon-briefcase:before { content: '\e8bb'; } /* '' */ +.icon-floppy:before { content: '\e8d0'; } /* '' */ +.icon-cd:before { content: '\e8ae'; } /* '' */ +.icon-folder:before { content: '\e850'; } /* '' */ +.icon-doc-text:before { content: '\e84b'; } /* '' */ +.icon-calendar:before { content: '\e85b'; } /* '' */ +.icon-chart-line:before { content: '\e8c5'; } /* '' */ +.icon-chart-bar:before { content: '\e8c6'; } /* '' */ +.icon-clipboard:before { content: '\e8d1'; } /* '' */ +.icon-attach:before { content: '\e823'; } /* '' */ +.icon-bookmarks:before { content: '\e829'; } /* '' */ +.icon-book:before { content: '\e84f'; } /* '' */ +.icon-book-open:before { content: '\e84e'; } /* '' */ +.icon-phone:before { content: '\e854'; } /* '' */ +.icon-megaphone:before { content: '\e8d2'; } /* '' */ +.icon-upload:before { content: '\e82e'; } /* '' */ +.icon-download:before { content: '\e82d'; } /* '' */ +.icon-box:before { content: '\e852'; } /* '' */ +.icon-newspaper:before { content: '\e84d'; } /* '' */ +.icon-mobile:before { content: '\e8ac'; } /* '' */ +.icon-signal:before { content: '\e8a7'; } /* '' */ +.icon-camera:before { content: '\e80f'; } /* '' */ +.icon-shuffle:before { content: '\e894'; } /* '' */ +.icon-loop:before { content: '\e895'; } /* '' */ +.icon-arrows-ccw:before { content: '\e891'; } /* '' */ +.icon-light-down:before { content: '\e865'; } /* '' */ +.icon-light-up:before { content: '\e866'; } /* '' */ +.icon-mute:before { content: '\e85f'; } /* '' */ +.icon-sound:before { content: '\e860'; } /* '' */ +.icon-battery:before { content: '\e8a9'; } /* '' */ +.icon-search:before { content: '\e803'; } /* '' */ +.icon-key:before { content: '\e8d7'; } /* '' */ +.icon-lock:before { content: '\e824'; } /* '' */ +.icon-lock-open:before { content: '\e825'; } /* '' */ +.icon-bell:before { content: '\e83d'; } /* '' */ +.icon-bookmark:before { content: '\e828'; } /* '' */ +.icon-link:before { content: '\e822'; } /* '' */ +.icon-back:before { content: '\e820'; } /* '' */ +.icon-flashlight:before { content: '\e804'; } /* '' */ +.icon-chart-area:before { content: '\e8c7'; } /* '' */ +.icon-clock:before { content: '\e862'; } /* '' */ +.icon-rocket:before { content: '\e8dd'; } /* '' */ +.icon-block:before { content: '\e868'; } /* '' */ diff --git a/guides/content/assets/stylesheets/vendor/bourbon/_bourbon-deprecated-upcoming.scss b/guides/content/assets/stylesheets/vendor/bourbon/_bourbon-deprecated-upcoming.scss new file mode 100644 index 00000000000..c2140b713dc --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/_bourbon-deprecated-upcoming.scss @@ -0,0 +1,8 @@ +//************************************************************************// +// These mixins/functions are deprecated +// They will be removed in the next MAJOR version release +//************************************************************************// +@mixin box-shadow ($shadows...) { + @include prefixer(box-shadow, $shadows, spec); + @warn "box-shadow is deprecated and will be removed in the next major version release"; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/_bourbon.scss b/guides/content/assets/stylesheets/vendor/bourbon/_bourbon.scss new file mode 100755 index 00000000000..7b110809097 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/_bourbon.scss @@ -0,0 +1,52 @@ +// Custom Functions +@import "functions/compact"; +@import "functions/deprecated-webkit-gradient"; +@import "functions/flex-grid"; +@import "functions/grid-width"; +@import "functions/linear-gradient"; +@import "functions/modular-scale"; +@import "functions/px-to-em"; +@import "functions/radial-gradient"; +@import "functions/render-gradients"; +@import "functions/tint-shade"; +@import "functions/transition-property-name"; + +// CSS3 Mixins +@import "css3/animation"; +@import "css3/appearance"; +@import "css3/background"; +@import "css3/background-image"; +@import "css3/background-size"; +@import "css3/border-image"; +@import "css3/border-radius"; +@import "css3/box-sizing"; +@import "css3/columns"; +@import "css3/flex-box"; +@import "css3/font-face"; +@import "css3/hidpi-media-query"; +@import "css3/image-rendering"; +@import "css3/inline-block"; +@import "css3/keyframes"; +@import "css3/linear-gradient"; +@import "css3/perspective"; +@import "css3/radial-gradient"; +@import "css3/transform"; +@import "css3/transition"; +@import "css3/user-select"; +@import "css3/placeholder"; + +// Addons & other mixins +@import "addons/button"; +@import "addons/clearfix"; +@import "addons/font-family"; +@import "addons/hide-text"; +@import "addons/html5-input-types"; +@import "addons/position"; +@import "addons/prefixer"; +@import "addons/retina-image"; +@import "addons/size"; +@import "addons/timing-functions"; +@import "addons/triangle"; + +// Soon to be deprecated Mixins +@import "bourbon-deprecated-upcoming"; diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_button.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_button.scss new file mode 100644 index 00000000000..5e4587cda04 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_button.scss @@ -0,0 +1,273 @@ +@mixin button ($style: simple, $base-color: #4294f0) { + + @if type-of($style) == color { + $base-color: $style; + $style: simple; + } + + // Grayscale button + @if $base-color == grayscale($base-color) { + @if $style == simple { + @include simple($base-color, $grayscale: true); + } + + @else if $style == shiny { + @include shiny($base-color, $grayscale: true); + } + + @else if $style == pill { + @include pill($base-color, $grayscale: true); + } + } + + // Colored button + @else { + @if $style == simple { + @include simple($base-color); + } + + @else if $style == shiny { + @include shiny($base-color); + } + + @else if $style == pill { + @include pill($base-color); + } + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + + +// Simple Button +//************************************************************************// +@mixin simple($base-color, $grayscale: false) { + $color: hsl(0, 0, 100%); + $border: adjust-color($base-color, $saturation: 9%, $lightness: -14%); + $inset-shadow: adjust-color($base-color, $saturation: -8%, $lightness: 15%); + $stop-gradient: adjust-color($base-color, $saturation: 9%, $lightness: -11%); + $text-shadow: adjust-color($base-color, $saturation: 15%, $lightness: -18%); + + @if lightness($base-color) > 70% { + $color: hsl(0, 0, 20%); + $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); + } + + @if $grayscale == true { + $border: grayscale($border); + $inset-shadow: grayscale($inset-shadow); + $stop-gradient: grayscale($stop-gradient); + $text-shadow: grayscale($text-shadow); + } + + border: 1px solid $border; + border-radius: 3px; + box-shadow: inset 0 1px 0 0 $inset-shadow; + color: $color; + display: inline-block; + font-size: 11px; + font-weight: bold; + @include linear-gradient ($base-color, $stop-gradient); + padding: 7px 18px; + text-decoration: none; + text-shadow: 0 1px 0 $text-shadow; + -webkit-background-clip: padding-box; + + &:hover:not(:disabled) { + $base-color-hover: adjust-color($base-color, $saturation: -4%, $lightness: -5%); + $inset-shadow-hover: adjust-color($base-color, $saturation: -7%, $lightness: 5%); + $stop-gradient-hover: adjust-color($base-color, $saturation: 8%, $lightness: -14%); + + @if $grayscale == true { + $base-color-hover: grayscale($base-color-hover); + $inset-shadow-hover: grayscale($inset-shadow-hover); + $stop-gradient-hover: grayscale($stop-gradient-hover); + } + + box-shadow: inset 0 1px 0 0 $inset-shadow-hover; + cursor: pointer; + @include linear-gradient ($base-color-hover, $stop-gradient-hover); + } + + &:active:not(:disabled) { + $border-active: adjust-color($base-color, $saturation: 9%, $lightness: -14%); + $inset-shadow-active: adjust-color($base-color, $saturation: 7%, $lightness: -17%); + + @if $grayscale == true { + $border-active: grayscale($border-active); + $inset-shadow-active: grayscale($inset-shadow-active); + } + + border: 1px solid $border-active; + box-shadow: inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active, 0 1px 1px 0 #eee; + } +} + + +// Shiny Button +//************************************************************************// +@mixin shiny($base-color, $grayscale: false) { + $color: hsl(0, 0, 100%); + $border: adjust-color($base-color, $red: -117, $green: -111, $blue: -81); + $border-bottom: adjust-color($base-color, $red: -126, $green: -127, $blue: -122); + $fourth-stop: adjust-color($base-color, $red: -79, $green: -70, $blue: -46); + $inset-shadow: adjust-color($base-color, $red: 37, $green: 29, $blue: 12); + $second-stop: adjust-color($base-color, $red: -56, $green: -50, $blue: -33); + $text-shadow: adjust-color($base-color, $red: -140, $green: -141, $blue: -114); + $third-stop: adjust-color($base-color, $red: -86, $green: -75, $blue: -48); + + @if lightness($base-color) > 70% { + $color: hsl(0, 0, 20%); + $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); + } + + @if $grayscale == true { + $border: grayscale($border); + $border-bottom: grayscale($border-bottom); + $fourth-stop: grayscale($fourth-stop); + $inset-shadow: grayscale($inset-shadow); + $second-stop: grayscale($second-stop); + $text-shadow: grayscale($text-shadow); + $third-stop: grayscale($third-stop); + } + + border: 1px solid $border; + border-bottom: 1px solid $border-bottom; + border-radius: 5px; + box-shadow: inset 0 1px 0 0 $inset-shadow; + color: $color; + display: inline-block; + font-size: 14px; + font-weight: bold; + @include linear-gradient(top, $base-color 0%, $second-stop 50%, $third-stop 50%, $fourth-stop 100%); + padding: 8px 20px; + text-align: center; + text-decoration: none; + text-shadow: 0 -1px 1px $text-shadow; + + &:hover:not(:disabled) { + $first-stop-hover: adjust-color($base-color, $red: -13, $green: -15, $blue: -18); + $second-stop-hover: adjust-color($base-color, $red: -66, $green: -62, $blue: -51); + $third-stop-hover: adjust-color($base-color, $red: -93, $green: -85, $blue: -66); + $fourth-stop-hover: adjust-color($base-color, $red: -86, $green: -80, $blue: -63); + + @if $grayscale == true { + $first-stop-hover: grayscale($first-stop-hover); + $second-stop-hover: grayscale($second-stop-hover); + $third-stop-hover: grayscale($third-stop-hover); + $fourth-stop-hover: grayscale($fourth-stop-hover); + } + + cursor: pointer; + @include linear-gradient(top, $first-stop-hover 0%, + $second-stop-hover 50%, + $third-stop-hover 50%, + $fourth-stop-hover 100%); + } + + &:active:not(:disabled) { + $inset-shadow-active: adjust-color($base-color, $red: -111, $green: -116, $blue: -122); + + @if $grayscale == true { + $inset-shadow-active: grayscale($inset-shadow-active); + } + + box-shadow: inset 0 0 20px 0 $inset-shadow-active, 0 1px 0 #fff; + } +} + + +// Pill Button +//************************************************************************// +@mixin pill($base-color, $grayscale: false) { + $color: hsl(0, 0, 100%); + $border-bottom: adjust-color($base-color, $hue: 8, $saturation: -11%, $lightness: -26%); + $border-sides: adjust-color($base-color, $hue: 4, $saturation: -21%, $lightness: -21%); + $border-top: adjust-color($base-color, $hue: -1, $saturation: -30%, $lightness: -15%); + $inset-shadow: adjust-color($base-color, $hue: -1, $saturation: -1%, $lightness: 7%); + $stop-gradient: adjust-color($base-color, $hue: 8, $saturation: 14%, $lightness: -10%); + $text-shadow: adjust-color($base-color, $hue: 5, $saturation: -19%, $lightness: -15%); + + @if lightness($base-color) > 70% { + $color: hsl(0, 0, 20%); + $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); + } + + @if $grayscale == true { + $border-bottom: grayscale($border-bottom); + $border-sides: grayscale($border-sides); + $border-top: grayscale($border-top); + $inset-shadow: grayscale($inset-shadow); + $stop-gradient: grayscale($stop-gradient); + $text-shadow: grayscale($text-shadow); + } + + border: 1px solid $border-top; + border-color: $border-top $border-sides $border-bottom; + border-radius: 16px; + box-shadow: inset 0 1px 0 0 $inset-shadow, 0 1px 2px 0 #b3b3b3; + color: $color; + display: inline-block; + font-size: 11px; + font-weight: normal; + line-height: 1; + @include linear-gradient ($base-color, $stop-gradient); + padding: 5px 16px; + text-align: center; + text-decoration: none; + text-shadow: 0 -1px 1px $text-shadow; + -webkit-background-clip: padding-box; + + &:hover:not(:disabled) { + $base-color-hover: adjust-color($base-color, $lightness: -4.5%); + $border-bottom: adjust-color($base-color, $hue: 8, $saturation: 13.5%, $lightness: -32%); + $border-sides: adjust-color($base-color, $hue: 4, $saturation: -2%, $lightness: -27%); + $border-top: adjust-color($base-color, $hue: -1, $saturation: -17%, $lightness: -21%); + $inset-shadow-hover: adjust-color($base-color, $saturation: -1%, $lightness: 3%); + $stop-gradient-hover: adjust-color($base-color, $hue: 8, $saturation: -4%, $lightness: -15.5%); + $text-shadow-hover: adjust-color($base-color, $hue: 5, $saturation: -5%, $lightness: -22%); + + @if $grayscale == true { + $base-color-hover: grayscale($base-color-hover); + $border-bottom: grayscale($border-bottom); + $border-sides: grayscale($border-sides); + $border-top: grayscale($border-top); + $inset-shadow-hover: grayscale($inset-shadow-hover); + $stop-gradient-hover: grayscale($stop-gradient-hover); + $text-shadow-hover: grayscale($text-shadow-hover); + } + + border: 1px solid $border-top; + border-color: $border-top $border-sides $border-bottom; + box-shadow: inset 0 1px 0 0 $inset-shadow-hover; + cursor: pointer; + @include linear-gradient ($base-color-hover, $stop-gradient-hover); + text-shadow: 0 -1px 1px $text-shadow-hover; + -webkit-background-clip: padding-box; + } + + &:active:not(:disabled) { + $active-color: adjust-color($base-color, $hue: 4, $saturation: -12%, $lightness: -10%); + $border-active: adjust-color($base-color, $hue: 6, $saturation: -2.5%, $lightness: -30%); + $border-bottom-active: adjust-color($base-color, $hue: 11, $saturation: 6%, $lightness: -31%); + $inset-shadow-active: adjust-color($base-color, $hue: 9, $saturation: 2%, $lightness: -21.5%); + $text-shadow-active: adjust-color($base-color, $hue: 5, $saturation: -12%, $lightness: -21.5%); + + @if $grayscale == true { + $active-color: grayscale($active-color); + $border-active: grayscale($border-active); + $border-bottom-active: grayscale($border-bottom-active); + $inset-shadow-active: grayscale($inset-shadow-active); + $text-shadow-active: grayscale($text-shadow-active); + } + + background: $active-color; + border: 1px solid $border-active; + border-bottom: 1px solid $border-bottom-active; + box-shadow: inset 0 0 6px 3px $inset-shadow-active, 0 1px 0 0 #fff; + text-shadow: 0 -1px 1px $text-shadow-active; + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_clearfix.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_clearfix.scss new file mode 100644 index 00000000000..ca9903cf02e --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_clearfix.scss @@ -0,0 +1,29 @@ +// Micro clearfix provides an easy way to contain floats without adding additional markup +// +// Example usage: +// +// // Contain all floats within .wrapper +// .wrapper { +// @include clearfix; +// .content, +// .sidebar { +// float : left; +// } +// } + +@mixin clearfix { + *zoom: 1; + + &:before, + &:after { + content: " "; + display: table; + } + + &:after { + clear: both; + } +} + +// Acknowledgements +// Micro clearfix: [Nicolas Gallagher](http://nicolasgallagher.com/micro-clearfix-hack/) diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_font-family.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_font-family.scss new file mode 100644 index 00000000000..df8a80ddfcf --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_font-family.scss @@ -0,0 +1,5 @@ +$georgia: Georgia, Cambria, "Times New Roman", Times, serif; +$helvetica: "Helvetica Neue", Helvetica, Arial, sans-serif; +$lucida-grande: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif; +$monospace: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; +$verdana: Verdana, Geneva, sans-serif; diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_hide-text.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_hide-text.scss new file mode 100644 index 00000000000..d334403b97c --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_hide-text.scss @@ -0,0 +1,13 @@ +@mixin hide-text { + color: transparent; + font: 0/0 a; + text-shadow: none; +} + +// A CSS image replacement method that does not require the use of text-indent. +// +// Examples +// +// .ir { +// @include hide-text; +// } diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_html5-input-types.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_html5-input-types.scss new file mode 100644 index 00000000000..b184382d910 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_html5-input-types.scss @@ -0,0 +1,56 @@ +//************************************************************************// +// Generate a variable ($all-text-inputs) with a list of all html5 +// input types that have a text-based input, excluding textarea. +// http://diveintohtml5.org/forms.html +//************************************************************************// +$inputs-list: 'input[type="email"]', + 'input[type="number"]', + 'input[type="password"]', + 'input[type="search"]', + 'input[type="tel"]', + 'input[type="text"]', + 'input[type="url"]', + + // Webkit & Gecko may change the display of these in the future + 'input[type="color"]', + 'input[type="date"]', + 'input[type="datetime"]', + 'input[type="datetime-local"]', + 'input[type="month"]', + 'input[type="time"]', + 'input[type="week"]'; + +$unquoted-inputs-list: (); +@each $input-type in $inputs-list { + $unquoted-inputs-list: append($unquoted-inputs-list, unquote($input-type), comma); +} + +$all-text-inputs: $unquoted-inputs-list; + + +// Hover Pseudo-class +//************************************************************************// +$all-text-inputs-hover: (); +@each $input-type in $unquoted-inputs-list { + $input-type-hover: $input-type + ":hover"; + $all-text-inputs-hover: append($all-text-inputs-hover, $input-type-hover, comma); +} + +// Focus Pseudo-class +//************************************************************************// +$all-text-inputs-focus: (); +@each $input-type in $unquoted-inputs-list { + $input-type-focus: $input-type + ":focus"; + $all-text-inputs-focus: append($all-text-inputs-focus, $input-type-focus, comma); +} + +// You must use interpolation on the variable: +// #{$all-text-inputs} +// #{$all-text-inputs-hover} +// #{$all-text-inputs-focus} + +// Example +//************************************************************************// +// #{$all-text-inputs}, textarea { +// border: 1px solid red; +// } diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_position.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_position.scss new file mode 100644 index 00000000000..faad1cae50a --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_position.scss @@ -0,0 +1,42 @@ +@mixin position ($position: relative, $coordinates: 0 0 0 0) { + + @if type-of($position) == list { + $coordinates: $position; + $position: relative; + } + + $top: nth($coordinates, 1); + $right: nth($coordinates, 2); + $bottom: nth($coordinates, 3); + $left: nth($coordinates, 4); + + position: $position; + + @if $top == auto { + top: $top; + } + @else if not(unitless($top)) { + top: $top; + } + + @if $right == auto { + right: $right; + } + @else if not(unitless($right)) { + right: $right; + } + + @if $bottom == auto { + bottom: $bottom; + } + @else if not(unitless($bottom)) { + bottom: $bottom; + } + + @if $left == auto { + left: $left; + } + @else if not(unitless($left)) { + left: $left; + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_prefixer.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_prefixer.scss new file mode 100644 index 00000000000..203b71aaa4f --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_prefixer.scss @@ -0,0 +1,40 @@ +//************************************************************************// +// Example: @include prefixer(border-radius, $radii, webkit ms spec); +//************************************************************************// +$prefix-for-webkit: true !default; +$prefix-for-mozilla: true !default; +$prefix-for-microsoft: true !default; +$prefix-for-opera: true !default; +$prefix-for-spec: true !default; // required for keyframe mixin + +@mixin prefixer ($property, $value, $prefixes) { + @each $prefix in $prefixes { + + @if $prefix == webkit and $prefix-for-webkit == true { + -webkit-#{$property}: $value; + } + @else if $prefix == moz and $prefix-for-mozilla == true { + -moz-#{$property}: $value; + } + @else if $prefix == ms and $prefix-for-microsoft == true { + -ms-#{$property}: $value; + } + @else if $prefix == o and $prefix-for-opera == true { + -o-#{$property}: $value; + } + @else if $prefix == spec and $prefix-for-spec == true { + #{$property}: $value; + } + @else { + @warn "Unrecognized prefix: #{$prefix}"; + } + } +} + +@mixin disable-prefix-for-all() { + $prefix-for-webkit: false; + $prefix-for-mozilla: false; + $prefix-for-microsoft: false; + $prefix-for-opera: false; + $prefix-for-spec: false; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_retina-image.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_retina-image.scss new file mode 100644 index 00000000000..ed300715e43 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_retina-image.scss @@ -0,0 +1,32 @@ +@mixin retina-image($filename, $background-size, $extension: png, $retina-filename: null, $asset-pipeline: false) { + @if $asset-pipeline { + background-image: image_url($filename + "." + $extension); + } + @else { + background-image: url($filename + "." + $extension); + } + + @include hidpi { + + @if $asset-pipeline { + @if $retina-filename { + background-image: image_url($retina-filename + "." + $extension); + } + @else { + background-image: image_url($filename + "@2x" + "." + $extension); + } + } + + @else { + @if $retina-filename { + background-image: url($retina-filename + "." + $extension); + } + @else { + background-image: url($filename + "@2x" + "." + $extension); + } + } + + background-size: $background-size; + + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_size.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_size.scss new file mode 100644 index 00000000000..342e41b79f1 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_size.scss @@ -0,0 +1,44 @@ +@mixin size($size) { + @if length($size) == 1 { + @if $size == auto { + width: $size; + height: $size; + } + + @else if unitless($size) { + width: $size + px; + height: $size + px; + } + + @else if not(unitless($size)) { + width: $size; + height: $size; + } + } + + // Width x Height + @if length($size) == 2 { + $width: nth($size, 1); + $height: nth($size, 2); + + @if $width == auto { + width: $width; + } + @else if not(unitless($width)) { + width: $width; + } + @else if unitless($width) { + width: $width + px; + } + + @if $height == auto { + height: $height; + } + @else if not(unitless($height)) { + height: $height; + } + @else if unitless($height) { + height: $height + px; + } + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_timing-functions.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_timing-functions.scss new file mode 100644 index 00000000000..51b24109149 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_timing-functions.scss @@ -0,0 +1,32 @@ +// CSS cubic-bezier timing functions. Timing functions courtesy of jquery.easie (github.com/jaukia/easie) +// Timing functions are the same as demo'ed here: http://jqueryui.com/demos/effect/easing.html + +// EASE IN +$ease-in-quad: cubic-bezier(0.550, 0.085, 0.680, 0.530); +$ease-in-cubic: cubic-bezier(0.550, 0.055, 0.675, 0.190); +$ease-in-quart: cubic-bezier(0.895, 0.030, 0.685, 0.220); +$ease-in-quint: cubic-bezier(0.755, 0.050, 0.855, 0.060); +$ease-in-sine: cubic-bezier(0.470, 0.000, 0.745, 0.715); +$ease-in-expo: cubic-bezier(0.950, 0.050, 0.795, 0.035); +$ease-in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335); +$ease-in-back: cubic-bezier(0.600, -0.280, 0.735, 0.045); + +// EASE OUT +$ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940); +$ease-out-cubic: cubic-bezier(0.215, 0.610, 0.355, 1.000); +$ease-out-quart: cubic-bezier(0.165, 0.840, 0.440, 1.000); +$ease-out-quint: cubic-bezier(0.230, 1.000, 0.320, 1.000); +$ease-out-sine: cubic-bezier(0.390, 0.575, 0.565, 1.000); +$ease-out-expo: cubic-bezier(0.190, 1.000, 0.220, 1.000); +$ease-out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000); +$ease-out-back: cubic-bezier(0.175, 0.885, 0.320, 1.275); + +// EASE IN OUT +$ease-in-out-quad: cubic-bezier(0.455, 0.030, 0.515, 0.955); +$ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1.000); +$ease-in-out-quart: cubic-bezier(0.770, 0.000, 0.175, 1.000); +$ease-in-out-quint: cubic-bezier(0.860, 0.000, 0.070, 1.000); +$ease-in-out-sine: cubic-bezier(0.445, 0.050, 0.550, 0.950); +$ease-in-out-expo: cubic-bezier(1.000, 0.000, 0.000, 1.000); +$ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860); +$ease-in-out-back: cubic-bezier(0.680, -0.550, 0.265, 1.550); diff --git a/guides/content/assets/stylesheets/vendor/bourbon/addons/_triangle.scss b/guides/content/assets/stylesheets/vendor/bourbon/addons/_triangle.scss new file mode 100644 index 00000000000..0e02aca2ca5 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/addons/_triangle.scss @@ -0,0 +1,45 @@ +@mixin triangle ($size, $color, $direction) { + height: 0; + width: 0; + + @if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) { + border-color: transparent; + border-style: solid; + border-width: $size / 2; + + @if $direction == up { + border-bottom-color: $color; + + } @else if $direction == right { + border-left-color: $color; + + } @else if $direction == down { + border-top-color: $color; + + } @else if $direction == left { + border-right-color: $color; + } + } + + @else if ($direction == up-right) or ($direction == up-left) { + border-top: $size solid $color; + + @if $direction == up-right { + border-left: $size solid transparent; + + } @else if $direction == up-left { + border-right: $size solid transparent; + } + } + + @else if ($direction == down-right) or ($direction == down-left) { + border-bottom: $size solid $color; + + @if $direction == down-right { + border-left: $size solid transparent; + + } @else if $direction == down-left { + border-right: $size solid transparent; + } + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_animation.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_animation.scss new file mode 100644 index 00000000000..08c3dbf157c --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_animation.scss @@ -0,0 +1,52 @@ +// http://www.w3.org/TR/css3-animations/#the-animation-name-property- +// Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties. + +// Official animation shorthand property. +@mixin animation ($animations...) { + @include prefixer(animation, $animations, webkit moz spec); +} + +// Individual Animation Properties +@mixin animation-name ($names...) { + @include prefixer(animation-name, $names, webkit moz spec); +} + + +@mixin animation-duration ($times...) { + @include prefixer(animation-duration, $times, webkit moz spec); +} + + +@mixin animation-timing-function ($motions...) { +// ease | linear | ease-in | ease-out | ease-in-out + @include prefixer(animation-timing-function, $motions, webkit moz spec); +} + + +@mixin animation-iteration-count ($values...) { +// infinite | + @include prefixer(animation-iteration-count, $values, webkit moz spec); +} + + +@mixin animation-direction ($directions...) { +// normal | alternate + @include prefixer(animation-direction, $directions, webkit moz spec); +} + + +@mixin animation-play-state ($states...) { +// running | paused + @include prefixer(animation-play-state, $states, webkit moz spec); +} + + +@mixin animation-delay ($times...) { + @include prefixer(animation-delay, $times, webkit moz spec); +} + + +@mixin animation-fill-mode ($modes...) { +// none | forwards | backwards | both + @include prefixer(animation-fill-mode, $modes, webkit moz spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_appearance.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_appearance.scss new file mode 100644 index 00000000000..3eb16e45de7 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_appearance.scss @@ -0,0 +1,3 @@ +@mixin appearance ($value) { + @include prefixer(appearance, $value, webkit moz ms o spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_background-image.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_background-image.scss new file mode 100644 index 00000000000..5f199d6da65 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_background-image.scss @@ -0,0 +1,44 @@ +//************************************************************************// +// Background-image property for adding multiple background images with +// gradients, or for stringing multiple gradients together. +//************************************************************************// + +@mixin background-image($images...) { + background-image: add-prefix($images, webkit); + background-image: add-prefix($images, moz); + background-image: add-prefix($images, ms); + background-image: add-prefix($images, o); + background-image: add-prefix($images); +} + + +@function add-prefix($images, $vendor: false) { + $images-prefixed: (); + + @for $i from 1 through length($images) { + $type: type-of(nth($images, $i)); // Get type of variable - List or String + + // If variable is a list - Gradient + @if $type == list { + $gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial) + $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) + + $gradient: render-gradients($gradient-args, $gradient-type, $vendor); + $images-prefixed: append($images-prefixed, $gradient, comma); + } + + // If variable is a string - Image + @else if $type == string { + $images-prefixed: join($images-prefixed, nth($images, $i), comma); + } + } + @return $images-prefixed; +} + + +//Examples: + //@include background-image(linear-gradient(top, orange, red)); + //@include background-image(radial-gradient(50% 50%, cover circle, orange, red)); + //@include background-image(url("/images/a.png"), linear-gradient(orange, red)); + //@include background-image(url("image.png"), linear-gradient(orange, red), url("image.png")); + //@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red)); diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_background-size.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_background-size.scss new file mode 100644 index 00000000000..f8834094f67 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_background-size.scss @@ -0,0 +1,3 @@ +@mixin background-size ($lengths...) { + @include prefixer(background-size, $lengths, webkit moz ms o spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_background.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_background.scss new file mode 100644 index 00000000000..70d0d7ea906 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_background.scss @@ -0,0 +1,107 @@ +//************************************************************************// +// Background property for adding multiple backgrounds using shorthand +// notation. +//************************************************************************// + +@mixin background( + $background-1 , $background-2: false, + $background-3: false, $background-4: false, + $background-5: false, $background-6: false, + $background-7: false, $background-8: false, + $background-9: false, $background-10: false, + $fallback: false +) { + $backgrounds: compact($background-1, $background-2, + $background-3, $background-4, + $background-5, $background-6, + $background-7, $background-8, + $background-9, $background-10); + + $fallback-color: false; + @if (type-of($fallback) == color) or ($fallback == "transparent") { + $fallback-color: $fallback; + } + @else { + $fallback-color: extract-background-color($backgrounds); + } + + @if $fallback-color { + background-color: $fallback-color; + } + background: background-add-prefix($backgrounds, webkit); + background: background-add-prefix($backgrounds, moz); + background: background-add-prefix($backgrounds, ms); + background: background-add-prefix($backgrounds, o); + background: background-add-prefix($backgrounds); +} + +@function extract-background-color($backgrounds) { + $final-bg-layer: nth($backgrounds, length($backgrounds)); + @if type-of($final-bg-layer) == list { + @for $i from 1 through length($final-bg-layer) { + $value: nth($final-bg-layer, $i); + @if type-of($value) == color { + @return $value; + } + } + } + @return false; +} + + +@function background-add-prefix($backgrounds, $vendor: false) { + $backgrounds-prefixed: (); + + @for $i from 1 through length($backgrounds) { + $shorthand: nth($backgrounds, $i); // Get member for current index + $type: type-of($shorthand); // Get type of variable - List or String + + // If shorthand is a list + @if $type == list { + $first-member: nth($shorthand, 1); // Get first member of shorthand + + // Linear Gradient + @if index(linear radial, nth($first-member, 1)) { + $gradient-type: nth($first-member, 1); // linear || radial + + // Get actual gradient (red, blue) + $gradient-args: false; + $shorthand-start: false; + // Linear gradient and positioning, repeat, etc. values + @if type-of($first-member) == list { + $gradient-args: nth($first-member, 2); + $shorthand-start: 2; + } + // Linear gradient only + @else { + $gradient-args: nth($shorthand, 2); // Get actual gradient (red, blue) + $shorthand-start: 3; + } + + $gradient: render-gradients($gradient-args, $gradient-type, $vendor); + @for $j from $shorthand-start through length($shorthand) { + $gradient: join($gradient, nth($shorthand, $j), space); + } + $backgrounds-prefixed: append($backgrounds-prefixed, $gradient, comma); + } + + // Image with additional properties + @else { + $backgrounds-prefixed: append($backgrounds-prefixed, $shorthand, comma); + } + + } + + // If shorthand is a simple string, color or image + @else if $type == string { + $backgrounds-prefixed: join($backgrounds-prefixed, $shorthand, comma); + } + } + @return $backgrounds-prefixed; +} + +//Examples: + //@include background(linear-gradient(top, orange, red)); + //@include background(radial-gradient(50% 50%, cover circle, orange, red)); + //@include background(url("/images/a.png") no-repeat, linear-gradient(orange, red)); + //@include background(url("image.png") center center, linear-gradient(orange, red), url("image.png")); diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_border-image.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_border-image.scss new file mode 100644 index 00000000000..da4f20ba492 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_border-image.scss @@ -0,0 +1,56 @@ +@mixin border-image($images) { + -webkit-border-image: border-add-prefix($images, webkit); + -moz-border-image: border-add-prefix($images, moz); + -o-border-image: border-add-prefix($images, o); + border-image: border-add-prefix($images); +} + +@function border-add-prefix($images, $vendor: false) { + $border-image: (); + $images-type: type-of(nth($images, 1)); + $first-var: nth(nth($images, 1), 1); // Get type of Gradient (Linear || radial) + + // If input is a gradient + @if $images-type == string { + @if ($first-var == "linear") or ($first-var == "radial") { + @for $i from 2 through length($images) { + $gradient-type: nth($images, 1); // Get type of gradient (linear || radial) + $gradient-args: nth($images, $i); // Get actual gradient (red, blue) + $border-image: render-gradients($gradient-args, $gradient-type, $vendor); + } + } + + // If input is a URL + @else { + $border-image: $images; + } + } + + // If input is gradient or url + additional args + @else if $images-type == list { + @for $i from 1 through length($images) { + $type: type-of(nth($images, $i)); // Get type of variable - List or String + + // If variable is a list - Gradient + @if $type == list { + $gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial) + $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) + $border-image: render-gradients($gradient-args, $gradient-type, $vendor); + } + + // If variable is a string - Image or number + @else if ($type == string) or ($type == number) { + $border-image: append($border-image, nth($images, $i)); + } + } + } + @return $border-image; +} + +//Examples: +// @include border-image(url("image.png")); +// @include border-image(url("image.png") 20 stretch); +// @include border-image(linear-gradient(45deg, orange, yellow)); +// @include border-image(linear-gradient(45deg, orange, yellow) stretch); +// @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round); +// @include border-image(radial-gradient(top, cover, orange, yellow, orange)); diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_border-radius.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_border-radius.scss new file mode 100644 index 00000000000..7c171901090 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_border-radius.scss @@ -0,0 +1,22 @@ +//************************************************************************// +// Shorthand Border-radius mixins +//************************************************************************// +@mixin border-top-radius($radii) { + @include prefixer(border-top-left-radius, $radii, spec); + @include prefixer(border-top-right-radius, $radii, spec); +} + +@mixin border-bottom-radius($radii) { + @include prefixer(border-bottom-left-radius, $radii, spec); + @include prefixer(border-bottom-right-radius, $radii, spec); +} + +@mixin border-left-radius($radii) { + @include prefixer(border-top-left-radius, $radii, spec); + @include prefixer(border-bottom-left-radius, $radii, spec); +} + +@mixin border-right-radius($radii) { + @include prefixer(border-top-right-radius, $radii, spec); + @include prefixer(border-bottom-right-radius, $radii, spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_box-sizing.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_box-sizing.scss new file mode 100644 index 00000000000..f07e1d412e3 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_box-sizing.scss @@ -0,0 +1,4 @@ +@mixin box-sizing ($box) { +// content-box | border-box | inherit + @include prefixer(box-sizing, $box, webkit moz spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_columns.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_columns.scss new file mode 100644 index 00000000000..42274a4eebb --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_columns.scss @@ -0,0 +1,47 @@ +@mixin columns($arg: auto) { +// || + @include prefixer(columns, $arg, webkit moz spec); +} + +@mixin column-count($int: auto) { +// auto || integer + @include prefixer(column-count, $int, webkit moz spec); +} + +@mixin column-gap($length: normal) { +// normal || length + @include prefixer(column-gap, $length, webkit moz spec); +} + +@mixin column-fill($arg: auto) { +// auto || length + @include prefixer(columns-fill, $arg, webkit moz spec); +} + +@mixin column-rule($arg) { +// || || + @include prefixer(column-rule, $arg, webkit moz spec); +} + +@mixin column-rule-color($color) { + @include prefixer(column-rule-color, $color, webkit moz spec); +} + +@mixin column-rule-style($style: none) { +// none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid + @include prefixer(column-rule-style, $style, webkit moz spec); +} + +@mixin column-rule-width ($width: none) { + @include prefixer(column-rule-width, $width, webkit moz spec); +} + +@mixin column-span($arg: none) { +// none || all + @include prefixer(column-span, $arg, webkit moz spec); +} + +@mixin column-width($length: auto) { +// auto || length + @include prefixer(column-width, $length, webkit moz spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_flex-box.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_flex-box.scss new file mode 100644 index 00000000000..3e741e66966 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_flex-box.scss @@ -0,0 +1,52 @@ +// CSS3 Flexible Box Model and property defaults + +// Custom shorthand notation for flexbox +@mixin box($orient: inline-axis, $pack: start, $align: stretch) { + @include display-box; + @include box-orient($orient); + @include box-pack($pack); + @include box-align($align); +} + +@mixin display-box { + display: -webkit-box; + display: -moz-box; + display: box; +} + +@mixin box-orient($orient: inline-axis) { +// horizontal|vertical|inline-axis|block-axis|inherit + @include prefixer(box-orient, $orient, webkit moz spec); +} + +@mixin box-pack($pack: start) { +// start|end|center|justify + @include prefixer(box-pack, $pack, webkit moz spec); +} + +@mixin box-align($align: stretch) { +// start|end|center|baseline|stretch + @include prefixer(box-align, $align, webkit moz spec); +} + +@mixin box-direction($direction: normal) { +// normal|reverse|inherit + @include prefixer(box-direction, $direction, webkit moz spec); +} + +@mixin box-lines($lines: single) { +// single|multiple + @include prefixer(box-lines, $lines, webkit moz spec); +} + +@mixin box-ordinal-group($int: 1) { + @include prefixer(box-ordinal-group, $int, webkit moz spec); +} + +@mixin box-flex($value: 0.0) { + @include prefixer(box-flex, $value, webkit moz spec); +} + +@mixin box-flex-group($int: 1) { + @include prefixer(box-flex-group, $int, webkit moz spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_font-face.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_font-face.scss new file mode 100644 index 00000000000..029ee8fe88c --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_font-face.scss @@ -0,0 +1,23 @@ +// Order of the includes matters, and it is: normal, bold, italic, bold+italic. + +@mixin font-face($font-family, $file-path, $weight: normal, $style: normal, $asset-pipeline: false ) { + @font-face { + font-family: $font-family; + font-weight: $weight; + font-style: $style; + + @if $asset-pipeline == true { + src: font-url('#{$file-path}.eot'); + src: font-url('#{$file-path}.eot?#iefix') format('embedded-opentype'), + font-url('#{$file-path}.woff') format('woff'), + font-url('#{$file-path}.ttf') format('truetype'), + font-url('#{$file-path}.svg##{$font-family}') format('svg'); + } @else { + src: url('#{$file-path}.eot'); + src: url('#{$file-path}.eot?#iefix') format('embedded-opentype'), + url('#{$file-path}.woff') format('woff'), + url('#{$file-path}.ttf') format('truetype'), + url('#{$file-path}.svg##{$font-family}') format('svg'); + } + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_hidpi-media-query.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_hidpi-media-query.scss new file mode 100644 index 00000000000..111e4009b5a --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_hidpi-media-query.scss @@ -0,0 +1,10 @@ +// HiDPI mixin. Default value set to 1.3 to target Google Nexus 7 (http://bjango.com/articles/min-device-pixel-ratio/) +@mixin hidpi($ratio: 1.3) { + @media only screen and (-webkit-min-device-pixel-ratio: $ratio), + only screen and (min--moz-device-pixel-ratio: $ratio), + only screen and (-o-min-device-pixel-ratio: #{$ratio}/1), + only screen and (min-resolution: #{round($ratio*96)}dpi), + only screen and (min-resolution: #{$ratio}dppx) { + @content; + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_image-rendering.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_image-rendering.scss new file mode 100644 index 00000000000..abc7ee1aa47 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_image-rendering.scss @@ -0,0 +1,13 @@ +@mixin image-rendering ($mode:optimizeQuality) { + + @if ($mode == optimize-contrast) { + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: optimize-contrast; + } + + @else { + image-rendering: $mode; + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_inline-block.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_inline-block.scss new file mode 100644 index 00000000000..3272a0010b3 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_inline-block.scss @@ -0,0 +1,8 @@ +// Legacy support for inline-block in IE7 (maybe IE6) +@mixin inline-block { + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_keyframes.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_keyframes.scss new file mode 100644 index 00000000000..1585fe20dad --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_keyframes.scss @@ -0,0 +1,51 @@ +// Adds keyframes blocks for supported prefixes, removing redundant prefixes in the block's content +@mixin keyframes($name) { + $original-prefix-for-webkit: $prefix-for-webkit; + $original-prefix-for-mozilla: $prefix-for-mozilla; + $original-prefix-for-microsoft: $prefix-for-microsoft; + $original-prefix-for-opera: $prefix-for-opera; + $original-prefix-for-spec: $prefix-for-spec; + + @if $original-prefix-for-webkit { + @include disable-prefix-for-all(); + $prefix-for-webkit: true; + @-webkit-keyframes #{$name} { + @content; + } + } + @if $original-prefix-for-mozilla { + @include disable-prefix-for-all(); + $prefix-for-mozilla: true; + @-moz-keyframes #{$name} { + @content; + } + } + @if $original-prefix-for-microsoft { + @include disable-prefix-for-all(); + $prefix-for-microsoft: true; + @-ms-keyframes #{$name} { + @content; + } + } + @if $original-prefix-for-opera { + @include disable-prefix-for-all(); + $prefix-for-opera: true; + @-o-keyframes #{$name} { + @content; + } + } + @if $original-prefix-for-spec { + $prefix-for-spec: true !default; + @include disable-prefix-for-all(); + $prefix-for-spec: true; + @keyframes #{$name} { + @content; + } + } + + $prefix-for-webkit: $original-prefix-for-webkit; + $prefix-for-mozilla: $original-prefix-for-mozilla; + $prefix-for-microsoft: $original-prefix-for-microsoft; + $prefix-for-opera: $original-prefix-for-opera; + $prefix-for-spec: $original-prefix-for-spec; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_linear-gradient.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_linear-gradient.scss new file mode 100644 index 00000000000..4d880d7723c --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_linear-gradient.scss @@ -0,0 +1,43 @@ +@mixin linear-gradient($pos, $G1, $G2: false, + $G3: false, $G4: false, + $G5: false, $G6: false, + $G7: false, $G8: false, + $G9: false, $G10: false, + $deprecated-pos1: left top, + $deprecated-pos2: left bottom, + $fallback: false) { + // Detect what type of value exists in $pos + $pos-type: type-of(nth($pos, 1)); + + // If $pos is missing from mixin, reassign vars and add default position + @if ($pos-type == color) or (nth($pos, 1) == "transparent") { + $G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5; + $G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos; + $pos: top; // Default position + } + + $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); + + // Set $G1 as the default fallback color + $fallback-color: nth($G1, 1); + + // If $fallback is a color use that color as the fallback color + @if (type-of($fallback) == color) or ($fallback == "transparent") { + $fallback-color: $fallback; + } + + background-color: $fallback-color; + background-image: deprecated-webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $full); // Safari <= 5.0 + background-image: -webkit-linear-gradient($pos, $full); // Safari 5.1+, Chrome + background-image: -moz-linear-gradient($pos, $full); + background-image: -ms-linear-gradient($pos, $full); + background-image: -o-linear-gradient($pos, $full); + background-image: unquote("linear-gradient(#{$pos}, #{$full})"); +} + + +// Usage: Gradient position is optional, default is top. Position can be a degree. Color stops are optional as well. +// @include linear-gradient(#1e5799, #2989d8); +// @include linear-gradient(#1e5799, #2989d8, $fallback:#2989d8); +// @include linear-gradient(top, #1e5799 0%, #2989d8 50%); +// @include linear-gradient(50deg, rgba(10, 10, 10, 0.5) 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%); diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_perspective.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_perspective.scss new file mode 100644 index 00000000000..1d0cc0077cc --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_perspective.scss @@ -0,0 +1,8 @@ +@mixin perspective($depth: none) { + // none | + @include prefixer(perspective, $depth, webkit moz o spec); +} + +@mixin perspective-origin($value: 50% 50%) { + @include prefixer(perspective-origin, $value, webkit moz o spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_placeholder.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_placeholder.scss new file mode 100644 index 00000000000..127126a8376 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_placeholder.scss @@ -0,0 +1,18 @@ +$placeholders: '-webkit-input-placeholder', + '-moz-placeholder', + '-ms-input-placeholder'; + +@mixin placeholder { + @each $placeholder in $placeholders { + @if $placeholder == "-webkit-input-placeholder" { + &::#{$placeholder} { + @content; + } + } + @else { + &:#{$placeholder} { + @content; + } + } + } +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_radial-gradient.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_radial-gradient.scss new file mode 100644 index 00000000000..19279b09e15 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_radial-gradient.scss @@ -0,0 +1,78 @@ +// Requires Sass 3.1+ +@mixin radial-gradient($G1, $G2, + $G3: false, $G4: false, + $G5: false, $G6: false, + $G7: false, $G8: false, + $G9: false, $G10: false, + $pos: 50% 50%, + $shape-size: ellipse cover, + $deprecated-pos1: center center, + $deprecated-pos2: center center, + $deprecated-radius1: 0, + $deprecated-radius2: 460, + $fallback: false) { + + @each $value in $G1, $G2 { + $first-val: nth($value, 1); + $pos-type: type-of($first-val); + + @if ($pos-type != color) or ($first-val != "transparent") { + @if ($pos-type == number) + or ($first-val == "center") + or ($first-val == "top") + or ($first-val == "right") + or ($first-val == "bottom") + or ($first-val == "left") { + + $pos: $value; + + @if $pos == $G1 { + $G1: false; + } + } + + @else if + ($first-val == "ellipse") + or ($first-val == "circle") + or ($first-val == "closest-side") + or ($first-val == "closest-corner") + or ($first-val == "farthest-side") + or ($first-val == "farthest-corner") + or ($first-val == "contain") + or ($first-val == "cover") { + + $shape-size: $value; + + @if $value == $G1 { + $G1: false; + } + + @else if $value == $G2 { + $G2: false; + } + } + } + } + + $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); + + // Set $G1 as the default fallback color + $first-color: nth($full, 1); + $fallback-color: nth($first-color, 1); + + @if (type-of($fallback) == color) or ($fallback == "transparent") { + $fallback-color: $fallback; + } + + background-color: $fallback-color; + background-image: deprecated-webkit-gradient(radial, $deprecated-pos1, $deprecated-pos2, $full, $deprecated-radius1, $deprecated-radius2); // Safari <= 5.0 + background-image: -webkit-radial-gradient($pos, $shape-size, $full); + background-image: -moz-radial-gradient($pos, $shape-size, $full); + background-image: -ms-radial-gradient($pos, $shape-size, $full); + background-image: -o-radial-gradient($pos, $shape-size, $full); + background-image: unquote("radial-gradient(#{$pos}, #{$shape-size}, #{$full})"); +} + +// Usage: Gradient position and shape-size are required. Color stops are optional. +// @include radial-gradient(50% 50%, circle cover, #1e5799, #efefef); +// @include radial-gradient(50% 50%, circle cover, #eee 10%, #1e5799 30%, #efefef); diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_transform.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_transform.scss new file mode 100644 index 00000000000..8cc35963d55 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_transform.scss @@ -0,0 +1,15 @@ +@mixin transform($property: none) { +// none | + @include prefixer(transform, $property, webkit moz ms o spec); +} + +@mixin transform-origin($axes: 50%) { +// x-axis - left | center | right | length | % +// y-axis - top | center | bottom | length | % +// z-axis - length + @include prefixer(transform-origin, $axes, webkit moz ms o spec); +} + +@mixin transform-style ($style: flat) { + @include prefixer(transform-style, $style, webkit moz ms o spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_transition.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_transition.scss new file mode 100644 index 00000000000..f89fc4af8b7 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_transition.scss @@ -0,0 +1,36 @@ +// Shorthand mixin. Supports multiple parentheses-deliminated values for each variable. +// Example: @include transition (all, 2.0s, ease-in-out); +// @include transition ((opacity, width), (1.0s, 2.0s), ease-in, (0, 2s)); +// @include transition ($property:(opacity, width), $delay: (1.5s, 2.5s)); + +@mixin transition ($properties...) { + @if length($properties) >= 1 { + @include prefixer(transition, $properties, webkit moz ms o spec); + } + + @else { + $properties: all 0.15s ease-out 0; + @include prefixer(transition, $properties, webkit moz ms o spec); + } +} + +@mixin transition-property ($properties...) { + -webkit-transition-property: transition-property-names($properties, 'webkit'); + -moz-transition-property: transition-property-names($properties, 'moz'); + -ms-transition-property: transition-property-names($properties, 'ms'); + -o-transition-property: transition-property-names($properties, 'o'); + transition-property: transition-property-names($properties, false); +} + +@mixin transition-duration ($times...) { + @include prefixer(transition-duration, $times, webkit moz ms o spec); +} + +@mixin transition-timing-function ($motions...) { +// ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() + @include prefixer(transition-timing-function, $motions, webkit moz ms o spec); +} + +@mixin transition-delay ($times...) { + @include prefixer(transition-delay, $times, webkit moz ms o spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/css3/_user-select.scss b/guides/content/assets/stylesheets/vendor/bourbon/css3/_user-select.scss new file mode 100644 index 00000000000..1380aa8baa9 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/css3/_user-select.scss @@ -0,0 +1,3 @@ +@mixin user-select($arg: none) { + @include prefixer(user-select, $arg, webkit moz ms spec); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_compact.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_compact.scss new file mode 100644 index 00000000000..871500e3394 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_compact.scss @@ -0,0 +1,11 @@ +// Remove `false` values from a list + +@function compact($vars...) { + $list: (); + @each $var in $vars { + @if $var { + $list: append($list, $var, comma); + } + } + @return $list; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_deprecated-webkit-gradient.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_deprecated-webkit-gradient.scss new file mode 100644 index 00000000000..91269295259 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_deprecated-webkit-gradient.scss @@ -0,0 +1,44 @@ +// Render Deprecated Webkit Gradient - Linear || Radial +//************************************************************************// +@function deprecated-webkit-gradient($type, + $deprecated-pos1, $deprecated-pos2, + $full, + $deprecated-radius1: false, $deprecated-radius2: false) { + $gradient-list: (); + $gradient: false; + $full-length: length($full); + $percentage: false; + $gradient-type: $type; + + @for $i from 1 through $full-length { + $gradient: nth($full, $i); + + @if length($gradient) == 2 { + $color-stop: color-stop(nth($gradient, 2), nth($gradient, 1)); + $gradient-list: join($gradient-list, $color-stop, comma); + } + + @else if $gradient != null { + @if $i == $full-length { + $percentage: 100%; + } + + @else { + $percentage: ($i - 1) * (100 / ($full-length - 1)) + "%"; + } + + $color-stop: color-stop(unquote($percentage), $gradient); + $gradient-list: join($gradient-list, $color-stop, comma); + } + } + + @if $type == radial { + $gradient: -webkit-gradient(radial, $deprecated-pos1, $deprecated-radius1, $deprecated-pos2, $deprecated-radius2, $gradient-list); + } + + @else if $type == linear { + $gradient: -webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $gradient-list); + } + + @return $gradient; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_flex-grid.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_flex-grid.scss new file mode 100644 index 00000000000..3bbd8665732 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_flex-grid.scss @@ -0,0 +1,39 @@ +// Flexible grid +@function flex-grid($columns, $container-columns: $fg-max-columns) { + $width: $columns * $fg-column + ($columns - 1) * $fg-gutter; + $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; + @return percentage($width / $container-width); +} + +// Flexible gutter +@function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) { + $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; + @return percentage($gutter / $container-width); +} + +// The $fg-column, $fg-gutter and $fg-max-columns variables must be defined in your base stylesheet to properly use the flex-grid function. +// This function takes the fluid grid equation (target / context = result) and uses columns to help define each. +// +// The calculation presumes that your column structure will be missing the last gutter: +// +// -- column -- gutter -- column -- gutter -- column +// +// $fg-column: 60px; // Column Width +// $fg-gutter: 25px; // Gutter Width +// $fg-max-columns: 12; // Total Columns For Main Container +// +// div { +// width: flex-grid(4); // returns (315px / 995px) = 31.65829%; +// margin-left: flex-gutter(); // returns (25px / 995px) = 2.51256%; +// +// p { +// width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; +// float: left; +// margin: flex-gutter(4); // returns (25px / 315px) = 7.936508%; +// } +// +// blockquote { +// float: left; +// width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; +// } +// } \ No newline at end of file diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_grid-width.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_grid-width.scss new file mode 100644 index 00000000000..8e63d83d602 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_grid-width.scss @@ -0,0 +1,13 @@ +@function grid-width($n) { + @return $n * $gw-column + ($n - 1) * $gw-gutter; +} + +// The $gw-column and $gw-gutter variables must be defined in your base stylesheet to properly use the grid-width function. +// +// $gw-column: 100px; // Column Width +// $gw-gutter: 40px; // Gutter Width +// +// div { +// width: grid-width(4); // returns 520px; +// margin-left: $gw-gutter; // returns 40px; +// } diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_linear-gradient.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_linear-gradient.scss new file mode 100644 index 00000000000..5030d6ea08a --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_linear-gradient.scss @@ -0,0 +1,6 @@ +@function linear-gradient($gradients...) { + $type: linear; + $type-gradient: append($type, $gradients, comma); + + @return $type-gradient; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_modular-scale.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_modular-scale.scss new file mode 100644 index 00000000000..dddccb52241 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_modular-scale.scss @@ -0,0 +1,40 @@ +@function modular-scale($value, $increment, $ratio) { + @if $increment > 0 { + @for $i from 1 through $increment { + $value: ($value * $ratio); + } + } + + @if $increment < 0 { + $increment: abs($increment); + @for $i from 1 through $increment { + $value: ($value / $ratio); + } + } + + @return $value; +} + +// div { +// Increment Up GR with positive value +// font-size: modular-scale(14px, 1, 1.618); // returns: 22.652px +// +// Increment Down GR with negative value +// font-size: modular-scale(14px, -1, 1.618); // returns: 8.653px +// +// Can be used with ceil(round up) or floor(round down) +// font-size: floor( modular-scale(14px, 1, 1.618) ); // returns: 22px +// font-size: ceil( modular-scale(14px, 1, 1.618) ); // returns: 23px +// } +// +// modularscale.com + +@function golden-ratio($value, $increment) { + @return modular-scale($value, $increment, 1.618) +} + +// div { +// font-size: golden-ratio(14px, 1); // returns: 22.652px +// } +// +// goldenratiocalculator.com diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_px-to-em.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_px-to-em.scss new file mode 100644 index 00000000000..2eb1031c60e --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_px-to-em.scss @@ -0,0 +1,8 @@ +// Convert pixels to ems +// eg. for a relational value of 12px write em(12) when the parent is 16px +// if the parent is another value say 24px write em(12, 24) + +@function em($pxval, $base: 16) { + @return ($pxval / $base) * 1em; +} + diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_radial-gradient.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_radial-gradient.scss new file mode 100644 index 00000000000..13b8e7af8d0 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_radial-gradient.scss @@ -0,0 +1,57 @@ +// This function is required and used by the background-image mixin. +@function radial-gradient($G1, $G2, + $G3: false, $G4: false, + $G5: false, $G6: false, + $G7: false, $G8: false, + $G9: false, $G10: false, + $pos: 50% 50%, + $shape-size: ellipse cover) { + + @each $value in $G1, $G2 { + $first-val: nth($value, 1); + $pos-type: type-of($first-val); + + @if ($pos-type != color) or ($first-val != "transparent") { + @if ($pos-type == number) + or ($first-val == "center") + or ($first-val == "top") + or ($first-val == "right") + or ($first-val == "bottom") + or ($first-val == "left") { + + $pos: $value; + + @if $pos == $G1 { + $G1: false; + } + } + + @else if + ($first-val == "ellipse") + or ($first-val == "circle") + or ($first-val == "closest-side") + or ($first-val == "closest-corner") + or ($first-val == "farthest-side") + or ($first-val == "farthest-corner") + or ($first-val == "contain") + or ($first-val == "cover") { + + $shape-size: $value; + + @if $value == $G1 { + $G1: false; + } + + @else if $value == $G2 { + $G2: false; + } + } + } + } + + $type: radial; + $gradient: compact($pos, $shape-size, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); + $type-gradient: append($type, $gradient, comma); + + @return $type-gradient; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_render-gradients.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_render-gradients.scss new file mode 100644 index 00000000000..fe7c799ebe1 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_render-gradients.scss @@ -0,0 +1,14 @@ +// User for linear and radial gradients within background-image or border-image properties + +@function render-gradients($gradients, $gradient-type, $vendor: false) { + $vendor-gradients: false; + @if $vendor { + $vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient($gradients); + } + + @else if $vendor == false { + $vendor-gradients: "#{$gradient-type}-gradient(#{$gradients})"; + $vendor-gradients: unquote($vendor-gradients); + } + @return $vendor-gradients; +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_tint-shade.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_tint-shade.scss new file mode 100644 index 00000000000..f7172004ac6 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_tint-shade.scss @@ -0,0 +1,9 @@ +// Add percentage of white to a color +@function tint($color, $percent){ + @return mix(white, $color, $percent); +} + +// Add percentage of black to a color +@function shade($color, $percent){ + @return mix(black, $color, $percent); +} diff --git a/guides/content/assets/stylesheets/vendor/bourbon/functions/_transition-property-name.scss b/guides/content/assets/stylesheets/vendor/bourbon/functions/_transition-property-name.scss new file mode 100644 index 00000000000..54cd4228112 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/bourbon/functions/_transition-property-name.scss @@ -0,0 +1,22 @@ +// Return vendor-prefixed property names if appropriate +// Example: transition-property-names((transform, color, background), moz) -> -moz-transform, color, background +//************************************************************************// +@function transition-property-names($props, $vendor: false) { + $new-props: (); + + @each $prop in $props { + $new-props: append($new-props, transition-property-name($prop, $vendor), comma); + } + + @return $new-props; +} + +@function transition-property-name($prop, $vendor: false) { + // put other properties that need to be prefixed here aswell + @if $vendor and $prop == transform { + @return unquote('-'+$vendor+'-'+$prop); + } + @else { + @return $prop; + } +} \ No newline at end of file diff --git a/guides/content/assets/stylesheets/vendor/icons/_spree_index_header.scss b/guides/content/assets/stylesheets/vendor/icons/_spree_index_header.scss new file mode 100644 index 00000000000..103fa85ccbb --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/icons/_spree_index_header.scss @@ -0,0 +1,40 @@ +@font-face { + font-family: 'icomoon'; + src:url('/shared/fonts/icomoon.eot'); + src:url('/shared/fonts/icomoon.eot') format('embedded-opentype'), + url('/shared/fonts/icomoon.woff') format('woff'), + url('/shared/fonts/icomoon.ttf') format('truetype'), + url('/shared/fonts/icomoon.svg#icomoon') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"], [class*=" icon-"] { + font-family: 'icomoon'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-wombat:before { + content: "\e600"; +} +.icon-api:before { + content: "\e000"; +} +.icon-developer:before { + content: "\e001"; +} +.icon-release-notes:before { + content: "\e003"; +} +.icon-user:before { + content: "\e004"; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/_neat-helpers.scss b/guides/content/assets/stylesheets/vendor/neat/_neat-helpers.scss new file mode 100644 index 00000000000..86021b1bffb --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/_neat-helpers.scss @@ -0,0 +1,8 @@ +// Functions +@import "functions/private"; +@import "functions/new-breakpoint"; +@import "functions/px-to-em"; + +// Settings +@import "settings/grid"; +@import "settings/visual-grid"; diff --git a/guides/content/assets/stylesheets/vendor/neat/_neat.scss b/guides/content/assets/stylesheets/vendor/neat/_neat.scss new file mode 100644 index 00000000000..cb5876b82db --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/_neat.scss @@ -0,0 +1,21 @@ +// Bourbon Neat +// MIT Licensed +// Copyright (c) 2012-2013 thoughtbot, inc. + +// Helpers +@import "neat-helpers"; + +// Grid +@import "grid/private"; +@import "grid/reset"; +@import "grid/grid"; +@import "grid/omega"; +@import "grid/outer-container"; +@import "grid/span-columns"; +@import "grid/row"; +@import "grid/shift"; +@import "grid/pad"; +@import "grid/fill-parent"; +@import "grid/media"; +@import "grid/to-deprecate"; +@import "grid/visual-grid"; diff --git a/guides/content/assets/stylesheets/vendor/neat/functions/_new-breakpoint.scss b/guides/content/assets/stylesheets/vendor/neat/functions/_new-breakpoint.scss new file mode 100644 index 00000000000..d89dcd101b2 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/functions/_new-breakpoint.scss @@ -0,0 +1,16 @@ +@function new-breakpoint($query:$feature $value $columns, $total-columns: $grid-columns) { + + @if length($query) == 1 { + $query: $default-feature nth($query, 1) $total-columns; + } + + @else if length($query) == 2 or length($query) == 4 { + $query: append($query, $total-columns); + } + + @if not belongs-to($query, $visual-grid-breakpoints) { + $visual-grid-breakpoints: append($visual-grid-breakpoints, $query, comma); + } + + @return $query; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/functions/_private.scss b/guides/content/assets/stylesheets/vendor/neat/functions/_private.scss new file mode 100644 index 00000000000..136a6ff3a16 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/functions/_private.scss @@ -0,0 +1,107 @@ +// Checks if a number is even +@function is-even($int) { + @if $int%2 == 0 { + @return true; + } + + @return false; +} + +// Checks if an element belongs to a list +@function belongs-to($tested-item, $list) { + @each $item in $list { + @if $item == $tested-item { + @return true; + } + } + + @return false; +} + +// Contains display value +@function contains-display-value($query) { + @if belongs-to(table, $query) or belongs-to(block, $query) or belongs-to(inline-block, $query) or belongs-to(inline, $query) { + @return true; + } + + @return false; +} + +// Parses the first argument of span-columns() +@function container-span($span: $span) { + @if length($span) == 3 { + $container-columns: nth($span, 3); + @return $container-columns; + } + + @else if length($span) == 2 { + $container-columns: nth($span, 2); + @return $container-columns; + } + + @else { + @return $grid-columns; + } +} + +// Generates a striped background +@function gradient-stops($grid-columns, $color: $visual-grid-color) { + $transparent: rgba(0,0,0,0); + + $column-width: flex-grid(1, $grid-columns); + $gutter-width: flex-gutter($grid-columns); + $column-offset: $column-width; + + $values: ($transparent 0, $color 0); + + @for $i from 1 to $grid-columns*2 { + @if is-even($i) { + $values: append($values, $transparent $column-offset); + $values: append($values, $color $column-offset); + $column-offset: $column-offset + $column-width; + } + + @else { + $values: append($values, $color $column-offset); + $values: append($values, $transparent $column-offset); + $column-offset: $column-offset + $gutter-width; + } + } + + @return $values; +} + +// Layout direction +@function get-direction($layout, $default) { + $direction: nil; + + @if $layout == LTR or $layout == RTL { + $direction: direction-from-layout($layout); + } @else { + $direction: direction-from-layout($default); + } + + @return $direction; +} + +@function direction-from-layout($layout) { + $direction: nil; + + @if $layout == LTR { + $direction: right; + } @else { + $direction: left; + } + + @return $direction; +} + +@function get-opposite-direction($direction) { + $opposite-direction: left; + + @if $direction == left { + $opposite-direction: right; + } + + @return $opposite-direction; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/functions/_px-to-em.scss b/guides/content/assets/stylesheets/vendor/neat/functions/_px-to-em.scss new file mode 100644 index 00000000000..058e51e8b51 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/functions/_px-to-em.scss @@ -0,0 +1,3 @@ +@function em($pxval, $base: 16) { + @return ($pxval / $base) * 1em; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_fill-parent.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_fill-parent.scss new file mode 100644 index 00000000000..859c97790bf --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_fill-parent.scss @@ -0,0 +1,7 @@ +@mixin fill-parent() { + width: 100%; + + @if $border-box-sizing == false { + @include box-sizing(border-box); + } +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_grid.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_grid.scss new file mode 100644 index 00000000000..e074b6c536c --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_grid.scss @@ -0,0 +1,5 @@ +@if $border-box-sizing == true { + * { + @include box-sizing(border-box); + } +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_media.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_media.scss new file mode 100644 index 00000000000..7c9872fb521 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_media.scss @@ -0,0 +1,51 @@ +@mixin media($query:$feature $value $columns, $total-columns: $grid-columns) { + + @if length($query) == 1 { + @media screen and ($default-feature: nth($query, 1)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 2 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 3 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 3); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 4 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 5 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 5); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else { + @warn "Wrong number of arguments for breakpoint(). Read the documentation for more details."; + } +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_omega.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_omega.scss new file mode 100644 index 00000000000..902459bcbce --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_omega.scss @@ -0,0 +1,79 @@ +// Remove last element gutter +@mixin omega($query: block, $direction: default) { + $table: if(belongs-to(table, $query), true, false); + $auto: if(belongs-to(auto, $query), true, false); + + @if $direction != default { + @warn "The omega mixin will no longer take a $direction argument. To change the layout direction, use row($direction) or set $default-layout-direction instead." + } @else { + $direction: get-direction($layout-direction, $default-layout-direction); + } + + @if length($query) == 1 { + @if $auto { + &:last-child { + margin-#{$direction}: 0; + } + } + + @else if contains-display-value($query) { + @if $table { + padding-#{$direction}: 0; + } + + @else { + margin-#{$direction}: 0; + } + } + + @else { + @include nth-child($query, $direction); + } + } + + @else if length($query) == 2 { + @if $table { + @if $auto { + &:last-child { + padding-#{$direction}: 0; + } + } + + @else { + &:nth-child(#{nth($query, 1)}) { + padding-#{$direction}: 0; + } + } + } + + @else { + @if $auto { + &:last-child { + margin-#{$direction}: 0; + } + } + + @else { + @include nth-child(nth($query, 1), $direction); + } + } + } + + @else { + @warn "Too many arguments passed to the omega() mixin." + } +} + +@mixin nth-child($query, $direction) { + $opposite-direction: get-opposite-direction($direction); + + &:nth-child(#{$query}) { + margin-#{$direction}: 0; + } + + @if type-of($query) == number { + &:nth-child(#{$query}+1) { + clear: $opposite-direction; + } + } +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_outer-container.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_outer-container.scss new file mode 100644 index 00000000000..22c541f4553 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_outer-container.scss @@ -0,0 +1,8 @@ +@mixin outer-container { + @include clearfix; + max-width: $max-width; + margin: { + left: auto; + right: auto; + } +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_pad.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_pad.scss new file mode 100644 index 00000000000..3ef5d80e45b --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_pad.scss @@ -0,0 +1,8 @@ +@mixin pad($padding: flex-gutter()) { + $padding-list: null; + @each $value in $padding { + $value: if($value == 'default', flex-gutter(), $value); + $padding-list: join($padding-list, $value); + } + padding: $padding-list; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_private.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_private.scss new file mode 100644 index 00000000000..acd1b5b74d6 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_private.scss @@ -0,0 +1,21 @@ +$parent-columns: $grid-columns !default; +$fg-column: $column; +$fg-gutter: $gutter; +$fg-max-columns: $grid-columns; +$container-display-table: false !default; +$layout-direction: nil !default; + +@function flex-grid($columns, $container-columns: $fg-max-columns) { + $width: $columns * $fg-column + ($columns - 1) * $fg-gutter; + $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; + @return percentage($width / $container-width); +} + +@function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) { + $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; + @return percentage($gutter / $container-width); +} + +@function grid-width($n) { + @return $n * $gw-column + ($n - 1) * $gw-gutter; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_reset.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_reset.scss new file mode 100644 index 00000000000..f670019e4b4 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_reset.scss @@ -0,0 +1,12 @@ +@mixin reset-display { + $container-display-table: false; +} + +@mixin reset-layout-direction { + $layout-direction: $default-layout-direction; +} + +@mixin reset-all { + @include reset-display; + @include reset-layout-direction; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_row.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_row.scss new file mode 100644 index 00000000000..582603dd015 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_row.scss @@ -0,0 +1,17 @@ +@mixin row($display: block, $direction: $default-layout-direction) { + @include clearfix; + $layout-direction: $direction; + + @if $display == table { + display: table; + @include fill-parent; + table-layout: fixed; + $container-display-table: true; + } + + @else { + display: block; + $container-display-table: false; + } +} + diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_shift.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_shift.scss new file mode 100644 index 00000000000..e39208ef0de --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_shift.scss @@ -0,0 +1,10 @@ +@mixin shift($n-columns: 1) { + + $direction: get-direction($layout-direction, $default-layout-direction); + $opposite-direction: get-opposite-direction($direction); + + margin-#{$opposite-direction}: $n-columns * flex-grid(1, $parent-columns) + $n-columns * flex-gutter($parent-columns); + + // Reset nesting context + $parent-columns: $grid-columns; +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_span-columns.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_span-columns.scss new file mode 100644 index 00000000000..97902d62c44 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_span-columns.scss @@ -0,0 +1,45 @@ +@mixin span-columns($span: $columns of $container-columns, $display: block) { + + $columns: nth($span, 1); + $container-columns: container-span($span); + $display-table: false; + + $direction: get-direction($layout-direction, $default-layout-direction); + $opposite-direction: get-opposite-direction($direction); + + @if $container-columns != $grid-columns { + $parent-columns: $container-columns; + } @else { + $parent-columns: $grid-columns; + } + + @if $container-display-table == true { + $display-table: true; + } @else if $display == table { + $display-table: true; + } @else { + $display-table: false; + } + + @if $display-table { + display: table-cell; + padding-#{$direction}: flex-gutter($container-columns); + width: flex-grid($columns, $container-columns) + flex-gutter($container-columns); + + &:last-child { + width: flex-grid($columns, $container-columns); + padding-#{$direction}: 0; + } + } + + @else { + display: block; + float: #{$opposite-direction}; + margin-#{$direction}: flex-gutter($container-columns); + width: flex-grid($columns, $container-columns); + + &:last-child { + margin-#{$direction}: 0; + } + } +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_to-deprecate.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_to-deprecate.scss new file mode 100644 index 00000000000..d0a681fd12e --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_to-deprecate.scss @@ -0,0 +1,57 @@ +@mixin breakpoint($query:$feature $value $columns, $total-columns: $grid-columns) { + @warn "The breakpoint() mixin was renamed to media() in Neat 1.0. Please update your project with the new syntax before the next version bump."; + + @if length($query) == 1 { + @media screen and ($default-feature: nth($query, 1)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 2 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 3 { + @media screen and (nth($query, 1): nth($query, 2)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 3); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 4 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: $total-columns; + @content; + $grid-columns: $default-grid-columns; + } + } + + @else if length($query) == 5 { + @media screen and (nth($query, 1): nth($query, 2)) and (nth($query, 3): nth($query, 4)) { + $default-grid-columns: $grid-columns; + $grid-columns: nth($query, 5); + @content; + $grid-columns: $default-grid-columns; + } + } + + @else { + @warn "Wrong number of arguments for breakpoint(). Read the documentation for more details."; + } +} + +@mixin nth-omega($nth, $display: block, $direction: default) { + @warn "The nth-omega() mixin is deprecated. Please use omega() instead."; + @include omega($nth $display, $direction); +} diff --git a/guides/content/assets/stylesheets/vendor/neat/grid/_visual-grid.scss b/guides/content/assets/stylesheets/vendor/neat/grid/_visual-grid.scss new file mode 100644 index 00000000000..1c822fd3220 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/grid/_visual-grid.scss @@ -0,0 +1,41 @@ +@mixin grid-column-gradient($values...) { + background-image: deprecated-webkit-gradient(linear, left top, left bottom, $values); + background-image: -webkit-linear-gradient(left, $values); + background-image: -moz-linear-gradient(left, $values); + background-image: -ms-linear-gradient(left, $values); + background-image: -o-linear-gradient(left, $values); + background-image: unquote("linear-gradient(left, #{$values})"); +} + +@if $visual-grid == true or $visual-grid == yes { + body:before { + content: ''; + display: inline-block; + @include grid-column-gradient(gradient-stops($grid-columns)); + height: 100%; + left: 0; + margin: 0 auto; + max-width: $max-width; + opacity: $visual-grid-opacity; + position: fixed; + right: 0; + width: 100%; + pointer-events: none; + + @if $visual-grid-index == back { + z-index: -1; + } + + @else if $visual-grid-index == front { + z-index: 9999; + } + + @each $breakpoint in $visual-grid-breakpoints { + @if $breakpoint != nil { + @include media($breakpoint) { + @include grid-column-gradient(gradient-stops($grid-columns)); + } + } + } + } +} diff --git a/guides/content/assets/stylesheets/vendor/neat/settings/_grid.scss b/guides/content/assets/stylesheets/vendor/neat/settings/_grid.scss new file mode 100644 index 00000000000..f1dcda47804 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/settings/_grid.scss @@ -0,0 +1,7 @@ +$column: golden-ratio(1em, 3) !default; // Column width +$gutter: golden-ratio(1em, 1) !default; // Gutter between each two columns +$grid-columns: 12 !default; // Total number of columns in the grid +$max-width: em(1088) !default; // Max-width of the outer container +$border-box-sizing: true !default; // Makes all elements have a border-box layout +$default-feature: min-width; // Default @media feature for the breakpoint() mixin +$default-layout-direction: LTR !default; diff --git a/guides/content/assets/stylesheets/vendor/neat/settings/_visual-grid.scss b/guides/content/assets/stylesheets/vendor/neat/settings/_visual-grid.scss new file mode 100644 index 00000000000..611c2b37274 --- /dev/null +++ b/guides/content/assets/stylesheets/vendor/neat/settings/_visual-grid.scss @@ -0,0 +1,5 @@ +$visual-grid: false !default; // Display the base grid +$visual-grid-color: #EEE !default; +$visual-grid-index: back !default; // Show grid behind content (back) or overlay it over the content (front) +$visual-grid-opacity: 0.4 !default; +$visual-grid-breakpoints: () !default; diff --git a/guides/content/developer/advanced/developer_tips.md b/guides/content/developer/advanced/developer_tips.md new file mode 100644 index 00000000000..becbcd462fb --- /dev/null +++ b/guides/content/developer/advanced/developer_tips.md @@ -0,0 +1,113 @@ +--- +title: "Developer Tips and Tricks" +section: advanced +--- + +## Overview + +This guide presents accumulated wisdom from person-years of Spree use. + +## Upgrade Considerations + +### The important commands + +`spree -update` was removed in favor of Bundler. + +Before updating, you will want to ensure the installed spree gem is +up-to-date by modifying `Gemfile` to match the new spree version and +run `bundle update`. + +Thanks to Rails 3.1 Mountable Engine, the update process is +"non-destructive" than in previous versions of Spree. The core files are encapsulated +separately from sandbox, thus upgrading to newer files will not override nor replace +sandbox's customized files. + +This makes it easier to see when and how some file has changed – which +is often useful if you need to update a customized version. + +### Dos and Don'ts + +!!! +Try to avoid modifying `config/boot.rb` and +`config/environment.rb`: use [initializers](#initializers) instead. +!!! + +### Tracking changes for overridden code + +Be aware that core changes might have an impact on the components you +have overridden in your project. +You might need to patch your local copies, or ensure that such copies +interact correctly with changed code (e.g. using appropriate ids in HTML to allow the JavaScript to +work). + +If you can help us generalise the core code so that your preferred +effect is achieved by altering a few parameters, this will be more useful than duplicating several +files. Ideas and suggestions are always welcome. + +### Initializers + +Initializers are run during startup, and are the recommended way to +execute certain settings. You can put initializers in extensions, thus have a way to execute +extension-specific configurations. + +See the [extensions guide](extensions_tutorial.html#extension-initializers) for +more information. + +## Debugging techniques + +### Use tests! + +Use `rake spec` and `rake test` to test basic functioning after you've +made changes. + +### Analysing crashes on a non-local machine + +If you're testing on a server, whether in production or development +mode, the following code in one +of your `FOO_extension.rb` files might save some time. It triggers +local behaviour for users who have +an admin role. One useful consequence is that uncaught exceptions will +show the detailed error page +instead of `404.html`, so you don't have to hunt through the server +logs. + +```ruby +Spree::BaseController.class_eval do + def local_request? + ENV["RAILS_ENV"] !="production" || current_user.present? && + current_user.has_role?(:admin) + end +end +``` + +## Managing large projects + +### To fork or not to fork… + +Suppose there's a few details of Spree that you want to override due to +personal or client preference, +but which aren't the usual things that you'd override (like views) - so +something like tweaks to the models or controllers. + +You could hide these away in your site extension, but they could get +mixed up with your real site customizations. You could also fork Spree and run your site on this +forked version, but this can also be a headache to get right. There's also the hassle of tracking +changes to `spree/master` and pulling them into your project at the right time. + +So here's a compromise: have an extra extension, say `spree-tweaks`, to +contain your small collection of modified files, which is loaded first in the extension order. The +benefits are: + +- it's clear what you are overriding, and easier to check against core + changes +- you can base your project on an official gem release or a + `spree/master` commit stage +- such tweaks can become part of your client site project and be + managed with SCM etc. + +If you find yourself wanting extensive changes to core, this technique +probably won't work so well. +But then again, if this is the case, then you probably want to look +seriously at splitting some +code off into stand-alone extensions and then see whether any of the +other code should be contributed to the core. diff --git a/guides/content/developer/advanced/seo.markdown b/guides/content/developer/advanced/seo.markdown new file mode 100644 index 00000000000..808a9d72651 --- /dev/null +++ b/guides/content/developer/advanced/seo.markdown @@ -0,0 +1,122 @@ +--- +title: "Seo Considerations" +section: advanced +--- + +## Overview + +Search Engine Optimization is an important area to address when +implementing and developing an ecommerce solution to ensure competitive +search engine performance. The following guide outlines current Spree +Search Engine Optimization features and future optimization development +possibilities. + + +## Existing Search Engine Optimization Features + +Chapter 1 contains a description of the work that has been completed to +address common search engine optimization issues. + +### Relevant, Meaningful URLs + +The helper method `seo_url(taxon)` yields SEO friendly URLs such as [demo.spreecommerce.com/products/xm-direct2-interface-adapter](http://demo.spreecommerce.com/products/xm-direct2-interface-adapter) and [demo.spreecommerce.com/t/categories/headphones](http://demo.spreecommerce.com/t/categories/headphones). +Each controller is configured to serve the content using these keyword-relevant, meaningful URLs. + +### On Page Keyword Targeting + +Several enhancements have been made to improve on-page keyword +targeting. The admin interface provides the ability to manage meta +descriptions and meta keywords at the product level. Additionally, H1 +tags are used throughout the site for product and taxonomy names. The +ease of extension development and layout changes allows you to target +keywords throughout the site. + +### Clean Content + +Spree uses Skeleton, a responsive CSS framework that allows clean HTML +that also responds well to any screen size. Having clean HTML with +minimal inline JavaScript and CSS is considered to be a factor in search +engine optimization. + +### On Site Performance Optimization + +Spree has been configured to serve one CSS and one JavaScript file on +every page (excluding extension inclusions). Minimizing HTTP requests is +considered an important factor in search engine optimization as the +server performance is an important influence in the search engine crawl +behavior for a site. + +### Google Analytics integration + +Google Analytics has been integrated into the Spree core and can be +managed from the "Analytics Trackers" section of the admin. Google +Analytics is not included on your store if this preference is not set. +The Google Analytics setup includes e-commerce conversion tracking. + +## Planned Search Engine Optimization Features + +Although several common search engine optimization issues have been +addressed, we are always looking for the new best practices in SEO. +Contributions to address issues will be very welcome. Visit the +[contributing to spree section](contributing.html) to learn +more about contributing. + +### Product and Taxonomy Page Title Enhancements + +Page titles are an important part of search engine optimization and +should be meaningful and relevant to the page content. There are a few +configuration settings for getting the best page titles possible. "Site +Name" will appear in the beginning of each of your titles. When +possible, we assign an appropriate title after that (product name, taxon +name, etc), but when we can't do that, we use the "Default Seo Title". + +### Alt Attribute on Product Images + +The alt attribute on product images currently pulls data from product +titles or the image filename. Enhancing the image alt tag can improve +image search performance. + +### Known Duplicate Content Issues + +In the Spree demo, it is a known issue that +[demo.spreecommerce.com](http://demo.spreecommerce.com/) contains +duplicate content to +[demo.spreecommerce.com/products](http://demo.spreecommerce.com/products). +Duplicate content can be a detriment to search engine performance as +external links are divided among duplicate content pages. As a result, +duplicate content pages may not only not be excluded from the main +search engine index, but pages may also rank poorly in comparison to +other sites where all external links go to one non-duplicated page. + +### Integration of Content Management System or Content + +There has been quite a bit of interest in development of [CMS +integration into +Spree](https://groups.google.com/forum/#!searchin/spree-user/cms). Having +good content is an important part of search engine optimization, as it +not only can improve on page keyword targeting, but it also can improve +the popularity of a site which can in turn improve search engine +optimization. + +### Tool Integration + +In addition to integration of Google Analytics, several other tools can +be implemented for SEO purposes such as Bing Webmaster Tools, Google +Webmaster Tools and Quantcast. Social media optimization tools such as +Pinterest, Reddit, Digg, Delicious, Facebook, Google+ and Twitter may +also be integrated to improve social networking site performance. + +## Spree SEO Extensions + +The following list shows extensions that can improve search engine +performance. Refer to the GitHub README for developer notes. + +- [Static Content Management](https://github.com/spree/spree_static_content) +- [Spree Sitemap Generation](https://github.com/romul/spree_dynamic_sitemaps) +- [Product Reviews](https://github.com/spree/spree_reviews) + +## External Search Engine Optimization Efforts + +Spree cannot control factors such as external links, quality of external +links, server performance and capabilities. These areas should not be +ignored in implementation of search engine optimization efforts. diff --git a/guides/content/developer/core/addresses.md b/guides/content/developer/core/addresses.md new file mode 100644 index 00000000000..2e04877f025 --- /dev/null +++ b/guides/content/developer/core/addresses.md @@ -0,0 +1,41 @@ +--- +title: "Addresses" +section: core +--- + +## Address + +The `Address` model in the `spree` gem is used to track address information, mainly for orders. Address information can also be tied to the `Spree::User` objects which come from the `spree_auth_devise` extension. + +Addresses have the following attributes: + +* `firstname`: The first name for the person at this address. +* `lastname`: The last name for the person at this address. +* `address1`: The address's first line. +* `address2`: The address's second line. +* `city`: The city where the address is. +* `zipcode`: The postal code. +* `phone`: The phone number. +* `state_name`: The name for the state. +* `alternative_phone`: The alternative phone number. +* `company`: A company name. + +Addresses can also link to countries and states. An address must always link to a `Spree::Country` object. It can optionally link to a `Spree::State` object, but only in the cases where the related country has no states listed. In that case, the state information is still required, and is kept within the `state_name` field on the address record. An easy way to get the state information for the address is to call `state_text` on that object. + +## Zones + +When an order's address is linked to a country or a state, that can ultimately affect different features of the order, including shipping availability and taxation. The way these effects work is through zones. + +A zone is comprised of many different "zone members", which can either be a set of countries or a set of states. + +Every order has a "tax zone", which indicates if a user should or shouldn't be taxed when placing an order. For more information, please see the [Taxation](taxation) guide. + +In addition to tax zones, orders also have shipping methods. These are provided to the user based on their address information, and once selected lock in how an order is going to be shipped to that user. For more information, please see the [Shipments](shipments) guide. + +## Countries + +Countries within Spree are used as a container for states. Countries can be zone members, and also link to an address. The difference between one country and another on an address record can determine which tax rates and shipping methods are used for the order. + +## States + +States within Spree are used to scope address data slightly more than country. States are useful for tax purposes, as different states in a country may impose different tax rates on different products. In addition to this, different states may cause different tax rates and shipping methods to be used for an order, similar to how countries affect it also. diff --git a/guides/content/developer/core/adjustments.md b/guides/content/developer/core/adjustments.md new file mode 100644 index 00000000000..534bed9b1ad --- /dev/null +++ b/guides/content/developer/core/adjustments.md @@ -0,0 +1,100 @@ +--- +title: "Adjustments" +section: core +--- + +## Overview + +An `Adjustment` object tracks an adjustment to the price of an [Order](orders), an order's [Line Item](orders#line-items), or an order's [Shipments](shipments) within a Spree Commerce storefront. + +Adjustments can be either positive or negative. Adjustments with a positive value are sometimes referred to as "charges" while adjustments with a negative value are sometimes referred to as "credits." These are just terms of convenience since there is only one `Spree::Adjustment` model in a storefront which handles this by allowing either positive or negative values. + +Adjustments can either be considered included or additional. An "included" adjustment is an adjustment to the price of an item which is included in that price of an item. A good example of this is a GST/VAT tax. An "additional" adjustment is an adjustment to the price of the item on top of the original item price. A good example of that would be how sales tax is handled in countries like the United States. + +Adjustments have the following attributes: + +* `amount` The dollar amount of the adjustment. +* `label`: The label for the adjustment to indicate what the adjustment is for. +* `eligible`: Indicates if the adjustment is eligible for the thing it's adjusting. +* `mandatory`: Indicates if this adjustment is mandatory; i.e that this adjustment *must* be applied regardless of its eligibility rules. +* `state`: Can either be `open`, `closed`, or `finalized`. Once it is in the `finalized` state, it cannot be changed. +* `included`: Whether or not this adjustment affects the final price of the item it is applied to. Used only for tax adjustments which may themselves be included in the price. + +Along with these attributes, an adjustment links to three polymorphic objects: + +* A source +* An adjustable + +The *source* is the source of the adjustment. Typically a `Spree::TaxRate` object or a `Spree::PromotionAction` object. + +The *adjustable* is the object being adjusted, which is either an order, line item or shipment. + +Adjustments can come from one of two locations: + +* Tax Rates +* Promotions + +An adjustment's `label` attribute can be used as a good indicator of where the adjustment is coming from. + +## Adjustment Scopes + +There are some helper methods to return the different types of adjustments: + +```ruby +scope :shipping, -> { where(adjustable_type: 'Spree::Shipment') } +scope :is_included, -> { where(included: true) } +scope :additional, -> { where(included: false) } +``` + +* `open`: All open adjustments. +* `eligible`: All eligible adjustments for the order. Useful for determining which adjustments are applying to the adjustable. +* `tax`: All adjustments which have a source that is a `Spree::TaxRate` object +* `price`: All adjustments which adjust a `Spree::LineItem` object. +* `shipping`: All adjustments which adjust a `Spree::Shipment` object. +* `promotion`: All adjustments where the source is a `Spree::PromotionAction` object. +* `optional`: All adjustments which are not `mandatory`. +* `return_authorization`: All adjustments where the source is a `Spree::ReturnAuthorization`. +* `eligible`: Adjustments which have been determined to be `eligible` for their adjustable. +* `charge`: Adjustments which *increase* the price of their adjustable. +* `credit`: Adjustments which *decrease* the price of their adjustable. +* `optional`: Adjustments which are not mandatory. +* `included`: Adjustments which are included in the object's price. Typically tax adjustments. +* `additional`: Adjustments which modify the object's price. The default for all adjustments. + +These scopes can be called on either the `Spree::Adjustment` class itself, or on an `adjustments` association. For example, calling any one of these three is +valid: + +```ruby +Spree::Adjustment.eligible +order.adjustments.eligible +line_item.adjustments.eligible +shipment.adjustments.eligible +``` + +## Adjustment Associations + +As of Spree 2.2, you are able to retrieve the specific adjustments of an Order, a Line Item or a Shipment. + +An order itself, much like line items and shipments, can have its own individual modifications. For instance, an order with over $100 of line items may have 10% off. To retrieve these adjustments on the order, call the `adjustments` association: + +```ruby +order.adjustments +``` + +If you want to retrieve all the adjustments for all the line items, shipments and the order itself, call the `all_adjustments` method: + +```ruby +order.all_adjustments +``` + +If you want to grab just the line item adjustments, call `line_item_adjustments`: + +```ruby +order.line_item_adjustments +``` + +Simiarly, if you want to grab the adjustments applied to shipments, call `shipment_adjustments`: + +```ruby +order.shipment_adjustments +``` diff --git a/guides/content/developer/core/calculators.md b/guides/content/developer/core/calculators.md new file mode 100644 index 00000000000..c505c162247 --- /dev/null +++ b/guides/content/developer/core/calculators.md @@ -0,0 +1,249 @@ +--- +title: "Calculators" +section: core +--- + +## Overview + +Spree makes extensive use of the `Spree::Calculator` model and there are several subclasses provided to deal with various types of calculations (flat rate, percentage discount, sales tax, VAT, etc.) All calculators extend the `Spree::Calculator` class and must provide the following methods: + +```ruby +def self.description + # Human readable description of the calculator +end + +def compute(object=nil) + # Returns the value after performing the required calculation +end +``` + +Calculators link to a `calculable` object, which are typically one of `Spree::ShippingMethod`, `Spree::TaxRate`, or `Spree::Promotion::Actions::CreateAdjustment`. These three classes use the [`Spree::Core::CalculatedAdjustment`](#calculated-adjustments) module to provide an easy way to calculate adjustments for their objects. + +## Available Calculators + +The following are descriptions of the currently available calculators in Spree. If you would like to add your own, please see the [Creating a New Calculator](#creating-a-new-calculator) section. + +### Default Tax + +For information about this calculator, please read the [Taxation](taxation) guide. + +### Flat Percent Per Item Total + +This calculator has one preference: `flat_percent` and can be set like this: + +```ruby +calculator.preferred_flat_percent = 10 +``` + +This calculator takes an order and calculates an amount using this calculation: + +```ruby +[item total] x [flat percentage] +``` + +For example, if an order had an item total of $31 and the calculator was configured to have a flat percent amount of 10, the discount would be $3.10, because $31 x 10% = $3.10. + +### Flat Rate + +This calculator can be used to provide a flat rate discount. + +This calculator has two preferences: `amount` and `currency`. These can be set like this: + +```ruby +calculator.preferred_amount = 10 +calculator.currency = "USD" +``` + +The currency for this calculator is used to check to see if a shipping method is available for an order. If an order's currency does not match the shipping method's currency, then that shipping method will not be displayed on the frontend. + +This calculator can take any object and will return simply the preferred amount. + +### Flexi Rate + +This calculator is typically used for promotional discounts when you want a specific discount for the first product, and then subsequent discounts for other products, up to a certain amount. + +This calculator takes three preferences: + +* `first_item`: The discounted price of the first item(s). +* `additional_item`: The discounted price of subsequent items. +* `max_items`: The maximum number of items this discount applies to. + +The calculator computes based on this: + +[first item discount] + (([items_count*] - 1) x [additional item discount]) + +* up to the `max_items` + +Thus, if you have ten items in your shopping cart, your `first_item` preference is set to $10, your `additional_items` preference is set to $5, and your `max_items` preference is set to 4, the total discount would be $25: + +* $10 for the first item +* $5 for each of the 3 subsequent items: $5 * 3 = $15 +* $0 for the remaining 6 items + +### Free Shipping + +This calculator will take an object, and then work out the shipping total for that object. Useful for when you want to apply free shipping to an order. + +$$$ +This is a little confusing and vague. Need to investigate more and explain better. Also, might this be obsolete with the new split shipments functionality? +$$$ + +### Per Item + +The Per Item calculator computes a value for every item within an order. This is useful for providing a discount for a specific product, without it affecting others. + +This calculator takes two preferences: + +* `amount`: The amount per item to calculate. +* `currency`: The currency for this calculator. + +This calculator depends on its `calculable` responding to a `promotion` method, which should return a `Spree::Promotion` (or similar) object. This object should then return a list of rules, which should respond to a `products` method. This is used to return a result of matching products. + +The list of matching products is compared against the line items for the order being calculated. If any of the matching products are included in the order, they are eligible for this calculator. The calculation is this: + +[matching product quantity] x [amount] + +Every matching product within an order will add to the calculator's total. For example, assuming the calculator has an `amount` of 5 and there's an order with the following line items: + +* Product A: $15.00 x 2 (within matching products) +* Product B: $10.00 x 1 (within matching products) +* Product C: $20.00 x 4 (excluded from matching products) + +The calculation would be: + + = (2 x 5) + (1 x 5) + = 10 + 5 + +meaning the calculator will compute an amount of 15. + +### Percent Per Item + +The Percent Per Item calculator works in a near-identical fashion to the [Per Item Calculator](#per-item), with the exception that rather than providing a flat-rate per item, it is a percentage. + +Assuming a calculator amount of 10% and an order such as this: + +* Product A: $15.00 x 2 (within matching products) +* Product B: $10.00 x 1 (within matching products) +* Product C: $20.00 x 4 (excluded from matching products) + +The calculation would be: + + = ($15 x 2 x 10%) + ($10 x 10%) + = ($30 x 10%) + ($10 x 10%) + = $3 + $1 + +The calculator will calculate a discount of $4. + +### Price Sack + +The Price Sack calculator is useful for when you want to provide a discount for an order which is over a certain price. The calculator has four preferences: + +* `minimal_amount`: The minimum amount for the line items total to trigger the calculator. +* `discount_amount`: The amount to discount from the order if the line items total is equal to or greater than the `minimal_amount`. +* `normal_amount`: The amount to discount from the order if the line items total is less than the `minimal_amount`. +* `currency`: The currency for this calculator. Defaults to the currency you have set for your store with `Spree::Config[:currency]` + +Suppose you have a Price Sack calculator with a `minimal_amount` preference of $50, a `normal_amount` preference of $2, and a `discount_amount` of $5. An order with a line items total of $60 would result in a discount of $5 for the whole order. An order of $20 would result in a discount of $2. + +## Creating a New Calculator + +To create a new calculator for Spree, you need to do two things. The first is to inherit from the `Spree::Calculator` class and define `description` and `compute` methods on that class: + +```ruby +class CustomCalculator < Spree::Calculator + def self.description + # Human readable description of the calculator + end + + def compute(object=nil) + # Returns the value after performing the required calculation + end +end +``` + +If you are creating a new calculator for shipping methods, please be aware that you need to inherit from `Spree::ShippingCalculator` instead, and define a `compute_package` method: + +```ruby +class CustomCalculator < Spree::ShippingCalculator + def self.description + # Human readable description of the calculator + end + + def compute_package(package) + # Returns the value after performing the required calculation + end +end +``` + +The second thing is to register this calculator as a tax, shipping, or promotion adjustment calculator by calling code like this at the end of `config/initializers/spree.rb` inside your application (`config` variable defined for brevity): + +```ruby +config = Rails.application.config +config.spree.calculators.tax_rates << CustomCalculator +config.spree.calculators.shipping_methods << CustomCalculator +config.spree.calculators.promotion_actions_create_adjustments << CustomCalculator +``` + +For example if your calculator is placed in `app/models/spree/calculator/shipping/my_own_calculator.rb` you should call: + +```ruby +config = Rails.application.config +config.spree.calculators.shipping_methods << Spree::Calculator::Shipping::MyOwnCalculator +``` + +### Determining Availability + +By default, all shipping method calculators are available at all times. If you wish to make this dependent on something from the order, you can re-define the `available?` method inside your calculator: + +```ruby +class CustomCalculator < Spree::Calculator + def available?(object) + object.currency == "USD" + end +end +``` + +## Calculated Adjustments + +If you wish to use Spree's calculator functionality for your own application, you can include the `Spree::Core::CalculatedAdjustments` module into a model of your choosing. + +```ruby +class Plan < ActiveRecord::Base + include Spree::Core::CalculatedAdjustments +end +``` + +To have calculators available for this class, you will need to register them: + +```ruby +config.spree.calculators.plans << CustomCalculator +``` + +Then you can access these calculators by calling this method: + +```ruby +Plan.calculators +``` + +Using this method, you can then display the calculators as you please. Each object for this new class will need to have a calculator associated so that adjustments can be calculated on them. + +This module provides a `has_one` association to a `calculator` object, as well as some convenience helpers for creating and updating adjustments for objects. Assuming that an object has a calculator associated with it first, creating an adjustment is simple: + +```ruby +plan.create_adjustment("#{plan.name}", , ) +``` + +To update this adjustment: + +```ruby +plan.update_adjustment(, ) +``` + +To work out what the calculator would compute an amount to be: + +```ruby +plan.compute_amount() +``` + +`create_adjustment`, `update_adjustment` and `compute_amount` will call `compute` on the `Calculator` object. This `calculable` amount is whatever object your +`CustomCalculator` class supports. diff --git a/guides/content/developer/core/inventory.md b/guides/content/developer/core/inventory.md new file mode 100644 index 00000000000..ff82dd365e9 --- /dev/null +++ b/guides/content/developer/core/inventory.md @@ -0,0 +1,60 @@ +--- +title: "Inventory" +section: core +--- + +## Overview + +Spree uses a hybrid approach for tracking inventory: On-hand inventory is stored as a count on a variant `StockItem`. This gives good performance for stores with large inventories. Back-ordered, sold, or shipped products are stored as individual `InventoryUnit` objects so they can have relevant information attached to them. + +What if you don't need to track inventory? We have come up with a design that basically shields users of simple stores from much of this complexity. Simply set `Spree::Config[:track_inventory_levels]` to `false` and you never have to worry about it. + +New products created in the system can be given a starting "on hand" inventory level. You can subsequently set new inventory levels and the correct things will happen, e.g. adding new on-hand inventory to an out-of-stock product that has some backorders will first fill the backorders then update the product with the remaining inventory count. + +As of Spree 2.0, there is a new Stock Management system in place that allows for fine-grained control over inventory for products and variants. + +## Stock Management + +### Stock Locations + +Stock Locations are the locations where your inventory is shipped from. Each `StockLocation` has many `stock_items` and `stock_movements`. + +Stock Locations are created in the admin interface (Configuration → Stock Locations). Note that a `StockItem` will be added to the newly created `StockLocation` for each variant in your application. + +### Stock Items + +Stock Items represent the inventory at a stock location for a specific variant. Stock item count on hand can be increased or decreased by creating stock movements. + +*** +Note: Stock items are created automatically for each stock location you have. You don't need to manage these manually. +*** + +!!! +**Count On Hand** is no longer an attribute on variants. It has been moved to stock items, as those are now used for inventory management. +!!! + +### Stock Movements + +![image](../images/developer/core/stock_movements.png) + +Stock movements allow you to manage the inventory of a stock item for a stock location. Stock movements are created in the admin interface by first navigating to the product you want to manage. Then, follow the "Stock Management" link in the sidebar. + +As shown in the image above, you can increase or decrease the count on hand available for a variant at a stock location. To increase the count on hand, make a stock movement with a positive quantity. To decrease the count on hand, make a stock movement with a negative quantity. + +### Stock Transfers + +![image](../images/developer/core/stock_transfers.png) + +Stock transfers allow you to move inventory in bulk from one stock location to another stock location. Transfers are created in the admin interface by first navigating to the Configuration page. Then, follow the "Stock Transfers" link. + +![image](../images/developer/core/new_stock_transfer.png) + +As shown in the image above, you can move stock from one location to a different location. This is done by selecting a source location, a destination location, and one or more variants. You are also able to set the quantity for each variant individually. + +If you check "Receive Stock" while creating a new transfer, your stock transfer will only have a destination stock location. + +## Return Authorizations + +After an order is shipped, administrators can approve the return of some part (maybe all) of an order via the "Return Authorizations" tab in the single order console. To create a new return authorization, you should indicate which part of the order is being returned, what the reason for the return is, and what the resulting credit should be. The sale price of the product is shown for reference, but you can choose any value you like. + +After the authorization is created, you can return later to its edit page and click on the 'Received' button to register the return of goods. This will create a credit adjustment on the order, which you can apply (i.e. refund) to the order's credit card via the payments screen. Spree will log the events in the order's history. \ No newline at end of file diff --git a/guides/content/developer/core/orders.md b/guides/content/developer/core/orders.md new file mode 100644 index 00000000000..aa2c73a9e81 --- /dev/null +++ b/guides/content/developer/core/orders.md @@ -0,0 +1,123 @@ +--- +title: "Orders" +section: core +--- + +## Overview + +The `Order` model is one of the key models in Spree. It provides a central place around which to collect information about a customer order - including line items, adjustments, payments, addresses, return authorizations, and shipments. + +Orders have the following attributes: + +* `number`: The unique identifier for this order. It begins with the letter R and ends in a 9-digit number. This number is shown to the users, and can be used to find the order by calling `Spree::Order.find_by_number(number)`. +* `item_total`: The sum of all the line items for this order. +* `adjustment_total`: The sum of all adjustments on this order. +* `total`: The result of the sum of the `item_total` and the `adjustment_total`. +* `payment_total`: The total value of all finalized payments. +* `shipment_total`: The total value of all shipments' costs. +* `additional_tax_total`: The sum of all shipments' and line items' `additional_tax`. +* `included_tax_total`: The sum of all shipments' and line items' `included_tax`. +* `promo_total`: The sum of all shipments', line items' and promotions' `promo_total`. +* `state`: The current state of the order. To read more about the states an order goes through, read [The Order State Machine](#the-order-state-machine) section of this guide. +* `email`: The email address for the user who placed this order. Stored in case this order is for a guest user. +* `user_id`: The ID for the corresponding user record for this order. Stored only if the order is placed by a signed-in user. +* `completed_at`: The timestamp of when the order was completed. +* `bill_address_id`: The ID for the related `Address` object with billing address information. +* `ship_address_id`: The ID for the related `Address` object with shipping address information. +* `shipping_method_id`: The ID for the related `ShippingMethod` object. +* `created_by_id`: The ID of object that created this order. +* `shipment_state`: The current shipment state of the order. For possible states, please see the [Shipments guide](shipments). +* `payment_state`: The current payment state of the order. For possible states, please see the [Payments guide](payments). +* `special_instructions`: Any special instructions for the store to do with this order. Will only appear if `Spree::Config[:shipping_instructions]` is set to `true`. +* `currency`: The currency for this order. Determined by the `Spree::Config[:currency]` value that was set at the time of order. +* `last_ip_address`: The last IP address used to update this order in the frontend. +* `channel`: The channel specified when importing orders from other stores. e.g. amazon. +* `item_count`: The total value of line items' quantity. +* `approver_id`: The ID of user that approved this order. +* `confirmation_delivered`: Boolean value indicating that confirmation email was delivered. +* `guest_token`: The guest token stored corresponding to token stored in cookies. +* `canceler_id`: The ID of user that canceled this order. +* `store_id`: The ID of `Store` in which this order was created. + + +Some methods you may find useful: + +* `outstanding_balance`: The outstanding balance for the order, calculated by taking the `total` and subtracting `payment_total`. +* `display_item_total`: A "pretty" version of `item_total`. If `item_total` was `10.0`, `display_item_total` would be `$10.00`. +* `display_adjustment_total`: Same as above, except for `adjustment_total`. +* `display_total`: Same as above, except for `total`. +* `display_outstanding_balance`: Same as above, except for `outstanding_balance`. + +## The Order State Machine + +Orders flow through a state machine, beginning at a `cart` state and ending up at a `complete` state. The intermediary states can be configured using the [Checkout Flow API](checkout). + +The default states are as follows: + +* `cart` +* `address` +* `delivery` +* `payment` +* `confirm` +* `complete` + +The `payment` state will only be triggered if `payment_required?` returns `true`. + +The `confirm` state will only be triggered if `confirmation_required?` returns `true`. + +The `complete` state can only be reached in one of two ways: + +1. No payment is required on the order. +2. Payment is required on the order, and at least the order total has been received as payment. + +Assuming that an order meets the criteria for the next state, you will be able to transition it to the next state by calling `next` on that object. If this returns `false`, then the order does *not* meet the criteria. To work out why it cannot transition, check the result of an `errors` method call. + +## Line Items + +Line items are used to keep track of items within the context of an order. These records provide a link between orders, and [Variants](products#variants). + +When a variant is added to an order, the price of that item is tracked along with the line item to preserve that data. If the variant's price were to change, then the line item would still have a record of the price at the time of ordering. + +* Inventory tracking notes + +$$$ +Update this section after Chris+Brian have done their thing. +$$$ + +## Addresses + +An order can link to two `Address` objects. The shipping address indicates where the order's product(s) should be shipped to. This address is used to determine which shipping methods are available for an order. + +The billing address indicates where the user who's paying for the order is located. This can alter the tax rate for the order, which in turn can change how much the final order total can be. + +For more information about addresses, please read the [Addresses](addresses) guide. + +## Adjustments + +Adjustments are used to affect an order's final cost, either by decreasing it ([Promotions](promotions)) or by increasing it ([Shipping](shipments), [Taxes](taxation)). + +For more information about adjustments, please see the [Adjustments](adjustments) guide. + +## Payments + +Payment records are used to track payment information about an order. For more information, please read the [Payments](payments) guide. + +## Return Authorizations + +$$$ +document return authorizations. +$$$ + +## OrderPopulator + +$$$ +Add documentation about the OrderPopulator class here +$$$ + +## Updating an Order + +If you change any aspect of an `Order` object within code and you wish to update the order's totals -- including associated adjustments and shipments -- call the `update!` method on that object, which calls out to the `OrderUpdater` class. + +For example, if you create or modify an existing payment for the order which would change the order's `payment_state` to a different value, calling `update!` will cause the `payment_state` to be recalculated for that order. + +Another example is if a `LineItem` within the order had its price changed. Calling `update!` will cause the totals for the order to be updated, the adjustments for the order to be recalculated, and then a final total to be established. diff --git a/guides/content/developer/core/payments.md b/guides/content/developer/core/payments.md new file mode 100644 index 00000000000..54c06d5ae04 --- /dev/null +++ b/guides/content/developer/core/payments.md @@ -0,0 +1,194 @@ +--- +title: "Payments" +section: core +--- + +## Overview + +Spree has a highly flexible payments model which allows multiple payment methods to be available during checkout. The logic for processing payments is decoupled from orders, making it easy to define custom payment methods with their own processing logic. + +Payment methods typically represent a payment gateway. Gateways will process card payments, and may also include non-gateway methods of payment such as Check, which is provided in Spree by default. + +The `Payment` model in Spree tracks payments against [Orders](orders). Payments relate to a `source` which indicates how the payment was made, and a `PaymentMethod`, indicating the processor used for this payment. + +When a payment is created, it is given a unique, 8-character identifier. This is used when sending the payment details to the payment processor. Without this identifier, some payment gateways mistakenly reported duplicate payments. + +A payment can go through many different states, as illustrated below. + +![Payment flow](/images/developer/core/payment_flow.jpg) + +An explanation of the different states: + +* `checkout`: Checkout has not been completed +* `processing`: The payment is being processed (temporary – intended to prevent double submission) +* `pending`: The payment has been processed but is not yet complete (ex. authorized but not captured) +* `failed`: The payment was rejected (ex. credit card was declined) +* `void`: The payment should not be counted against the order +* `completed`: The payment is completed. Only payments in this state count against the order total + +The state transition for these is handled by the processing code within Spree; however, you are able to call the event methods yourself to reach these states. The event methods are: + +* `started_processing` +* `failure` +* `pend` +* `complete` +* `void` + +## Payment Methods + +Payment methods represent the different options a customer has for making a payment. Most sites will accept credit card payments through a payment gateway, but there are other options. Spree also comes with built-in support for a Check payment, which can be used to represent any offline payment. There are also third-party extensions that provide support for some other interesting options such as [better_spree_paypal_express](https://github.com/spree-contrib/better_spree_paypal_express). + +A `PaymentMethod` can have the following attributes: + +* `type`: The subclass of `Spree::PaymentMethod` this payment method represents. Uses rails single table inheritance feature. +* `name`: The visible name for this payment method +* `description`: The description for this payment method +* `active`: Whether or not this payment method is active. Set it `false` to hide it in frontend. +* `environment`: The Rails environment (`Rails.env`) where this payment method is active +* `display_on`: Determines where the payment method can be visible. Values can be `front` for frontend, `back` for backend or `both` for both. + +### Payment Method Visibility + +The appearance of the payment methods on the frontend and backend depend on several criteria used by the `PaymentMethod.available` method. The code is this: + +```ruby +def self.available(display_on = 'both') + all.select do |p| + p.active && + (p.display_on == display_on.to_s || p.display_on.blank?) && + (p.environment == Rails.env || p.environment.blank?) + end +end +``` + +If a payment method meets these criteria, then it will be available. + +### Auto-Capturing + +By default, a payment method's `auto_capture?` method depends on the `Spree::Config[:auto_capture]` preference. If you have set this preference to `true`, but don't want a payment method to be auto-capturable like other payment methods in your system, you can override the `auto_capture?` method in your +`PaymentMethod` subclass: + +```ruby +class FancyPaymentMethod < Spree::PaymentMethod + def auto_capture? + false + end +end +``` + +The result of this method determines if a payment will be automatically captured (true) or only authorized (false) during the processing of the payment. + +## Payment Processing + +Payment processing in Spree supports many different gateways, but also attempts to comply with the API provided by the [active_merchant](https://github.com/shopify/active_merchant) gem where possible. + +### Gateway Options + +For every gateway action, a list of gateway options are passed through. + +* `email` and `customer`: The email address related to the order +* `ip`: The last IP address for the order +* `order_id`: The Order's `number` attribute, plus the `identifier` for each payment, generated when the payment is first created +* `shipping`: The total shipping cost for the order, in cents +* `tax`: The total tax cost for the order, in cents +* `subtotal`: The item total for the order, in cents +* `currency`: The 3-character currency code for the order +* `discount`: The promotional discount applied to the order +* `billing_address`: A hash containing billing address information +* `shipping_address`: A hash containing shipping address information + +The billing address and shipping address data is as follows: + +* `name`: The combined `first_name` and `last_name` from the address +* `address1`: The first line of the address information +* `address2`: The second line of address information +* `city`: The city of the address +* `state`: An abbreviated version of the state name or, failing that, the state name itself, from the related `State` object. If that fails, the `state_name` attribute from the address. +* `country`: The ISO name for the country. For example, United States of America is "US", Australia is "AU". +* `phone`: The phone number associated with the address + +### Credit Card Data + +Spree stores only the type, expiration date, name and last four digits for the card on your server. This data can then be used to present to the user so that they can verify that the correct card is being used. All credit card data sent through forms is sent through immediately to the gateways, and is not stored for any period of time. + +### Processing Walkthrough + +When an order is completed in spree, each `Payment` object associated with the order has the `process!` method called on it (unless `payment_required?` for the order returns `false`), in order to attempt to automatically fulfill the payment required for the order. If the payment method requires a source, and the payment has a source associated with it, then Spree will attempt to process the payment. Otherwise, the payment will need to be processed manually. + +If the `PaymentMethod` object is configured to auto-capture payments, then the `Payment#purchase!` method will be called, which will call `PaymentMethod#purchase` like this: + +```ruby +payment_method.purchase(, , ) +``` + +If the payment is *not* configured to auto-capture payments, the `Payment#authorize!` method will be called, with the same arguments as the `purchase` method above: + +```ruby +payment_method.authorize(, , ) +``` + +How the payment is actually put through depends on the `PaymentMethod` sub-class' implementation of the `purchase` and `authorize` methods. + +The returned object from both the `purchase` and `authorize` methods on the payment method objects must be an `ActiveMerchant::Billing::Response` object. This response object is then stored (in YAML) in the `spree_log_entries` table. Log entries can be retrieved with a call to the `log_entries` association on any `Payment` object. + +If the `purchase!` route is taken and is successful, the payment is marked as `completed`. If it fails, it is marked as `failed`. If the `authorize` method is successful, the payment is transitioned to the `pending` state so that it can be manually captured later by calling the `capture!` method. If it is unsuccessful, it is also transitioned to the `failed` state. + +*** +Once a payment has been saved, it also updates the order. This may trigger the `payment_state` to change, which would reflect the current payment state of the order. The possible states are: + +* `balance_due`: Indicates that payment is required for this order +* `failed`: Indicates that the last payment for the order failed +* `credit_owed`: This order has been paid for in excess of its total +* `paid`: This order has been paid for in full. +*** + +!!! +You may want to keep tabs on the number of orders with a `payment_state` of `failed`. A sudden increase in the number of such orders could indicate a problem with your credit card gateway and most likely indicates a serious problem affecting customer satisfaction. You should check the latest `log_entries` for the most recent payments in the store if this is happening. +!!! + +### Log Entries + +Responses from payment gateways within Spree are typically `ActiveMerchant::Billing::Response` objects. When Spree handles a response from a payment gateway, it will serialize the object as YAML and store it in the database as a log entry for a payment. These responses can be useful for debugging why a payment has failed. + +You can get a list of these log entries by calling the `log_entries` on any `Spree::Payment` object. To get the `Active::Merchant::Billing::Response` out of these `Spree::LogEntry` objects, call the `details` method. + +## Supported Gateways + +Access to a number of payment gateways is handled with the usage of the [spree_gateway](https://github.com/spree/spree_gateway) extension. This extension currently supports the following gateways: + +* Authorize.Net +* Balanced +* Beanstram +* Braintree +* eWAY +* LinkPoint +* Moneris +* PayPal +* Sage Pay +* Samurai +* Skrill +* Stripe +* USA ePay +* WorldPay + +With the `spree_gateway` gem included in your application's `Gemfile`, these gateways will be selectable in the admin backend for payment methods. + +*** +These are just some of the gateways which are supported by the Active Merchant gem. You can see a [list of all the Active Merchant gateways on that project's GitHub page](https://github.com/Shopify/active_merchant#supported-direct-payment-gateways). + +In order to implement a new gateway in the spree_gateway project, please refer to the other gateways within `app/models/spree/gateway` inside that project. +*** + +## Adding your custom gateway + +In order to make your custom gateway show up on backend list of available payment methods +you need to add it to spree config list of payment methods first. That can be achieved +by adding the following code in your spree.rb for example: + +```ruby +Rails.application.config.spree.payment_methods << YourCustomGateway +``` + +[better_spree_paypal_express](https://github.com/spree-contrib/better_spree_paypal_express) and +[spree-adyen](https://github.com/spree/spree-adyen) are good examples of standalone custom gateways. +No dependency on spree_gateway or activemerchant required. diff --git a/guides/content/developer/core/preferences.md b/guides/content/developer/core/preferences.md new file mode 100644 index 00000000000..f915ce2a8de --- /dev/null +++ b/guides/content/developer/core/preferences.md @@ -0,0 +1,510 @@ +--- +title: "Preferences" +section: core +--- + +## Overview + +Spree Preferences support general application configuration and preferences per model instance. Spree comes with preferences for your store like `logo` and `currency`. Additional preferences can be added by your application or included extensions. + +To implement preferences for a model, simply add a new column called `preferences`. This is an example migration for the `spree_products` table: + +```ruby +class AddPreferencesColumnToSpreeProducts < ActiveRecord::Migration + def up + add_column :spree_products, :preferences, :text + end + + def down + remove_column :spree_products, :preferences + end +end +``` + +This will work because `Spree::Product` is a subclass of `Spree::Base`. If found, the `preferences` attribute gets serialized into a `Hash` and merged with the default values. + +As another example, you might want to add preferences for users to manage their notification settings. Just make sure your `User` model inherits from `Spree::Base` then add the `preferences` column. You'll then be able to define preferences for `User`s without adding extra columns to the database table. + +> If you're using `spree_auth_devise`, note that the provided `Spree::User` doesn't inherit from `Spree::Base`. + +Extensions may add to the Spree General Settings or create their own namespaced preferences. + +The first several sections of this guide describe preferences in a very general way. If you're just interested in making modifications to the existing preferences, you can skip ahead to the [Configuring Spree Preferences section](#configuring-spree-preferences). If you would like a more in-depth understanding of the underlying concepts used by the preference system, please read on. + +### Motivation + +Preferences for models within an application are very common. Although the rule of thumb is to keep the number of preferences available to a minimum, sometimes it's necessary if you want users to have optional preferences like disabling e-mail notifications. + +Both use cases are handled by Spree Preferences. They are easy to define, provide quick cached reads, persist across restarts and do not require additional columns to be added to your models' tables. + +## General Settings + +Spree comes with many application-wide preferences. They are defined in `core/app/models/spree/app_configuration.rb` and made available to your code through `Spree::Config`, e.g., `Spree::Config.site_name`. + +A limited set of the general settings are available in the admin interface of your store (`/admin/general_settings`). + +You can add additional preferences under the `spree/app_configuration` namespace or create your own subclass of `Preferences::Configuration`. + +```ruby +# These will be saved with key: spree/app_configuration/hot_salsa +Spree::AppConfiguration.class_eval do + preference :hot_salsa, :boolean + preference :dark_chocolate, :boolean, :default => true + preference :color, :string + preference :favorite_number + preference :language, :string, :default => 'English' +end + +# Spree::Config is an instance of Spree::AppConfiguration +Spree::Config.hot_salsa = false + +# Create your own class +# These will be saved with key: kona/store_configuration/hot_coffee +Kona::StoreConfiguration < Preferences::Configuration + preference :hot_coffee, :boolean + preference :color, :string, :default => 'black' +end + +KONA::STORE_CONFIG = Kona::StoreConfiguration.new +puts KONA::STORE_CONFIG.hot_coffee +``` + +## Defining Preferences + +You can define preferences for a model within the model itself: + +```ruby +class User < ActiveRecord::Base + preference :hot_salsa, :boolean + preference :dark_chocolate, :boolean, :default => true + preference :color, :string + preference :favorite_number, :integer + preference :language, :string, :default => "English" +end +``` + +In the above model, five preferences have been defined: + +* `hot_salsa` +* `dark_chocolate` +* `color` +* `favorite_number` +* `language` + +For each preference, a data type is provided. The types available are: + +* `boolean` +* `string` +* `password` +* `integer` +* `text` +* `array` +* `hash` + +An optional default value may be defined which will be used unless a value has been set for that specific instance. + +## Accessing Preferences + +Once preferences have been defined for a model, they can be accessed either using the shortcut methods that are generated for each preference or the generic methods that are not specific to a particular preference. + +### Shortcut Methods + +There are several shortcut methods that are generated. They are shown below. + +Query methods: + +```ruby +user.prefers_hot_salsa? # => false +user.prefers_dark_chocolate? # => false +``` + +Reader methods: + +```ruby +user.preferred_color # => nil +user.preferred_language # => "English" +``` + +Writer methods: + +```ruby +user.prefers_hot_salsa = false # => false +user.preferred_language = "English" # => "English" +``` + +Check if a preference is available: + +```ruby +user.has_preference? :hot_salsa +``` + +### Generic Methods + +Each shortcut method is essentially a wrapper for the various generic methods shown below: + +Query method: + +```ruby +user.prefers?(:hot_salsa) # => false +user.prefers?(:dark_chocolate) # => false +``` + +Reader methods: + +```ruby +user.preferred(:color) # => nil +user.preferred(:language) # => "English" +``` + +```ruby +user.get_preference :color +user.get_preference :language +``` + +Writer method: + +```ruby +user.set_preference(:hot_salsa, false) # => false +user.set_preference(:language, "English") # => "English" +``` + +### Accessing All Preferences + +You can get a hash of all stored preferences by accessing the `preferences` helper: + +```ruby +user.preferences # => {"language"=>"English", "color"=>nil} +``` + +This hash will contain the value for every preference that has been defined for the model instance, whether the value is the default or one that has been previously stored. + +### Default and Type + +You can access the default value for a preference: + +```ruby +user.preferred_color_default # => 'blue' +``` + +Types are used to generate forms or display the preference. You can also get the type defined for a preference: + +```ruby +user.preferred_color_type # => :string +``` + +## Configuring Spree Preferences + +Up until now we've been discussing the general preference system that was adapted to Spree. This has given you a general idea of what types of preference features are theoretically supported. Now, let's start to look specifically at how Spree is using these preferences for configuration. + +### Reading the Current Preferences + +At the heart of Spree preferences lies the `Spree::Config` constant. This object provides general access to the configuration settings anywhere in the application. + +These settings can be accessed from initializers, models, controllers, views, etc. + +The `Spree::Config` constant returns an instance of `Spree::AppConfiguration` which is where the default values for all of the general Spree preferences are defined. + +You can access these preferences directly in code. To see this in action, just fire up `rails console` and try the following: + +```ruby +>> Spree::Config.admin_interface_logo +=> "logo/spree_50.png" +>> Spree::Config.admin_products_per_page +=> 10 +``` + +The above examples show the default configuration values for these preferences. The defaults themselves are coded within the `Spree::AppConfiguration` class. + +```ruby +class Spree::AppConfiguration < Configuration + #... snip ... + preference :allow_guest_checkout, :boolean, default: true + #... snip ... +end +``` + +If you are using the default preferences without any modifications, then nothing will be stored in the database. If you set a value for the preference it will save it to `spree_preferences` or in our `preferences` column. It will use a memory cached version to maintain performance. + +### Overriding the Default Preferences + +The default Spree preferences in `Spree::AppConfiguration` can be changed using the `set` method of the `Spree::Config` module. For example to set the number of products shown on the products listing in the admin interface we could do the following: + +```ruby +>> Spree::Config.admin_products_per_page = 20 +=> 20 +>> Spree::Config.admin_products_per_page +=> 20 +``` + +Here we are changing a preference to something other than the default as specified in `Spree::AppConfiguration`. In this case the preference system will persist the new value in the `spree_preferences` table. + +### Configuration Through the Spree Initializer + +During the Spree installation process, an initializer file is created within your application's source code. The initializer is found under `config/initializers/spree.rb`: + +```ruby +Spree.config do |config| + # Example: + # Uncomment to override the default site name. + # config.site_name = "Spree Demo Site" +end +``` + +The `Spree.config` block acts as a shortcut to setting `Spree::Config` multiple times. If you have multiple default preferences you would like to override within your code you may override them here. Using the initializer for setting the defaults is a nice shortcut, and helps keep your preferences organized in a standard location. + +For example if you would like to change the logo location and if you want to tax using the shipping address you can accomplish this by doing the following: + +```ruby +Spree.config do |config| + config.admin_interface_logo = 'logo/my_store.png' + config.tax_using_ship_address = true +end +``` + +*** +Initializing preferences in `config/initializer.rb` will overwrite any changes that were made through the admin user interface when you restart. +*** + +### Configuration Through the Admin Interface + +The Spree admin interface has several different screens where various settings can be configured. For instance, the `admin/general_settings` URL in your Spree application can be used to configure the values for the site name and the site URL. This is basically equivalent to calling `Spree::Config.set(currency: "CDN", currency_thousands_separator: " ")` directly in your Ruby code. + +## Site-Wide Preferences + +You can define preferences that are site-wide and don't apply to a specific instance of a model by creating a configuration file that inherits from `Spree::Preferences::Configuration`. + +```ruby +class Spree::MyApplicationConfiguration < Spree::Preferences::Configuration + preference :theme, :string, :default => "Default" + preference :show_splash_page, :boolean + preference :number_of_articles, :integer +end +``` + +In the above configuration file, three preferences have been defined: + +* theme +* show_splash_page +* number_of_articles + +It is recommended to create the configuration file in the `lib/` directory. + +*** +Extensions can also define site-wide preferences. For more information on using preferences like this with extensions, check out the [Extensions Tutorial](extensions_tutorial). +*** + +### Configuring Site-Wide Preferences + +The recommended way to configure site-wide preferences is through an initializer. Let's take a look at configuring the preferences defined in the previous configuration example. + +```ruby +module Spree + MyApp::Config = Spree::MyApplicationConfiguration.new +end + +MyApp::Config[:theme] = "blue_theme" +MyApp::Config[:show_spash_page] = true +MyApp::Config[:number_of_articles] = 5 +``` + +The `MyApp` name used here is an example and should be replaced with your actual application's name, found in `config/application.rb`. + +The above example will configure the preferences we defined earlier. Take note of the second line. In order to set and get preferences using `MyApp::Config`, we must first instantiate the configuration object. + +## Spree Configuration Options + +This section lists all of the configuration options for the current version of Spree. + +`address_requires_state` + +Will determine if the state field should appear on the checkout page. Defaults to `true`. + +`admin_interface_logo` + +The path to the logo to display on the admin interface. Can be different from `Spree::Config[:logo]`. Defaults to `logo/spree_50.png`. + +`admin_products_per_page` + +How many products to display on the products listing in the admin interface. Defaults to 10. + +`allow_backorder_shipping` + +Determines if an `InventoryUnit` can ship or not. Defaults to `false`. + +`allow_checkout_on_gateway_error` + +Continues the checkout process even if the payment gateway error failed. Defaults to `false`. + +`allow_ssl_in_development_and_test` + +Enables SSL support in development and test environments. Defaults to `false`. + +`allow_ssl_in_production` + +Enables SSL support in production environment. Defaults to `true`. + +`allow_ssl_in_staging` + +Enables SSL support in production environment. Defaults to `true`. + +`alternative_billing_phone` + +Determines if an alternative phone number should be present for the billing address on the checkout page. Defaults to `false`. + +`alternative_shipping_phone` + +Determines if an alternative phone number should be present for the shipping address on the checkout page. Defaults to `false`. + +`always_put_site_name_in_title` + +Determines if the site name (`current_store.site_name`) should be placed into the title. Defaults to `true`. + +`attachment_default_url` + +Tells `Paperclip` the form of the URL to use for attachments which are missing. + +`attachment_path` + +Tells `Paperclip` the path at which to store images. + +`attachment_styles` + +A JSON hash of different styles that are supported by attachments. Defaults to: + +```json +{ + "mini":"48x48>", + "small":"100x100>", + "product":"240x240>", + "large":"600x600>" +} +``` + +`attachment_default_style` + +A key from the list of styles from `Spree::Config[:attachment_styles]` that is the default style for images. Defaults to the the `product` style. + +`auto_capture` + +Depending on whether or not Spree is configured to "auto capture" the credit card, either a purchase or an authorize operation will be performed on the card (via the current credit card gateway). Defaults to `false`. + +`checkout_zone` + +Limits the checkout to countries from a specific zone, by name. Defaults to `nil`. + +`company` + +Determines whether or not a field for "Company" displays on the checkout pages for shipping and billing addresses. Defaults to `false`. + +`currency` + +The three-letter currency code for the currency that prices will be displayed in. Defaults to "USD". + +`currency_symbol_position` + +The position of the symbol for a currency. Can be either `before` or `after`. Defaults to `before`. + +`display_currency` + +Determines whether or not a currency is displayed with a price. Defaults to `false`. + +`default_country_id` + +The default country's id. Defaults to 214, as this is the id for the United States within the seed data. + +`layout` + +The path to the layout of your application, relative to the `app/views` directory. Defaults to `spree/layouts/spree_application`. To make Spree use your application's layout rather than Spree's default, use this: + +```ruby +Spree.config do |config| + config.layout = "application" +end +``` + +`logo` + +The logo to display on your frontend. Defaults to `logo/spree_50.png`. + +`max_level_in_taxons_menu` + +The number of levels to descend when viewing a taxon menu. Defaults to `1`. + +`orders_per_page` + +The number of orders to display on the orders listing in the admin backend. Defaults to `15`. + +`prices_inc_tax` + +Determines if prices are labelled as including tax or not. Defaults to `false`. + +`shipment_inc_vat` + +Determines if shipments should include VAT calculations. Defaults to `false`. + +`shipping_instructions` + +Determines if shipping instructions are requested or not when checking out. Defaults to `false`. + +`show_descendents` + +Determines if taxon descendants are shown when showing taxons. Defaults to `true`. + +`show_only_complete_orders_by_default` + +Determines if, on the admin listing screen, only completed orders should be shown. Defaults to `true`. + +`show_variant_full_price` + +Determines if the variant's full price or price difference from a product should be displayed on the product's show page. Defaults to `false`. + +`tax_using_ship_address` + +Determines if tax information should be based on shipping address, rather than the billing address. Defaults to `true`. + +`track_inventory_levels` + +Determines if inventory levels should be tracked when products are purchased at checkout. This option causes new `InventoryUnit` objects to be created when a product is bought. Defaults to `true`. + +## S3 Support + +To configure Spree to upload images to S3, put these lines into `config/initializers/spree.rb`: + +```ruby +Spree.config do |config| + config.use_s3 = true + config.s3_bucket = '' + config.s3_access_key = "" + config.s3_secret = "" +end +``` + +It's also a good idea to not include the `rails_root` path inside the `attachment_path` configuration option, which by default is this: + +```ruby +:rails_root/public/spree/products/:id/:style/:basename.:extension +``` + +To change this, add the following line underneath the `s3_secret` configuration setting: + +```ruby +config.attachment_path = '/spree/products/:id/:style/:basename.:extension' +``` + +If you're using the Western Europe S3 server, you will need to set two additional options inside this block: + +```ruby +Spree.config do |config| + ... + config.attachment_url = ":s3_eu_url" + config.s3_host_alias = "s3-eu-west-1.amazonaws.com" +end +``` + +Additionally, you will need to tell `paperclip` how to construct the URLs for your images by placing this code outside the `config` block inside `config/initializers/spree.rb`: + +```ruby +Paperclip.interpolates(:s3_eu_url) do |attachment, style| + "#{attachment.s3_protocol}://#{Spree::Config[:s3_host_alias]}/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}" +end +``` diff --git a/guides/content/developer/core/products.md b/guides/content/developer/core/products.md new file mode 100644 index 00000000000..2841fc4d12e --- /dev/null +++ b/guides/content/developer/core/products.md @@ -0,0 +1,140 @@ +--- +title: "Products" +section: core +--- + +## Overview + +`Product` records track unique products within your store. These differ from [Variants](#variants), which track the unique variations of a product. For instance, a product that's a T-shirt would have variants denoting its different colors. Together, Products and Variants describe what is for sale. + +Products have the following attributes: + +* `name`: short name for the product +* `description`: The most elegant, poetic turn of phrase for describing your product's benefits and features to your site visitors +* `permalink`: An SEO slug based on the product name that is placed into the URL for the product +* `available_on`: The first date the product becomes available for sale online in your shop. If you don't set the `available_on` attribute, the product will not appear among your store's products for sale. +* `deleted_at`: The date the product is no longer available for sale in the store +* `meta_description`: A description targeted at search engines for search engine optimization (SEO) +* `meta_keywords`: Several words and short phrases separated by commas, also targeted at search engines + +To understand how variants come to be, you must first understand option types and option values. + +## Option Types and Option Values + +Option types denote the different options for a variant. A typical option type would be a size, with that option type's values being something such as "Small", "Medium" and "Large". Another typical option type could be a color, such as "Red", "Green", or "Blue". + +A product can be assigned many option types, but must be assigned at least one if you wish to create variants for that product. + +## Variants + +`Variant` records track the individual variants of a `Product`. Variants are of two types: master variants and normal variants. + +Variant records can track some individual properties regarding a variant, such as height, width, depth, and cost price. These properties are unique to each variant, and so are different from [Product Properties](#product-properties), which apply to all variants of that product. + +### Master Variants + +Every single product has a master variant, which tracks basic information such as a count on hand, a price and a SKU. Whenever a product is created, a master variant for that product will be created too. + +Master variants are automatically created along with a product and exist for the sole purpose of having a consistent API when associating variants and [line items](orders#line-items). If there were no master variant, then line items would need to track a polymorphic association which would either be a product or a variant. + +By having a master variant, the code within Spree to track is simplified. + +### Normal Variants + +Variants which are not the master variant are unique based on [option type and option value](#option_type) combinations. For instance, you may be selling a product which is a Baseball Jersey, which comes in the sizes "Small", "Medium" and "Large", as well as in the colors of "Red", "Green" and "Blue". For this combination of sizes and colors, you would be able to create 9 unique variants: + +* Small, Red +* Small, Green +* Small, Blue +* Medium, Red +* Medium, Green +* Medium, Blue +* Large, Red +* Large, Green +* Large, Blue + +## Images + +Images link to a product through its master variant. The sub-variants for the product may also have their own unique images to differentiate them in the frontend. + +Spree automatically handles creation and storage of several size versions of each image (via the Paperclip plugin). The default styles are as follows: + +```ruby +:styles => { + :mini => '48x48>', + :small => '100x100>', + :product => '240x240>', + :large => '600x600>' +} +``` + +These sizes can be changed by altering the value of `Spree::Config[:attachment_styles]`. Once `Spree::Config[:attachment_styles]` has been changed, you *must* regenerate the paperclip thumbnails by running this command: + +```bash +$ bundle exec rake paperclip:refresh:thumbnails CLASS=Spree::Image +``` + +If you want to change the image that is displayed when a product has no image, simply create new versions of the files within [Spree's app/assets/images/noimage directory](https://github.com/spree/spree/tree/master/frontend/app/assets/images/noimage). These image names must match the keys within `Spree::Config[:attachment_styles]`. + +## Product Properties + +Product properties track individual attributes for a product which don't apply to all products. These are typically additional information about the item. For instance, a T-Shirt may have properties representing information about the kind of material used, as well as the type of fit the shirt is. + +A `Property` should not be confused with an [`OptionType`](#option_type), which is used when defining [Variants](#variants) for a product. + +You can retrieve the value for a property on a `Product` object by calling the `property` method on it and passing through that property's name: + +```bash +$ product.property("material") +=> "100% Cotton" +``` + +You can set a property on a product by calling the `set_property` method: + +```ruby +product.set_property("material", "100% cotton") +``` + +If this property doesn't already exist, a new `Property` instance with this name will be created. + +## Multi-Currency Support + +`Price` objects track a price for a particular currency and variant combination. For instance, a [Variant](#variants) may be available for $15 (15 USD) and €7 (7 Euro). + +This presence or lack of a price for a variant in a particular currency will determine if that variant is visible in the frontend. If no variants of a product have a particular price value for the site's current currency, that product will not be visible in the frontend. + +You may see what price a product would be in the current currency (`Spree::Config[:currency]`) by calling the `price` method on that instance: + +```bash +$ product.price +=> "15.99" +``` + +To find a list of currencies that this product is available in, call `prices` to get a list of related `Price` objects: + +```bash +$ product.prices +=> [#Shipping Methods) as follows: + +|Name|Zone|Calculator| +|---:|---:|---:| +|USPS Ground|US|Flexi Rate($5,$2)| +|FedEx|EU_VAT|FlatRate-per-item($10)| + +### Advanced Setup + +Consider you sell products to a single zone (US) and you ship from 2 locations (Stock Locations): + +* New York +* Los Angeles + +and you work with 3 deliverers (Shipping Methods): + +* FedEx +* DHL +* US postal service + +and your products can be classified into 3 Shipping Categories: + +* Light +* Regular +* Heavy + +and their pricing is as follow: + +FedEx charges: + +* $10 for all light items regardless of how many you have +* $2 per regular item +* $20 for the first heavy item and $15 for each additional one + +DHL charges: + +* $5 per item if it's light or regular +* $50 per item if it's heavy + +USPS charges: + +* $8 per item if it's light or regular +* $20 per item if it's heavy + +To achieve this setup you need the following configuration: + +* 4 Shipping Categories: Default, Light, Regular and Heavy +* 3 Shipping Methods (Configuration->Shipping Methods): FedEx, DHL, USPS +* 2 Stock Locations (Configuration->Stock Locations): New York, Los Angeles + +|S. Category / S. Method|DHL|FedEx|USPS| +|---:|---:|---:|---:| +|Light|Per Item ($5)|Flat Rate ($10)|Per Item ($8)| +|Regular|Per Item ($5)|Per Item ($2)|Per Item ($8)| +|Heavy|Per Item ($50)|Flexi Rate($20,$15)|Per Item ($20)| + +## Design & Functionality + +To properly leverage Spree's shipping system's flexibility you must understand a few key concepts: + +* Shipping Methods +* Zones +* Shipping Categories +* Calculators (through Shipping Rates) + +### Shipping Methods + +Shipping methods are the actual services used to send the product. For example: + +* UPS Ground +* UPS One Day +* FedEx 2Day +* FedEx Overnight +* DHL International + +Each shipping method is only applicable to a specific `Zone`. For example, you wouldn't be able to get a package delivered internationally using a domestic-only shipping method. You can't ship from Dallas, USA to Rio de Janeiro, Brazil using UPS Ground (a US-only carrier). + +If you are using shipping categories, these can be used to qualify or disqualify a given shipping method. + +*** +**Note**: Shipping methods can now have multiple shipping categories assigned to them. This allows the shipping methods available to an order to be determined by the shipping categories of the items in a shipment. +*** + +### Zones + +Zones serve as a mechanism for grouping geographic areas together into a single entity. You can read all about how to configure and use Zones in the [Zones Guide](addresses#zones). + +The Shipping Address entered during checkout will define the zone the customer is in and limit the Shipping Methods available to him. + +### Shipping Categories + +Shipping Categories are useful if you sell products whose shipping pricing vary depending on the type of product (TVs and Mugs, for instance). + +*** +For simple setups, where shipping for all products is priced the same (ie. T-shirt-only shop), all products would be assigned to the default shipping category for the store. +*** + +Some examples of Shipping Categories would be: + +* Light (for lightweight items like stickers) +* Regular +* Heavy (for items over a certain weight) + +Shipping Categories are created in the admin interface ("Configuration" -> "Shipping Categories") and then assigned to products ("Products" -> "Edit"). + +$$$ +Follow up: on a clean install + seed data I ended up with two Shipping Categories - "Default Shipping" and "Default" +$$$ + +During checkout, the shipping categories of the products in your order will determine which calculator will be used to price its shipping for each Shipping Method. + +### Calculators + +A Calculator is the component responsible for calculating the shipping price for each available Shipping Method. + +Spree ships with 5 default Calculators: + +* Flat rate (per order) +* Flat rate (per item/product) +* Flat percent +* Flexible rate +* Price sack + +Flexible rate is defined as a flat rate for the first product, plus a different flat rate for each additional product. + +You can define your own calculator if you have more complex needs. In that case, check out the [Calculators Guide](calculators). + +## UI + +### What the Customer Sees + +In the standard system, there is no mention of shipping until the checkout phase. + +After entering a shipping address, the system displays the available shipping options and their costs for each shipment in the order. Only the shipping options whose zones include the _shipping_ address are presented. + +The customer must choose a shipping method for each shipment before proceeding to the next stage. At the confirmation step, the shipping cost will be shown and included in the order's total. + +*** +You can enable collection of extra _shipping instructions_ by setting the option `Spree::Config.shipping_instructions` to `true`. This is set to `false` by default. See [Shipping Instructions](#shipping-instructions) below. +*** + +### What the Order's Administrator Sees + +`Shipment` objects are created during checkout for an order. Initially each records just the shipping method and the order it applies to. The administrator can update the record with the actual shipping cost and a tracking code, and may also (once only) confirm the dispatch. This confirmation causes a shipping date to be set as the time of confirmation. + +## Advanced Shipping Methods + +Spree comes with a set of calculators that should fit most of the shipping situations that may arise. If the calculators that come with Spree are not enough for your needs, you might want to use an extension - if one exists to meet your needs - or create a custom one. + +### Extensions + +There are a few Spree extensions which provide additional shipping methods, including special support for fees imposed by common carriers, or support for bulk orders. See the [Spree Extension Registry](http://spreecommerce.com/extensions) for the latest information. + +### Writing Your Own + +For more detailed information, check out the section on [Calculators](calculators). + +Your calculator should accept an array of `LineItem` objects and return a cost. It can look at any reachable data, but typically uses the address, the order and the information from variants which are contained in the line_items. + +## Product Configuration + +Store administrators can assign products to specific ShippingCategories or include extra information in variants to enable the calculator to determine results. + +Each product has a `ShippingCategory`, which adds product-specific information to the calculations beyond the standard information from the shipment. Standard information includes: + +* Destination address +* Variants and quantities +* Weight and dimension information, if given, for a variant + +`ShippingCategory` is basically a wrapper for a string. One use is to code up specific rates, eg. "Fixed $20" or "Fixed $40", from which a calculator could extract imposed prices (and not go through its other calculations). + +### Variant Configuration + +Variants can be specified with weight and dimension information. Some shipping method calculators will use this information if it is present. + +## Shipping Instructions + +The option `Spree::Config[:shipping_instructions]` controls collection of additional shipping instructions. This is turned off (set to `false`) by default. If an order has any shipping instructions attached, they will be shown in an order's shipment admin page and can also be edited at that stage. Observe that instructions are currently attached to the _order_ and not to actual _shipments_. + +## The Active Shipping Extension + +The popular `spree_active_shipping` extension harnesses the `active_shipping` gem to interface with carrier APIs such as USPS, Fedex and UPS, ultimately providing Spree-compatible calculators for the different delivery services of those carriers. + +To install the `spree-active-shipping` extension add the following to your `Gemfile`: + +```ruby +gem 'spree_active_shipping' +gem 'active_shipping', :git => 'git://github.com/Shopify/active_shipping.git' +``` + +and run `bundle install` from the command line. + +As an example of how to use the [spree_active_shipping extension](https://github.com/spree/spree_active_shipping) we'll demonstrate how to configure it to work with the USPS API. The other carriers follow a very similar pattern. + +For each USPS delivery service you want to offer (e.g. "USPS Media Mail"), you will need to create a `ShippingMethod` with a descriptive name ("Configuration" -> "Shipping Methods") and a `Calculator` (registered in the `active_shipping` extension) that ties the delivery service and the shipping method together. + +### Default Calculators + +The `spree_active_shipping` extension comes with several pre-configured calculators out of the box. For example, here are the ones provided for the USPS carrier: + +```ruby +def activate + [ + #... calculators for Fedex and UPS not shown ... + Calculator::Usps::MediaMail, + Calculator::Usps::ExpressMail, + Calculator::Usps::PriorityMail, + Calculator::Usps::PriorityMailSmallFlatRateBox, + Calculator::Usps::PriorityMailRegularMediumFlatRateBoxes, + Calculator::Usps::PriorityMailLargeFlatRateBox + ].each(&:register) +end +``` + +Each USPS delivery service you want to make available at checkout has to be associated with a corresponding shipping method. Which shipping methods are made available at checkout is ultimately determined by the zone of the customer's shipping address. The USPS' basic shipping categories are domestic and international, so we'll set up zones to mimic this distinction. We need to set up two zones then - a domestic one, consisting of the USA and its territories; and an international one, consisting of all other countries. + +With zones in place, we can now start adding some shipping methods through the admin panel. The only other essential requirement to calculate the shipping total at checkout is that each product and variant be assigned a weight. + +The `spree_active_shipping` gem needs some configuration variables set in order to consume the carrier web services. + +```ruby + # these can be set in an initializer in your site extension + Spree::ActiveShipping::Config.set(:usps_login => "YOUR_USPS_LOGIN") + Spree::ActiveShipping::Config.set(:fedex_login => "YOUR_FEDEX_LOGIN") + Spree::ActiveShipping::Config.set(:fedex_password => "YOUR_FEDEX_PASSWORD") + Spree::ActiveShipping::Config.set(:fedex_account => "YOUR_FEDEX_ACCOUNT") + Spree::ActiveShipping::Config.set(:fedex_key => "YOUR_FEDEX_KEY") +``` + +### Adding Additional Calculators + +Additional delivery services that are not pre-configured as a calculator in the `spree_active_shipping` extension can be easily added. Say, for example, you need First Class International Parcels via the US Postal Service. + +First, create a calculator class that inherits from `Calculator::Usps::Base` and implements a description class method: + +```ruby +class Calculator::Usps::FirstClassMailInternationalParcels < Calculator::Usps::Base + def self.description + "USPS First-Class Mail International Package" + end +end +``` + +!!! +Note that, unlike calculators that you write yourself, these calculators do not have to implement a `compute` instance method that returns a shipping amount. The superclasses take care of that requirement. +!!! + +There is one gotcha to bear in mind: the string returned by the `description` method must _exactly_ match the name of the USPS delivery service. To determine the exact spelling of the delivery service, you'll need to examine what gets returned from the API: + +```ruby +class Calculator::ActiveShipping < Calculator + def compute(line_items) + #.... + rates = retrieve_rates(origin, destination, packages(order)) + # the key of this hash is the name you need to match + # raise rates.inspect + + return nil unless rates + rate = rates[self.description].to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0) + return nil unless rate + # divide by 100 since active_shipping rates are expressed as cents + + return rate/100.0 + end + + def retrieve_rates(origin, destination, packages) + #.... + # carrier is an instance of ActiveMerchant::Shipping::USPS + response = carrier.find_rates(origin, destination, packages) + # turn this beastly array into a nice little hash + h = Hash[*response.rates.collect { |rate| [rate.service_name, rate.price] }.flatten] + #.... + end +end +``` + +As you can see in the code above, the `spree_active_shipping` gem returns an array of services with their corresponding prices, which the `retrieve_rates` method converts into a hash. Below is what would get returned for an order with an international destination: + +```ruby +{ + "USPS Priority Mail International Flat Rate Envelope"=>1345, + "USPS First-Class Mail International Large Envelope"=>376, + "USPS USPS GXG Envelopes"=>4295, + "USPS Express Mail International Flat Rate Envelope"=>2895, + "USPS First-Class Mail International Package"=>396, + "USPS Priority Mail International Medium Flat Rate Box"=>4345, + "USPS Priority Mail International"=>2800, + "USPS Priority Mail International Large Flat Rate Box"=>5595, + "USPS Global Express Guaranteed Non-Document Non-Rectangular"=>4295, + "USPS Global Express Guaranteed Non-Document Rectangular"=>4295, + "USPS Global Express Guaranteed (GXG)"=>4295, + "USPS Express Mail International"=>2895, + "USPS Priority Mail International Small Flat Rate Box"=>1345 +} +``` + +From all of the viable shipping services in this hash, the `compute` method selects the one that matches the description of the calculator. At this point, an optional flat handling fee (set via preferences) can be added: + +```ruby +rate = rates[self.description].to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0) +``` + +Finally, don't forget to register the calculator you added. In extensions, this is accomplished with the `activate` method: + +```ruby +def activate + Calculator::Usps::FirstClassMailInternationalParcels.register +end +``` + +## Filtering Shipping Methods On Criteria Other Than the Zone + +Ordinarily, it is the zone of the shipping address that determines which shipping methods are displayed to a customer at checkout. Here is how the availability of a shipping method is determined: + +```ruby +class Spree::Stock::Estimator + def shipping_methods(package) + shipping_methods = package.shipping_methods + shipping_methods.delete_if { |ship_method| !ship_method.calculator.available?(package.contents) } + shipping_methods.delete_if { |ship_method| !ship_method.include?(order.ship_address) } + shipping_methods.delete_if { |ship_method| !(ship_method.calculator.preferences[:currency].nil? || ship_method.calculator.preferences[:currency] == currency) } + shipping_methods + end +end +``` + +Unless overridden, the calculator's `available?` method returns `true` by default. It is, therefore, the zone of the destination address that filters out the shipping methods in most cases. However, in some circumstances it may be necessary to filter out additional shipping methods. + +Consider the case of the USPS First Class domestic shipping service, which is not offered if the weight of the package is greater than 13oz. Even though the USPS API does not return the option for First Class in this instance, First Class will appear as an option in the checkout view with an unfortunate value of 0, since it has been set as a Shipping Method. + +To ensure that First Class shipping is not available for orders that weigh more than 13oz, the calculator's `available?` method must be overridden as follows: + +```ruby +class Calculator::Usps::FirstClassMailParcels < Calculator::Usps::Base + def self.description + "USPS First-Class Mail Parcel" + end + + def available?(order) + multiplier = 1.3 + weight = order.line_items.inject(0) do |weight, line_item| + weight + (line_item.variant.weight ? (line_item.quantity * line_item.variant.weight * multiplier) : 0) + end + #if weight in ounces > 13, then First Class Mail is not available for the order + weight > 13 ? false : true + end +end +``` + +## Split Shipments + +### Introduction + +Split shipments are a new feature as of Spree 2.0 that addresses the needs of complex Spree stores that require sophisticated shipping and warehouse logic. This includes detailed inventory management and allows for shipping from multiple locations. + +![image](../images/developer/core/split_shipments_checkout.png) + +### Creating Proposed Shipments + +This section steps through the basics of what is involved in determining shipments for an order. There are a lot of pieces that make up this process. They are explained in detail in the [Components of Split Shipments](#components-of-split-shipments) section of this guide. + +The process of determining shipments for an order is triggered by calling `create_proposed_shipments` on an `Order` object while transitioning to the `delivery` state during checkout. This process will first delete any existing shipments for an order and then determine the possible shipments available for that order. + +`create_proposed_shipments` will initially call `Spree::Stock::Coordinator.new(@order).packages`. This will return an array of packages. In order to determine which items belong in which package when they are being built, Spree uses an object called a `Splitter`, described in more detail [below](#the-packer). + +After obtaining the array of available packages, they are converted to shipments on the order object. Shipping rates are determined and inventory units are created during this process as well. + +At this point, the checkout process can continue to the delivery step. + +## Components of Split Shipments + +This section describes the four main components that power split shipments: [The Coordinator](#the-coordinator), [The Packer](#the-packer), [The Prioritizer](#the-prioritizer), and [The Estimator](#the-estimator). + +### The Coordinator + +The `Spree::Stock::Coordinator` is the starting point for determining shipments when calling `create_proposed_shipments` on an order. Its job is to go through each `StockLocation` available and determine what can be shipped from that location. + +The `Spree::Stock::Coordinator` will ultimately return an array of packages which can then be easily converted into shipments for an order by calling `to_shipment` on them. + +### The Packer + +A `Spree::Stock::Packer` object is an important part of the `create_proposed_shipments` process. Its job is to determine possible packages for a given StockLocation and order. It uses rules defined in classes known as `Splitters` to determine what packages can be shipped from a `StockLocation`. + +For example, we may have two splitters for a stock location. One splitter has a rule that any order weighing more than 50lbs should be shipped in a separate package from items weighing less. Our other splitter is a catch-all for any item weighing less than 50lbs. So, given one item in an order weighing 60lbs and two items weighing less, the Packer would use the rules defined in our splitters to come up with two separate packages: one containing the single 60lb item, the other containing our other two items. + +#### Default Splitters + +Spree comes with two default splitters which are run in sequence. This means that the first splitter takes the packages array from the order, and each subsequent splitter takes the output of the splitter that came before it. + +Let's take a look at what the default splitters do: + +* **Shipping Category Splitter**: Splits an order into packages based on items' shipping categories. This means that each package will only have items that all belong to the same shipping category. +* **Weight Splitter**: Splits an order into packages based on a weight threshold. This means that each package has a mass weight. If a new item is added to the order and it causes a package to go over the weight threshold, a new package will be created so that all packages weigh less than the threshold. You can set the weight threshold by changing `Spree::Stock::Splitter::Weight.threshold` (defaults to `150`) in an initializer. + +#### Custom Splitters + +Note that splitters can be customized, and creating your own can be done with relative ease. By inheriting from `Spree::Stock::Splitter::Base`, you can create your own splitter. + +For an example of a simple splitter, take a look at Spree's [weight based splitter](https://github.com/spree/spree/blob/235e470b242225d7c75c7c4c4c033ee3d739bb36/core/app/models/spree/stock/splitter/weight.rb). This splitter pulls items with a weight greater than 150 into their own shipment. + +After creating your splitter, you need to add it to the array of splitters Spree +uses. To do this, add the following to your application's spree initializer +`spree.rb` file: + +```ruby +Rails.application.config.spree.stock_splitters << Spree::Stock::Splitter::CustomSplitter +``` + +You can also completely override the splitters used in Spree, rearrange them, etc. +To do this, add the following to your `spree.rb` file: + +```ruby +Rails.application.config.spree.stock_splitters = [ + Spree::Stock::Splitter::CustomSplitter, + Spree::Stock::Splitter::ShippingCategory +] +``` + +Or if you don't want to split packages just set the option above to an empty +array. e.g. a store with the following configuration in spree.rb won't have any +package splitted. + +```ruby +Rails.application.config.spree.stock_splitters = [] +``` + +If you want to add different splitters for each `StockLocation`, you need to decorate the `Spree::Stock::Coordinator` class and override the `splitters` method. + +### The Prioritizer + +A `Spree::Stock::Prioritizer` object will decide which `StockLocation` should ship which package from an order. The prioritizer will attempt to come up with the best shipping situation available to the user. + +By default, the prioritizer will first select packages where the items are on hand. Then it will try to find packages where items are backordered. During this process, the `Spree::Stock::Adjuster` is also used to ensure each package has the correct number of items. + +The prioritizer is also a customization point. If you want to customize which packages should take priority for the order during this process, you can override the `sort_packages` method in `Spree::Stock::Prioritizer`. + +#### Customizing the Adjuster + +The `Adjuster` visits each package in an order and ensures the correct number of items are in each package. To customize this functionality, you need to do two things: + +* Subclass the [Spree::Stock::Adjuster](https://github.com/spree/spree/blob/a55db75bbebc40f5705fc3010d1e5a2190bde79b/core/app/models/spree/stock/adjuster.rb) class and override the the `adjust` method to get the desired functionality. +* Decorate the `Spree::Stock::Coordinator` and override the `prioritize_packages` method, passing in your custom adjuster class to the `Prioritizer` initializer. For example, if our adjuster was called `Spree::Stock::CustomAdjuster`, we would do the following: + +```ruby +Spree::Stock::Coordinator.class_eval do + def prioritize_packages(packages) + prioritizer = Prioritizer.new(order, packages, Spree::Stock::CustomAdjuster) + prioritizer.prioritized_packages + end +end +``` + +### The Estimator + +The `Spree::Stock::Estimator` loops through the packages created by the packer in order to calculate and attach shipping rates to them. This information is then returned to the user so they can select shipments for their order and complete the checkout process. diff --git a/guides/content/developer/core/taxation.md b/guides/content/developer/core/taxation.md new file mode 100644 index 00000000000..0c9088e4acf --- /dev/null +++ b/guides/content/developer/core/taxation.md @@ -0,0 +1,138 @@ +--- +title: Taxation +section: core +--- + +## Overview + +Spree represents taxes for an order by using `tax_categories` and `tax_rates`. + +Products within Spree can be linked to Tax Categories, which are then used to influence the taxation rate for the products when they are purchased. One Tax Category can be set to being the default for the entire system, which means that if a product doesn't have a related tax category, then this default tax category would be used. + +A `tax_category` can have many `tax_rates`, which indicate the rate at which the products belonging to a specific tax category will be taxed at. A tax rate links a tax rate to a particular zone (see [Addresses](addresses) for more information about zones). When an order is placed in a specific zone, any of the products for that order which have a tax zone that matches the order's tax zone will be taxed. + +The standard sales tax policies commonly found in the USA can be modeled as well as Value Added Tax (VAT) which is commonly used in Europe. These are not the only types of tax rules that you can model in Spree. Once you obtain a sufficient understanding of the basic concepts you should be able to model the tax rules of your country or jurisdiction. + +*** +Taxation within the United States can get exceptionally complex, with different states, counties and even cities having different taxation rates. If you are shipping interstate within the United States, we would strongly advise you to use the [Spree Tax Cloud](https://github.com/spree-contrib/spree_tax_cloud) extension so that you get correct tax rates. +*** + +## Tax Categories + +The Spree default is to treat everything as exempt from tax. In order for a product to be considered taxable, it must belong to a tax category. The tax category is a special concept that is specific to taxation. The tax category is normally never seen by the user so you could call it something generic like "Taxable Goods." If you wish to tax certain products at different rates, however, then you will want to choose something more descriptive (ex. "Clothing."). + +*** +It can be somewhat tedious to set the tax category for every product. We're currently exploring ways to make this simpler. If you are importing inventory from another source you will likely be writing your own custom Ruby program that automates this process. +*** + +## Tax Rates + +A tax rate is essentially a percentage amount charged based on the sales price. Tax rates also contain other important information. + +* Whether product prices are inclusive of this tax +* The zone in which the order address must fall within +* The tax category that a product must belong to in order to be considered taxable. + +Spree will calculate tax based on the best matching zone for the order. It's also possible to have more than one applicable tax rate for a single zone. In order for a tax rate to apply to a particular product, that product must have a tax category that matches the tax category of the tax rate. + +## Basic Examples + +Let's say you need to charge 5% tax for all items that ship to New York and 6% on only clothing items that ship to Pennsylvania. This will mean you need to construct two different zones: one zone containing just the state of New York and another zone consisting of the single state of Pennsylvania. + +Here's another hypothetical scenario. You would like to charge 10% tax on all electronic items and 5% tax on everything else. This tax should apply to all countries in the European Union (EU). In this case you would construct just a single zone consisting of all the countries in the EU. The fact that you want to charge two different rates depending on the type of good does not mean you need two zones. + +*** +Please see the [Addresses guide](addresses) for more information on constructing a zone. +*** + +## Default Tax Zone + +Spree also has the concept of a default tax zone. When a user is adding items to their cart we do not yet know where the order will be shipped to, and so it's assumed that the cart is within the default tax zone until later. In some cases we may want to estimate the tax for the order by assuming the order falls within a particular tax zone. + +Why might we want to do this? The primary use case for this is for countries where there is already tax included in the price. In the EU, for example, most products have a Value Added Tax (VAT) included in the price. There are cases where it may be desirable to show the portion of the product price that includes tax. In order to calculate this tax amount we need to know the zone (and corresponding Tax Rate) that was assumed in the price. + +We may also reduce the order total by the tax amount if the order is being shipped outside the tax jurisdiction. Again, this requires us to know the zone assumed in making the original tax calculation so that the tax amount can be backed out. + +## Shipping vs. Billing Address + +Most tax jurisdictions base the tax on the shipping address of where the order is being shipped to. So in these cases the shipping address is used when determining the tax zone. Spree does, however, allow you to use the billing address to determine the zone. + +To determine tax based on billing address instead of shipping address you will need to set the `Spree::Config[:tax_using_ship_address]` preference to `false`. + +*** +`Zone.match` is a method used to determine the most applicable zone for taxation. In the case of multiple matches, the closer match will be used, with State zone matches having priority over Country zone matches. +*** + +## Calculators + +In order to charge tax in Spree you also need a `Spree::Calculator`. In most cases you should be able to use Spree's `DefaultTax` calculator. It is suitable for both sales tax and price-inclusive tax scenarios. For more information, please read the [Calculators guide](calculators). + +*** +The `DefaultTax` calculator uses the item total (exclusive of shipping) when computing sales tax. +*** + +## Tax Types + +There are two basic types of tax that a store owner might need to contend with. In the United States (and some other countries) store owners sometimes need to charge what is known as "sales tax." In the European Union (EU) and other countries stores owners need to deal with "tax inclusive" pricing which is often called Value Added Tax (VAT). + +*** +Most taxes can be considered one of these two types. For instance, in Australia customers pay a Goods and Services Tax (GST). This is basically equivalent to VAT in Europe. +*** + +In some cases you may need to charge one type of tax for orders falling within one zone and another type of tax for orders falling within a different zone. There are even some rare situations where you may need to charge both types of tax in the same zone. Spree supports all of these scenarios. + +### Sales Tax + +Sales tax is the default tax type for any tax rate in Spree. + +Let's take an example of a sales tax situation for the United States. Imagine that we have a zone that covers all of North America and that the zone is used for a tax rate which applies a 5% tax on products with the tax category of "Clothing". + +If the customer purchases a single clothing item for $17.99 and they live in the United States (which is within the North America zone we defined) they are eligible to pay sales tax. + +The sales tax calculation is $17.99 x 5% for a total tax of $0.8995, which is rounded up to two decimal places, to $0.90. This tax amount is then applied to the order as an adjustment. + +*** +See the [Adjustments Guide](adjustments) if you need more information on adjustments. +*** + +If the quantity of the item is changed to 2, then the tax amount doubles: ($17.99 x 2) x 0.05 is $1.799, which is again rounded up to two decimal places, applying a tax adjustment of $1.80. + +Let's now assume that we have another product that's a coffee mug, which doesn't have the "Clothing" tax category applied to it. Let's also assume this product costs $13.99, and there's no default tax category set up for the system. Under these circumstances, the coffee mug will not be taxed when it's added to the order. + +Finally, if the taxable address (either the shipping or billing, depending on the `Spree::Config[:tax_using_ship_address]` setting) is changed for the order to outside this taxable zone, then the tax adjustment on the order will be removed. If the address is changed back, the tax rate will be applied once more. + +### Tax Included + +Many jurisdictions have what is commonly referred to as a Value Added Tax (VAT.) In these cases the tax is typically applied to the price. This means that prices for items are "inclusive of tax" and no additional tax needs to be applied during checkout. + +In the case of tax inclusive pricing the store owner should enter all prices inclusive of tax. This makes it easy for Spree to display the tax inclusive price since it won't have to be constantly calculated on the fly. + +*** +Keep in mind that each order records the price a customer paid (including the tax) as part of the line item record. This means you don't have to worry about changing prices or tax rates affecting older orders. +*** + +If you are going to designate one or more tax rates as being included in the price then you must first set up a default tax zone. Spree will not allow you to proceed without performing this step. This default zone is needed in cases where a customer might not be eligible to pay the tax. + +*** +You must choose a default tax zone if you are going to mark at a tax rate as being included in your product prices. +*** + +When tax is included in the price there is no order adjustment needed (unlike the sales tax case). Stores are, however, typically interested in showing the amount of tax the user paid. These totals are for informational purposes only and do not affect the order total. + +Let's start by looking at an example where there is a 5% included on all products and it's included in the price. We'll further assume that this tax should only apply to orders within the United Kingdom (UK). + +In the case where the order address is within the UK and we purchase a single clothing item for £17.99 we see an order total of £17.99. The tax rate adjustment applied is £17.99 x 5%, which is £0.8995, and that is rounded up to two decimal places, becoming £0.90. + +Now let's increase the quantity on the item from 1 to 2. The order total changes to £35.98 with a tax total of £1.799, which is again rounded up to now being £1.80. + +Next we'll add a different clothing item costing £19.99 to our order. Since both items are clothing and taxed at the same rate, they can be reduced to a single total, which means there's a single adjustment still applied to the order, calculated like this: (£17.99 + £19.99) x 0.05 = £1.899, rounded up to two decimal places: £1.90. + +Now let's assume an additional tax rate of 10% on a "Consumer Electronics" tax category. When we add a product with this tax category to our order with a price of £16.99, there will be a second adjustment added to the order, with a calculated total of £16.99 x 10%, which is £1.699. Rounded up, it's £1.70. + +Finally, if the order's address is changed to being outside this tax zone, then there will be two negative adjustments applied to remove these tax rates from the order. + +### Additional Examples + +!!! +All of the examples in this guide are meant to be used for illustrative purposes. They are not meant to be used as definitive interpretations of tax law. You should consult your accountant or attorney for guidance on how much tax to collect and under what circumstances. +!!! diff --git a/guides/content/developer/customization/asset.markdown b/guides/content/developer/customization/asset.markdown new file mode 100644 index 00000000000..fa66b99feb0 --- /dev/null +++ b/guides/content/developer/customization/asset.markdown @@ -0,0 +1,241 @@ +--- +title: "Asset Customization" +section: customization +--- + +## Overview + +This guide covers how Spree manages its JavaScript, stylesheet and image +assets and how you can extend and customize them including: + +- Understanding Spree's use of the Rails asset pipeline +- Managing application specific assets +- Managing extension specific assets +- Overriding Spree's core assets + +## Spree's Asset Pipeline + +With the release of 3.1 Rails now supports powerful asset management +features and Spree fully leverages these features to further extend and +simplify its customization potential. Using asset customization +techniques outlined below you be able to adapt all the JavaScript, +stylesheets and images contained in Spree to easily provide a fully +custom experience. + +All Spree generated (or upgraded) applications include an `app/assets` +directory (as is standard for all Rails 3.1 apps). We've taken this one +step further by subdividing each top level asset directory (images, +JavaScript files, stylesheets) into frontend and backend directories. This is +designed to keep assets from the frontend and backend from conflicting with each other. + +A typical assets directory for a Spree application will look like: + + app + |-- assets + |-- images + | |-- spree + | |-- frontend + | |-- backend + |-- javascripts + | |-- spree + | |-- frontend + | | |-- all.js + | |-- backend + | |-- all.js + |-- stylesheets + | |-- spree + | |-- frontend + | | |-- all.css + | |-- backend + | |-- all.css + +Spree also generates four top level manifests (all.css & all.js, see +above) that require all the core extension's and site specific +stylesheets / JavaScript files. + +### How core extensions (engines) manage assets + +All core engines have been updated to provide four asset manifests that +are responsible for bundling up all the JavaScript files and stylesheets +required for that engine. + +For example, Spree provides the following manifests: + + vendor + |-- assets + |-- javascripts + | |-- spree + | |-- frontend + | | |-- all.js + | |-- backend + | |-- all.js + |-- stylesheets + | |-- spree + | |-- frontend + | | |-- all.css + | |-- backend + | |-- all.css + +These manifests are included by default by the +relevant all.css or all.js in the host Spree application. For example, +`vendor/assets/javascripts/spree/backend/all.js` includes: + +```ruby +//= require jquery +//= require jquery_ujs + +//= require spree/backend + +//= require_tree . +``` + +External JavaScript libraries, stylesheets and images have also be +relocated into vendor/assets (again Rails 3.1 standard approach), and +all core extensions no longer have public directories. + +## Managing your application's assets + +Assets that customize your Spree store should go inside the appropriate +directories inside `vendor/assets/images/spree`, `vendor/assets/javascripts/spree`, +or `vendor/assets/stylesheets/spree`. This is done so that these assets do +not interfere with other parts of your application. + +## Managing your extension's assets + +We're suggesting that all third party extensions should adopt the same +approach as Spree and provide the same four (or less depending on +what the extension requires) manifest files, using the same directory +structure as outlined above. + +Third party extension manifest files will not be automatically included +in the relevant all.(js|css) files so it's important to document the +manual inclusion in your extensions installation instructions or provide +a Rails generator to do so. + +For an example of an extension using a generator to install assets and +migrations take a look at the [install_generator for spree_fancy](https://github.com/spree/spree_fancy/blob/master/lib/generators/spree_fancy/install/install_generator.rb). + +## Overriding Spree's core assets + +Overriding or replacing any of Spree's internal assets is even easier +than before. It's recommended to attempt to replace as little as +possible in a given JavaScript or stylesheet file to help ease future +upgrade work required. + +The methods listed below will work for both applications, extensions and +themes with one noticeable difference: Extension & theme asset files +will not be automatically included (see above for instructions on how to +include asset files from your extensions / themes). + +### Overriding individual CSS styles + +Say for example you want to replace the following CSS snippet: + +```css +/* spree/app/assets/stylesheets/spree/frontend/screen.css */ + +div#footer { + clear: both; +} +``` + +You can now just create a new stylesheet inside +`your_app/vendor/assets/stylesheets/spree/frontend/` and include the following CSS: + +```css +/* vendor/assets/stylesheets/spree/frontend/foo.css */ + +div#footer { + clear: none; + border: 1px solid red; +} +``` + +The `frontend/all.css` manifest will automatically include `foo.css` and it +will actually include both definitions with the one from `foo.css` being +included last, hence it will be the rule applied. + +### Overriding entire CSS files + +To replace an entire stylesheet as provided by Spree you simply need to +create a file with the same name and save it to the corresponding path +within your application's or extension's `vendor/assets/stylesheets` +directory. + +For example, to replace `spree/frontend/all.css` you would save the replacement +to `your_app/vendor/assets/stylesheets/spree/frontend/all.css`. + +*** +This same method can also be used to override stylesheets provided by +third-party extensions. +*** + +### Overriding individual JavaScript functions + +A similar approach can be used for JavaScript functions. For example, if +you wanted to override the `show_variant_images` method: + +```javascript + // spree/app/assets/javascripts/spree/frontend/product.js + +var show_variant_images = function(variant_id) { + $('li.vtmb').hide(); + $('li.vtmb-' + variant_id).show(); + var currentThumb = $('#' + + $("#main-image").data('selectedThumbId')); + + // if currently selected thumb does not belong to current variant, + // nor to common images, + // hide it and select the first available thumb instead. + + if(!currentThumb.hasClass('vtmb-' + variant_id) && + !currentThumb.hasClass('tmb-all')) { + var thumb = $($('ul.thumbnails li:visible').eq(0)); + var newImg = thumb.find('a').attr('href'); + $('ul.thumbnails li').removeClass('selected'); + thumb.addClass('selected'); + $('#main-image img').attr('src', newImg); + $("#main-image").data('selectedThumb', newImg); + $("#main-image").data('selectedThumbId', thumb.attr('id')); + } +} +``` + +Again, just create a new JavaScript file inside +`your_app/vendor/assets/javascripts/spree/frontend` and include the new method +definition: + +```javascript + // your_app/vendor/assets/javascripts/spree/frontend/foo.js + +var show_variant_images = function(variant_id) { + alert('hello world'); +} +``` + +The resulting `frontend/all.js` would include both methods, with the latter +being the one executed on request. + +### Overriding entire JavaScript files + +To replace an entire JavaScript file as provided by Spree you simply +need to create a file with the same name and save it to the +corresponding path within your application's or extension's +`app/assets/javascripts` directory. + +For example, to replace `spree/frontend/all.js` you would save the replacement to +`your_app/vendor/assets/javascripts/spree/frontend/all.js`. + +*** +This same method can be used to override JavaScript files provided +by third-party extensions. +*** + +### Overriding images + +Finally, images can be replaced by substituting the required file into +the same path within your application or extension as the file you would +like to replace. + +For example, to replace the Spree logo you would simply copy your logo +to: `your_app/vendor/assets/images/logo/spree_50.png`. diff --git a/guides/content/developer/customization/authentication.markdown b/guides/content/developer/customization/authentication.markdown new file mode 100644 index 00000000000..44bf314eaeb --- /dev/null +++ b/guides/content/developer/customization/authentication.markdown @@ -0,0 +1,280 @@ +--- +title: "Custom Authentication" +section: customization +--- + +## Overview + +This guide covers using a custom authentication setup with Spree, such +as one provided by your own application. This is ideal in situations +where you want to handle the sign-in or sign-up flow of your application +uniquely, outside the realms of what would be possible with Spree. After +reading this guide, you will be familiar with: + +- Setting up Spree to work with your custom authentication + +## Background + +Traditionally, applications that use Spree have needed to use the +*Spree::User* model that came with the *spree_auth* component of Spree. +With the advent of 1.2, this is no longer a restriction. The +*spree_auth* component of Spree has been removed and is now purely +opt-in. If you have an application that has used the *spree_auth* +component in the past and you wish to continue doing so, you will need +to add this extra line to your *Gemfile*: + +```ruby +gem 'spree_auth_devise', :git => "git://github.com/spree/spree_auth_devise" +``` + +By having this authentication component outside of Spree, applications +that wish to use their own authentication may do so, and applications +that have previously used *spree_auth*'s functionality may continue +doing so by using this gem. + +### The User Model + +This guide assumes that you have a pre-existing model inside your +application that represents the users of your application already. This +model could be provided by gems such as +[Devise](https://github.com/plataformatec/devise) or +[Sorcery](https://github.com/NoamB/sorcery). This guide also assumes +that the application that this *User* model exists in is already a Spree +application. + +This model **does not** need to be called *User*, but for the purposes +of this guide the model we will be referring to **will** be called +*User*. If your model is called something else, do some mental +substitution wherever you see *User*. + +#### Initial Setup + +To begin using your custom *User* class, you must first edit Spree's +initializer located at *config/initializers/spree.rb* by changing this +line: + +```ruby +Spree.user_class = "Spree::User" +``` + +To this: + +```ruby +Spree.user_class = "User" +``` + +Next, you need to run the custom user generator for Spree which will +create two files. The first is a migration that will add the necessary +Spree fields to your users table, and the second is an extension that +lives at *lib/spree/authentication_helpers.rb* to the +*Spree::Core::AuthenticationHelpers* module inside of Spree. + +Run this generator with this command: + +```bash +$ bundle exec rails g spree:custom_user User +``` + +This will tell the generator that you want to use the *User* class as +the class that represents users in Spree. Run the new migration by +running this: + +```bash +$ bundle exec rake db:migrate +``` + +Next you will need to define some methods to tell Spree where to find +your application's authentication routes. + +#### Authentication Helpers + +There are some authentication helpers of Spree's that you will need to +possibly override. The file at *lib/spree/authentication_helpers.rb* +contains the following code to help you do that: + +```ruby +module Spree + module AuthenticationHelpers + def self.included(receiver) + receiver.send :helper_method, :spree_login_path + receiver.send :helper_method, :spree_signup_path + receiver.send :helper_method, :spree_logout_path + receiver.send :helper_method, :spree_current_user + end + + def spree_current_user + current_person + end + + def spree_login_path + main_app.login_path + end + + def spree_signup_path + main_app.signup_path + end + + def spree_logout_path + main_app.logout_path + end + end +end + +Spree::BaseController.send :include, Spree::AuthenticationHelpers +Spree::Api::BaseController.send :include, Spree::AuthenticationHelpers +ApplicationController.send :include, Spree::AuthenticationHelpers +``` + +Each of the methods defined in this module return values that are the +most common in Rails applications today, but you may need to customize +them. In order, they are: + +* ***spree_current_user***: Used to tell Spree what the current user +of a request is. +* ***spree_login_path***: The location of the login/sign in form in +your application. +* ***spree_signup_path***: The location of the sign up form in your +application. +* ***spree_logout_path***: The location of the logout feature of your +application. + +*** +URLs inside the *spree_login_path*, *spree_signup_path* and +*spree_logout_path* methods **must** have *main_app* prefixed if they +are inside your application. This is because Spree will otherwise +attempt to route to a *login_path*, *signup_path* or *logout_path* +inside of itself, which does not exist. By prefixing with *main_app*, +you tell it to look at the application's routes. +*** + +You will need to define the *login_path*, *signup_path* and +*logout_path* routes yourself, by using code like this inside your +application's *config/routes.rb* if you're using Devise: + +```ruby +devise_scope :person do + get '/login', :to => "devise/sessions#new" + get '/signup', :to => "devise/registrations#new" + delete '/logout', :to => "devise/sessions#destroy" +end +``` + +Of course, this code will be different if you're not using Devise. +Simply do not use the *devise_scope* method and change the controllers +and actions for these routes. + +You can also customize the *spree_login_path*, *spree_signup_path* +and *spree_logout_path* methods inside +*lib/spree/authentication_helpers.rb* to use the routing helper methods +already provided by the authentication setup you have, if you wish. + +*** +Any modifications made to *lib/spree/authentication_helpers.rb* +while the server is running will require a restart, as wth any other +modification to other files in *lib*. +*** + +## The User Model + +Once you have specified *Spree.user_class* correctly, there will be +some new methods added to your *User* class. The first of these methods +are the ones added for the *has_and_belongs_to_many* association +called "spree_roles". This association will retrieve all the roles that +a user has for Spree. + +The second of these is the *spree_orders* association. This will return +all orders associated with the user in Spree. There's also a +*last_incomplete_spree_order* method which will return the last +incomplete spree order for the user. This is used internal to Spree to +persist order data across a user's login sessions. + +The third and fourth associations are for address information for a +user. When a user places an order, the address information for that +order will be linked to that user so that it is available for subsequent +orders. + +The next method is one called *has_spree_role?* which can be used to +check if a user has a specific role. This method is used internally to +Spree to check if the user is authorized to perform specific actions, +such as accessing the admin section. Admin users of your system should +be assigned the Spree admin role, like this: + +```ruby +user = User.find_by(email: "master@example.com") +user.spree_roles << Spree::Role.find_or_create_by(name: "admin") +``` + +To test that this has worked, use the *has_spree_role?* method, like +this: + +```ruby +user.has_spree_role?("admin") +``` + +If this returns *true*, then the user has admin permissions within +Spree. + +Finally, if you are using the API component of Spree, there are more +methods added. The first is the *spree_api_key* getter and setter +methods, used for the API key that is used with Spree. The next two +methods are *generate_spree_api_key!* and *clear_spree_api_key* +which will generate and clear the Spree API key respectively. + +## Login link + +To make the login link appear on Spree pages, you will need to use a +Deface override. Create a new file at +*app/overrides/auth_login_bar.rb* and put this content inside it: + +```ruby +Deface::Override.new(:virtual_path => "spree/shared/_nav_bar", + :name => "auth_shared_login_bar", + :insert_before => "li#search-bar", + :partial => "spree/shared/login_bar", + :disabled => false, + :original => 'eb3fa668cd98b6a1c75c36420ef1b238a1fc55ad') +``` + +This override references a partial called "spree/shared/login_bar". +This will live in a new partial called +*app/views/spree/shared/_login_bar.html.erb* in your application. You +may choose to call this file something different, the name is not +important. This file will then contain this code: + +```erb +<%% if spree_current_user %> +
        • + <%%= link_to Spree.t(:logout), spree_logout_path, :method => :delete %> +
        • +<%% else %> +
        • + <%%= link_to Spree.t(:login), spree_login_path %> +
        • +
        • + <%%= link_to Spree.t(:signup), spree_signup_path %> +
        • +<%% end %> +``` + +This will then use the URL helpers you have defined in +*lib/spree/authentication_helpers.rb* to define three links, one to +allow users to logout, one to allow them to login, and one to allow them +to signup. These links will be visible on all customer-facing pages of +Spree. + +## Signup promotion + +In Spree, there is a promotion that acts on the user signup which will +not work correctly automatically when you're not using the standard +authentication method with Spree. To fix this, you will need to trigger +this event after a user has successfully signed up in your application +by setting a session variable after successful signup in whatever +controller deals with user signup: + +```ruby +session[:spree_user_signup] = true +``` + +This line will cause the Spree event notifiers to be notified of this +event and to apply any promotions to an order that are triggered once a +user signs up. diff --git a/guides/content/developer/customization/checkout.md b/guides/content/developer/customization/checkout.md new file mode 100644 index 00000000000..45edf6af86a --- /dev/null +++ b/guides/content/developer/customization/checkout.md @@ -0,0 +1,384 @@ +--- +title: "The Checkout Flow API" +section: customization +--- + +## Overview + +The Spree checkout process has been designed for maximum flexibility. It's been redesigned several times now, each iteration has benefited from the feedback of real world deployment experience. It is relatively simple to customize the checkout process to suit your needs. Secure transmission of customer information is possible via SSL and credit card information is never stored in the database. + +The customization of the flow of the checkout can be done by using Spree's `checkout_flow` DSL, described in the [Checkout Flow DSL](#the-checkout-flow-dsl) section below. + +## Default Checkout Steps + +The Spree checkout process consists of the following steps. With the exception of the Registration step, each of these steps corresponds to a state of the `Spree::Order` object: + +* Registration (Optional - only if using spree_auth_devise extension, can be toggled through the `Spree::Auth::Config[:registration_step]` configuration setting) +* Address Information +* Delivery Options (Shipping Method) +* Payment +* Confirmation + +The following sections will provide a walk-though of a checkout from a user's perspective, and offer some information on how to configure the default behavior of the various steps. + +### Registration + +Prior to beginning the checkout process, the customer will be prompted to create a new account or to login to their existing account. By default, there is also a "guest checkout" option which allows users to specify only their email address if they do not wish to create an account. + +Technically, the registration step is not an actual state in the `Spree::Order` state machine. The `spree_auth_devise` gem (an extension that comes with Spree by default) adds the `check_registration` before filter to the all actions of `Spree::CheckoutController` (except for obvious reasons the `registration` and `update_registration` actions), which redirects to a registration page unless one of the following is true: + +* `Spree::Auth::Config[:registration_step]` preference is not `true` +* user is already logged in +* the current order has an email address associated with it + +The method is defined like this: + +```ruby +def check_registration + return unless Spree::Auth::Config[:registration_step] + return if spree_current_user or current_order.email + store_location + redirect_to spree.checkout_registration_path +end +``` + +The configuration of the guest checkout option is done via [Preferences](preferences). Spree will allow guest checkout by default. Use the `allow_guest_checkout` preference to change the default setting. + +### Address Information + +This step allows the customer to add both their billing and shipping information. Customers can click the "use billing address" option to use the same address for both. Selecting this option will have the effect of hiding the shipping address fields using JavaScript. If users have disabled JavaScript, the section will not disappear but it will copy over the address information once submitted. If you would like to automatically copy the address information via JavaScript on the client side, that is an exercise left to the developer. We have found the server side approach to be simpler and easier to maintain. + +The address fields include a select box for choosing state/province. The list of states will be populated via JavaScript and will contain all of the states listed in the database for the currently selected country. If there are no states configured for a particular country, or if the user has JavaScript disabled, the select box will be replaced by a text field instead. + +*** +The default "seed" data for Spree only includes the U.S. states. It's easy enough to add states or provinces for other countries but beyond the scope of the Spree project to maintain such a list. +*** + +*** +The state field can be disabled entirely by using the `Spree::Config[:address_requires_state]` preference. You can also allow for an "alternate phone" field by using the `Spree::Config[:alternative_billing_phone]` and `Spree::Config[:alternative_shipping]` fields. +*** + +The list of countries that appear in the country select box can also be configured. Spree will list all countries by default, but you can configure exactly which countries you would like to appear. The list can be limited to a specific set of countries by configuring the `Spree::Config[:checkout_zone]` preference and setting its value to the name of a [Zone](addresses#zones) containing the countries you wish to use. Spree assumes that the list of billing and shipping countries will be the same. You can always change this logic via an extension if this does not suit your needs. + +### Delivery Options + +$$$ +Better shipment documentation here after split_shipments merge. +$$$ + +During this step, the user may choose a delivery method. Spree assumes the list of shipping methods to be dependent on the shipping address. This is one of the reasons why it is difficult to support single page checkout for customers who have disabled JavaScript. + +### Payment + +This step is where the customer provides payment information. This step is intentionally placed last in order to minimize security issues with credit card information. Credit card information is never stored in the database so it would be impossible to have a subsequent step and still be able to submit the information to the payment gateway. Spree submits the information to the gateway before saving the model so that the sensitive information can be discarded before saving the checkout information. + +Spree stores only the last four digits of the credit card number along with the expiration information. The full credit card number and verification code are never stored in the Spree database. + +Several gateways such as ActiveMerchant and Beanstream provide a secure method for storing a "payment profile" in your database. This approach typically involves the use of a "token" which can be used for subsequent purchases but only with your merchant account. If you are using a secure payment profile it would then be possible to show a final "confirmation" step after payment information is entered. + +If you do not want to use a gateway with payment profiles then you will need to customize the checkout process so that your final step submits the credit card information. You can then perform an authorization before the order is saved. This is perfectly secure because the credit card information is not ever saved. It's transmitted to the gateway and then discarded like normal. + +!!! +Spree discards the credit card number after this step is processed. If +you do not have a gateway with payment profiles enabled then your card +information will be lost before it's time to authorize the card. +!!! + +For more information about payments, please see the [Payments guide](payments). + +### Confirmation + +This is the final opportunity for the customer to review their order before +submitting it to be processed. Users have the opportunity to return to any step +in the process using either the back button or by clicking on the appropriate +step in the "progress breadcrumb." + +This step is disabled by default (except for payment methods that support +payment profiles), but can be enabled by overriding the `confirmation_required?` +method in `Spree::Order`. + +## Checkout Architecture + +The following is a detailed summary of the checkout architecture. A complete +understanding of this architecture will allow you to be able to customize the +checkout process to handle just about any scenario you can think of. Feel free +to skip this section and come back to it later if you require a deeper +understanding of the design in order to customize your checkout. + +### Checkout Routes + +Three custom routes in spree_core handle all of the routing for a checkout: + +```ruby +put '/checkout/update/:state', :to => 'checkout#update', :as => :update_checkout +get '/checkout/:state', :to => 'checkout#edit', :as => :checkout_state +get '/checkout', :to => 'checkout#edit', :as => :checkout +``` + +The '/checkout' route maps to the `edit` action of the +`Spree::CheckoutController`. A request to this route will redirect to the +current state of the current order. If the current order was in the "address" +state, then a request to '/checkout' would redirect to '/checkout/address'. + +The '/checkout/:state' route is used for the previously mentioned route, and +also maps to the `edit` action of `Spree::CheckoutController`. + +The '/checkout/update/:state' route maps to the +`Spree::CheckoutController#update` action and is used in the checkout form to +update order data during the checkout process. + +### Spree::CheckoutController + +The `Spree::CheckoutController` drives the state of an order during checkout. +Since there is no "checkout" model, the `Spree::CheckoutController` is not a +typical RESTful controller. The spree_core and spree_auth_devise gems expose a +few different actions for the `Spree::CheckoutController`. + +The `edit` action renders the checkout/edit.html.erb template, which then +renders a partial with the current state, such as +`app/views/spree/checkout/address.html.erb`. This partial shows state-specific +fields for the user to fill in. If you choose to customize the checkout flow to +add a new state, you will need to create a new partial for this state. + +The `update` action performs the following: + +* Updates the `current_order` with the paramaters passed in from the current + step. +* Transitions the order state machine using the `next` event after successfully + updating the order. +* Executes callbacks based on the new state after successfully transitioning. +* Redirects to the next checkout step if the `current_order.state` is anything + other than `complete`, else redirect to the `order_path` for `current_order` + +*** +For security reasons, the `Spree::CheckoutController` will not update the +order once the checkout process is complete. It is therefore impossible for an +order to be tampered with (ex. changing the quantity) after checkout. +*** + +### Filters + +The `spree_core` and the default authentication gem (`spree_auth_devise`) gems +define several `before_filters` for the `Spree::CheckoutController`: + +* `load_order`: Assigns the `@order` instance variable and sets the `@order.state` to the `params[:state]` value. This filter also runs the "before" callbacks for the current state. +* `check_authorization`: Verifies that the `current_user` has access to `current_order`. +* `check_registration`: Checks the registration status of `current_user` and redirects to the registration step if necessary. + +### The Order Model and State Machine + + The `Spree::Order` state machine is the foundation of the checkout process. Spree makes use of the [state_machine](https://github.com/pluginaweek/state_machine) gem in the `Spree::Order` model as well as in several other places (such as `Spree::Shipment` and `Spree::InventoryUnit`.) + +The default checkout flow for the `Spree::Order` model is defined in +`app/models/spree/order/checkout.rb` of spree_core. + +An `Spree::Order` object has an initial state of 'cart'. From there any number +of events transition the `Spree::Order` to different states. Spree does not +have a separate model or database table for the shopping cart. What the user +considers a "shopping cart" is actually an in-progress `Spree::Order`. An order +is considered in-progress, or incomplete when its `completed_at` attribute is +`nil`. Incomplete orders can be easily filtered during reporting and it's also +simple enough to write a quick script to periodically purge incomplete orders +from the system. The end result is a simplified data model along with the +ability for store owners to search and report on incomplete/abandoned orders. + +*** +For more information on the state machine gem please see the [README](https://github.com/pluginaweek/state_machine) +*** + +## Checkout Customization + +It is possible to override the default checkout workflow to meet your store's needs. + +### Customizing an Existing Step + +Spree allows you to customize the individual steps of the checkout process. +There are a few distinct scenarios that we'll cover here. + +* Adding logic either before or after a particular step. +* Customizing the view for a particular step. + +### Adding Logic Before or After a Particular Step + +The state_machine gem allows you to implement callbacks before or after +transitioning to a particular step. These callbacks work similarly to [Active Record Callbacks](http://guides.rubyonrails.org/active_record_callbacks.html) +in that you can specify a method or block of code to be executed prior to or +after a transition. If the method executed in a before_transition returns false, +then the transition will not execute. + +So, for example, if you wanted to verify that the user provides a valid zip code +before transitioning to the delivery step, you would first implement a +`valid_zip_code?` method, and then tell the state machine to run this method +before that transition, placing this code in a file called +`app/models/spree/order_decorator.rb`: + +```ruby +Spree::Order.state_machine.before_transition :to => :delivery, :do => :valid_zip_code? +``` + +This callback would prevent transitioning to the `delivery` step if +`valid_zip_code?` returns false. + +### Customizing the View for a Particular Step + +Each of the default checkout steps has its own partial defined in the +spree frontend `app/views/spree/checkout` directory. Changing the view for an +existing step is as simple as overriding the relevant partial in your site +extension. It's also possible the default partial in question defines a usable +theme hook, in which case you could add your functionality by using +[Deface](https://github.com/spree/deface) + +## The Checkout Flow DSL + +Since Spree 1.2, Spree comes with a new checkout DSL that allows you succinctly define the +different steps of your checkout. This new DSL allows you to customize *just* +the checkout flow, while maintaining the unrelated admin states, such as +"canceled" and "resumed", that an order can transition to. Ultimately, it +provides a shorter syntax compared with overriding the entire state machine for +the `Spree::Order` class. + +The default checkout flow for Spree is defined like this, adequately +demonstrating the abilities of this new system: + +```ruby +checkout_flow do + go_to_state :address + go_to_state :delivery + go_to_state :payment, if: ->(order) { + order.update_totals + order.payment_required? + } + go_to_state :confirm, if: ->(order) { order.confirmation_required? } + go_to_state :complete + remove_transition :from => :delivery, :to => :confirm +``` + +we can pass a block on each checkout step definition and work some logic to +figure if the step is required dynamically. e.g. the confirm step might only +be necessary for payment gateways that support payment profiles. + +These conditional states present a situation where an order could transition +from delivery to one of payment, confirm or complete. In the default checkout, +we never want to transition from delivery to confirm, and therefore have removed +it using the `remove_transition` method of the Checkout DSL. The resulting +transitions between states look like the image below: + +$$$ +State diagram +$$$ + +These two helper methods are provided on `Spree::Order` instances for your +convenience: + +* `checkout_steps`: returns a list of all the potential states of the checkout. +* `has_step?`: Used to check if the current order fulfills the requirements for a specific state. + +If you want a list of all the currently available states for the checkout, use +the `checkout_steps` method, which will return the steps in an array. + +### Modifying the checkout flow + +To add or remove steps to the checkout flow, you can use the `insert_checkout_step` +and `remove_checkout_step` helpers respectively. + +The `insert_checkout_step` takes a `before` or `after` option to determine where to +insert the step: + +```ruby +insert_checkout_step :new_step, :before => :address +# or +insert_checkout_step :new_step, :after => :address +``` + +The `remove_checkout_step` will remove just one checkout step at a time: + +```ruby +remove_checkout_step :address +remove_checkout_step :delivery +``` + +What will happen here is that when a user goes to checkout, they will be asked +to (potentially) fill in their payment details and then (potentially) confirm +the order. This is the default behavior of the payment and the confirm steps +within the checkout. If they are not required to provide payment or confirmation +for this order then checking out this order will result in its immediate completion. + +To completely re-define the flow of the checkout, use the `checkout_flow` helper: + +```ruby +checkout_flow do + go_to_state :payment + go_to_state :complete +end +``` + +### The Checkout View +After creating a checkout step, you'll need to create a partial for the checkout +controller to load for your custom step. If your additonal checkout step is +`new_step` you'll need to a `spree/checkout/_new_step.html.erb` partial. + +### The Checkout "Breadcrumb" + +The Spree code automatically creates a progress "breadcrumb" based on the +available checkout states. The states listed in the breadcrumb come from the +`Spree::Order#checkout_steps` method. If you add a new state you'll want to add +a translation for that state in the relevant translation file located in the +`config/locales` directory of your extension or application: + +```ruby +en: + order_state: + new_step: New Step +``` + +*** +The default use of the breadcrumb is entirely optional. It does not need +to correspond to checkout states, nor does every state need to be represented. +Feel free to customize this behavior to meet your exact requirements. +*** + +## Payment Profiles + +The default checkout process in Spree assumes a gateway that allows for some +form of third party support for payment profiles. An example of such a service +would be [Authorize.net CIM](http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/) +Such a service allows for a secure and PCI compliant means of storing the users +credit card information. This allows merchants to issue refunds to the credit +card or to make changes to an existing order without having to leave Spree and +use the gateway provider's website. More importantly, it allows us to have a +final "confirmation" step before the order is processed since the number is +stored securely on the payment step and can still be used to perform the +standard authorization/capture via the secure token provided by the gateway. + +Spree provides a wrapper around the standard active merchant API in order to +provide a common abstraction for dealing with payment profiles. All `Gateway` +classes now have a `payment_profiles_supported?` method which indicates whether +or not payment profiles are supported. If you are adding Spree support to a +`Gateway` you should also implement the `create_profile` method. The following +is an example of the implementation of `create_profile` used in the +`AuthorizeNetCim` class: + +```ruby +# Create a new CIM customer profile ready to accept a payment +def create_profile(payment) + if payment.source.gateway_customer_profile_id.nil? + profile_hash = create_customer_profile(payment) + payment.source.update_attributes({ + :gateway_customer_profile_id => profile_hash[:customer_profile_id], + :gateway_payment_profile_id => profile_hash[:customer_payment_profile_id]) + }) + end +end +``` + +!!! +Most gateways do not yet support payment profiles but the default +checkout process of Spree assumes that you have selected a gateway that supports +this feature. This allows users to enter credit card information during the +checkout without having to store it in the database. Spree has never stored +credit card information in the database but prior to the use of profiles, the +only safe way to handle this was to post the credit card information in the +final step. It should be possible to customize the checkout so that the credit +card information is entered on the final step and then you can authorize the +card before Spree automatically discards the sensitive data before saving. +!!! diff --git a/guides/content/developer/customization/i18n.markdown b/guides/content/developer/customization/i18n.markdown new file mode 100644 index 00000000000..b60cdee977c --- /dev/null +++ b/guides/content/developer/customization/i18n.markdown @@ -0,0 +1,177 @@ +--- +title: "Internationalization" +section: customization +--- + +## Overview + +This guide covers how Spree uses Rails' internationalization features, and how +you can leverage and extend these features in your Spree contributions and +extensions. + +## How Spree i18n works + +Spree uses the standard Rails approach to internationalization so we suggest +take some time to review the [official Rails i18n +guide](http://guides.rubyonrails.org/i18n.html) and the +[rails-i18n.org wiki](http://rails-i18n.org/wiki) to help you get started. + +### The spree_i18n project + +Spree now stores all of the translation information in a separate GitHub project +known as [spree_i18n](https://github.com/spree/spree_i18n). This is a stand +alone project with a large number of volunteer committers who maintain the +locale files. This is basically the same approach followed by the Rails project +which keeps their localizations in +[rails-i18n](https://github.com/svenfuchs/rails-i18n). + +The project is actually a Spree extension. This extension contains translations and +uses the [globalize3 gem](https://github.com/svenfuchs/globalize3) to provide +translations for model records. + +!!! +You will need to install the [spree_i18n](https://github.com/spree/spree_i18n) +gem if you want to use any of the community supplied translations of Spree. +!!! + +### Translation Files + +Each language is stored in a YAML file located in `config/locales`. Each YAML +file contains one top level key which is the language code for the translations +contained within that file. The following is a snippet showing the basic layout +of a locale file: + +```yaml +pt-BR: + spree: + say_no: "Não" + say_yes: "Sim" +``` + +*** +All translations for Spree are "namespaced" within the `spree` key so that they +don't conflict with translations from other parts of the parent application. +*** + +#### Localization Files + +Spree maintains its localization information in a YAML file using a naming +convention similar to that of the Rails project. Each of the localization +filenames contains a prefix representing the language code of the locale. For +example, the Russian translation is contained in `config/locales/ru.yml`. + +*** +Spree has over 43 locale files and counting. See the [GitHub +Repository](https://github.com/spree/spree_i18n/tree/master/config/locales) for a +complete list. +*** + +#### Required Files + +Each locale that you wish to support will require both a Rails and Spree +translation. The required Spree translation files are available automatically +when you install the `spree_i18n` gem. + +You don't need to copy any files from `spree_i18n` or `rails-i18n` for their +translations to be available within your application. They are made available +automatically, because both `spree_i18n` and `rails-i18n` are railties. + +### Translating Views + +When reviewing the source of any view in Spree you'll notice that all text is +rendered by passing a string to a helper method similar to: + +```erb +<%%= Spree.t(:price) %> +``` + +The *Spree.t()* helper method looks up the currently configured locale and retrieves +the translated value from the relevant locale YAML file. Assuming a default +locale, this translation would be fetched from the en translations collated from +the application, `spree_i18n` and `rails-i18n`. Its relative key within those +translation files would need to be this: + +```yaml +en: + spree: + price: Price +``` + +### The Default Locale + +Since Spree is basically a Rails application it has the same default locale as +any Rails application. The default locale is `en` which use the English +language. We can verify this in the rails console + +```ruby +>> I18n.locale +=> :en +``` + +You can also see in the console how the default locale values are translated +into English + +```ruby +>> Spree.t(:action) +=> Action +``` + +## Deploying the Translations + +The `spree_i18n` gem is configured in the same manner as any Rubygem in a Rails +application. Simply add it to the `Gemfile.` using the git url. + +```ruby +gem 'spree_i18n', :github => 'spree/spree_i18n' +``` + +### Setting the Default Locale + +The default locale for Rails, and therefore Spree, is `en`. This can be changed by setting +`config.i18n.default_locale` in `config/application.rb`. This setting is ignored +unless the relevant translation file are within `#{Rails.root}/config/locales` +or the `spree_i18n` gem. + +### Setting the Default Currency + +*** +This functionality was new in Spree 1.2. Please refer to the appropriate +guide if you are using an older version. +*** + +In earlier versions of Spree, we used `number_to_currency` to display prices for +products. This caused a problem when somebody selected a different I18n locale, +as the prices would be displayed in their currency: 20 Japanese Yen, rather than +20 American Dollars, for instance. + +To fix this problem, we're now parsing the prices through the Money gem which +will display prices consistently across all I18n locales. To now change the +currency for your site, go to Admin, then Configuration, then General Settings. +Changing the currency will only change the currency symbol across all prices of +your store. + +There are three configuration options for currency: + +* `Spree::Config[:currency]`: 3-letter currency code representing the current currency. +* `Spree::Config[:currency_symbol_position]`: Whether to include the symbol before or after the monetary amount. +* `Spree::Config[:display_currency]`: Whether or not to display the currency with prices. + +## Creating and Modifying Locales + +While we have used [LocaleApp](http://localeapp.com) in the past to manage the translations for the spree_i18n project, Localeapp does not have support for different branches within the same project. As such, please submit Pull Requests or issues directly to https://github.com/spree/spree_i18n for missing translations. + +## Localizing Extensions + +Spree extensions can contain their own `config/locales` directory where +developers can include YAML files for each language they wish to support. + +We strongly urge all extension developers to ensure all customer facing text is +rendered via the `Spree.t()` helper method even if they only include a single default +language locale file (as other users can simply include the required YAML file +and translations in their site extension). + +*** +Since Spree extensions are equivalent to Rails Engines they can provide +localization information automatically (just like a standalone Rails +application.) +*** diff --git a/guides/content/developer/customization/index.md b/guides/content/developer/customization/index.md new file mode 100644 index 00000000000..1c910b1f688 --- /dev/null +++ b/guides/content/developer/customization/index.md @@ -0,0 +1,90 @@ +--- +title: "Customization Overview" +section: customization +--- + +This guide explains the customization and extension techniques you can +use to adapt a generic Spree store to meet your specific design and +functional requirements, including: + +- Explanation of how customizations are organized and shared +- Overview of the three major customization options: View, Asset & + Logic + +For more detailed information and a step-by-step tutorial on creating +extensions for Spree be sure to checkout the +[Extensions](extensions_tutorial.html) guide. + +### Managing Customizations + +Spree supports three methods for managing and organizing your +customizations. While they all support exactly the same options for +customization they differ in terms of re-usability. So before you get +started you need to decide what sort of customizations you are going to +make. + +#### Application Specific + +Application specific customizations are the most common type of +customization applied to Spree. It's generally used by developers and +designers to tweak Spree's behaviour or appearance to match a particular +business's operating procedures, branding, or provide a unique feature. + +All application specific customizations are stored within the host +application where Spree is installed (please see the Installation +section of the [Getting Started with Spree](getting_started_tutorial.html) guide, +for how to setup the host application). Application customizations are +not generally shared or re-used in any way. + +#### Extension + +Extensions enable developers to enhance or add functionality to Spree, +and are generally discrete pieces of functionality that are shared and +intended to be installed in multiple Spree implementations. + +Extensions are generally distributed as ruby gems and implemented as +standard Rails 3 engines so they provide a natural way to bundle all the +changes needed to implement larger features. + +Visit the [Extension Registry](http://spreecommerce.com/extensions) to +get an idea of the type and volume of extensions available. + +#### Theme + +Themes are designed to overhaul the entire look and feel of a Spree +store (or its administration system). Themes are distributed in exactly +the same manner as extensions, but don't generally include logic +customizations. + +*** +For more implementation details on Extensions and Themes please +refer to the [Extensions & Themes](extensions_tutorial.html) guide. +*** + +### Customization Options + +Once you've decided how you're going to manage your customizations, you +then need to choose the correct option to achieve the desired changes. + +#### View Customizations + +Allows you to change and/or extend the look and feel of a Spree store +(and its administration system). For details see the [View +Customization](view_customization.html) guide. + +#### Asset Customizations + +Allows changing the static assets provided by Spree, this includes +stylesheets, JavaScript files and images. For details see the [Asset +Customization](asset_customization.html) guide. + +#### Use S3 for storage + +Setup Spree to store your images on S3. For details see the + [Use S3 for storage](s3_storage.html) guide + +#### Logic Customizations + +Enables the changing and/or extension of the logic of Spree to meet your +specific business requirements. For details see the [Logic +Customization](logic_customization.html) guide. diff --git a/guides/content/developer/customization/logic.markdown b/guides/content/developer/customization/logic.markdown new file mode 100644 index 00000000000..abf73b6b55d --- /dev/null +++ b/guides/content/developer/customization/logic.markdown @@ -0,0 +1,172 @@ +--- +title: Logic Customization +section: customization +--- + +## Overview + +This guide explains how to customize the internal Spree code to meet +your exact business requirements including: + +- Extending and overriding existing Spree models and controllers +- Changing the output from an existing Spree controller action +- Customizing the image handling functionality. + +## Extending Classes + +All of Spree's business logic (models, controllers, helpers, etc) can +easily be extended / overridden to meet your exact requirements using +standard Ruby idioms. + +Standard practice for including such changes in your application or +extension is to create a file within the relevant **app/models/spree** or +**app/controllers/spree** directory with the original class name with +**_decorator** appended. + +**Adding a custom method to the Product model:** +app/models/spree/product_decorator.rb + +```ruby +Spree::Product.class_eval do + def some_method + ... + end +end +``` + +**Adding a custom action to the ProductsController:** +app/controllers/spree/products_controller_decorator.rb + +```ruby +Spree::ProductsController.class_eval do + def some_action + ... + end +end +``` + +*** +The exact same format can be used to redefine an existing method. +*** + +### Accessing Product Data + +If you extend the Products controller with a new method, you may very +well want to access product data in that method. You can do so by using +the :load_data before_filter. + +```ruby +Spree::ProductsController.class_eval do + before_filter :load_data, :only => :some_action + + def some_action + ... + end +end +``` + +*** +:load_data will use params[:id] to lookup the product by its +permalink. +*** + +## Overriding Controller Action Responses + +With the release of 0.60.0 Spree now supports a new way of overriding or +changing the output of an existing controller's action without needing +to completely override the method (while also easily avoiding double +render exceptions). + +### respond_override method + +The **respond_override** method is used to customize the response from +any action, and is built on top of Rails 3's **respond_with** method +(that all Spree controllers are now using). It accepts a hash of options +using the following syntax: + +```ruby +respond_override :action_name => { :format => { :result => lambda { ... response ... } } } +``` + +- **:action_name** - Can be any existing action within a controller + (i.e. :update, :create, :new), provided that action is using + respond_with to define its response. +- **:format** - The format of the request, (i.e. :html, :json, :js, + etc). All Spree controllers have a class level **respond_to** + method call that defines which formats the controller's actions will + respond to. +- **:result** - Two possible results are available on any given + response, :success or :failure. :success being the default for most + actions. However, actions that change or create models will have a + :failure response if validation fails for the model being updated. +- **lambda** - The lambda passed contains the actual code to create + the desired custom response (i.e. render or redirect_to). A lambda + must be passed to ensure the code is evaluated at the correct time. + +### Example Usage + +If you wanted to render a custom partial for the index action of +ProductsController, you could include the following in your +**app/controllers/spree/products_controller_decorator.rb** file. + +```ruby +Spree::ProductsController.class_eval do + respond_override :index => { :html => + { :success => lambda { render 'shared/some_file' } } } +end +``` + +Or if you wanted to redirect on the failure to create in +Admin::ProductsController, you would use: + +```ruby +Spree::Admin::ProductsController.class_eval do + respond_override :create => { :html => { :failure => lambda { + redirect_to some_url } } } +end +``` + +### Caveats + +- If an action does not use **respond_with** to define its response + the **respond_override** will not work. +- Some actions contain several **respond_with** calls so any + **respond_override** defined on it will be executed for any of the + **respond_with** instances, so it's important to check the model + state / logic within the lambda passed to prevent overriding all + possible responses with the same override. + +## Product Images + +Spree uses Thoughtbot's +[paperclip](https://github.com/thoughtbot/paperclip) gem to manage +images for products. All the normal paperclip options are available on +the Image class. If you want to modify the default Spree product and +thumbnail image sizes, simply create an image_decorator.rb file in your +app model directory, and override the attachment sizes: + +```ruby +Spree::Image.class_eval do + attachment_definitions[:attachment][:styles] = { + :mini => '48x48>', # thumbs under image + :small => '100x100>', # images on category view + :product => '240x240>', # full product image + :large => '600x600>' # light box image + } +end +``` + +You may also add additional image sizes for use in your templates +(:micro for shopping cart view, for example). + +### Image resizing option syntax + +Default behavior is to resize the image and maintain aspect ratio (i.e. +the :product version of a 480x400 image will be 240x200). Some commonly +used options are: + +- trailing #, image will be centrally cropped, ensuring the requested +dimensions +- trailing >, image will only be modified if it is currently larger +than the requested dimensions. (i.e. the :small thumb for a 100x100 +original image will be unchanged) diff --git a/guides/content/developer/customization/s3_storage.markdown b/guides/content/developer/customization/s3_storage.markdown new file mode 100644 index 00000000000..6ad49b6eac5 --- /dev/null +++ b/guides/content/developer/customization/s3_storage.markdown @@ -0,0 +1,49 @@ +--- +title: "Use S3 for storage" +section: customization +--- + +## Overview + +Currently the Spree backend does not give you the option anymore to configure s3 for image storage. +This guide covers how you can use S3 for storing assets in Spree. + +### How to use S3 + +Start with adding AWS-SDK to your gemfile with: `gem 'aws-sdk'`, then install the gem by running `bundle install`. + +When that's done you need to configure Spree to use s3. You can add an initializer or just use the spree.rb initializer located at `config/intializers/spree.rb`. + +```ruby +attachment_config = { + + s3_credentials: { + access_key_id: ENV['AWS_ACCESS_KEY_ID'], + secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], + bucket: ENV['S3_BUCKET_NAME'] + }, + + storage: :s3, + s3_headers: { "Cache-Control" => "max-age=31557600" }, + s3_protocol: "https", + bucket: ENV['S3_BUCKET_NAME'], + url: ":s3_domain_url", + + styles: { + mini: "48x48>", + small: "100x100>", + product: "240x240>", + large: "600x600>" + }, + + path: "/spree/:class/:id/:style/:basename.:extension", + default_url: "/spree/:class/:id/:style/:basename.:extension", + default_style: "product" +} + +attachment_config.each do |key, value| + Spree::Image.attachment_definitions[:attachment][key.to_sym] = value +end + +``` +Note that I use the `url: ":s3_domain_url"` setting, this enabled the DNS lookup for your images without specifying the specific zone endpoint. You need to use a bucket name that makes a valid subdomain. So do not use dots if you are planning on using the DNS lookup config. diff --git a/guides/content/developer/customization/view.markdown b/guides/content/developer/customization/view.markdown new file mode 100644 index 00000000000..5d4172435df --- /dev/null +++ b/guides/content/developer/customization/view.markdown @@ -0,0 +1,309 @@ +--- +title: "View Customization" +section: customization +--- + +## Overview +View customization allows you to extend or replace any view within a +Spree store. This guide explains the options available, including: + +- Using Deface for view customization +- Replacing entire view templates + +## Using Deface + +Deface is a standalone Rails library that enables you to customize Erb +templates without needing to directly edit the underlying view file. +Deface allows you to use standard CSS3 style selectors to target any +element (including Ruby blocks), and perform an action against all the +matching elements. + +For example, take the Checkout Registration template, which looks like +this: + +```erb +<%%= render 'spree/shared/error_messages', :target => user %> +

          <%%= Spree.t(:registration) %>

          +
          +
          + +
          + <%% if Spree::Config[:allow_guest_checkout] %> +
          + <%%= render 'spree/shared/error_messages', :target => order %> + +

          <%%= Spree.t(:guest_user_account) %>

          + + <%%= form_for order, :url => update_checkout_registration_path, :method => :put, :html => { :id => 'checkout_form_registration' } do |f| %> +

          + <%%= f.label :email, Spree.t(:email) %>
          + <%%= f.email_field :email, :class => 'title' %> +

          +

          <%%= f.submit Spree.t(:continue), :class => 'button primary' %>

          + <%% end %> +
          + <%% end %> +
          +``` + +If you wanted to insert some code just before the +#registration+ div on the page you would define an override as follows: + +```ruby +Deface::Override.new(:virtual_path => "spree/checkout/registration", + :insert_before => "div#registration", + :text => "

          Registration is the future!

          ", + :name => "registration_future") +``` + +This override **inserts**

          Registration is the future!

          **before** the div with the id of "registration". + +### Available actions + +Deface applies an __action__ to element(s) matching the supplied CSS selector. These actions are passed when defining a new override are supplied as the key while the CSS selector for the target element(s) is the value, for example: + +```ruby +:remove => "p.junk" + +:insert_after => "div#wow p.header" + +:insert_bottom => "ul#giant-list" +``` + +Deface currently supports the following actions: + +* :remove - Removes all elements that match the supplied selector +* :replace - Replaces all elements that match the supplied selector, with the content supplied +* :replace_contents - Replaces the contents of all elements that match the supplied selector +* :surround - Surrounds all elements that match the supplied selector, expects replacement markup to contain <%%= render_original %> placeholder +* :surround_contents - Surrounds the contents of all elements that match the supplied selector, expects replacement markup to contain <%%= render_original %> placeholder +* :insert_after - Inserts after all elements that match the supplied selector +* :insert_before - Inserts before all elements that match the supplied selector +* :insert_top - Inserts inside all elements that match the supplied selector, as the first child +* :insert_bottom - Inserts inside all elements that match the supplied selector, as the last child +* :set_attributes - Sets attributes on all elements that match the supplied selector, replacing existing attribute value if present or adding if not. Expects :attributes option to be passed. +* :add_to_attributes - Appends value to attributes on all elements that match the supplied selector, adds attribute if not present. Expects :attributes option to be passed. +* :remove_from_attributes - Removes value from attributes on all elements that match the supplied selector. Expects :attributes option to be passed. + +*** +Not all actions are applicable to all elements. For example, :insert_top and :insert_bottom expects a parent element with children. +*** + +### Supplying content + +Deface supports three options for supplying content to be used by an override: + +* :text - String containing markup +* :partial - Relative path to a partial +* :template - Relative path to a template + +*** +As Deface operates on the Erb source the content supplied to an override can include Erb, it is not limited to just HTML. You also have access to all variables accessible in the original Erb context. +*** + +### Targeting elements + +While Deface allows you to use a large subset of CSS3 style selectors (as provided by Nokogiri), the majority of Spree's views have been updated to include a custom HTML attribute (data-hook), which is designed to provide consistent targets for your overrides to use. + +As Spree views are changed over coming versions, the original HTML elements maybe edited or be removed. We will endeavour to ensure that data-hook / id combination will remain consistent within any single view file (where possible), thus making your overrides more robust and upgrade proof. + +For example, spree/products/show.html.erb looks as follows: + +```erb +
          + <%% body_id = 'product-details %> +
          +
          +
          +
          + <%%= render 'image' %> +
          + +
          + <%%= render 'thumbnails', :product => product %> +
          +
          + +
          + <%%= render 'properties' %> +
          + +
          +
          + +
          +
          + +
          + +

          <%%= accurate_title %>

          + +
          + <%%= product_description(product) rescue Spree.t(:product_has_no_description) %> +
          + +
          + <%%= render 'cart_form' %> +
          +
          + + <%%= render 'taxons' %> +
          +
          +
          +``` + +As you can see from the example above the `data-hook` can be present in +a number of ways: + +- On elements with **no** `id` attribute the `data-hook` attribute + contains a value similar to what would be included in the `id` + attribute. +- On elements with an `id` attribute the `data-hook` attribute does + **not** normally contain a value. +- Occasionally on elements with an `id` attribute the `data-hook` will + contain a value different from the elements id. This is generally to + support migration from the old 0.60.x style of hooks, where the old + hook names were converted into `data-hook` versions. + +The suggested way to target an element is to use the `data-hook` +attribute wherever possible. Here are a few examples based on +**products/show.html.erb** above: + +```ruby +:replace => "[data-hook='product_show']" + +:insert_top => "#thumbnails[data-hook]" + +:remove => "[data-hook='cart_form']" +``` + +You can also use a combination of both styles of selectors in a single +override to ensure maximum protection against changes: + +```ruby + :insert_top => "[data-hook='thumbnails'], #thumbnails[data-hook]" + ``` + +### Targeting ruby blocks + +Deface evaluates all the selectors passed against the original erb view +contents (and importantly not against the finished / generated HTML). In +order for Deface to make ruby blocks contained in a view parseable they +are converted into a pseudo markup as follows. + +*** +Version 1.0 of Deface, used in Spree 2.1, changed the code tag syntax. +Formerly code tags were parsed as `` and ``. They are now parsed as `` and ``. +Deface overrides which used selectors like `code[erb-loud]` should now +use `erb[loud]`. +*** + +Given the following Erb file: + +```erb +<%% if products.empty? %> + <%%= Spree.t(:no_products_found) %> +<%% elsif params.key?(:keywords) %> +

          <%%= Spree.t(:products) %>

          +<%% end %> +``` + +Would be seen by Deface as: + +```html + + if products.empty?
          + Spree.t(:no_products_found)
          + elsif params.key?(:keywords) + +

          Spree.t(:products)

          + + end + +``` + +So you can target ruby code blocks with the same standard CSS3 style +selectors, for example: + +```ruby +:replace => "erb[loud]:contains('t(:products)')" + +:insert_before => "erb[silent]:contains('elsif')" +``` + +### View upgrade protection + +To ensure upgrading between versions of Spree is as painless as +possible, Deface supports an `:original` option that can contain a +string of the original content that's being replaced. When Deface is +applying the override it will ensure that the current source matches the +value supplied and will output to the Rails application log if they are +different. + +These warnings are a good indicator that you need to review the source +and ensure your replacement is adequately replacing all the +functionality provided by Spree. This will help reduce unexpected issues +after upgrades. + +Once you've reviewed the new source you can update the `:original` value +to new source to clear the warning. + +*** +Deface removes all whitespace from both the actual and `:original` +source values before comparing, to reduce false warnings caused by basic +whitespace differences. +*** + +### Organizing Overrides + +The suggested method for organizing your overrides is to create a +separate file for each override inside the **app/overrides** directory, +naming each file the same as the **:name** specified within. + +*** +Using this method will ensure your overrides are compatible with +future theming developments (editor). +*** + +### More information on Deface + +For more information and sample overrides please refer to its +[README](https://github.com/railsdog/deface) file on GitHub. + +You can also see how Deface internals work, and test selectors using the +[Deface Test Harness](http://deface.heroku.com) application. + +## Template Replacements + +Sometimes the customization required to a view are so substantial that +using a Deface override seems impractical. Spree also supports the +duplication of views within an application or extension that will +completely replace the file of the same name in Spree. + +To override any of Spree's default views including those for the admin +interface, simply create a file with the same filename in your app/views +directory. + +For example, to override the main layout, create the file +YOUR_SITE_OR_EXTENSION/app/views/spree/layouts/spree_application.html.erb + +*** +It's important to ensure you copy the correct version of a view +into your application or extension, as copying a mismatched version +could lead to hard to debug issues. We suggest using `bundle show spree` +to get the location of the Spree code you're actually running and then +copying the relevant file from there. +*** + +### Drawbacks of template replacements + +Whenever you copy an entire view into your extension or application you +are adding a significant maintenance overhead to your application when +it comes to upgrading to newer versions of Spree. When upgrading between +versions you need to compare each template that's been replaced to +ensure to replicate any changes from the newer Spree version in your +locally copied version. + +To this end we strongly suggest you use Deface to achieve the desired +customizations wherever possible. diff --git a/guides/content/developer/deployment/ansible-ubuntu.md b/guides/content/developer/deployment/ansible-ubuntu.md new file mode 100644 index 00000000000..9644a4a69a9 --- /dev/null +++ b/guides/content/developer/deployment/ansible-ubuntu.md @@ -0,0 +1,193 @@ +--- +title: "Deploying to Ubuntu using Ansible" +section: deployment +--- + +## Overview + +Along with the [Manual Ubuntu Deployment Guide](/developer/manual-ubuntu.html), Spree can also be set up using [Ansible](http://ansibleworks.com). From Ansible's website: + +> Ansible is a radically simple IT orchestration engine that makes your applications and systems easier to deploy. Avoid writing scripts or custom code to deploy and update your applications— automate in a language that approaches plain English, using SSH, with no agents to install on remote systems. + +To set up a server using Ansible, we're going to use what's referred to as a [playbook](http://www.ansibleworks.com/docs/playbooks.html). This particular playbook is available from [radar/ansible-rails-app](https://github.com/radar/ansible-rails-app) on GitHub and will install the following things: + +- Ruby 2.0.0-p253 +- PostgreSQL 9.3 +- nginx +- Puma (jungle) +- ImageMagick + +With the playbook, you may wish to customize it to install a different version of Ruby, a different database system, Apache rather than nginx or unicorn instead of puma. It's extremely flexible. For this guide however, we will just cover the things that the default playbook does. + +## Set up Ansible + +Ansible works using a *control machine*, which just needs to be a system that has Python 2.6 installed. To set up Ansible on the control machine, follow [this guide](http://www.ansibleworks.com/docs/intro_installation.html#id11). + +## Playbook introduction + +Before we can run the playbook, we'll need to set up where the server is located. Rename the `hosts.example` file within the `ansible-rails-app` repository to `hosts` and put in the location of your server. + +The playbook has this setup within `ruby-webapp.yml`: + +```yaml +- hosts: all + user: root + vars_files: + - vars/defaults.yml + + roles: + - webserver + - database +``` + +This tells Ansible that on all hosts specified within the `hosts` file, we want to use the user `root` and the variables from `vars/defaults.yml`. On these hosts, we want to give them the roles of `webserver` and `database`. Since we're only setting up one host here, that is a good setup. If we wanted the server and the database to be on separate hosts, then we would need to configure it as such within the playbook. + +*** +If your server's default user is not `root`, then remember to change that here. +*** + +In `vars/defaults.yml`, we set up some variables that our playbook will reference later on: + +```yaml +## webapp + +webserver_name: spree.example.com +deploy_directory: /data/spree +app_name: spree + +## stolen from https://github.com/jgrowl/ansible-playbook-ruby-from-src +rubyTmpDir: /usr/local/src +rubyUrl: http://cache.ruby-lang.org/pub/ruby/2.0/ruby-2.0.0-p353.tar.gz +rubyCompressedFile: ruby-2.0.0-p353.tar.gz +rubyName: ruby-2.0.0-p353 +tmpRubyPath: {{rubyTmpDir}}/{{rubyName}} +``` + +Before we can run the playbook, we'll need to set up key-based authentication on the server so we are not asked for our password. To do this, we can run this command: + + scp ~/.ssh/id_rsa.pub root@:~/.ssh/authorized_keys + +To ensure that this worked, try connecting to the server: + + ssh root@ + +If you are not prompted for your password, then key-based authentication is setup. + +You will need to also set up the deployment key for the deploy user. This is done in `roles/webserver/tasks/deploy.yml` with this line: + +```yaml +- authorized_key: user=deploy key="{{ lookup('file', '/Users/example/.ssh/id_rsa.pub') }}" +``` + +Change this path to point to the path on your system where your public key resides. + +## Running the playbook + +Within the repository, there is a directory called "roles" which contains two sub-directories for the roles that are defined within `ruby-webapp.yml`. In each of these sub-directories there is another directory called `tasks` which defines the tasks that should be run for these roles. The `main.yml` within these directories lists the tasks that need to be run. + +Within `roles/webserver/tasks/main.yml`, we have this: + +```yaml +- include: deploy.yml tags=deploy +- include: puma.yml tags=puma +- include: nginx.yml tags=nginx +``` + +Within `roles/database/tasks.main.yml`, we have this: + +```yaml +- include: postgresql.yml tags=postgresql +``` + +We can run the playbook with this command: + + ansible-playbook ruby-webapp.yml -t deploy,postgresql,nginx + +The `-t` option tells Ansible that we want to run only the tasks tagged with those tags, in that order. + +### Deploy tasks + +Tasks with the `deploy` tag will be run first, and those tasks live within `roles/webserver/tasks/deploy.yml`. These tasks perform the following actions: + +* Updates apt-get to ensure latest packages are available +* Installs dependencies for Ruby +* Installs application-specific dependencies +* Installs Ruby from ruby-lang.org +* Creates a deployment user called "deploy" +* Copies over the public key so that key-based authentication for "deploy" works +* Creates the deployment directory +* Makes the shared directories for Capistrano to deploy into later on +* Inserts the database.yml to be used for the application +* Installs the Bundler gem + +These are all the basic steps to setup a Ruby installation on the server, as well as a directory on the server to deploy the application into. + +### PostgreSQL tasks + +The next tag is the `postgresql` tag, which will run the tasks within `roles/database/tasks/postgresql.yml`. These tasks do these actions: + +* Installs PostgreSQL dependencies +* Installs PostgreSQL 9.3 from Postgresql.org's own apt repository +* Sets up a secure `pg_hba.conf` using a template +* Sets up `postgresql.conf` using a template +* Ensures the PostgreSQL service has started +* Creates the PostgreSQL user for the application +* Creates the PostgreSQL database for the application + +### nginx tasks + +The final tag that we provided was the `nginx` tag, which will run the tasks listed within `roles/webserver/tasks/nginx.yml`. These tasks do these things: + +* Installs nginx +* Removes the default nginx app configuration +* Sets up the application's configuration using a template +* Ensures the nginx service has been started. + +## Using Capistrano to deploy + +When the playbook finishes, Ruby, PostgreSQL and nginx will be installed and from this point we can then deploy the application to the server using Capistrano. We can set up Capistrano within our application by running this command: + + cap install + +This sets up the basic Capistrano files within the application that we need to deploy. The `ansible-rails-app` repository contains a `deploy.rb` which you can use as a starting point within your application. + +Before you do anything else, uncomment these three lines in `Capfile`: + + require 'capistrano/bundler' + require 'capistrano/rails/assets' + require 'capistrano/rails/migrations' + +You will also need to add these gems to the Gemfile: + + group :development do + gem 'capistrano', '~> 3.0' + gem 'capistrano-bundler', '1.1.1' + gem 'capistrano-rails', '1.1.0' + end + +Then configure `config/deploy/production.rb` to point to the correct server, and finally run this command to deploy: + + bundle exec cap production deploy + +One of the final steps, the one that restarts Puma, will probably fail because we have not yet set up Puma on the server. We can rectify this by setting that up on the server using Ansible within the `ansible-rails-app` directory: + + ansible-playbook ruby-webapp.yml -t puma + +The tasks performed are as follows: + +* Sets up a puma-manager to manage the Puma services +* Copies configuration for puma to the server +* Adds puma init script +* Adds config/puma/production.rb to the application's shared directory +* Creates shared/tmp/sockets within the deploy directory +* Ensures the puma-manager service is started. + +Running the deploy command again will now succeed: + + bundle exec cap production deploy + +## Seeding data + +You can also choose to seed the Spree store with some sample data by running this command: + + bundle exec cap production spree_sample:load diff --git a/guides/content/developer/deployment/deployment_options.markdown b/guides/content/developer/deployment/deployment_options.markdown new file mode 100644 index 00000000000..7979037d3e8 --- /dev/null +++ b/guides/content/developer/deployment/deployment_options.markdown @@ -0,0 +1,107 @@ +--- +title: "Deployment Options" +section: deployment +--- + +## Overview + +This guide will discuss the requirements and options for deploying Spree +in production and staging environments. After reading it, you should be +familiar with: + +- Common server configurations used when deploying Spree. +- How memory availability affects Spree's performance and scalability +- Typical memory footprints for Spree servers. + +Spree can be deployed like any other Rails application using any +combination of the usual software components like Apache, Nginx, +Unicorn, Passenger, Ruby, Capistrano etc. + +Normally the first concern when deploying your store is choosing the +correct software stack, server sizes and roles. + +*** +This guide assumes basic familiarity with Spree. Please refer to +the [Getting Started Guide](getting_started_tutorial) for details on how to +get up and running for Spree. You will have an easier time understanding +the extra complications of a production setup. +*** + +### Server Sizing & Configurations + +While most smaller Spree installations can operate extremely well on +relatively modest single server configurations, stores that need to +handle a larger number of concurrent users frequently chose a +multi-server configuration to distribute the workload and simplify +server configurations. + +*** +Determining how many concurrent users any server configuration is +capable of handling is entirely dependant on the specific application +and its use of extensions and customizations. All server sizing +guidelines provided here are purely estimates based on basic Spree +instances. +*** + +### Memory (RAM) requirements + +Spree's scalability (like most Rails applications) is directly affected +by number of application worker processes available to handle requests +as they arrive. The number of worker processes you can configure for a +given server is directly related the amount of RAM available. + +*** +The **Spree Deployment Service** allows you to configure the +number of unicorn workers deployed on each application server. +*** + +### Single Server + +Single server configurations are well suited to low volume Spree +production installations, or staging / qa environments. Single server +configurations include both the application and database software +components running on a single host, and generally need to be configured +to run less workers to allow some free memory for the database +component. + +#### Suggested initial memory & worker values + +These values are guidelines for the minimum requirements for a single +server production environment. Staging and QA can perform acceptably +with less resources, generally half of what is listed below. + +|Spree Version|RAM (minimum)|Unicorn Workers| +|------------:|------------:|--------------:| +|1.0.0|1024MB|2-4| +|0.70.x|1024MB|2-4| +|0.60.x|1024MB|2-4| +|0.11.x|512|2-3| + +!!! +Adding too many workers for the amount of available memory will +have a negative effect on overall performance so it's important to get +the worker count correct for your application / memory combination. +!!! + +### Multi-Server + +When you separate your Spree application on multiple servers, generally +one or more servers are configured as dedicated application servers and +one server is configured as a dedicated database server. + +#### Suggested initial memory & worker values + +These values are guidelines for the minimum requirements for each +Application Server in a production environment. + +|Spree Version|RAM (minimum)|Unicorn Workers| +|------------:|------------:|--------------:| +|1.0.0|512MB|3-5| +|0.70.x|512MB|3-5| +|0.60.x|512MB|3-5| +|0.11.x|512MB|2-4| + +Database servers for use in multi-server configurations generally +require a minimum of 512MB RAM when serving one or two applicaiton +servers, however this should be increased when more or larger capacity +application servers are used. diff --git a/guides/content/developer/deployment/deployment_tips.markdown b/guides/content/developer/deployment/deployment_tips.markdown new file mode 100644 index 00000000000..cf597ace1e0 --- /dev/null +++ b/guides/content/developer/deployment/deployment_tips.markdown @@ -0,0 +1,160 @@ +--- +title: "Deployment Tips" +section: deployment +--- + +## Overview + +This guide is intended to provide some generally useful hints and tips\ +for troubleshooting standard deployment issues, including: + +* How static assets are served in production +* Enabling & Configuringn SSL use within Spree +* Email configuration +* and more ... + +## Serving Static Assets + +Rails applications (including Spree) use the convention of storing +public assets (images, JavaScripts, stylesheets, etc.) in a directory +named `public`. In development environments, Rails itself will +automatically handle requests for this static content by serving it from +the `public` directory. In production mode, however, Rails is not +configured to serve public assets unless specifically enabled. This +leaves you with two options. + +### Configure Rails to Serve Public Assets + +Rails has a `config.serve_static_assets` setting that allows you to +override its default behavior in the production environment. If you want +Rails to serve you public assets you will need to change this setting in +`config/environments/production.rb` of your Rails app as follows: + +```ruby + config.serve_static_assets = true +``` + +*** +There is a good reason why this is disabled by default in Rails +which is that Rails is not a general purpose web server. Servers such as +Apache and Nginx are optimized for rapidly serving up static content. +You should consider the advice of the Rails core team and let your +webserver do what it does best (as described in the next section.) +*** + +#### Configure the Web Server to Use the *public* Directory + +The recommended approach for handling static assets is to allow your web +server to handle serving these files. If you want to follow this +approach just make sure that it's configured properly in the +`config/environments/production.rb` of your Rails app. + +```ruby +config.serve_static_assets = false +``` +*** +This is the default setting of Rails so it's also fine if this setting is missing or commented out. +*** + +The following is an example of how to configure Apache so that its document root is pointing to the `public` folder. + +```bash + +ServerName www.mystore.com +DocumentRoot /webapps/mystore/public + +Allow from all +Options ~~MultiViews + + +``` + +Each web server will have its own method for doing this so please consult the appropriate documentation for more details. + +## Enabling SSL + +Spree supports SSL and contains a special filter to require SSL for certain sensitive pages It also has the ability to redirect SSL requests that do not require SSL back to standard unencrypted HTTP. The code for this is built right into Spree and is based on the +[ssl_requirement](https://github.com/rails/ssl_requirement/tree/master) by David Heinemeier Hansson. + +The default behavior for Spree depends on the Rails environment as +follows: + +|*.Environment | *.SSL Enabled| +|---|---:| +|Development|False| +|Test|False| +|Staging|True| +|Production|True| + +*** +The "staging" environment is not one of the default environments +created by Rails. However, many developers use a staging environment, +which generally should mimic the production environment as much as +possible. You may, however, make minor changes such as disabling email +or sending email to a test account instead of the designated recipient. +*** + +### SSL Preferences + +SSL behavior in Spree is determined by several different preferences. + +|*.Preference | *.Default Value| +|---|---:| +|allow_ssl_in_production|true| +|allow_ssl_in_staging|true| +|allow_ssl_in_development_and_test|false| + +For more information on preferences in general you may wish to read the +[Preference Guide](preferences.html). + +### Changing the Default Settings + +If you do not wish to use SSL in production or staging, or if you wish to enable SSL in development mode, you will have to change the `:allow_ssl_in_production` configuration setting. This can be done via the admin interface as shown below: + +![Changing SSL Setting](../images/developer/change_ssl_setting.png "Changing SSL Setting") + +If you need to change any of the above default settings, it is also +recommended to fix the setting in an initializer . In +`/config/initializers/spree.rb`, add a line such as: + +```ruby +config.allow_ssl_in_staging = false +``` + +## Performance Tips + +## Running in Production Mode + +If you are noticing that Spree seems to be running slowly you should +make sure that you are running in "production mode." You can start your +server in production mode as follows: + +```bash +$ bundle exec rails server -e production +``` + +Please consult your web server documentation for more details on +enabling production mode for your particular web server. + +## Passenger Timeout + +If you are running on [Passenger](http://www.modrails.com) then you may be noticing that the first request to your Spree application is very slow if the application has been idle for some time (or you have just restarted.) Consider changing the [PassengerPoolIdleTime](http://www.modrails.com/documentation/Users%20guide%20Apache.html#PassengerPoolIdleTime) as described in the Passenger documentation. + +## Caching + +Most stores spend a lot of time serving up the same pages over and over +again. In many cases, the content being served is exactly identical or +nearly identical for every user. In such cases, a caching solution may +be appropriate and can improve server performance by bypassing time +consuming operations such as database access. Rails provides several +excellent [caching +options](http://guides.rubyonrails.org/caching_with_rails.html) that you +should consider investigating. + +A detailed description of Rails caching is beyond the scope of this +guide. + +*** +The Spree core team is actively considering some form of basic +caching support to make it easier to leverage the Rails caching options. +*** diff --git a/guides/content/developer/deployment/heroku.md b/guides/content/developer/deployment/heroku.md new file mode 100644 index 00000000000..dfe7cc1b651 --- /dev/null +++ b/guides/content/developer/deployment/heroku.md @@ -0,0 +1,134 @@ +--- +title: "Deploying to Heroku" +section: deployment +--- + +## Overview + +This article will walk you through configuring and deploying your Spree +application to Heroku. + +This guide assumes that your application is deploy-ready and that you have a +Heroku application already created on the Heroku stack for this application. If +you don't have a Heroku app already, follow [this +guide](https://devcenter.heroku.com/articles/creating-apps). + +*** +Heroku's tools assume that your application is version controlled by Git, as +Git is used to push the code to Heroku. +*** + +## Configuring your application + +### Specify Ruby version + +You should speficy the exact ruby version you want to run in your Gemfile: + +```ruby +ruby '2.0.0' +``` + +Keep in mind that Spree 2.0.0 requires a version of Ruby greater than or equal to Ruby 1.9.3. +See [Heroku Ruby support page](https://devcenter.heroku.com/articles/ruby-support#build-behavior) +for details on build behaviour related to Ruby versions. + +### Add Heroku 12 Factor Gem + +Add the [Heroku 12 Factor gem](https://github.com/heroku/rails_12factor) to your Gemfile: + +```ruby +gem 'rails_12factor', group: :production +``` + +This will enable your application to serve static assets and direct logging to stdout. + +### Rails 4 + +As of rails 4 things got a bit more complicated to deploy spree apps on heroku. +Spree versions up to 2.2.0 require a db connection on initialization. Heroku +won't allow the db connection though the first time you deploy the app, probably +because it doesn't know which database to connect to yet. + +A possible work around for this is to uninstall spree from your rails app, +deploy it to heroku and only then install spree again, e.g. by reverting +your previous commits, so that you get a successful deploy. + +Also look into this [github thread](https://github.com/spree/spree/issues/3749#issuecomment-30987342) +and all related for further info on how you could accomplish a successful +heroku deploy. + +Fortunately a lot of work has been done so that Spree 2.3 doesn't touch db +on initialization. This issue about [preferences on initialization](https://github.com/spree/spree/issues/3833) +contains most of the context related. + +### Asset Pipeline Rails 3 + +When deploying to Heroku by default Rails will attempt to intialize itself +before the assets are precompiled. This step will fail because the application +will attempt to establish a database connection, which Heroku will not have set +up yet. + +To work around this issue, put this line underneath the other `config.assets` +lines inside `config/application.rb`: + +```ruby +config.assets.initialize_on_precompile = false +``` + +The assets for your application will still be precompiled, it's just that Rails +won't be intialized during this process. + +*** + +### Paperclip image quality issues +Heroku currently defaults to a surprisingly old version of ImageMagick (6.5 as of March 2014) which can cause problems. Aside from the fact that 6.5 is missing some of the newer command line arguments that Paperclip can invoke, its [image conversion quality is noticeably inferior](http://i.imgur.com/dqeNdlW.png) to that of the current release. You can easily work around this by [using a Heroku buildpack to provide the latest ImageMagick release](https://github.com/spree/spree/pull/3104#issuecomment-36977413). You may have to `:reprocess!` your images after upgrading ImageMagick. + +### S3 Support + +Because Heroku's filesystem is readonly, you will need to configure Spree to +upload the assets to an off-site server, such as S3. If you don't have an S3 +account already, you can [set one up here](http://aws.amazon.com/s3/) + +This guide will assume that you have an S3 account already, along with a bucket +under that account for your files to go into, and that you have generated the +access key and secret for your S3 account. + +To configure Spree to upload images to S3, please refer to the following [documentation](http://guides.spreecommerce.com/developer/s3_storage.html) or follow an [equivalent solution](https://devcenter.heroku.com/articles/paperclip-s3) from Heroku's website. + + +## Pushing to Heroku + +Once you have configured the above settings, you can push your Spree application +to Heroku: + +```bash +$ git push heroku master +``` + +Once your application is on Heroku, you will need to set up the schema by +running this command: + +```bash +$ heroku run rake db:migrate +``` + +You may then wish to set up an admin user as well which can be done by loading +the rails console: + +```bash +$ heroku run rails console +``` + +And then running this code: + +```ruby +user = Spree::User.create!(:email => "you@example.com", :password => "yourpassword") +user.spree_roles.create!(:name => "admin") +``` + +Exit out of the console and then attempt to sign in to your application to +verify these credentials. + +## SSL Support + +For information about SSL support with Heroku, please read their [SSL Guide](https://devcenter.heroku.com/articles/ssl). diff --git a/guides/content/developer/deployment/manual-ubuntu.md b/guides/content/developer/deployment/manual-ubuntu.md new file mode 100644 index 00000000000..4e3e36038ed --- /dev/null +++ b/guides/content/developer/deployment/manual-ubuntu.md @@ -0,0 +1,826 @@ +--- +title: "Deploying to Ubuntu" +section: deployment +--- + +## Overview + +This guide will walk you through configuring and deploying your Spree +application to an environment on Ubuntu 13.04. + +This guide assumes an absolutely clean-slate Ubuntu 13.04 Server install, and +covers setting up the following things: + +* A user for the application +* Operating system dependencies required for Ruby, Rails and Spree +* Ruby 2.1.0 +* Rails 4.0.2 +* PostgreSQL +* Unicorn + nginx (with SSL) +* Seed data for your store + +## Initial Server Setup + +The first thing you will need on your server is a user account on the server +which will be responsible for providing a container for your application's +install. + +### A user account + +*** +For the purposes of this guide, the user's account on the system will be +called "spree", but you may choose to call it whatever you wish. +*** + +To set up this new user, run these commands on the server: + +```bash +$ useradd -d /home/spree -m -s /bin/bash spree +$ passwd spree +``` + +Set a new password for the user and remember it, as you will require it in just a moment. + +### Key-based authentication + +The next thing to set up is secure key-based authentication on the server. This +will involve setting up a private key on your system, copying over the related +public key to the server, asserting that you can now login without providing a +password, and then disabling password authentication on the server to increase +security. + +On the remote server, set up an `.ssh` directory to contain the new public key +for a user by running these commands: + +```bash +$ mkdir /home/spree/.ssh +$ chown spree:spree /home/spree/.ssh +$ chmod 700 /home/spree/.ssh +``` + +This directory is used to authenticate key-based authentication when using SSH. + +On your local machine, generate a private key using `ssh-keygen` like this: + +```bash +$ ssh-keygen -t rsa +``` + +Set the filename to be [your home directory]/.ssh/spree_rsa. + +You can choose to enter a password if you wish. All that would mean is that you would need to provide that password to use the key. + +*** +If you already have a private key, you can use that one. +*** + +Once you've finished generating this key, you will need to copy the public +version of this key over to the new server. To do this, run this command: + +```bash +$ scp ~/.ssh/spree_rsa.pub spree@[your server's address]:~/.ssh/authorized_keys +``` + +The password you will need to enter here is the password for the user account on +the remote server. + +Once you've set this up, you will then be able to use key-based authentication +to connect to the server: + +```bash +$ ssh spree@[your server's address] -i [your home directory]/.ssh/spree_rsa +``` + +To save having to use the `-i` option here, you can place the following lines +inside `.ssh/config` on your local machine: + + Host [your server's address] + IdentityFile ~/.ssh/spree_rsa + +You should now follow the same steps for the `root` user on your server as well, +so that you can authenticate with the same key to access both the deployment +user and root accounts. You may choose to use a completely different key if you +wish. + +Once you have verified -- by connecting via SSH to the remote server -- that +both accounts work without password authentication, you can now disable +password-based authentication. + +To disable password-based authentication, you will need to uncomment this line +within `/etc/ssh/ssh_config` and change the "yes" value to "no": + + #PasswordAuthentication yes + +It should be this when you're done: + + PasswordAuthentication no + +Then you will need to restart the SSH daemon on the server, by running this +command: + +```bash +$ service ssh restart +``` + +After this, if you attempt to run `ssh spree@localhost` from within the server +itself, it will return "Permission denied (publickey)", indicating that it has +not attempted to authenticate with a password, but instead with a publickey, +which the server does not have configured. + +Now that the user is set up on your system and access to it and root's account +are locked down a bit tighter, it's time to set up Ruby. + +## Operating System Dependencies and Ruby + +To install Ruby, you are going to use the "RVM":http://rvm.io tool. This tool +provides a simple way of installing a version of Ruby onto your server. + +To install it, run these commands: + +```bash +$ curl -L https://get.rvm.io | bash -s stable +$ . ~/.bashrc +``` + +Next, you will need to install the operating system dependencies required for +Ruby. Run this command to install the dependencies: + +```bash +rvm requirements +``` + +You will also need to install a JavaScript runtime. You can install the `nodejs` +package from `apt-get`: + +```bash +$ apt-get install -y nodejs +``` + +Or, you can put a dependency for `therubyracer` gem into your `Gemfile`: + +```ruby +group :production do + gem 'therubyracer' +end +``` + +You will also need the `imagemagick` package, which is used to handle image +manipulation which is used when you upload product images in your store: + +```bash +$ apt-get install -y imagemagick +``` + +Once these dependencies are installed, switch back into the `spree` user and +install Ruby 2.1.0 by running this command: + +```bash +$ rvm install 2.1.0 +``` + +This command will take a couple of minutes to finish running. + +Once it's finished running, run this command to make that version of Ruby the +default for this user: + +```bash +$ rvm use 2.1.0 --default +``` + +Ensure that this version of Ruby is really the new default by running this +command: + +```bash +ruby -v +``` + +It should output something similar to this: + +```bash +ruby 2.1.0p0 (2013-12-25 revision 44422) [x86_64-linux] +``` + +You now have a version of Ruby correctly configured on your server. + +### Deploying to the server + +The next step is to put your Spree application onto the server. To do this, you +will use the deployment tool called +[Capistrano](https://github.com/capistrano/capistrano/wiki). + +Install Capistrano on your local system by running this command: + +```bash +$ gem install capistrano +``` + +Then, inside the directory for your Spree app, run this command to set up a +Capistrano deploy configuration: + +```bash +$ capify . +``` + +This command will create two files: a `Capfile` and a `config/deploy.rb`. The +`config/deploy.rb` file is where you will be configuring how Capistrano chooses +to deploy your application. Open this file now and you will see the following +lines (comments removed): + +```ruby +set :application, "set your application name here" +set :repository, "set your repository location here" + +set :scm, :subversion +# Or: `accurev`, `bzr`, `cvs`, `darcs`, `git`, `mercurial`, `perforce`, `subversion` or `none` + +role :web, "your web-server here" # Your HTTP server, Apache/etc +role :app, "your app-server here" # This may be the same as your `Web` server +role :db, "your primary db-server here", :primary => true # This is where Rails migrations will run +role :db, "your slave db-server here" +``` + +The contents of this file tell Capistrano about the deployment of your +application. + +The `application` variable tells Capistrano the name of your application, and +the `repository` variable tells it where it can find the source of your +application. The `scm` variable tells Capistrano the type of source control +system you're using. If you're using Spree, there's a high chance that's going +to be `:git`, so change that over now. Change the `application` and `repository` +now to accurately reflect your application. + +The next batch of things to configure in this file are the different "roles". +These tell Capistrano which servers play which roles within your server +architecture. Within this guide, you've been working with a single server and +will continue to do so. Therefore, these roles should look like this: + +```ruby +server = "[your server's address]" +role :web, server +role :app, server +role :db, server, :primary => true +``` + +After this, you will need to tell Capistrano the account name to use for +deploying to your server. In this guide, we've used "spree" so far, but you may +have chosen to use something different. To tell Capistrano the user to use, put +this line inside your `config/deploy.rb`: + +```ruby +set :user, "spree" +``` + +You will also need to tell it the path to deploy at. By default in Capistrano, +this path is `/u/apps/[application_name]`. There is probably no `/u/` directory on the +server, so that won't work for you immediately. You already have a +self-contained user account on the server, so deploying the application to that +user's home directory would make better sense. Add this line to +`config/deploy.rb` to do that: + +```ruby +set :deploy_to, "/home/spree/#{application}" +``` + +You will also need to tell Capistrano to never use sudo, since you're going to +be operating as a user without sudo permission: + +```ruby +set :use_sudo, false +``` + +Along with this, you will also need to tell it to use the `bash` shell, as you +will need access to the commands for gems such as `bundler`, which are provided +by RVM. + +```ruby +default_run_options[:shell] = '/bin/bash --login' +``` + +And because all the Rails-specific commands are going to need to run on the +production environment, it'd be a great idea to add this to the configuration as +well: + +```ruby +default_environment["RAILS_ENV"] = 'production' +``` + +With that configuration, your `config/deploy.rb` should look like this: + +```ruby +set :application, "[name]" +set :repository, "[repository]" +set :scm, :git +server = "[your server's address]" + +role :web, server +role :app, server +role :db, server, :primary => true # This is where Rails migrations will run + +set :user, "spree" + +set :deploy_to, "/home/spree/#{application}" +set :use_sudo, false + +default_run_options[:shell] = '/bin/bash --login' +default_environment["RAILS_ENV"] = 'production' +``` + +To set up the server for Capistrano, run `cap deploy:setup`. This will create +the required Capistrano directories for your application inside +`/home/spree/[name]`. + +You will need to add another two lines to the top of this file as well so that +the application's gem dependencies are installed onto the server with Bundler, +and the assets are precompiled. These two lines are this: + +```ruby +require "bundler/capistrano" +load "deploy/assets" +``` + +To attempt to deploy the actual application to the server, run `cap deploy`. If +the `repository` option points to GitHub, this will fail because the server has +never verified GitHub's host key. + + [[your server's address]] executing command + ** [[your server's address] :: err] Host key verification failed. + ** [[your server's address] :: err] fatal: The remote end hung up unexpectedly + +To verify this, run this command: + +```bash +$ ssh github.com +``` + +This will ask you to verify that GitHub's RSA key fingerprint is +`16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48`. If that's correct, type "yes" +to the prompt and GitHub's host key will now be verified. + +When you run `cap deploy` again, you'll see this error: + + [[your server's address]] executing command + ** [[your server's address] :: err] Permission denied (publickey). + ** [[your server's address] :: err] fatal: The remote end hung up unexpectedly + +This means that your application does not have a deploy key setup for it on +GitHub. To set up a deploy key for the application, run this command as the +"spree" user on the server: + +```bash +$ ssh-keygen -t rsa +``` + +!!! +Do not enter a password for this key. Otherwise you will need to use it +every time you deploy. +!!! + +Run `cat ~/.ssh/id_rsa.pub` on the server to get the deploy key. Go to your +application's repository on GitHub and go to "Settings", then "Deploy Keys" and +enter the whole key into the form there, giving it a memorable name if you ever +need it again. + +When you run `cap deploy` again, Capistrano will clone your application's code +from GitHub to your server into a directory such as `/home/spree/[application's +name]/releases/[timestamp]`. Inside this directory -- because you're requiring +the `bundler/capistrano` tasks at the top of the `config/deploy.rb` file -- +Capistrano will run `bundle install` with a couple of options, pointing Bundler +at the application's gemfile and placing the gems into a shared directory at +`/home/spree/[application]/shared/bundle`. This is so that every release of your +application can use the same bundle. This `bundle install` command will also not +install the development and test dependencies for your application, as you won't +need them on your production server. + +The `cap deploy` command this time will also run an asset precompilation step, +thanks to the `deploy/assets` loading at the top of `config/deploy.rb`. This +step will compile the assets to the `public/assets` directory in the current +release's directory. + +The next step is to set up a database server for the data for your Rails app. + +## Setting up a database server + +The database server we will be using in this guide will be the +"PostgreSQL":http://postgresql.org database server. Once this is setup, you will +be able to tell capistrano to run the migrations on your application, creating +the necessary tables for your Spree store. + +To install PostgreSQL, run this command: + +```bash +$ apt-get install -y postgresql +``` + +You will also need to install its development headers, which the `pg` gem will +use to connect to the database: + +```bash +$ apt-get install -y libpq-dev +``` + +Once those two packages are installed, you will need to create a new database +for your application to use. This database should have the same name as the +server's deploy user account, which in this guide has been "spree" so far. Yours +could be different. To set up this database, run this command as `root`: + +```bash +$ sudo -u postgres createdb spree +$ sudo -u postgres createuser spree +``` + +To get your application to connect to this database, you will need to set up a +`database.yml` file on the server. This file needs to be kept on the server in a +common location where it can be copied over into the latest deployed version of +the application, and so you should place it at `/home/spree/[application's +name]/shared/config/database.yml`. Inside this file, put this content: + +```yaml +production: + adapter: postgresql + database: spree + ``` + +If you're not already using the PostgreSQL adapter on your application, as +specified by `gem 'pg'` in your `Gemfile`, you'll need to add this gem to your +`Gemfile` now, inside a `production` group: + +```ruby +group :production do + gem 'pg' +end +``` + +*** +If you need to add this gem, you will need to run `bundle install` on your +server and commit`push your `Gemfile` and `Gemfile.lock` to Git. +*** + +!!! +You should always develop and deploy on the same database adapter! If you don't, +you may run into incompatibility issues between your development and deployment +setups which can be difficult to track down. +!!! + +Now when you run `cap deploy`, you will need Capistrano to automatically copy +over the `database.yml` file from the shared directory into the current deploy +path. To make Capistrano do this, put these lines into `config/deploy.rb` for +your application: + +```ruby +task :symlink_database_yml do + run "rm #{release_path}/config/database.yml" + run "ln -sfn #{shared_path}/config/database.yml #{release_path}/config/database.yml" +end +after "bundle:install", "symlink_database_yml" +``` + +After `bundle install` has finished running on the server, Capistrano will now +copy over the `config/database.yml` into the current path. In order for +Capistrano to deploy *and* run your migrations, you will need to ru` `cap +deploy` and then `cap deploy:migrate`. Run those commands now. You should see +the migrations run on the server. + +The next step is to set up the web server to serve requests for your +application. + +## Setting up a web server + +The web server you'll be using here will be the Unicorn web server which will +run the Rails application instances, and then those instances will have an nginx +frontend which will serve the requests coming from the people who are visiting +your store. + +### Setting up Unicorn + +To set up unicorn for your application, add the `unicorn` gem to your `Gemfile`, +inside the `production` group: + +```ruby +group :production do + gem 'pg' + gem 'unicorn' +end +``` + +Unicorn requires some configuration in order to work, which belongs in +`config/unicorn.rb`. This is the content required for Unicorn: + +```ruby +# config/unicorn.rb +# Set environment to development unless something else is specified +env = ENV["RAILS_ENV"] || "development" + +# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete documentation. +worker_processes 4 + +# listen on both a Unix domain socket and a TCP port, +# we use a shorter backlog for quicker failover when busy +listen "/tmp/[application's name].socket", backlog: 64 + +# Preload our app for more speed +preload_app true + +# nuke workers after 30 seconds instead of 60 seconds (the default) +timeout 30 + +pid "/tmp/unicorn.[application's name].pid" + +# Production specific settings +if env == "production" + # Help ensure your application will always spawn in the symlinked + # "current" directory that Capistrano sets up. + working_directory "/home/spree/[application's name]/current" + + # feel free to point this anywhere accessible on the filesystem user 'spree' + shared_path = "/home/spree/[application's name]/shared" + + stderr_path "#{shared_path}/log/unicorn.stderr.log" + stdout_path "#{shared_path}/log/unicorn.stdout.log" +end + +before_fork do |server, worker| + # the following is highly recomended for Rails + "preload_app true" + # as there's no need for the master process to hold a connection + if defined?(ActiveRecord::Base) + ActiveRecord::Base.connection.disconnect! + end + + # Before forking, kill the master process that belongs to the .oldbin PID. + # This enables 0 downtime deploys. + old_pid = "/tmp/unicorn.[application's name].pid.oldbin" + if File.exists?(old_pid) && server.pid != old_pid + begin + Process.kill("QUIT", File.read(old_pid).to_i) + rescue Errno::ENOENT, Errno::ESRCH + # someone else did our job for us + end + end +end + +after_fork do |server, worker| + # the following is *required* for Rails + "preload_app true" + if defined?(ActiveRecord::Base) + ActiveRecord::Base.establish_connection + end + + # if preload_app is true, then you may also want to check and + # restart any other shared sockets/descriptors such as Memcached, + # and Redis. TokyoCabinet file handles are safe to reuse + # between any number of forked children (assuming your kernel + # correctly implements pread()/pwrite() system calls) +end +``` + +Remember to replace `[application's name]` above with your actual application +name. Run `bundle install` and commit and push your `Gemfile`, `Gemfile.lock` +and `config/unicorn.rb` files to Git. + +Next, you will need to add tasks to Capistrano to ensure that the Unicorn +workers are restarted after a `cap deploy`. To do this, put these lines into +your `config/deploy.rb`: + +```ruby +namespace :unicorn do + desc "Zero-downtime restart of Unicorn" + task :restart, except: { no_release: true } do + run "kill -s USR2 `cat /tmp/unicorn.[application's name].pid`" + end + + desc "Start unicorn" + task :start, except: { no_release: true } do + run "cd #{current_path} ; bundle exec unicorn_rails -c config/unicorn.rb -D" + end + + desc "Stop unicorn" + task :stop, except: { no_release: true } do + run "kill -s QUIT `cat /tmp/unicorn.[application's name].pid`" + end +end + +after "deploy:restart", "unicorn:restart" +``` + +Commit your `config/deploy.rb` to Git, push the changes to GitHub and run `cap +deploy` again to ensure the latest code is available on your server. This will +include the `unicorn` gem which will be vital for the next step: setting up +nginx and getting it to serve requests from your application. + +### Setting up nginx + +To install nginx, run this command as `root`: + +```bash +$ apt-get install nginx +``` + +Once this command is installed, you will then need to configure nginx to serve +requests from your unicorn workers. To do this, put this content inside +`/etc/nginx/nginx.conf`: + + user spree; + + # Change this depending on your hardware + worker_processes 4; + pid /var/run/nginx.pid; + + events { + worker_connections 1024; + multi_accept on; + } + + http { + types_hash_bucket_size 512; + types_hash_max_size 2048; + + sendfile on; + tcp_nopush on; + tcp_nodelay off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + gzip_disable "msie6"; + + gzip_proxied any; + gzip_min_length 500; + gzip_types text/plain text/css application/json application/x-javascript + text/xml application/xml application/xml+rss text/javascript; + + ## # Virtual Host Configs ## + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; + } + +This file sets up nginx-specific settings. You will need another file to tell +nginx where your application is. Create another file at +`/etc/nginx/sites-enabled/[your application's name]`, and fill it with this +content: + + upstream [your server's address] { + # fail_timeout=0 means we always retry an upstream even if it failed + # to return a good HTTP response (in case the Unicorn master nukes a + # single worker for timing out). + server unix:/tmp/[your application's name].socket fail_timeout=0; + } + + server { + # if you're running multiple servers, instead of "default" you should + # put your main domain name here + listen 80 default; + + # you could put a list of other domain names this application answers + server_name [your server's address]; + + root /home/spree/[your application's name]/current/public; + access_log /var/log/nginx/[your server's address]_access.log; + rewrite_log on; + + location / { + #all requests are sent to the UNIX socket + proxy_pass http://[your server's address]; + proxy_redirect off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + client_max_body_size 10m; + client_body_buffer_size 128k; + + proxy_connect_timeout 90; + proxy_send_timeout 90; + proxy_read_timeout 90; + + proxy_buffer_size 4k; + proxy_buffers 4 32k; + proxy_busy_buffers_size 64k; + proxy_temp_file_write_size 64k; + } + + # if the request is for a static resource, nginx should serve it directly + # and add a far future expires header to it, making the browser + # cache the resource and navigate faster over the website + location ~ ^/(system|assets|spree)/ { + root /home/spree/[application's name]/current/public; + expires max; + break; + } + } + +*** +The final `location` block here tells nginx to serve asset requests for three +separate paths from the `public` directory: `system`, `assets` and `spree`. + +The `system` directory is where Paperclip typically would store assets. Spree's +assets are located separately, under `spree`. + +The `assets` directory will contain the other assets for your application from +the [asset pipeline](http://guides.rubyonrails.org/asset_pipeline.html). +*** + +With these settings in place, you can start up nginx by running `service nginx +start` as root on the remote server. Next, you can start the unicorn processes +by running `cap unicorn:start` from your local machine. Once these are running, +you will be able to access your site at [your server's address]. You should see +your store's homepage here if everything is correctly set up. + +### Setting up SSL + +*** +This part of the guide assumes you have the relevant SSL certificate files +(a file ending in `.crt`, and another in `.key`) already and just need to know +where to put them. +*** + +The `*.crt` file belongs in `/etc/ssl/certs`, and the `*.key` file belongs in +`/etc/ssl/private`. Put these files there now. + +To get nginx to work with SSL, you will need to edit +`/etc/nginx/sites-enabled/[application's name]` and inside the `server {` block, +put these lines: + + listen 443 ssl; + + ssl_certificate /etc/ssl/certs/[your certificate's name].crt; + ssl_certificate_key /etc/ssl/private/[your key's name].key; + +Take this time to ensure that you definitely have this line inside this file as +well: + + proxy_set_header X-Forwarded-Proto $scheme; + +Without this line, you would get a redirect loop when you attempted to sign in +to your Spree store. + +That is all the SSL configuration you will need for your server. To verify that +it works, attempt to visit the login page for your application, or the admin +area. + +## Loading seed data + +Now with the database and web servers set up for your Spree store correctly, the +final thing you will need is seed data. This data contains things such as +countries, states, zones, zone members and an admin role. + +To install this data, run this command on the server, inside the current +directory: + +```bash +RAILS_ENV=production bundle exec rake db:seed +``` + +If you have `spree_auth_devise` installed, this command will also prompt you for +a username and password for your admin user. If you're not using +`spree_auth_devise`, then you will need to set up a new user account manually in +the console and assign it the admin role, like this: + +```ruby +user = User.create!(:email => "email@example.com", :password => "topsekret") +user.spree_roles << Spree::Role.find_by_name("admin") +user.save! +``` + +Note that your `User` model may require additional attributes before it can be +created. + +Once you have the data seeded by the `rake db:seed` command, you will need to +log into the admin interface and create a shipping method and a payment method +so that orders can be delivered and paid for. + +### Symlinking images + +*** +You don't need to follow the steps in this section if you're using S3 or another +cloud hosting provider. This section is only necessary for local file storage. +*** + +The final step in configuring the server is symlinking the images so that on +subsequent deploys they don't disappear. To do this, you can create a new `spree` +directory within the `shared` directory by using this command: + +```bash +mkdir -p /home/spree/[application's name]/shared/spree +``` + +This is the directory where all the uploads for the application will live. This +directory should be symlinked over to the application upon every deployment, and +to do that you can add this content to your `config/deploy.rb`: + +```ruby +namespace :images do + task :symlink, :except => { :no_release => true } do + run "rm -rf #{release_path}/public/spree" + run "ln -nfs #{shared_path}/spree #{release_path}/public/spree" + end +end +after "bundle:install", "images:symlink" +``` + +Now upon every deploy, Capistrano will symlink the `spree` directory in the `shared` +directory into the current version of the app so that the product images are +persisted across releases. \ No newline at end of file diff --git a/guides/content/developer/deployment/ninefold.md b/guides/content/developer/deployment/ninefold.md new file mode 100644 index 00000000000..48da5df4e70 --- /dev/null +++ b/guides/content/developer/deployment/ninefold.md @@ -0,0 +1,111 @@ +--- +title: "Deploying to Ninefold" +section: deployment +--- + +## Overview + +This guide will walk you through deploying your Spree application with [Ninefold](http://ninefold.com). + +This guide, like the others, assumes your app is in a 'ready-to-deploy' state and that your code is available in a hosted Git repository. Ninefold requires access to this repository in order to deploy your application. + +*** +Ninefold deployments require you to use Postgresql. This means you need to have the pg gem specified in the production group in your gemfile. + +Alternatively, you can choose not to have Ninefold provision your database for you and you can setup a stand-alone server to host your MySQL or nosql database. If you choose this option, Ninefold's database functionality (backups, replication, promotion, etc.) will not be available. +*** + +## Initial setup + +### Create a Ninefold account + +[Sign up](http://ninefold.com) with a username, your email address, and a password. + +## Deployment + +Once you've signed up, click the blue "Deploy now" button. This starts the deployment wizard which will step you through the process. + +### Step 1. Deploy app + +In this step, choose your hosted Git repository. Ninefold pulls your code in from [Github](http://github.com), [Bitbucket](http://bitbucket.com), or from your private Git URL. + +Sign in with your credentials for the repository and grant Ninefold permission to view your repository listings. + +*** +Git URL is an option if you want one specific repository to be visible to Ninefold. If it is a private repository, the SSH key will need to be added to your repository. + +Skip down to step 3 if you've chosen the Git URL option. +*** + +### Step 2. Specify your repository + +Choose the correct account, your Spree repository, and the branch you want to deploy from (this defaults to master). + +Uncheck the box if you do not wish to have Ninefold automatically redeploy your app for every code revision. + +Click Next. + +### Step 3. Choose your architecture + +Here, you will choose the correct architecture for your Spree application. Please consult the [Deployment options](http://guides.spreecommerce.com/developer/deployment_options.html) guide for RAM requirements. + +If you have set up your Spree application to use Redis for background work, our recommendation is to click "Create a dedicated worker server." + +Click Next. + +### Step 4. Configuration: Your app is now ready to be deployed + +This step allows you to configure different settings for your Spree application. + +Environment variables, such as S3 credentials, should be pasted into this section. + +Add-ons such as Memcached, New Relic, and SendGrid can be chosen if you require them. + +Click Next to begin the deploy process. + +*** +During the deployment process, Ninefold runs the following commands for you: `rake db:setup`, `rake db:migrate`, and `rake assets:precompile`. +*** + +### Code revisions + +If you have automatic deployment turned on, every time you push new code to your Git repository, Ninefold will redeploy for you. + +If this function has been turned off, log into Ninefold, click on your Spree app, and click Redeploy. + +*** +Alternatively, a redeploy can be done through the Ninefold CLI +*** + +## Ninefold CLI + +Ninefold provides an easy to use command line interface to manage your Spree app. To install the CLI, run this command in the root directory of your Spree application: + +```bash +$ gem install ninefold +``` + +To log into your Ninefold account, type `ninefold signin`, and to view commands, type `ninefold help`. + +The CLI is especially great for getting database backups, running console, checking logs, and running rake commands. More information about the CLI can be found here: [Ninefold CLI](https://github.com/ninefold/cli) + +### Creating a Spree admin user + +Type in Terminal: + +```bash +$ ninefold console +``` + +Choose the Spree app, and Rails console will load up. At the prompt, type: + +```ruby +user = Spree::User.create!(:email => "your_email@example.com", :password => "yourpassword") +user.spree_roles.create!(:name => "admin") +``` + +Exit out of the console; your admin user should now be created. + +## SSL Certificates + +FOr information about SSL certificates on Ninefold, please check out the guide here: [SSL Certificates](https://help.ninefold.com/hc/en-us/articles/200847294-SSL-Certificates) \ No newline at end of file diff --git a/guides/content/developer/deployment/requesting_and_configuring_ssl.markdown b/guides/content/developer/deployment/requesting_and_configuring_ssl.markdown new file mode 100644 index 00000000000..1936d4b9481 --- /dev/null +++ b/guides/content/developer/deployment/requesting_and_configuring_ssl.markdown @@ -0,0 +1,114 @@ +--- +title: "Requesting And Configuring SSL" +section: "deployment" +--- + +## Overview + +This article will walk you through generating an SSL Certificate Request +and Private Key, and installing the certificate once it's returned from +your Certificate Authority. + +## Generating a Certificate Request and Private Key + +If you already have an SSL certificate file and private key you can skip +this step. In order to get an SSL certificate from a Certificate +Authority (like GoDaddy or Verisign) you need to create a certificate +request (csr file) and a private key (key file). Both of theses files +can be automatically generated by running the command below. + +The CSR file contains some basic information on your domain name and +company location, and this file is submitted to the Certificate +Authority when purchasing your certificate. + +*** +The Key file contains the private key for this new SSL certificate +and should be stored in a secure location, and not shared with anyone. +It should not be sent to the certificate authority when requesting the +cert. +*** + +#### Required Information + +Your Certificate Request will require the following information: + +**CN - Common Name:** The fully qualified domain name that clients will +use to reach your server. To secure https://www.example.com, your common +name must be www.example.com or **.example.com for a wildcard +certificate. + +**O - Organization Name:** The exact legal name of your organization. +Example: "SpreeCommerce, Inc." If you do not have a legal registered +organization name, you should enter your own full name here. + +**OU - Department :** Many people leave this field blank. This is the +department within your organization which you want to appear in the +certificate. It will be listed in the certificate's subject as +Organizational Unit, or "ou." Example: Web Administration, Web Security, +Marketing + +**L - Location / City:** The city where your organization is legally +located. + +**ST- State or Province:** The state or province where your organization +is legally located. + +**C - Country:** The county where your organization is legally located. + +**Key Size:** 2048 is considered the minimum value. + +### Creating the Certificate Request & Private Key + +You must have the OpenSSL library installed to execute this command, all +Spree Hosting servers have this command available so it's best to run +the command directly on your server. + +This example command below is for illustration purposes only, you must +substitute your information in the relevant locations. There are some tools +available that will generate the proper `openssl` command for you +([this one](https://www.digicert.com/easy-csr/openssl.htm), for example). + +```bash +$ openssl req -new -newkey rsa:2048 -nodes -out www_example_com.csr + -keyout www_example_com.key -subj "/C=US/ST=MD/L=Chevy + Chase/O=SpreeCommerce, Inc /OU= /CN=www.example.com" +``` + +Be sure to change the `-out` and `-keyout` values to match your domain +name, while preserving the correct extensions. + +Once the command is executed you will have two new files created within +the current directory: + +**www_example_com.csr** - This is the Certificate Request, this must +be submitted to the Certificate Authority when purchasing your +certificate. + +**www_example_com.key** - This is your Private Key and must be kept +securely until the certificate is delivered to you by the Certificate +Authority. + +### Installing the Certificate + +When you receive your certificate from the Certificate Authority it is +generally called example.com.crt and maybe bundled with other Chain CRT +files. If you received multiple CRT files from your Certificate +Authority please refer to the installation instructions provided by them +for more details on installation, generally you just need to create one +new CRT file and combine the contents of all CRT files provided. + +Now that you have one single CRT file you are ready to install it on +your server: + +Copy the CRT file onto the server and save it to `/etc/ssl/spree.crt` directory. + +Move the private key (KEY file) to the `/etc/ssl/spree.key` directory. + +Execute the following command to have Puppet automatically install and +restart your webserver: + +```bash +$ FACTER_db_pass=YOUR_DB_PASSWORD sudo puppet agent —test``` + +It's important that the file names and locations match exactly those +listed above otherwise Puppet will not be able to locate them. \ No newline at end of file diff --git a/guides/content/developer/deployment/shelly-cloud.md b/guides/content/developer/deployment/shelly-cloud.md new file mode 100644 index 00000000000..b0485b0ee61 --- /dev/null +++ b/guides/content/developer/deployment/shelly-cloud.md @@ -0,0 +1,103 @@ +--- +title: "Deploying to Shelly Cloud" +section: deployment +--- + +## Overview + +This guide will show you how to deploy the Spree Commerce to +[Shelly Cloud](https://shellycloud.com/). Before we can start +you need to [create an account](https://shellycloud.com/sign_up). +Shelly Cloud is a git-based cloud hosting, so make sure that +your application is under Git version control system. + +## Requirements + +First you need to install client tool and login to your account + + $ gem install shelly + $ shelly login + +This procedure will upload your SSH public key. + +To fill up application requirements you need to just add one gem into +your `Gemfile` + + gem 'rails', '4.0.2' + + +gem 'shelly-dependencies' + +`shelly-dependencies` gem includes `thin` and `rake` gems. It will +also configure your application to serve static assets and compile +assets on request. + +## Creating new cloud + +Create new cloud for your application by running `shelly add`. +Just remember to choose PostgreSQL as a database engine. + +As you can see the [Cloudfile](https://shellycloud.com/documentation/cloudfile) +appeared. It defines your cloud structure. Now you have to add it +to your git repository and push to `shelly` remote: + + $ git add Cloudfile + $ git commit -m "Added Cloudfile for Shelly Cloud" + $ git push shelly master + +*** +There is no need to configure PostgreSQL credentials, because +Shelly Cloud generates it by default. More information can be +found in [documentation](https://shellycloud.com/documentation/managing_databases). +*** + +## Importing sample Spree data + +You can load sample Spree data by running + + $ shelly rake 'spree_sample:load' + +or just + + $ shelly rake 'db:seed' + +for loading basic seeds. + +*** +Shelly Cloud will run `db:setup` task if there is no `db/schema.rb` +file. During the seed task (which is a part of setup), Spree +asks for user login and password. Shelly Cloud is non interactive, so you +need to set `AUTO_ACCEPT` flag as an environment variable. You can set it +using [dotenv](https://shellycloud.com/documentation/environment_variables#dotenv) +gem. +*** + +## Storing files + +Shelly Cloud provides +[local file storage](https://shellycloud.com/documentation/storing_files) +which is shared between all virtual servers. +The only thing that you have to do is to create +`config/deploy/shelly/before_restart` +[hook](https://shellycloud.com/documentation/deployment_hooks) with the +following content: + + set -e + + mkdir -p disk/spree + ln -s ../disk/spree public/spree + +## Pushing to Shelly Cloud + +Shelly Cloud is a git-based cloud hosting, so all you need to do is to run + + $ git push shelly master + +for push and deploy your application. Now you can start your cloud by +running + + $ shelly start + +*** +You do not have to run migrations manually. Shelly Cloud runs +`rake db:migrate` task on each deployment. +*** diff --git a/guides/content/developer/index.md b/guides/content/developer/index.md new file mode 100644 index 00000000000..9bea22589b7 --- /dev/null +++ b/guides/content/developer/index.md @@ -0,0 +1,15 @@ +--- +title: Spree Developer Documentation +--- + +## Spree Developer Documentation + +This part of Spree's documentation covers the technical aspects of Spree. If you are working with Rails and are building a Spree store, this is the documentation for you. + +**The best place to start is the [Getting Started guide](/developer/getting_started_tutorial.html).** + +Otherwise, please see the [user documentation](/user/index.html). + +## Wombat Developer Documentation + +If you're interested in building an integration for Wombat then the best place to start is our [Wombat support portal](https://support.wombat.co). diff --git a/guides/content/developer/source/about.md b/guides/content/developer/source/about.md new file mode 100644 index 00000000000..2d224080337 --- /dev/null +++ b/guides/content/developer/source/about.md @@ -0,0 +1,154 @@ +--- +title: About the Code +section: source-code +--- + +## What is Spree? + +Spree is a full featured e-commerce platform written for the Ruby on Rails framework. It is designed to make programming commerce applications easier by making several assumptions about what most developers needs to get started. Spree is a production ready store that can be used "out of the box", but more importantly, it is also a developer tool that can be used as a solid foundation for a more sophisticated application than what is generally possible with traditional open source offerings. + +Spree is 100% [open source](http://en.wikipedia.org/wiki/Open_source). It is licensed under the very permissive [New BSD License](http://spreecommerce.com/license). You are free to use the software as you see fit, at no charge. Perhaps more important than the cost, Spree is a true open source community. Spree has hundreds of contributors who have used and improved it while building their own e-commerce solutions. + +## Motivation + +The goal of the project is to build a complete open source commerce +solution for Ruby on Rails. At the start of this project, the Rails +commerce space was immature and lacking serious solutions for developers +with complex business needs. In the past, Rails has suffered from "small +project mentality." Most open source projects in Rails are maintained by +a single individual and tend to be limited in scope. Spree seeks to +create a large and healthy open source community that developers of +other languages have come to expect. + +The founder of Spree was motivated to start the project after failing to +find an existing community in the Rails space dedicated to this vision. +In addition, he was motivated by unsuccessful efforts to use other open +source solutions in other programming languages, including (but not +limited to) the Magento and OSCommerce platforms. These solutions were +deemed to be unsatisfactory when challenged with even the simplest +practical cases of use. + +### Opinionated Commerce + +David Heinemeier Hansson (the creator of Rails) is well known for saying +that Rails is "opinionated software." Spree continues this fine +tradition of adopting a few strong (possibly controversial) opinions +which drive its development. + +#### No Solution Will Satisfy Everyone + +No solution can possibly solve everyone's needs perfectly. There are +simply too many ways in which people do business for us to tailor to +each individual need. Rather than come up short (like so many projects +before it did), Spree's approach is to simply accept this and not even +try. Instead Spree tries to focus on solving 90% of the bulk that most +commerce projects face. The remaining 10% will need to be addressed by +the end developer familiar with the client's exact business +requirements. + +#### Online Commerce is not for "Noobs" + +Rails developers are the target audience for this application - not +business owners. No serious company would ever try to run an online +store by just paying some fool on Craig's List to install OSCommerce for +them. Serious businesses have complicated needs that require paying one +or more software professionals to solve them. Spree seeks to be the +platform that developers use as the foundation for their project rather +than having to start from scratch or settle for less with other +software. + +#### Developers Need Complete Control + +Most business owners will not be satisfied with the generic templates +offered by other platforms. Why should they? They want their website to +look just like the other professional sites they see on the web. Most +businesses have very specific shipping and taxation rules as well. Spree +needs to be flexible enough to accommodate most situations. Sensible +defaults should be provided with an eye towards allowing further +customization. + +#### Stay Focused + +This is perhaps the most important principle behind the design +philosophy. We need to stay focused on core functionality (the 90% that +everybody needs.) For this reason it is not appropriate for Spree to +attempt to become a Content Management System (CMS). There are already +some pretty good Rails based CMS projects out there such as +[Radiant](http://radiantcms.org). CMS is definitely important but it is +a big enough task to warrant its own project. Spree will definitely be +looking at ways to integrate with existing CMS platforms, we just won't +be attempting to reinvent the CMS concept. + +## Requirements + +This guide is designed for beginners who want to get started with a +Spree application from scratch. It assumes a basic working knowledge of +Ruby on Rails. To get the most out of this guide, you need to have some +prerequisites installed: + +- The [Ruby](http://www.ruby-lang.org/en/downloads) language +- The [RubyGems](http://rubyforge.org/frs/?group_id=126) packaging + system +- The [Ruby on Rails](http://rubyonrails.org/download) gems +- A working installation of [SQLite](http://www.sqlite.org) + (preferred), [MySQL](http://www.mysql.com), or + [PostgreSQL](http://www.postgresql.org) + +*** +The SQLite database system is the default for development, since it +is relatively easy to set up compared to MySQL or PostgreSQL. For a +production system, we would recommend MySQL or PostgreSQL. Once you've decided, +You might consider using this in development as well, to reduce risk of +database specific bugs. +*** + +It is highly recommended that you **familiarize yourself with Ruby on +Rails before diving into Spree**. You will find it much easier to follow +what's going on with a Spree application if you understand basic Ruby +syntax. + +There are many excellent online resources for learning Ruby on Rails, +including: + +- [Rails Guides](http://guides.rubyonrails.org) +- [Railscasts (Free Screencasts)](http://railscasts.com/) + +There are also some good free resources on the internet for learning +Ruby, including: + +- [Mr. Neighborly's Humble Little Ruby + Book](http://www.humblelittlerubybook.com) +- [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/) +- [Why's (Poignant) Guide to + Ruby](http://mislav.uniqpath.com/poignant-guide/) + +## Performance Considerations + +Rails 3.1 introduced the concept of the asset pipeline. Unfortunately this causes some significant performance issues when running Spree in development mode. The good news is you can improve performance significantly by using a special precompile task. + +```bash +$ bundle exec rake assets:precompile:nondigest +``` + +Using the precompile rake task in development will prevent any changes to asset files from being automatically included in when you reload the page. You must re-run the precompile task for changes to become available. + +Rails also provides the following rake task that will delete the entire `public/assets` directory, this can be helpful to clear out development assets before committing. + +```bash +$ bundle exec rake assets:clean +``` + +It might also be worthwhile to include the public/assets directory in your `.gitignore` file. + +## Open Source License + +Copyright © 2007-2013, Spree Commerce Inc. and other contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +- Neither the name of Spree Commerce Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner of contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. diff --git a/guides/content/developer/source/contributing.md b/guides/content/developer/source/contributing.md new file mode 100644 index 00000000000..3959a55d132 --- /dev/null +++ b/guides/content/developer/source/contributing.md @@ -0,0 +1,299 @@ +--- +title: Contributing to Spree +section: source-code +--- + +## Overview + +Spree is an open source project. Anyone can use the code, but more importantly, anyone can contribute. This is a group effort and we value your input. Please consider making a contribution to help improve the Spree project. This guide covers: + +* How to file a ticket when you discover a bug +* How to contribute fixes and improvements to the core +* Information on how to improve the documentation + +## Who can contribute? + +Spree is an open source project and as such contributions are always welcome. Our community is one which encourages involvement from all developers regardless of their ability level. We ask that you be patient with the other members of the community and maintain a respectful attitude towards other people's work. Open source is a great way to learn a new technology so don't be afraid to jump right in, even if you are new to Ruby/Rails. + +## Before you contribute + +Open source projects tend to be a collaborative effort. Since many people are relying upon Spree for their real world applications, changes to the code can have major implications. Before you write a bug fix or code a new feature, you should find out if anybody is interested in your proposed change. You may find that the thing you're trying to "fix" is actually desired behavior. You might also discover that someone else is working on it. Either way you can save yourself valuable time by announcing your intentions before starting work. + +### Mailing List + +One of the best places for communication is the [spree-user mailing list](http://groups.google.com/group/spree-user). You can search this list of previous discussions and get a sense for whether you're trying to address something new. + +### IRC + +There is a #spree chat room on the irc.freenode.net network. Sometimes the core contributors hang out there and you can get some feedback on your idea. More information on how to connect is on our [blog](http://spreecommerce.com/blog/irc-101). + +!!! +The #spree chat room is not monitored as carefully as the mailing list. Sometimes you'll get lucky and someone will answer your question but we can't provide real time responses to everyone with a question/problem/idea. The mailing list is a much better way to communicate, and it gives everyone the chance to provide a thoughtful response. It's also more permanent than anything discussed on IRC. +!!! + +### Notification via GitHub Issues + +You can also search existing bug reports/issues and file a new one if you do not find an issue relevant to your proposed change. See [Filing an Issue](#filing-an-issue) for more details. + +*** +The important thing is that you communicate your intention in advance of doing a lot of work. Simple bug fixes and non-controversial changes do not require this approach but you can save some time by suggesting an improvement and having it rejected before you write a bunch of the code. +*** + +## Spree's Release Policy + +* Deprecation warnings are to be added in patch releases (i.e. 2.2.1) and the code being deprecated will only be removed in minor versions. For example, if a deprecation warning is added in 2.2.1, 2.2.2 will still contain the same deprecation warning but in 2.3.0 the deprecation warning and the code will be gone. + +* Master branch receives *all* patches, including new features and breaking API changes (with deprecation warnings, if necessary). + +* One branch "back" from master (currently 2-4-stable) receives patches that fix all bugs, and security issues, and modifications for recently added features (for example, split shipments). Breaking API changes should be avoided, but if unavoidable then a deprecation warning MUST be provided before that change takes place. + +* Two branches "back" from master (currently 2-3-stable) receives patches for major and minor issues, and security problems. Absolutely no new features, tweaking or API changes. + +* Three branches back from master (currently 2-2-stable) receives patches for major issues and security problems. The severity of an issue will be determined by the person investigating the issue. Absolutely no features, tweaking or API changes. + +* Four branches and more "back" from master (currently 2-1-stable and lesser) receive patches only for security issues, as people are still using these branches and may not be able to or wish to upgrade to the latest version of Spree. + +To determine how often minor branches of Spree are released, please check the [RubyGems version page for Spree](http://rubygems.org/gems/spree/versions). + +We strongly encourage people to upgrade to using the latest Spree releases to avoid being stuck on a release that is no longer maintained. + +## Filing an Issue + +If you would like to file a bug report, please create an issue in our [GitHub Issues Tracker](https://github.com/spree/spree/issues). You should do a basic search of the issues database before creating a new issue to ensure that you are not creating a duplicate issue. + +When filing an issue on the Spree project, please provide these details: + +* A comprehensive list of steps to reproduce the issue. +* What you're *expecting* to happen compared with what's *actually* happening. +* The version of Spree *and* the version of Rails. +* Your application's complete Gemfile, as text (*not as an image*) +* Any relevant stack traces ("Full trace" preferred) + +Without this information, we may be unable to debug your issue promptly. This information is typically all that is required in order to be able to solve a problem quickly and efficiently. + +*** +Please do not assign labels or create new labels to your issue. We will assign the appropriate labels to ensure your ticket is handled in the appropriate manner. +*** + +### When to File an Issue + +You should file an issue if you have found (or suspect) a bug in the "core" functionality of Spree. If you have found a bug in one of the extensions, please file an issue with the appropriate extension project in GitHub. If you're not sure if the behavior you're experiencing is intentional just ask on the mailing list and someone will encourage you to file a ticket if your issue sounds like a bug (as opposed to a feature.) + +### How We Prioritize Issues + +Spree is a very large project with lots of activity. We try our best to respond to all of the questions and issues our users have. We use the following criteria to prioritize issues: + +* Does this bug effect the latest stable release? +* Are there details on how to reproduce the problem? +* Is there a patch associated with the issue? +* Is there a test included in the patch? +* Has someone else verified the bug? + +We give highest priority to issues where the answer is "yes" to all of these questions. Next highest priority is for issues that answer "yes" to most of these questions, particularly the first few criteria. + +*** +You need to include a brief description of the problem and simple steps needed to reproduce it. If you fail to supply this minimum level of information your issue will likely be ignored. +*** + +### Providing a Patch + +If you are filing an issue and supplying a patch at the same time, please file a ["Pull Request"](#creating-a-pull-request) instead. The pull request will also create an issue at the same time but it's superior to just creating an issue because the code and issue can be linked. + +If the ticket already exists, however, and you want to supply a patch after the fact, you can simply reference the issue number in your commit message. For example, if your commit fixed issue #123 you could use the following commit message: + + Fixed a problem with Facebook authentication. + + [Fixes #123] + +GitHub will automatically detect this commit message when you push it and link the issue. Please see the detailed [GitHub Issues](https://github.com/blog/831-issues-2-0-the-next-generation) blog post for more details. + +We add code from Pull Requests to spree using the [hub gem](https://github.com/defunkt/hub), in particular the `hub am` command, which is used like this: + +```bash +$ hub am -3 https://github.com/spree/spree/pull/ +``` + +This command will apply the commits from the pull request to the current branch, using a 3-way merge as a fallback. This means that we can run this command in order to apply the patch to different branches. A pull request applied in this way leads to tidier commit history, with no merge commits visible. + +*** +For this reason, there is no need to create multiple pull requests for Spree's different branches. Please only create one pull request per change and simply mention inside the pull request that it can apply to multiple branches. If a patch does not apply cleanly to a branch, the Spree team may ask you for another one which applies to the other branch. +*** + +## Feature Requests + +We're interested in hearing your ideas for new features but creating feature requests in the Issue Tracker is not the proper way to ask for a feature. A feature request is any idea you have to improve the software experience that is not strictly related to a bug or error of omission. + +*** +Feature requests that are accompanied by source code are always welcome. In this case you should read the next section on [creating a pull request](#creating-a-pull-request). +*** + +!!! +Feature requests without accompanying code will be closed immediately. We simply cannot respond efficiently to feature requests through our Issue Tracker. If you want to suggest a feature, please use the [mailing list](http://groups.google.com/group/spree-user). +!!! + +## Creating a Pull Request + +If you are going to contribute code to the Spree project, the best mechanism for doing this is to create a pull request in GitHub. If you're unfamiliar with the general concept of pull requests you may want to read more on [pull requests in GitHub](https://help.github.com/articles/using-pull-requests). + +*** +If your code is associated with an existing issue then you can [provide a patch](#providing-a-patch) instead of creating a pull request. +*** + +### Creating a Fork + +The official Spree source code is maintained in GitHub under the [spree/spree](https://github.com/spree/spree) project. There is a [core team](http://spreecommerce.com/core_team) of developers who are responsible for maintaining the quality of the source code. Your changes will ultimately need to be merged into the official project by a core member. + +Thanks to [GitHub](https://github.com/), however, you do not have to wait for a core member to get started with your fix. You simply need to "fork" the project and then start hacking. + +*** +See the GitHub guide on [creating forks](https://help.github.com/articles/fork-a-repo) for more details. +*** + +### Topic Branches + +Git branches are "cheap." Creating branches in Git is incredibly easy and it's an ideal way to isolate a specific set of changes. By keeping a specific set of changes isolated, it will help us to navigate your fork and apply only the changes we're interested in. You should create a clean branch based on the latest spree/master when doing this. It is important you follow these steps exactly, it will prevent you from accidentally including unrelated changes from your local repository into the branch. + +For example, if we were submitting a patch to fix an issue with the CSS in the flash error message you could create a branch as follows: + +```bash +$ git remote add upstream git://github.com/spree/spree.git +$ git fetch upstream +$ git checkout -b fix-css-for-error-flash --track upstream/master +``` + +The fetch command will grab all of the latest commits from the Spree master branch. Don't worry, it doesn't permanently alter your working repository and you can return to your master branch later. The track part of the command will tell git that this branch should track with the remote version of the upstream master. This is another way of saying that the branch should be based on a clean copy of the latest official source code (without any of your unrelated local changes.) + +You can then do work locally on this topic branch and push it up to your GitHub fork when you are done. So in our previous example we do something like: + +```bash +$ git push origin fix-css-for-error-flash +``` + +Of course if you want the fix for yourself to use in your own local code you should probably merge it down to your own personal master branch that you're using for development + +```bash +$ git checkout master +$ git merge fix-css-for-error-flash +``` + +You should probably also clean up after yourself a little. The branch has been pushed to GitHub and you've merged it locally so you don't really need a local copy of the branch laying around. + +```bash +$ git branch -D fix-css-for-error-flash +``` + +### Follow the Coding Conventions + +Spree follows a simple set of coding style conventions. + +* Two spaces, no tabs. +* No trailing whitespace. Blank lines should not have any space. +* Indent after private/protected. +* Prefer `&&`/`||` over `and`/`or`. +* Prefer class << self block over self.method for class methods. +* `my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. +* `a = b` and not `a=b`. +* `{ a + b }` and not `{a + b}` +* -> symbol over lambda +* Ruby 1.9 hash syntax { key: :value } over Ruby 1.8 hash syntax { :key => :value } +* Alphabetize the class methods to keep them organized. +* Follow the conventions you see used in the source already. + +These are some guidelines and please use your best judgment in using them. + +### Including a Test + +Ideally your pull request will also include a test that verifies a bug (or the absence of the new feature) before your fix and also verifies proper functionality when you are finished. Please read the [Testing Guide](testing) for more information on writing and running your tests. + +*** +Pull requests with tests are given top priority. Failure to include a test will likely delay acceptance of your patch. +*** + +### Creating the Pull Request + +Once your code is ready to go and you have pushed your [topic branch](#topic-branches) to GitHub then you are ready to create the pull request and notify the Spree team that your contribution is ready. You do this by browsing your project in GitHub and changing to the topic branch you just pushed. Once you are on the topic branch simply create a pull request by pressing the "Pull Request" button. + +*** +The GitHub guide on [pull requests](https://help.github.com/articles/using-pull-requests) describes this in more detail with screenshots if you're still confused on this part. +*** + +## Contributing to the Documentation + +Improvements to the documentation are encouraged. The primary source of documentation are the guides (_HINT: You are reading one now._). You may make edits to the guide in your fork and then pull request for them to be updated to this site. + +To build the documentation normally simply clone and install. + +```bash +$ git clone git://github.com/spree/spree.git +$ cd spree/guides +$ bundle install +$ bundle exec guides build +``` + +Then simply use the nanoc command to compile and preview the guides in your browser at http://localhost:3000 + +```bash +$ nanoc compile +$ nanoc view +``` + +## Markdown Conventions + +It is helpful to standardize some markdown conventions so readers learn to recognize visual cues as they work their way through the documentation and tutorials. Following are the conventions used for the Spree documentation: + +####Class Names#### + +When referencing the name of a class, it should be capitalized. If you are writing explanatory prose and not a section of code, the class name should be blocked out with tick (`) marks. For example: + + To begin using your custom `User` class, you must first... + +Having the namespace for the class is optional, but should be included when omitting it could cause confusion. + +An instance of a class should be lowercase, normal font: + + You can view all of the orders for a particular user. + +####Buttons, Links, Section Names, Form Elements#### + +These should always reference the correct label and can have their names quoted. Examples: + +* Click the "Filter Results" button to update the results. +* Follow the "Stock Transfers" link. +* Information displayed in the "Purchase Funnel" section gives you information... +* If you check "Receive Stock" while creating a new transfer... + +####States, Attributes, Methods, Events, and Parameters#### +When referring to the state of an object - an order, for example - the state name should be lowercase and set off with tick (`) marks. For example: + + Orders that are in the `address` state do not have valid shipping and billing addresses assigned to them yet. + +This same style is used for attribute names and their settings, method names, event names, parameter names, parameter settings, and data types. + +####Path Names#### +Path names should be set off with tick (`) marks, and should include enough of the directory structure to make it clear which file is being referenced. For example: + + They are defined in `core/app/models/spree/app_configuration.rb`... + +####Adding Emphasis#### +Any text that needs to be emphasized should be in _italics_. + + Only the shipping options in the _shipping_ address are presented. + +####Terminal Blocks#### +You can specify terminal blocks by setting it off with ```bash. In addition, you can differentiate commands you are using from output returned by using the `$` precursor for input and `=>` precursor for output. + +```bash +$ irb +$ c = "Hello world" +$ c +=> "Hello world" +``` + +####Special Blocks#### + +Certain blocks of text can be wrapped in sets of three characters, which will place them in divs with appropriate CSS classes. They are: + +| *** | Notes. | +| !!! | Warnings. | +| $$$ | TODO's | +| --- | A title bar; especially useful for headings for code samples. | diff --git a/guides/content/developer/source/getting_help.md b/guides/content/developer/source/getting_help.md new file mode 100644 index 00000000000..80dda7378b3 --- /dev/null +++ b/guides/content/developer/source/getting_help.md @@ -0,0 +1,55 @@ +--- +title: Getting Help +section: source-code +--- + +## Overview + +There may be times when using Spree that the documentation doesn't answer all +the questions you may have. There are several other places on the internet where +you can go to ask Spree questions, and they are covered in this guide. + +## Mailing List + +The first of these places is our [Google Groups mailing +list](http://groups.google.com/group/spree-user). On this list, any user may ask +any question about Spree and it's up to the community to answer it, or in some +cases, a Spree employee may answer the question. + +This list is perfect for *general* questions about Spree. If you think you have +discovered a bug in Spree, please [file an issue on GitHub](#github-issues). + +Questions on this list may take some time to answer, so please be patient when +asking them. + +## Gitter + +Spree has a [Gitter chat room](https://gitter.im/spree/spree) similar to IRC that +can be used to discuss Spree in real-time. We prefer using Gitter over IRC for the chat logs, notifications, and other features. + +## IRC Channel + +The IRC channel for Spree is at #spree on irc.freenode.net. Here you can talk to +other users of Spree in real-time. Spree employees are represented in the channel with a `@` before their name on most clients. + +If you do not have an IRC client and still want to join the discussion, Freenode offers a webchat solution for their network. [Simply visit this link, enter your username and connect!](http://webchat.freenode.net/?channels=#spree) + +It is recommended that for people using IRC often, or on multiple networks that they download an IRC client. Popular IRC clients include: [Quassel (all platforms)](http://quassel-irc.org/), [XChat (windows and linux)](http://xchat.org/) and [mIRC (windows only)](http://www.mirc.com/). + +!!! +We prefer using Gitter over IRC for the chat logs, notifications, and other features. +!!! + +## GitHub Issues + +The [GitHub issues page for Spree](https://github.com/spree/spree/issues) shows +a list of currently open issues on Spree. If you think you have found a new +issue with Spree, please first read the [Filing an Issue](https://github.com/spree/spree/blob/master/CONTRIBUTING.md#filing-an-issue) +guidelines. If you provide the information we ask for there, we will be able to +help you with your issue much more effectively. + +!!! +If you think you have discovered a security issue with Spree, please do not +report them publically. Instead, email [our security +address](mailto:security@spreecommerce.com) +!!! diff --git a/guides/content/developer/source/index.md b/guides/content/developer/source/index.md new file mode 100644 index 00000000000..f84cb8afe76 --- /dev/null +++ b/guides/content/developer/source/index.md @@ -0,0 +1,22 @@ +--- +title: "Source Code" +section: source-code +--- + +## Source Code + +Spree's functionality is split across seven different components: + +* [**API**](http://api.spreecommerce.com): Provides a HTTP JSON API for several of Spree's components. +* [**Backend**](/developer/backend): Provides the admin backend component for Spree; + things like product and order management. +* [**cmd**](/developer/cmd): Provides the `spree` command, used for installing Spree + and generating extensions. +* [**Core**](/developer/core): Provides the minimum necessary functionality for Spree + to work. +* [**Dash**](/developer/dash): Provides the [Jirafe](http://jirafe.org) dashboard + functionality. +* [**Frontend**](/developer/frontend): Provides the front-facing functionality for + Spree; things like product viewing and checkout. +* [**Sample**](/developer/sample): Provides the sample data for Spree, used for + setting up a new Spree store. \ No newline at end of file diff --git a/guides/content/developer/source/navigating.md b/guides/content/developer/source/navigating.md new file mode 100644 index 00000000000..643f960a889 --- /dev/null +++ b/guides/content/developer/source/navigating.md @@ -0,0 +1,290 @@ +--- +title: Navigating the Source +section: source-code +--- + +## Overview + +This guide covers obtaining and running the source code. This is +primarily for developers who are interested in contributing code to the +Spree project or fixing the source code themselves. It is not necessary +to have a copy of the source code to run Spree. This guide covers the +following topics: + +- How Spree uses Git and GitHub to manage source code +- The various gems that comprise the Spree source code +- Building the gem from the source + +## Git + +The Spree source code is currently maintained in a +[Git](http://git-scm.com/) repository. Git is a distributed version +control system (DVCS) + +The authoritative git repository is hosted by +[GitHub](https://github.com/) and is located in the +[spree](https://github.com/spree/spree/tree/master) repository. You can +clone the git repository using the following command: + +```bash +$ git clone git://github.com/spree/spree.git +``` + +*** +If you are planning on contributing to Spree you should create a +fork through GitHub and push fixes to clearly labeled branches (see the +[Contributors Guide](contributing) for details.) +*** + +### Browsing the Repository and/or Downloading the Source Code + +You can easily browse the repository through GitHub's excellent [visual +interface](https://github.com/spree/spree/tree/master). GitHub also +contains a link to download a tarball copy of the latest source code as +well as links to [previous +versions](https://github.com/spree/spree/tags). + +### Git on Windows + +There are some well developed Git clients for Windows now. If you are on +a Windows box you might want to check out the +[msysgit](http://code.google.com/p/msysgit/) project. + +### Monitoring Changes in the Source + +If you would like to keep up to date on changes to the source you can +subscribe to the GitHub +[RSS feed](https://github.com/feeds/spree/commits/spree/master) and you +will be notified of all the commits. + +## Bundler + +Spree uses the very excellent [bundler](http://gembundler.com/) gem to +manage its dependencies. We are assuming you have basic familiarity with +bundler. A detailed explanation of bundler can be found on [Bundler's +site](http://gembundler.com/). + +You can install the gem dependencies for Spree after cloning the +repository using this Bundler-provided command: + +```bash +$ bundle install +``` + +This allows you to quickly and painlessly have the exact gem depedencies +you need to work with Bundler. + +## Layout and Structure + +### Collection of Gems + +The Spree gem itself is very minimal and consists mostly of a collection +of gems. These gems are maintained together in a single GitHub +repository and new versions of the gems are shipped with each new Spree +release. The official documentation (which you are reading now) covers +functionality provided by each of these gems. + +Within the Spree source, each of the gems is organized into +subdirectories as follows: + +| Gem | Directory | Description | +| :--------------| :---------| :-------------------------| +| spree_api | api | Provides REST API access | +| spree_backend | backend | Backend functionality | +| spree_cmd | cmd | Command line utility for installing Spree and creating extensions | +| spree_core | core | Core functionality - all other gems depend on this gem | +| spree_dash | dash | Simple overview dashboard | +| spree_frontend | frontend | Customer-facing functionality | +| spree_sample | sample | Sample data and images | + +### Use of Rails Engines + +Each of the gems in Spree makes use of Rails Engines. This functionality +was introduced in Rails 3.0 and allows Engines to behave in a manner +similar to fully-functional applications. Relying on this mechanism +provides several advantages: + +#### An Intuitive Mechanism for Customization + +Default Spree functionality is provided via the Rails engine gems. +Engines can provide several aspects traditionally associated with a +Rails application including (but not limited to): + +- Models, views and controllers +- Routes +- Helpers +- Rake tasks +- Generators +- Locales + +All of these elements can be overridden in the main Rails application. +Therefore, it is relatively simple to add Spree to your Rails +application and then customize it further by supplying your own elements +in that same application. A full discussion of Rails Engines is not +appropriate here. Please [consult the Rails Guides](http://edgeguides.rubyonrails.org/engines.html) for more information. + +#### Simple Distribution and Installation as Gems + +Using a Spree gem is as simple as adding it to your *Gemfile*: + +```ruby +gem 'spree_core', '2.1.0' +``` + +Distribution of Spree (and its extensions) is also consistent with Rails +standards and modularity (see next.) + +#### Consistency With a Rails Standard + +Prior to version 0.30.0, Spree used a complex custom mechanism for +implementing "engine-like" functionality. While it was difficult to +strip this functionality out of Spree, the benefits are well worth it. +Spree now receives all of the massive testing and attention to detail +that comes for free when using the Rails core engine functionality, +rather than a custom solution. + +#### Modularity + +There are differing opinions on what belongs in the "core." People often express their opinion that Spree is either "getting too fat" or +"lacks basic features." By relying on these engines (and distributing +them as gems), developers are free to use only the parts of Spree that +they find useful. For instance, this would allow you to omit promotions +functionality or to replace the authentication mechanism. + +For example, if you were to specify something like this in your +application's *Gemfile*: + +```ruby +gem 'spree', '2.1.0' +``` + +It would require all the individual parts of Spree. However, if you only +wanted to require the "core" and "backend" parts of Spree, you would do +this: + +```ruby +gem 'spree_core', '2.1.0' +gem 'spree_backend', '2.1.0' +``` + +## Building a Sandbox Application + +When working with the Spree source you may find yourself wanting to see +how the code performs in the context of an actual application. This is +due to the fact that Spree is intended to be distributed as a gem and is +not designed to be run as a standalone application. Spree includes a +helpful Rake task for setting up such a test application. + +To run this Rake task, go into the root of the Spree project and run +this command: + +```bash +$ bundle exec rake sandbox +``` + +This will create a barebones rails application configured with the Spree +gem. It runs the migrations for you and sets up the sample data. The +resulting `sandbox` folder is already ignored by `.gitignore` and it is +deleted and rebuilt from scratch each time the Rake task runs. + +## Building the Gem from the Source + +The Spree gem can easily be built from the source. Run these two +commands in the root of the Spree project to do this: + +```bash +$ bundle exec rake clean +$ bundle exec rake gem +``` + +Most likely you will want to build and install all of the related gems. +Fortunately, there is a simple Rake task for that. + +```bash +$ bundle exec rake gem:install +``` + +You can also build just one specific gem. + +```bash +$ cd core +$ bundle exec rake gem +``` + +## Tips for Working with the Source + +### Using the "Edge" Code + +If you are interested in simply using the latest edge code (as opposed +to contributing to it) then the simplest thing to do is add a *:github* +directive to your *Gemfile* and point it at the master branch. + +```ruby +gem 'spree', :github => 'spree/spree' +``` + +This will effectively use the latest code from the Git repository at the +time you run *bundle install*. This version of the code will be "frozen" +in your *Gemfile.lock* and will ensure that anyone else using your +project code is using the exact same version of the Spree code as you +are. You will need to update the bundle if you want to update to code +that is newer since the last time you updated. + +```bash +$ bundle update +``` + +### Developing on the "Edge" + +If you plan on using the edge code but also contributing back to Spree, +then you may be interested in the following approach. Create your Rails +app that will be using the Spree gem in a directory that has the same +parent as a locally cloned version of the Spree source. Then simply use +the following in your Gemfile. + +```ruby +gem 'spree', :path => '../spree' +``` + +*** +See the excellent [Bundler documentation](http://gembundler.com) for more details. +*** + +### "-stable" branches + +The Spree Git repository also contains stable branches for each minor Spree +version. For instance, there is a 2-1-stable branch which contains the latest +work for the 2.1.x branch of Spree. You may also decide to use this branch if you want the latest and greatest version of Spree: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '2-1-stable' +``` + +Similarly, all main Spree extensions use this versioning scheme as well. For example, here's a line that would be used for `spree_auth_devise`: + +```ruby +gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '2-1-stable' +``` + +!!! +While the best efforts of the Spree team are made to keep stable branches +stable, there has been situations in the past where changes required for a +net-positive result over the entire community have affected some applications +or extensions. If a change to a stable branch does break your application or +an extension, please report those breakages on the appropriate GitHub page. +!!! + +### Creating Extensions + +Spree provides a convenient generator for helping you to get started +with extensions. + +```bash +$ spree extension foo +``` + +*** +You need to have the Spree gem installed in order to use the `spree` command. +*** + +Please see the [Creating Extensions](extensions_tutorial) guide for more details. diff --git a/guides/content/developer/tutorials/decorators.md b/guides/content/developer/tutorials/decorators.md new file mode 100644 index 00000000000..9e052ec8dc2 --- /dev/null +++ b/guides/content/developer/tutorials/decorators.md @@ -0,0 +1,12 @@ +--- +title: Decorators +section: tutorial +--- + +## Introduction + +Seems like it would make sense to port the content from the old [Logic Customization Page](http://http://guides.spreecommerce.com/logic_customization.html) here + +A decorator is used as an example in [Extensions](../extensions_tutorial/) but this could go into more detail, including examples of controller decorators and how to test them. + + diff --git a/guides/content/developer/tutorials/deface_overrides_tutorial.md b/guides/content/developer/tutorials/deface_overrides_tutorial.md new file mode 100644 index 00000000000..242652658e8 --- /dev/null +++ b/guides/content/developer/tutorials/deface_overrides_tutorial.md @@ -0,0 +1,80 @@ +--- +title: Deface Overrides +section: tutorial +--- + +## Introduction + +This tutorial is a continuation of the previous one, [Extensions](extensions_tutorial), and begins where we left off in the last one. We have created a simple extension for promoting on-sale products on a "sales homepage". + +In this tutorial we are going to learn about [Deface](https://github.com/spree/deface) and how we can use it to improve our extension. As part of improving our extension, we will be updating the existing Spree admin interface so that we are able to set the `sale_price` for products. + +## What is Deface? + +Deface is a standalone Rails library that enables you to customize Erb templates +without needing to directly edit the underlying view file. Deface allows you to +use standard CSS3 style selectors to target any element (including Ruby blocks), +and perform an action against all the matching elements. Check out the +[Customization](view.html#using-deface) guide for more details. + +## Improving Our Extension Using Deface + +### The Goal + +Our goal is to add a field to the product edit admin page that allows the `sale_price` to be added or updated. We could do this by overriding the view Spree provides, but there are potential problems with this technique. If Spree updates the view in a new release we won't get the updated view as we are already overriding it. We would need to update our view with the new content from Spree and then add our customizations back in to stay fully up to date. + +Let's do this instead using Deface, which we just learned about. Using Deface will allow us to keep our view customizations in one spot, `app/overrides`, and make sure we are always using the latest implementation of the view provided by Spree. + +### The Implementation + +We want to override the product edit admin page, so the view we want to modify in this case is the product form partial. This file's path will be `spree/admin/products/_form.html.erb`. + +First, let's create the overrides directory with the following command: + +```bash +$ mkdir app/overrides +``` + +So we want to override `spree/admin/products/_form.html.erb`. Here is the part of the file we are going to add content to (you can also view the [full file](https://github.com/spree/spree/blob/master/backend/app/views/spree/admin/products/_form.html.erb)): + +```erb +
          +<%%= f.field_container :price do %> + <%%= f.label :price, raw(Spree.t(:master_price) + content_tag(:span, ' *', + :class => 'required')) %> + <%%= f.text_field :price, :value => number_to_currency(@product.price, + :unit => '') %> + <%%= f.error_message_on :price %> +<%% end %> +``` + +We want our override to insert another field container after the price field container. We can do this by creating a new file `app/overrides/add_sale_price_to_product_edit.rb` and adding the following content: + +```ruby +Deface::Override.new(:virtual_path => 'spree/admin/products/_form', + :name => 'add_sale_price_to_product_edit', + :insert_after => "erb[loud]:contains('text_field :price')", + :text => " + <%%= f.field_container :sale_price do %> + <%%= f.label :sale_price, raw(Spree.t(:sale_price) + content_tag(:span, ' *')) %> + <%%= f.text_field :sale_price, :value => + number_to_currency(@product.sale_price, :unit => '') %> + <%%= f.error_message_on :sale_price %> + <%% end %> + ") +``` + +We also need to delegate `sale_price` to the master variant in order to get the +updated product edit form working. + +We can do this by creating a new file `app/models/spree/product_decorator.rb` and adding the following content to it: + +```ruby +module Spree + Product.class_eval do + delegate_belongs_to :master, :sale_price + end +end +``` + +Now, when we head to `http://localhost:3000/admin/products` and edit a product, we should be able to set a sale price for the product and be able to view it on our sale page, `http://localhost:3000/sale`. Note that you will likely need to restart our example Spree application (created in the [Getting Started](getting_started_tutorial) tutorial). diff --git a/guides/content/developer/tutorials/extensions_tutorial.md b/guides/content/developer/tutorials/extensions_tutorial.md new file mode 100644 index 00000000000..0c9d820cd5a --- /dev/null +++ b/guides/content/developer/tutorials/extensions_tutorial.md @@ -0,0 +1,302 @@ +--- +title: Extensions +section: tutorial +--- + +## Introduction + +This tutorial continues where we left off in the [Getting Started](getting_started_tutorial) tutorial. Now that we have a basic Spree store up and running, let's spend some time customizing it. The easiest way to do this is by using Spree extensions. + +### What is a Spree Extension? + +Extensions are the primary mechanism for customizing a Spree site. They provide a convenient mechanism for Spree developers to share reusable code with one another. Even if you do not plan on sharing your extensions with the community, they can still be a useful way to reuse code within your organization. Extensions are also a convenient mechanism for organizing and isolating discrete chunks of functionality. + +### Finding Useful Spree Extensions in the Extension Registry + +The [Spree Extension Registry](http://spreecommerce.com/extensions) is a searchable collection of Spree Extensions written and maintained by members of the [Spree Community](http://spreecommerce.com/community). If you need to extend your Spree application's functionality, be sure to have a look in the Extension Registry first; you may find an extension that either implements what you need or provides a good starting point for your own implementation. If you write an extension and it might be useful to others, [publish it in the registry](http://spreecommerce.com/extensions/new) and people will be able to find it and contribute as well. + +## Installing an Extension + +We are going to be adding the [spree_fancy](https://github.com/spree/spree_fancy) extension to our store. SpreeFancy is a theme so it only changes the look and feel of the application. Extensions can also add models, controllers, and views to create new functionality, but spree_fancy is intended as a starting point to show how a barebones Spree application can be easily modified to give a nice look and feel. As a special bonus it's fully responsive and looks good on mobile devices as well as on larger screens. + +There are three steps we need to take to install spree_fancy. + +First, we need to add the gem to the bottom of our `Gemfile`: + +```ruby +gem 'spree_fancy', :git => 'git://github.com/spree/spree_fancy.git', :branch => '2-1-stable' +``` +**** + +Note that if you are using the edge version of spree, you should omit the branch parameter to get the latest version of spree_fancy. Alternatively, you should select the version of spree_fancy that corresponds with your version of spree. + +*** +If you are using a 2.1.x version of Spree, the above line will work fine. If you're using a 2.0.x version of Spree, you'll need to change the "branch" option to point to the "2-0-stable" branch. If you're using the "master" branch of Spree, change the "branch" argument for "spree_fancy" to be "master" as well. +*** + +Now, let's install the gem via Bundler with the following command: + +```bash +$ bundle install +``` + +Finally, let's copy over the required migrations and assets from the extension with the following command: + +```bash +$ bundle exec rails g spree_fancy:install +``` + +Answer **yes** when prompted to run migrations. + +When the last command is done running, you can start your application again and navigate to `http://localhost:3000` to see our brand new theme. + +## Creating an Extension + +### Getting Started + +Let's build a simple extension. Suppose we want the ability to mark certain products as being on sale. We'd like to be able to set a sale price on a product and show products that are on sale on a separate products page. This is a great example of how an extension can be used to build on the solid Spree foundation. + +So let's start by generating the extension. Run the following command from a directory of your choice outside of our Spree application: + +```bash +$ spree extension simple_sales +``` + +This creates a `spree_simple_sales` directory with several additional files and directories. After generating the extension make sure you change to its directory: + +```bash +$ cd spree_simple_sales +``` + +### Adding a Sale Price to Variants + +The first thing we need to do is create a migration that adds a sale_price column to [variants](http://guides.spreecommerce.com/products_and_variants.html#what-is-a-variant). + +We can do this with the following command: + +```bash +bundle exec rails g migration add_sale_price_to_spree_variants sale_price:decimal +``` + +Because we are dealing with prices, we need to now edit the generated migration to ensure the correct precision and scale. Edit the file `db/migrate/XXXXXXXXXXX_add_sale_price_to_spree_variants.rb` so that it contains the following: + +```ruby +class AddSalePriceToSpreeVariants < ActiveRecord::Migration + def change + add_column :spree_variants, :sale_price, :decimal, :precision => 8, :scale => 2 + end +end +``` + +### Adding Our Extension to the Spree Application + +Before we continue development of our extension, let's add it to the Spree application we created in the [last tutorial](/developer/getting_started.html). This will allow us to see how the extension works with an actual Spree store while we develop it. + +Within the `mystore` application directory, add the following line to the bottom of our `Gemfile`: + +```ruby +gem 'spree_simple_sales', :path => '../spree_simple_sales' +``` + +You may have to adjust the path somewhat depending on where you created the extension. You want this to be the path relative to the location of the `mystore` application. + +Once you have added the gem, it's time to bundle: + +```bash +$ bundle install +``` + +Finally, let's run the `spree_simple_sales` install generator to copy over the migration we just created (answer **yes** if prompted to run migrations): + +```bash +# context: Your Spree store's app root (i.e. Rails.root); not the extension's root path. +$ rails g spree_simple_sales:install +``` + +### Adding a Controller Action to HomeController + +Now we need to extend `Spree::HomeController` and add an action that selects "on sale" products. + +*** +Note for the sake of this example that `Spree::HomeController` is only included +in spree_frontend so you need to make it a dependency on your extensions *.gemspec file. +*** + +Make sure you are in the `spree_simple_sales` root directory and run the following command to create the directory structure for our controller decorator: + +```bash +$ mkdir -p app/controllers/spree +``` + +Next, create a new file in the directory we just created called `home_controller_decorator.rb` and add the following content to it: + +```ruby +module Spree + HomeController.class_eval do + def sale + @products = Product.joins(:variants_including_master).where('spree_variants.sale_price is not null').uniq + end + end +end +``` + +This will select just the products that have a variant with a `sale_price` set. + +We also need to add a route to this action in our `config/routes.rb` file. Let's do this now. Update the routes file to contain the following: + +```ruby +Spree::Core::Engine.routes.draw do + get "/sale" => "home#sale" +end +``` + +### Viewing On Sale Products + +#### Setting the Sale Price for a Variant + +Now that our variants have the attribute `sale_price` available to them, let's update the sample data so we have at least one product that is on sale in our application. We will need to do this in the rails console for the time being, as we have no admin interface to set sale prices for variants. We will be adding this functionality in the [next tutorial]() in this series, Deface overrides. + +So, in order to do this, first open up the rails console: + +```bash +$ rails console +``` + +Now, follow the steps I take in selecting a product and updating its master variant to have a sale price. Note, you may not be editing the exact same product as I am, but this is not important. We just need one "on sale" product to display on the sales page. + +```irb +> product = Spree::Product.first +=> # + +> variant = product.master +=> #, position: nil, lock_version: 0, on_demand: false, cost_currency: nil, sale_price: nil> + +> variant.sale_price = 8.00 +=> 8.0 + +> variant.save +=> true +``` + +### Creating a View + +Now we have at least one product in our database that is on sale. Let's create a view to display these products. + +First, create the required views directory with the following command: + +```bash +$ mkdir -p app/views/spree/home +``` + +Next, create the file `app/views/spree/home/sale.html.erb` and add the following content to it: + +```erb +
          + <%%= render 'spree/shared/products', :products => @products %> +
          +``` + +If you navigate to `http://localhost:3000/sale` you should now see the product(s) listed that we set a `sale_price` on earlier in the tutorial. However, if you look at the price, you'll notice that it's not actually displaying the correct price. This is easy enough to fix and we will cover that in the next section. + +### Decorating Variants + +Let's fix our extension so that it uses the `sale_price` when it is present. + +First, create the required directory structure for our new decorator: + +```bash +$ mkdir -p app/models/spree +``` + +Next, create the file `app/models/spree/variant_decorator.rb` and add the following content to it: + +```ruby +module Spree + Variant.class_eval do + alias_method :orig_price_in, :price_in + def price_in(currency) + return orig_price_in(currency) unless sale_price.present? + Spree::Price.new(:variant_id => self.id, :amount => self.sale_price, :currency => currency) + end + end +end +``` + +Here we alias the original method `price_in` to `orig_price_in` and override it. If there is a `sale_price` present on the product's master variant, we return that price. Otherwise, we call the original implementation of `price_in`. + +### Testing Our Decorator + +It's always a good idea to test your code. We should be extra careful to write tests for our Variant decorator since we are modifying core Spree functionality. Let's write a couple of simple unit tests for `variant_decorator.rb` + +#### Generating the Test App + +An extension is not a full Rails application, so we need something to test our extension against. By running the Spree `test_app` rake task, we can generate a barebones Spree application within our `spec` directory to run our tests against. + +We can do this with the following command from the root directory of our extension: + +```bash +$ bundle exec rake test_app +``` + +After this command completes, you should be able to run `rspec` and see the following output: + +```bash +No examples found. + +Finished in 0.00005 seconds +0 examples, 0 failures +``` + +Great! We're ready to start adding some tests. Let's replicate the extension's directory structure in our spec directory by running the following command + +```bash +$ mkdir -p spec/models/spree +``` + +Now, let's create a new file in this directory called `variant_decorator_spec.rb` and add the following tests to it: + +```ruby +require 'spec_helper' + +describe Spree::Variant do + describe "#price_in" do + it "returns the sale price if it is present" do + variant = create(:variant, :sale_price => 8.00) + expected = Spree::Price.new(:variant_id => variant.id, :currency => "USD", :amount => variant.sale_price) + + result = variant.price_in("USD") + + result.variant_id.should == expected.variant_id + result.amount.to_f.should == expected.amount.to_f + result.currency.should == expected.currency + end + + it "returns the normal price if it is not on sale" do + variant = create(:variant, :price => 15.00) + expected = Spree::Price.new(:variant_id => variant.id, :currency => "USD", :amount => variant.price) + + result = variant.price_in("USD") + + result.variant_id.should == expected.variant_id + result.amount.to_f.should == expected.amount.to_f + result.currency.should == expected.currency + end + end +end +``` + +These specs test that the `price_in` method we overrode in our `VariantDecorator` returns the correct price both when the sale price is present and when it is not. + +## Versioning your extension + +Different versions of Spree may act differently with your extension. It's advisable to keep different branches of your extension actively maintained for the different branches of Spree so that your extension will work with those different versions. + +It's advisable that your extension follows the same versioning pattern as Spree itself. If your extension is compatible with Spree 2.0.x, then create a `2-0-stable` branch on your extension and advise people to use that branch for your extension. If it's only compatible with 1.3.x, then create a 1-3-stable branch and advise the use of that branch. + +Having a consistent branching naming scheme across Spree and its extensions will reduce confusion in the long run. + +## Summary + +In this tutorial you learned how to both install extensions and create your own. A lot of core Spree development concepts were covered and you gained exposure to some of the Spree internals. + +In the [next part](deface_overrides_tutorial) of this tutorial series, we will cover [Deface](https://github.com/spree/deface) overrides and look at ways to improve our current extension. diff --git a/guides/content/developer/tutorials/getting_started_tutorial.md b/guides/content/developer/tutorials/getting_started_tutorial.md new file mode 100644 index 00000000000..d4bf4d4f057 --- /dev/null +++ b/guides/content/developer/tutorials/getting_started_tutorial.md @@ -0,0 +1,124 @@ +--- +title: Getting Started +section: tutorial +--- + +## Prerequisites + +Before starting this tutorial, make sure you have Ruby and RubyGems installed on your system. This is fairly straightforward, but differs depending on which operating system you use. + +By following this tutorial, you will create a simple Spree project called `mystore`. Before you can start building the application, you need to make sure that you have Rails itself installed. + +To run Spree 2.4 you need the latest Rails version, 4.1.8. + +### Installing Rails + +In most cases, the easiest way to install Rails is to take advantage of RubyGems: + +```bash +$ gem install rails -v 4.1.8 +``` + +### Installing Bundler + +Bundler is the current standard for maintaining Ruby gem dependencies. It is recommended that you have a decent working knowledge of Bundler and how it used within Rails before attempting to install Spree. You can install Bundler using the following command: + +```bash +$ gem install bundler +``` + +### Installing Image Magick + +Spree also uses the ImageMagick library for manipulating images. Using this library allows for automatic resizing of product images and the creation of product image thumbnails. ImageMagick is not a Rubygem and it can be a bit tricky to install. There are, however, several excellent sources of information on the Web for how to install it. A basic Google search should help you if you get stuck. + +If you are using OSX, a recommended approach is to install ImageMagick using [Homebrew](http://mxcl.github.com/homebrew/). This can be done with the following command: + +```bash +$ brew install imagemagick +``` + +If you are using Unix or Windows check out [Imagemagick.org](http://www.imagemagick.org/) for more detailed instructions on how to setup ImageMagick for your particular system. + +### Installing Spree + +The easiest way to get Spree setup is by installing the `spree_cmd` gem. This can be done with the following command: + +```bash +$ gem install spree_cmd +``` + +## Creating a New Spree Project + +The distribution of Spree as a Rubygem allows it to be used in a new Rails project or added to an existing Rails project. This guide will assume you are creating a brand new store and will walk you through the process, starting with the creation of a new Rails application. + +### Creating the Rails Application + +Let's start by creating a standard Rails application using the following command: + +```bash +$ rails _4.1.8_ new mystore +``` + +### Adding Spree to Your Rails Application + +Now that we have a basic Rails application we can add Spree to it. This approach would also work with existing Rails applications that have been around for a long time (assuming they are using the correct version of Rails.) + +After you create the store application, switch to its folder to continue work directly in that application: + +```bash +$ cd mystore +``` + +Now let's add Spree to our Rails application: + +```bash +$ spree install --auto-accept +``` + +*** +Note that this command will add the Spree dependencies to your gemfile. If you are using a custom build of Spree, or are bundling Spree from Github, you may want to use the rails generator provided by the `spree` gem instead: + +```bash +$ rails generate spree:install +``` + +This will run the spree generator using the version of Spree you have defined in your Gemfile. Running spree install with a custom source or build will generate an error as your Gemfile will be amended to require different versions of Spree. +*** + +## Hello, Spree! + +You now have a functional Spree application after running only a few commands! To see it, you need to start a web server on your development machine. You can do this by running another command: + +```bash +$ rails server +``` + +This will fire up an instance of the Webrick web server by default (Spree can also use several other web servers). To see your application in action, open a browser window and navigate to http://localhost:3000. You should see the Spree default home page: + +![Spree Application Home Page](/images/developer/spree_welcome.png) + +To stop the web server, hit Ctrl-C in the terminal window where it's running. In development mode, Spree does not generally require you to stop the server; changes you make in files will be automatically picked up by the server. + +### Logging Into the Backend + +The next thing you'll probably want to do is to log into the admin interface. Use your browser window to navigate to http://localhost:3000/admin. You can login with the username `spree@example.com` and password `spree123`. + +*** +If you elected not to use the `--auto-accept` option when you added Spree to your Rails app, and did not install the seed data, the admin user will not yet exist in your database. You can run a simple rake task to create a new admin user. + +```bash +$ rake spree_auth:admin:create +``` +*** + +Upon successful authentication, you should see the admin screen: + +![Admin Screen](/images/developer/overview.png) + +Feel free to explore some of the backend features that Spree has to offer and to verify that your installation is working properly. + +## Wrapping Up + +If you've followed the steps described in this tutorial, you should now have a fully functional Spree application up and running. + +This tutorial is part of a series. The next tutorial in this series is the [Extensions Tutorial](extensions_tutorial). diff --git a/guides/content/developer/tutorials/index.md b/guides/content/developer/tutorials/index.md new file mode 100644 index 00000000000..46fadac2e51 --- /dev/null +++ b/guides/content/developer/tutorials/index.md @@ -0,0 +1,6 @@ +--- +title: "Tutorials" +section: tutorial +--- + +## Tutorial diff --git a/guides/content/developer/tutorials/migration.markdown b/guides/content/developer/tutorials/migration.markdown new file mode 100644 index 00000000000..fd0c8c0cc59 --- /dev/null +++ b/guides/content/developer/tutorials/migration.markdown @@ -0,0 +1,481 @@ +--- +title: Migrating to Spree +section: advanced +--- + +## Overview + +This section explains how to convert existing sites or data sets for +use with Spree. It is a mix of tips and information about the relevant +APIs, and so is definitely intended for developers. After reading it you +should know: + +- techniques for programmatic import of products +- tips for migrating themes +- examples of the API in use. + +!!! +This documentation on this topic is out of date and we're +working to update it. In the meantime if you see things in here that are +confusing it's possible that they no longer apply, etc. +!!! + +## Overview + +This guide is a mix of tips and information about the relevant APIs, +intended to help simplify the process of getting a new site set up - +whether you're developing a fresh site or moving from an existing +commerce platform. + +The first section discusses various formats of data. Then we look in +detail at import of the product catalogue. Sometimes you may want to +import legacy order details, so there's a short discussion on this. + +Finally, there are some tips about how to ease the theme development +process. + +## Data Import Format + +This part discusses some options for getting data into the system, +including some discussion of using relevant formats. + +### Direct SQL import + +Can we just format our data as SQL tables and import it directly? In +principle yes, but it takes effort to get the format right, +particularly +when dealing with associations between tables, and you need to ensure +that the new data meets the system's validation rules. It's probably +easier to go the code route. + +There are cases where direct import is useful. One key case is when +moving between hosting platforms. Another is when cloning some project: +collaborators can just import a database dump prepared by someone else, +and save the time of the code import. + +### Rails Fixtures + +Spree uses fixtures to load up the sample data. It's a convenient +format for small collections of data, but can be tricky when working with +large data sets, especially if there are many interconnections and if you +need to be careful with validation. + +Note that Rails can dump slices of the database in fixture format. This +is sometimes useful. + +### SQL or XML legacy data + +This is the case where you are working with legacy data in formats like +SQL or XML, and the question is more how to get the useful data out. + +Some systems may be able to export their data in various standard +spreadsheet formats - it's worth checking for this. + +Tools like REXML or Nokogiri can be used to parse XML and either build +a spreadsheet representation or execute product-building actions +directly. + +For SQL, you can try to build a Rails interface to the data (eg. search +for help with legacy mappings) and dump a simplified format. It might +help +to use views or complex queries to flatten multi-table data into a +single table - which can then be treated like a spreadsheet. + +### Spreadsheet format + +Most of the information about products can be flattened into +spreadsheet +form, and a 2D table is convenient to work with. Clients are often +comfortable with the format too, and able to supply their inventories +in this format. + +For example, your spreadsheet could have the following columns: + +### Fixed Details + +- product name +- master price +- master sku +- taxon membership +- shipping category +- tax category +- dimensions and weight +- list of images +- description + +### Several Properties +- one column for each property type used in your catalogue + +### Variant Specifications +- option types for the product\ +- one variant per column, each listing the option values and the price/sku + +Note that if you know how many fixed columns and properties to expect, +then it's easy to determine which columns represent variants etc. + +Some of these columns might have simple punctuation etc. to add structure +to the field. For example, we've used: + +- Html tags in the description +- WxHxD for a shorthand for the dimensions +- "green & small = small_green_shirt @ $10.00" to code up a variant which is small and green, has sku *small_green_shirt* and costs $10. +- "foo\nbar" in the taxons column to encode membership of two taxons +- "alpha > beta > gamma" in the taxons column to encode membership a particular nesting. + +The taxon nesting notation is useful for when 'gamma' doesn't uniquely +identify a taxon (and so you need some context, ie a few ancestor +taxons), or for when the taxon structure isn't fixed in advance and so is +dynamically created as the products are entered. + +Another possibility for coding variants is to have each variant on a +separate row, and to leave the fixed fields empty when a row is a variant of the +last-introduced product. This is easier to read. + +### Seed code + +This is more a technique for getting the data loaded at the right time. +Technically, the product catalogue is *seed data*, standard data which +is needed for the app to work properly. + +Spree has several options for loading seed data, but perhaps the easiest +to use here is to put ruby files in *site/db/default/*. These files are +processed when *rake db:seed* is called, and will be processed in the order of the +migration timestamps. + +Your ruby script can use one of the XLS or CSV format reading libraries +to read an external file, or if the data set is not too big, you could +embed the CSV text in the script itself, eg. using the **END** convention. + +*** +If the order of loading is important, choose names for the files +so that alphabetical order gives the correct load order… +*** + +### Important system-wide settings + +A related but important topic is the Spree core settings that your app +will need to function correctly, eg to disable backordering or to +configure the mail subsystem. You can (mostly) set these from the admin +interface, but we recommend using initializers for these. See the +[preferences +guide](preferences.html#persisting-modifications-to-preferences) for +more info. + +## Catalog creation + +This section covers everything relating to import of a product set, +including the product details, variants, properties and options, +images, and taxons. + +### Preliminaries + +Let's assume that you are working from a CSV-compatible format, and so +are reading one product per row, and each row contains values for the fixed +details, properties, and variants configuration. + +We won't always explicitly save changes to records: we assume that your +upload scripts will call *save* at appropriate times or use +*update_attribute+ +etc. + +### Products + +Products must have at least a name and a price in order to pass +validation, and we set the description too. +```ruby +p = Spree::Product.create :name => 'some product', :price => 10.0, +:description => 'some text here' +``` + +Observe that the*permalink+ and timestamps are added automatically. +You may want to set the 'meta' fields for SEO purposes. + +*** +It's important to set the *available_on* field. Without this +being a date in the past, the product won't be listed in the standard +displays. +*** + +```ruby +p.available_on = Time.now +``` + +#### The Master variant + +Every product has a master variant, and this is created automatically +when the product is created. It is accessible via *p.master*, but note that many +of its fields are accessible through the product via delegation. Example: +*p.price* does the same as *p.master.price*. Delegation also allows field +modification, so *p.price = 2 * p.price* doubles the product's (master) price. + +The dimensions and weight fields should be self-explanatory. +The *sku* field holds the product's stock code, and you will want to set +this if the product does not have option variants. + +#### Stock levels + +If you don't have option variants, then you may also need to register +some stock for the master variant. The exact steps depend on how you +have configured Spree's [inventory system](inventory.html), but most sites +will just need to assign to *p.on_hand*, eg *p.on_hand = 100*. + +#### Shipping category + +A product's [shipping category](shipments.html#shipping-categories) field +provides product-specific information for the shipping +calculators, eg to indicate that a product requires additional insurance +or can only be surface shipped. If no special conditions are needed, you +can leave this field as nil. +The *Spree::ShippingCategory* model is effectively a wrapper for a +string. You can either generate the list of categories in advance, or use +*where.first_or_create* to reuse previous objects or create new ones +when required. + +```ruby +p.shipping_category = Spree::ShippingCategory.where(:name => 'Type A').first_or_create +``` + +#### Tax category + +This is a similar idea to the shipping category, and guides the +calculation of product taxes, eg to distinguish clothing items from electrical +goods. +The model wraps a name *and* a description (both strings), and you can +leave the field as nil if no special treatment is needed. + +You can use the *where.first_or_create* technique, though you probably +want to set up the entire [tax configuration](taxation.html) before you start +loading products. + +You can also fill in this information automatically at a *later* date, +e.g. use the taxon information to decide which tax categories something +belongs in. + +### Taxons + +Adding a product to a particular taxon is easy: just add the taxon to +the list of taxons for a product. + +```ruby +p.taxons << some_taxon +``` + +Recall that taxons work like subclassing in OO languages, so a product +in taxon T is also contained in T's ancestors, so you should usually assign a +product to the most specific applicable taxon - and do not need to assign it to +all of the taxon's ancestors.\ +However, you can assign products to as many taxons as you want, +including ancestor taxons. This feature is more useful with sibling taxons, e.g. +assigning a red and green shirt to both 'red clothes' and 'green +clothes'. + +*** +Yes, this also means that child taxons don't have to be distinct, ie +they can overlap. +*** + +When uploading from a spreadsheet, you might have one or more taxons +listed for a product, and these taxons will be identified by name. +Individual taxon names don't have to be unique, e.g. you could have +'shirts' under 'male clothing', and 'shirts' under 'female clothing'. +In this case, you need some context, eg 'male clothing > shirts' vs. +'female clothing > shirts'. + +Do you need to create the taxon structure in advance? Not always: as the +code below shows, it is possible to create taxons as and when they are +needed, but this can be cumbersome for deep hierarchies. One compromise is to +create the top levels (say the top 2 or 3 levels) in advance, then use +the taxon information column to do some product-specific fine tuning. + +The following code uses a list of (newline-separated) taxon descriptions- +possibly using 'A > B > C' style of context to assign the taxons for a product. Notice the use of +*where.first_or_create*. + +```ruby +# create outside of loop + main_taxonomy = Spree::Taxonomy.where(:name => 'Products').first_or_create + +# inside of main loop +the_taxons = [] +taxon_col.split(/[\r\n]*/).each do |chain| + taxon = nil + names = chain.split + names.each do |name| + taxon = Spree::Taxon.where.first_or_create + end + the_taxons << taxon +end +p.taxons = the_taxons + +``` + +You can use similar code to set up other taxonomies, e.g. to have a +taxonomy for brands and product ranges, like 'Guitars' with child +'Acoustic'. You could use various property or option values to drive the +creation of such taxonomies. + +### Product Properties + +The first step is to create the property 'types'. These should be +known in advance so you can define these at the start of the script. You +should give the internal name and presentation name. For simplicity, the code +examples have these names as the same string. + +```ruby +size_prop = Spree::Property.where(name: 'size', presentation: 'Size').first_or_create +``` + +Then you just set the value for the property-product pair. +Assuming value*size_info+ which is derived from the relevant +column, this means: +```ruby +Spree::ProductProperty.create :property => size_prop, :product => p, :value => size_info +``` + +#### Product prototypes + +The admin interface uses a system of 'prototypes' to speed up data +entry, which seeds a product with a given set of option types and (empty) +property values. It probably isn't so useful when creating products +programmatically, since the code will need to do the hard work of +creating variants and setting properties anyway. However, we mention it +here for completeness. + +### Variants + +Variants allow different versions of a product to be offered, e.g. +allowing variations in size and color for clothing. If a product comes in only +one configuration, you don't need to use variants - the master variant, +already created, is sufficient. + +Otherwise, you need to declare what the allowed option types are (e.g. +size, color, quality rating, etc) for your product, and then create variants +which (usually) have a single option value for each of the product's option +types (e.g. 'small' and 'red' etc). + +*** +Spree's core generally assumes that each variant has exactly one +option value for each of the product's option types, but the current +code is tolerant of missing values. Certain extensions may be more +strict, e.g. ones for providing advanced variant selection. +*** + +#### Creating variants + +New variants require only a product to be associated with, but it is +useful to set an identifying *sku* code too. The price field is optional: if it is not +explicitly set, the new variant will use the master variant's price (the same applies to +*cost_price* too). You can also set the *weight*, *width*, *height*, and *depth* too. + +```ruby +v = Spree::Variant.create :product => p, :sku => "some_sku_code", :price => NNNN +``` + +*** +The price is only copied at creation, so any subsequent changes to +a product's price will need to be copied to all of its variants. +*** + +Next, you may also want to register some stock for this variant. +The exact steps depend on how you have configured Spree's +[inventory system](inventory.html), but most sites +will just need to assign to *v.on_hand*, eg *v.on_hand = 100*. + +You now need to set some option types and values, so customers can +choose between the variants. + +#### Option types + +The option types to use will vary from product to product, so you will +need to give this information for each product - or assume a default +and only use different names when this column is empty. + +You can probably declare most of the option types in advance, and so +just look up the names when required, though for fine control, you can +use the *where.first_or_create* technique, with something like this: + +```ruby +p.option_types = option_names_col.map do |name| + Spree::OptionType.where(:name => name, :presentation => name).first_or_create +end +``` + +#### Option values + +Option values represent the choices possible for some option type. +Again, you could declare them in advance, or use *where.first_or_create*. You'll +probably find it easier to create/retrieve the option values as you create each variant. + +Suppose you are using a notation like *"Green & Small = small_green_shirt @ $10.00"* +to encode each variant in the spreadsheet, and this is stored in the variable +*opt_info*. The following extracts the three key pieces of information and sets +the option values for the new variant (see below for variant creation). + +```ruby +*,opts,sku,price = opt_info.match\s*=\s*\s*@.\*?)/).to_a +v = Spree::Variant.create :product => p, :sku => sku, :price => price +v.option_values = opts.split.map do |nm| + Spree::OptionValue.where.first_or_create +end +``` + +*** +You don't have to stick with system-wide option types: you can +create types specifically for groups of products such as a product range from a single +manufacturer. In such cases, the range might have a particular color +scheme and there can be advantages to isolating the scheme's options in its +own type and set of values, rather than trying to work with a more general +setup. It also avoids filling up a type with lots of similar options - +and so reduces the number of options when using faceted search etc. You can +also attach resources like color swatches to the more specific values. + +#### Ordering of option values +You might want option values to appear in a certain order, such as by +increasing size or by alphabetical order. The *Spree::OptionValue* model uses +*acts_as_list* for setting the order, and option types will use the *position* field when retrieving +their associated values. The position is scoped to the relevant option type. + +If you create option values in advance, just create them in the required +order and the plugin will set the *position* automatically. + +```ruby +color_type = Spree::OptionType.create :name => 'Color', :presentation => 'Color' +color_options = %w[Red Blue Green].split.map { |n| + Spree::OptionValue.create :name => n, :presentation => n, + :option_type => color_type } +``` + +Otherwise, you could enforce the ordering*after_ loading up all of the +variants, using something like this: + +```ruby +color_type.option_values.sort_by(&:name).each_with_index do |val,pos| + val.update_attribute(:position, pos + 1) +end +``` + +#### Further reading + +[Steph Skardal](https://github.com/stephskardal) has produced a useful +blog post on [product +optioning](http://blog.endpoint.com/2010/01/rails-ecommerce-spree-hooks-comments.html). +This discusses how the variant option representation works and how she +used it to build an extension for enhanced product option selection. + +### Product and Variant images + +Spree uses [paperclip](https://github.com/thoughtbot/paperclip) to +manage image attachments and their various size formats. (See the [Customization Guide](logic#product-images) for info on altering the image formats.) +You can attach images to products and to variants - the mechanism is +polymorphic. Given some local image file, the following will associate the image and +create all of the size formats. + +```ruby +#for image for product (all variants) represented by master variant +img = Spree::Image.create(:attachment => File.open(path), :viewable => product.master) + +#for image for single variant +img = Spree::Image.create(:attachment => File.open(path), :viewable => variant) +``` + +Paperclip also supports external [storage of images in S3](https://github.com/thoughtbot/paperclip/blob/master/lib/paperclip/storage.rb) diff --git a/guides/content/developer/tutorials/security.md b/guides/content/developer/tutorials/security.md new file mode 100644 index 00000000000..22c9cd8a9fc --- /dev/null +++ b/guides/content/developer/tutorials/security.md @@ -0,0 +1,295 @@ +--- +title: Security +section: advanced +--- + +## Overview + +Proper application design, intelligent programming, and secure infrastructure are all essential in creating a secure e-commerce store using any software (Spree included). The Spree team has done its best to provide you with the tools to create a secure and profitable web presence, but it is up to you to take these tools and put them in good practice. We highly recommend reading and understanding the [Rails Security Guide](http://guides.rubyonrails.org/security.html). + +## Reporting Security Issues + +Please do not announce potential security vulnerabilities in public. We have a [dedicated email address](mailto:security@spreecommerce.com). We will work quickly to determine the severity of the issue and provide a fix for the appropriate versions. We will credit you with the discovery of this patch by naming you in a blog post. + +If you would like to provide a patch yourself for the security issue **do not open a pull request for it**. Instead, create a commit on your fork of Spree and run this command: + +```bash +$ git format-patch HEAD~1..HEAD --stdout > patch.txt +``` + +This command will generate a file called `patch.txt` with your changes. Please email a description of the patch along with the patch itself to our [dedicated email address](mailto:security@spreecommerce.com). + +## Authentication + +If you install spree_auth_devise when setting up your app, we use a third party authentication library for Ruby known as [Devise](https://github.com/plataformatec/devise). This library provides a host of useful functionality that is in turn available to Spree, including the following features: + +* Authentication +* Strong password encryption (with the ability to specify your own algorithms) +* "Remember Me" cookies +* "Forgot my password" emails +* Token-based access (for REST API) + +### Devise Configuration + +*** +A default Spree install comes with the [spree_auth_devise](https://github.com/spree/spree_auth_devise) gem, which provides authentication for Spree using Devise. This section of the guide covers the default setup. If you're using your own authentication, please consult the manual for that authentication engine. +*** + +We have configured Devise to handle only what is needed to authenticate with a Spree site. The following details cover the default configurations: + +* Passwords are stored in the database encrypted with the salt. +* User authentication is done through the database query. +* User registration is enabled and the user's login is available immediately (no validation emails). +* There is a remember me and password recovery tool built in and enabled through Devise. + +These configurations represent a reasonable starting point for a typical e-commerce site. Devise can be configured extensively to allow for a different feature set but that is currently beyond the scope of this document. Developers are encouraged to visit the [Devise wiki](https://github.com/plataformatec/devise/wiki) for more details. + +### REST API + +The REST API behaves slightly differently than a standard user. First, an admin has to create the access key before any user can query the REST API. This includes generating the key for the admin him/herself. This is not the case if `Spree::Api::Config[:requires_authentication]` is set to `false`. + +In cases where `Spree::Api::Config[:requires_authentication] is set to `false`, read-only requests in the API will be possible for all users. For actions that modify data within Spree, a user will need to have an API key and then their user record would need to have permission to perform those actions. + +It is up to you to communicate that key. As an added measure, this authentication has to occur on every request made through the REST API as no session or cookies are created or stored for the REST API. + +### Authorization + +Spree uses the excellent [CanCan](https://github.com/ryanb/cancan) gem to provide authorization services. If you are unfamiliar with it, you should take a look at Ryan Bates' [excellent screencast](http://railscasts.com/episodes/192-authorization-with-cancan) on the topic (or read the [transcribed version](http://asciicasts.com/episodes/192-authorization-with-cancan)). A detailed explanation of CanCan is beyond the scope of this guide. + +### Default Rules + +The follow Spree source code is taken from `ability.rb` and provides some insight into the default authorization rules: + +```ruby +if user.respond_to?(:has_spree_role?) && user.has_spree_role?('admin') + can :manage, :all +else + ############################# + can [:read,:update,:destroy], Spree.user_class, :id => user.id + can :create, Spree.user_class + ############################# + can :read, Order do |order, token| + order.user == user || order.token && token == order.token + end + can :update, Order do |order, token| + order.user == user || order.token && token == order.token + end + can :create, Order + + can :read, Address do |address| + address.user == user + end + + ############################# + can :read, Product + can :index, Product + ############################# + can :read, Taxon + can :index, Taxon + ############################# +end +``` + +The above rule set has the following practical effects for Spree users + +* Admin role can access anything (the rest of the rules are ignored) +* Anyone can create a `User`, only the user associated with an account can perform read or update operations for that user. +* Anyone can create an `Order`, only the user associated with the order can perform read or update operations. +* Anyone can read product pages and look at lists of `Products` (including search operations). +* Anyone can read or view a list of `Taxons`. + +### Enforcing the Rules + +CanCan is only effective in enforcing authorization rules if it's asked. In other words, if the source code does not check permissions there is no way to deny access based on those permissions. This is generally handled by adding the appropriate code to your Rails controllers. For more information please see the [CanCan Wiki](https://github.com/ryanb/cancan/wiki). + +### Custom Authorization Rules + +We have modified the original CanCan concept to make it easier for extension developers and end users to add their own custom authorization rules. For instance, if you have an "artwork extension" that allows users to attach custom artwork to an order, you will need to add rules so that they have permissions to do so. + +The trick to adding custom authorization rules is to add an `AbilityDecorator` to your extension and then to register these abilities. The following code is an example of how to restrict access so that only the owner of the artwork can update it or view it. + +```ruby +class AbilityDecorator + include CanCan::Ability + def initialize(user) + can :read, Artwork do |artwork| + artwork.order && artwork.order.user == user + end + can :update, Artwork do |artwork| + artwork.order && artwork.order.user == user + end + end +end + +Spree::Ability.register_ability(AbilityDecorator) +``` + +### Custom Roles in the Admin Namespace + +If you plan on allowing a custom role you create to access the Spree administrative +panels, there are a couple of considerations to keep in mind. + +Spree authorizes all of its administrative panels with two CanCan authorization +commands: `:admin` and the name of the action being authorized. If you want a +custom role to be able to access a particular admin panel, you have to specify +that your role *can* access both :admin and the name of the action on the relevant +resource. For example, if you want your Sales Representatives to be able to access the Admin +Orders panel without giving them access to anything else in the Admin namespace, +you would have to specify the following in an `AbilityDecorator`: + +```ruby +class AbilityDecorator + include CanCan::Ability + def initialize(user) + if user.respond_to?(:has_spree_role?) && user.has_spree_role?('sales_rep') + can [:admin, :index, :show], Spree::Order + end + end +end + +Spree::Ability.register_ability(AbilityDecorator) +``` + +This is required by the following code in Spree's `Admin::BaseController` which +is the controller every controller in the Admin namespace inherits from. + +```ruby +def authorize_admin + if respond_to?(:model_class, true) && model_class + record = model_class + else + record = Object + end + authorize! :admin, record + authorize! action, record +end +``` + +If you need to create custom controllers for your own models under the Admin +namespace, you will need to manually specify the model your controller manipulates +by defining a `model_class` method in that controller. + +```ruby +module Spree + module Admin + class WidgetsController < BaseController + def index + # Relevant code in here + end + + private + def model_class + Widget + end + end + end +end +``` + +This is necessary because CanCan cannot, by default, detect the model used to +authorize controllers under the Admin namespace. By specifying `model_class`, Spree +knows what to tell CanCan to use to authorize your controller. + +### Tokenized Permissions + +There are situations where it may be desirable to restrict access to a particular resource without requiring a user to authenticate in order to have that access. Spree allows so-called "guest checkouts" where users just supply an email address and they're not required to create an account. In these cases you still want to restrict access to that order so only the original customer can see it. The solution is to use a "tokenized" URL. + +http://example.com/orders?token=aidik313dsfs49d + +Spree provides a `TokenizedPermission` model used to grant access to various resources through a secure token. This model works in conjunction with the `Spree::TokenResource` module which can be used to add tokenized access functionality to any Spree resource. + +```ruby +module Spree + module Core + module TokenResource + module ClassMethods + def token_resource + has_one :tokenized_permission, :as => :permissable + delegate :token, :to => :tokenized_permission, :allow_nil => true + after_create :create_token + end + end + + def create_token + permission = build_tokenized_permission + permission.token = token = ::SecureRandom::hex(8) + permission.save! + token + end + + def self.included(receiver) + receiver.extend ClassMethods + end + end + end +end + +ActiveRecord::Base.class_eval { include Spree::Core::TokenResource } +``` + +The `Order` model is one such model in Spree where this interface is already in use. The following code snippet shows how to add this functionality through the use of the `token_resource` declaration: + +```ruby +Spree::Order.class_eval do + token_resource +end +``` + +If we examine the default CanCan permissions for `Order` we can see how tokens can be used to grant access in cases where the user is not authenticated. + +```ruby +can :read, Spree::Order do |order, token| + order.user == user || order.token && token == order.token +end + +can :update, Spree::Order do |order, token| + order.user == user || order.token && token == order.token +end + +can :create, Spree::Order +``` + +This configuration states that in order to read or update an order, you must be either authenticated as the correct user, or supply the correct authorizing token. + +The final step is to ensure that the token is passed to CanCan when the authorization is performed, which is done in the controller. + +```ruby +authorize! action, resource, session[:access_token] +``` + +Of course this also assumes that the token has been stored in the session. Generally this can be achieved with a route that maps the token to the correct parameter: + +```ruby +get '/orders/:id/token/:token' => 'orders#show', :as => :token_order +``` + +This is followed by a call to store the token in the session for possible future access. + +```ruby + session[:access_token] ||= params[:token] +``` + +## Credit Card Data + +### PCI Compliance + +All store owners wishing to process credit card transactions should be familiar with [PCI Compliance](http://en.wikipedia.org/wiki/Pci_compliance). Spree makes +absolutely no warranty regarding PCI compliance (or anything else for that matter - see the [LICENSE](http://spreecommerce.com/license) for details.) We do, however, follow common sense security practices in handling credit card data. + +### Transmit Exactly Once + +Spree uses extreme caution in its handling of credit cards. In production mode, credit card data is transmitted to Spree via SSL. The data is immediately relayed to your chosen payment gateway and then discarded. The credit card data is never stored in the database (not even temporarily) and it exists in memory on the server for only a fraction of a second before it is discarded. + +Spree does store the last four digits of the credit card and the expiration month and date. You could easily customize Spree further if you wanted and opt out of storing even that little bit of information. + +### Payment Profiles + +Spree also supports the use of "payment profiles." This means that you can "store" a customer's credit card information in your database securely. More precisely you store a "token" that allows you to use the credit card again. The credit card gateway is actually the place where the credit card is stored. Spree ends up storing a token that can be used to authorize new charges on that same card without having to store sensitive credit card details. + +Spree has out of the box support for [Authorize.net CIM](http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/) payment profiles. + +### Other Options + +There are also third-party extensions for Paypal's [Express Checkout](https://merchant.paypal.com/cgi-bin/marketingweb?cmd=_render-content&content_ID=merchant/express_checkout) (formerly called Paypal Express.) These types of checkout services handle processing of the credit card information offsite (the data never touches your server) and greatly simplify the requirements for PCI compliance. + +[Braintree](https://braintreepayments.com) also offers a very interesting gateway option that achieves a similar benefit to Express Checkout but allows the entire process to appear to be taking place on the site. In other words, the customer never appears to leave the store during the checkout. They describe this as a "transparent redirect." The Braintree team is very interested in helping other Ruby developers use their gateway and have provided support to Spree developers in the past who were interested in using their product. diff --git a/guides/content/developer/tutorials/testing.md b/guides/content/developer/tutorials/testing.md new file mode 100644 index 00000000000..ba9d1fe512f --- /dev/null +++ b/guides/content/developer/tutorials/testing.md @@ -0,0 +1,69 @@ +--- +title: Testing Spree Applications +section: advanced +--- + +## Overview + +The Spree project currently uses [RSpec](http://rspec.info) for all of its tests. Each of the gems that makes up Spree has a test suite that can be run to verify the code base. + +The Spree test code is an evolving story. We started out with RSpec, then switched to Shoulda and now we're back to RSpec. RSpec has evolved considerably since we first tried it. When looking to improve the test coverage of Spree we took another look at RSpec and it was the clear winner in terms of strength of community and documentation. + +## Testing Spree Components + +Spree consists of several different gems (see the [Source Code Guide](navigating#layout-and-structure) for more details.) Each of these gems has its own test suite which can be found in the `spec` directory. Since these gems are also Rails engines, they can't really be tested in complete isolation - they need to be tested within the context of a Rails application. + +You can easily build such an application by using the Rake task designed for this purpose, running it inside the component you want to test: + +```bash +$ bundle exec rake test_app +``` + +This will build the appropriate test application inside of your `spec` directory. It will also add the gem under test to your `Gemfile` along with the `spree_core` gem (since all of the gems depend on this.) + +This rake task will regenerate the application (after deleting the existing one) each time you run it. It will also run the migrations for you automatically so that your test database is ready to go. There is no need to run `rake db:migrate` or `rake db:test:prepare` after running `test_app`. + +### Running the Specs + +Once your test application has been built, you can then run the specs in the standard RSpec manner: + +```bash +$ bundle exec rspec spec +``` + +We also set up a build script that mimics what our build server performs. You can run it from the root of the Spree project like this: + +```bash +$ bash build.sh +``` + +If you wish to run spec for a single file then you can do so like this: + +```bash +$ bundle exec rspec spec/models/spree/state_spec.rb +``` + +If you wish to test a particular line number of the spec file then you can do so like this: + +```bash +$ bundle exec rspec spec/models/spree/state_spec.rb:7 +``` + +### Using Factories + +Spree uses [factory_girl](https://github.com/thoughtbot/factory_girl) to create valid records for testing purpose. All of the factories are also packaged in the gem. So if you are writing an extension or if you just want to play with Spree models then you can use these factories as illustrated below. + +```bash +$ rails console +$ require 'spree/testing_support/factories' +``` + +The `spree_core` gem has a good number of factories which can be used for testing. If you are writing an extension or just testing Spree you can make use of these factories. + +## Testing Your Spree Application + +Currently, Spree does not come with any tests that you can install into your application. What we would advise doing instead is either copying the tests from the components of Spree and modifying them as you need them. + +$$$ +Rewrite the preceding paragraph - either copying/modifying or...? +$$$ \ No newline at end of file diff --git a/guides/content/developer/upgrades/index.md b/guides/content/developer/upgrades/index.md new file mode 100644 index 00000000000..d0194cd563b --- /dev/null +++ b/guides/content/developer/upgrades/index.md @@ -0,0 +1,21 @@ +--- +title: "Upgrade Guides" +section: upgrades +--- + +## Upgrade Guides + +We strongly advise upgrading Spree incrementally, rather than in one big go. For example, if you're upgrading a Spree store from 0.60.x to 2.4.x, you would read through all of these guides, one by one. + +If there are any issues with these guides, please let us know by [filing an issue](https://github.com/spree/spree/issues/new). + +* [2.3.x to 2.4.x](/developer/upgrades/two-dot-three-to-two-dot-four) +* [2.2.x to 2.3.x](/developer/upgrades/two-dot-two-to-two-dot-three) +* [2.1.x to 2.2.x](/developer/upgrades/two-dot-one-to-two-dot-two) +* [2.0.x to 2.1.x](/developer/upgrades/two-dot-oh-to-two-dot-one) +* [1.3.x to 2.0.x](/developer/upgrades/one-dot-three-to-two-dot-oh) +* [1.2.x to 1.3.x](/developer/upgrades/one-dot-two-to-one-dot-three) +* [1.1.x to 1.2.x](/developer/upgrades/one-dot-one-to-one-dot-two) +* [1.0.x to 1.1.x](/developer/upgrades/one-dot-oh-to-one-dot-one) +* [0.70.x to 1.0.x](/developer/upgrades/point-seventy-to-one-dot-oh) +* [0.60.x to 0.70.x](/developer/upgrades/point-sixty-to-point-seventy) diff --git a/guides/content/developer/upgrades/one-dot-oh-to-one-dot-one.md b/guides/content/developer/upgrades/one-dot-oh-to-one-dot-one.md new file mode 100644 index 00000000000..df64abe6472 --- /dev/null +++ b/guides/content/developer/upgrades/one-dot-oh-to-one-dot-one.md @@ -0,0 +1,70 @@ +--- +title: Upgrading Spree from 1.0.x to 1.1.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 1.0.x Spree store, to a 1.1.x store. This +guide has been written from the perspective of a blank Spree 1.0.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 1.1.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 1-1-stable branch. + +## Upgrade Rails + +Spree 1.1 depends on any Rails 3.2 release afer Rails 3.2.9. Ensure that you have that dependency specified in your Gemfile: + +```ruby +gem 'rails', '~> 3.2.9'``` + +Along with this, you may have to also update your assets group in the Gemfile: + +```ruby +group :assets do + gem 'sass-rails', '~> 3.2.5' + gem 'coffee-rails', '~> 3.2.1' + gem 'uglifier', '>= 1.0.3' +end + +gem 'jquery-rails', '2.1.4' +``` + +For more information, please refer to the [Upgrading Ruby on Rails guide](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-3-1-to-rails-3-2). + +## Upgrade Spree + +For best results, use the 1-1-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '1-1-stable'``` + +Run `bundle update spree`. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Remove references to spree_api assets + +Spree API no longer provides any asset files, so references to these must be removed from: + +* app/assets/stylesheets/store/all.css +* app/assets/stylesheets/admin/all.css +* app/assets/javascripts/store/all.js +* app/assets/javascripts/admin/all.js + +## Read the release notes + +For information about what has changed in this release, please read the [1.1.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_1_1_0.html). + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/one-dot-one-to-one-dot-two.md b/guides/content/developer/upgrades/one-dot-one-to-one-dot-two.md new file mode 100644 index 00000000000..3bd3f2f52c7 --- /dev/null +++ b/guides/content/developer/upgrades/one-dot-one-to-one-dot-two.md @@ -0,0 +1,58 @@ +--- +title: Upgrading Spree from 1.1.x to 1.2.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 1.1.x Spree store, to a 1.2.x store. This +guide has been written from the perspective of a blank Spree 1.1.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 1.2.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 1-2-stable branch. + +## Upgrade Spree + +For best results, use the 1-2-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '1-2-stable'``` + +Run `bundle update spree`. + +## Authentication dependency + +In this release, the `spree_auth` component was moved out of the main set of +gems into an extension, called `spree_auth_devise`. If you want to continue using Spree's authentication, then you will need to specify this extension as a dependency in your `Gemfile`: + +```ruby +gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-2-stable'``` + +Run `bundle install` to install this extension. + +### Rename current_user to current_spree_user + +To ensure that Spree does not conflict with any authentication provided by the application, Spree has renamed its `current_user` variable to `current_spree_user`. You should make this change wherever necessary within your application. + +Similar to this, any references to `@user` are now `@spree_user`. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +This may copy over additional migrations from spree_auth_devise and run them as well. + +## Read the release notes + +For information about changes contained with this release, please read the [1.2.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_1_2_0.html). + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/one-dot-three-to-two-dot-oh.md b/guides/content/developer/upgrades/one-dot-three-to-two-dot-oh.md new file mode 100644 index 00000000000..62b2b3afeda --- /dev/null +++ b/guides/content/developer/upgrades/one-dot-three-to-two-dot-oh.md @@ -0,0 +1,67 @@ +--- +title: Upgrading Spree from 1.3.x to 2.0.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 1.3.x Spree store, to a 2.0.x store. This +guide has been written from the perspective of a blank Spree 1.3.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 2.0.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 2-0-stable branch. + +Given that this is a major release, you may want to read through the [2.0.0 release notes](http://guides.spreecommerce.com/release_notes/spree_2_0_0.html) to see what has changed before proceeding with this upgrade. + +## Upgrade Spree + +For best results, use the 2-0-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '2-0-stable'``` + +Run `bundle update spree`. + +## Bump jquery-rails + +This version of Spree bumps the dependency for jquery-rails to this: + +```ruby +gem 'jquery-rails', '3.0.0'``` + +Ensure that you have a line such as this in your Gemfile to allow that dependency. + +## Remove middleware + +The two pieces of middleware previously inserted into `config/application.rb` have now been deprecated. Remove these two lines: + +```ruby +config.middleware.use "Spree::Core::Middleware::RedirectLegacyProductUrl" +config.middleware.use "Spree::Core::Middleware::SeoAssist"``` + +## Rename assets + +In Spree 2, assets have been renamed. + +In `store/all.css` and `store/all.js`, you will need to rename the references from `spree_core` to `spree_frontend`. Similarly to this, in `admin/all.css` and `admin/all.js`, you will need to rename the references from `spree_core` to `spree_backend`. + +Additionally, remove references to `spree_promo` from these files. That component of Spree has now been merged with the Core component. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Read the release notes + +For information about changes contained with this release, please read the [2.0.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_2_0_0.html). + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/one-dot-two-to-one-dot-three.md b/guides/content/developer/upgrades/one-dot-two-to-one-dot-three.md new file mode 100644 index 00000000000..81795cdde0e --- /dev/null +++ b/guides/content/developer/upgrades/one-dot-two-to-one-dot-three.md @@ -0,0 +1,89 @@ +--- +title: Upgrading Spree from 1.2.x to 1.3.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 1.2.x Spree store, to a 1.3.x store. This +guide has been written from the perspective of a blank Spree 1.2.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 1.3.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 1-3-stable branch. + +## Upgrade Spree + +For best results, use the 1-3-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '1-3-stable'``` + +Run `bundle update spree`. + +## Bump jquery-rails + +This version of Spree bumps the dependency for jquery-rails to this: + +```ruby +gem 'jquery-rails', '2.2.0'``` + +Ensure that you have a line such as this in your Gemfile to allow that dependency. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Replace money usages + +In older versions of Spree, we had a helper method called `money` which +occasionally formatted money amounts incorrectly. Specifically, if the `I18n.locale` was changed, currencies started to display in that amount, rather than the proper amount. An item that was once $100, would suddenly become 100¥ if the locale was switched to Japanese, for instance. + +In Spree 1.3, money handling +has been reworked by a major contribution by the [Free Running +Technologies](http://www.freerunningtech.com/) team. See [#2197](https://github.com/spree/spree/pull/2197) for details. + +Prices are now stored in a separate table, called `spree_prices`. This table tracks the variant, the price amount, and the currency. This allows for variants to have different prices in different currencies. + +Along with this, we introduced the `Spree::Money` class which is used to display amounts correctly. Where previously Spree would have done this: + +```erb +<%%= money adjustment.amount %>``` + +We now use this: + +```erb +<%%= adjustment.display_amount.to_html %>``` + +Alternatively, you can use `Spree::Money.new(amount)` to get a `Spree::Money` representation. Calling `to_html` on that object will format it neatly for HTML views, and calling `to_s` will format it nicely everywhere else. + +### Variant.active scope + +Along with these changes, the `Spree::Variant.active` scope now takes an argument for the currency. Whatever currency is specified will return variants in that currency. Previously it may have been enough to just do this: + +```ruby +@product.variants.active``` + +But now you must specify a currency: + +```ruby +@product.variants.active("USD")``` + +Or you can rely on the current currency within views: + +```ruby +@product.variants.active(current_currency)``` + +## Read the release notes + +For information about changes contained with this release, please read the [1.3.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_1_3_0.html). + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/point-seventy-to-one-dot-oh.md b/guides/content/developer/upgrades/point-seventy-to-one-dot-oh.md new file mode 100644 index 00000000000..7602ccb87db --- /dev/null +++ b/guides/content/developer/upgrades/point-seventy-to-one-dot-oh.md @@ -0,0 +1,88 @@ +--- +title: Upgrading Spree from 0.70.x to 1.0.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 0.70.x Spree store, to a 1.0.x store. This +guide has been written from the perspective of a blank Spree 0.70.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 1.0.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 1-0-stable branch. + +Worth noting here is that Spree 1.0 was the first release to properly use the +features of Rails engines. This means that Spree needs to be mounted manually +within the `config/routes.rb` file of the application, and that the classes +such as `Product` and `Variant` from Spree are now namespaced within a module, +so that they are now `Spree::Product` and `Spree::Variant`. Tables are +similarly namespaced (i.e. `spree_products` and `spree_variants`). + +Along with this, migrations must be copied over to the application using the +`rake railties:install:migrations` command, rather than a `rails g spree:site` +command as before. + +## Upgrade Rails + +Spree 1.0 depends on any Rails 3.1 release afer Rails 3.1.10. Ensure that you have that dependency specified in your Gemfile: + +```ruby +gem 'rails', '~> 3.1.10' + +## Upgrade Spree + +For best results, use the 1-0-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '1-0-stable'``` + +Run `bundle update spree`. + +## Rename middleware classes + +In `config/application.rb`, there are two pieces of middleware: + +```ruby +config.middleware.use "RedirectLegacyProductUrl" +config.middleware.use "SeoAssist"``` + +These classes are now namespaced within Spree: + +```ruby +config.middleware.use "Spree::Core::Middleware::RedirectLegacyProductUrl" +config.middleware.use "Spree::Core::Middleware::SeoAssist"``` + + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Mount the Spree engine + +Within `config/routes.rb`, you must now mount the Spree engine: + +```ruby +mount Spree::Core::Engine, :at => '/'``` + +This is the standard way of adding engines to Rails applications. + +## Remove spree_dash assets + +Spree's dash component was removed as a dependency of Spree, and so references +to its assets must be removed also. Remove references to spree_dash from: + +* app/assets/stylesheets/store/all.css +* app/assets/javascripts/store/all.js +* app/assets/stylesheets/admin/all.css +* app/assets/javascripts/admin/all.js + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/point-sixty-to-point-seventy.md b/guides/content/developer/upgrades/point-sixty-to-point-seventy.md new file mode 100644 index 00000000000..74fe2beecd8 --- /dev/null +++ b/guides/content/developer/upgrades/point-sixty-to-point-seventy.md @@ -0,0 +1,91 @@ +--- +title: Upgrading Spree from 0.60.x to 0.70.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 0.60.x Spree store, to a 0.70.x store. This +guide has been written from the perspective of a blank Spree 0.60.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 0.70.x store once this +upgrade is complete. + +## Upgrade Rails + +Spree 0.60.x depends on Rails 3.0.12, whereas Spree 0.70.x depends on any Rails +version from 3.1.1 up to 3.1.4. The first step in upgrading Spree is to +upgrade the Rails version in the `Gemfile`: + +```ruby +gem 'rails', '3.1.12'``` + +For more information, please read the [Upgrading Ruby on Rails Guide](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-3-0-to-rails-3-1). + +## Upgrade Spree + +For best results, use the 0-70-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '0-70-stable'``` + +Run `bundle update rails` and `bundle update spree` and verify that was successful. + +## Remove debug_rjs configuration + +In `config/environments/development.rb`, remove this line: + +```ruby +config.action_view.debug_rjs = true``` + +## Remove lib/spree_site.rb + +This file is no longer used in 0.70.x versions of Spree. + +## Set up new data + +To migrate the data across, use these commands: + +```bash +rails g spree:site +rake db:migrate``` + +## The Asset Pipline + +With the upgrade to Rails 3.1 comes the [asset pipeline](http://guides.rubyonrails.org/asset_pipeline.html). You need to add these gems to your Gemfile in order to support Spree's assets being served: + +```ruby +group :assets do + gem 'sass-rails', '~> 3.1.5' + gem 'coffee-rails', '~> 3.1.1' + + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + # gem 'therubyracer' + + gem 'uglifier', '>= 1.0.3' +end + +gem 'jquery-rails', '2.2.1'``` + +Along with these gems, you will need to enable assets within the class definition inside `config/application.rb`: + +```ruby +module YourStore + class Application < Rails::Application + + # ... + + # Enable the asset pipeline + config.assets.enabled = true + + # Version of your assets, change this if you want to expire all your assets + config.assets.version = '1.0' + + end +end``` + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/two-dot-oh-to-two-dot-one.md b/guides/content/developer/upgrades/two-dot-oh-to-two-dot-one.md new file mode 100644 index 00000000000..accbbd593c6 --- /dev/null +++ b/guides/content/developer/upgrades/two-dot-oh-to-two-dot-one.md @@ -0,0 +1,55 @@ +--- +title: Upgrading Spree from 2.0.x to 2.1.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 2.0.x Spree store, to a 2.1.x store. This +guide has been written from the perspective of a blank Spree 2.0.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 2.1.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 2-1-stable branch. + +This is the first Spree release which supports Rails 4 exclusively. Spree +releases after this point will continue to support Rails 4 only. + +## Upgrade Rails + +For this Spree release, you will need to upgrade your Rails version to at least 4.0.0. + +It is recommended to read through the [Upgrading Ruby on Rails +guide](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading- +from-rails-3-2-to-rails-4-0) to learn what needs to be done for your +application to migrate to Rails 4. + +```ruby +gem 'rails', '~> 4.0.0'``` + +## Upgrade Spree + +For best results, use the 2-1-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '2-1-stable'``` + +Run `bundle update spree`. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Read the release notes + +For information about changes contained with this release, please read the [2.1.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_2_1_0.html). + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/two-dot-one-to-two-dot-two.md b/guides/content/developer/upgrades/two-dot-one-to-two-dot-two.md new file mode 100644 index 00000000000..383eb4cf353 --- /dev/null +++ b/guides/content/developer/upgrades/two-dot-one-to-two-dot-two.md @@ -0,0 +1,61 @@ +--- +title: Upgrading Spree from 2.1.x to 2.2.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 2.1.x Spree store, to a 2.2.x store. This +guide has been written from the perspective of a blank Spree 2.1.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 2.2.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 2-2-stable branch. + +## Upgrade Rails + +For this Spree release, you will need to upgrade your Rails version to at least 4.0.6. + +```ruby +gem 'rails', '~> 4.0.6' +``` + +## Upgrade Spree + +For best results, use the 2-2-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '2-2-stable'``` + +Run `bundle update spree`. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Read the release notes + +For information about changes contained with this release, please read the [2.2.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_2_2_0.html). + +### Rename assets + +As mentioned in the release notes, asset paths have changed. Change the references on the left, to the ones on the right: + +* `admin/spree_backend` => `spree/backend` +* `store/spree_frontend` => `spree/frontend` + +This applies across the board on Spree, and may need to be done in your store's extensions. + +### Paperclip settings have been removed from master + +Please consult [this section](http://guides.spreecommerce.com/release_notes/spree_2_2_0.html#paperclip-settings-have-been-removed) of the release notes if you were using custom Paperclip settings. This will direct you what to do in that particular case. + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/two-dot-three-to-two-dot-four.md b/guides/content/developer/upgrades/two-dot-three-to-two-dot-four.md new file mode 100644 index 00000000000..f49264b5f15 --- /dev/null +++ b/guides/content/developer/upgrades/two-dot-three-to-two-dot-four.md @@ -0,0 +1,46 @@ +--- +title: Upgrading Spree from 2.3.x to 2.4.x +section: upgrades +--- + +This guide covers upgrading a 2.3.x Spree store, to a 2.4.x store. This +guide has been written from the perspective of a blank Spree 2.3.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 2.4.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 2-4-stable branch. + +## Upgrade Rails + +For this Spree release, you will need to upgrade your Rails version to at least 4.1.8. + +```ruby +gem 'rails', '~> 4.1.8' +``` + +## Upgrade Spree + +For best results, use the 2-4-stable branch from GitHub: + +```ruby +gem 'spree', github: 'spree/spree', branch: '2-4-stable'``` + +Run `bundle update spree`. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Read the release notes + +For information about changes contained within this release, please read the [2.4.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_2_4_0.html). + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/developer/upgrades/two-dot-two-to-two-dot-three.md b/guides/content/developer/upgrades/two-dot-two-to-two-dot-three.md new file mode 100644 index 00000000000..0caef3adaac --- /dev/null +++ b/guides/content/developer/upgrades/two-dot-two-to-two-dot-three.md @@ -0,0 +1,50 @@ +--- +title: Upgrading Spree from 2.2.x to 2.3.x +section: upgrades +--- + +## Overview + +This guide covers upgrading a 2.2.x Spree store, to a 2.3.x store. This +guide has been written from the perspective of a blank Spree 2.2.x store with +no extensions. + +If you have extensions that your store depends on, you will need to manually +verify that each of those extensions work within your 2.3.x store once this +upgrade is complete. Typically, extensions that are compatible with this +version of Spree will have a 2-3-stable branch. + +This is the first Spree release which supports Rails 4.1. + +## Upgrade Rails + +For this Spree release, you will need to upgrade your Rails version to at least 4.1.2. + +```ruby +gem 'rails', '~> 4.1.2' +``` + +## Upgrade Spree + +For best results, use the 2-3-stable branch from GitHub: + +```ruby +gem 'spree', :github => 'spree/spree', :branch => '2-3-stable'``` + +Run `bundle update spree`. + +## Copy and run migrations + +Copy over the migrations from Spree (and any other engine) and run them using +these commands: + + rake railties:install:migrations + rake db:migrate + +## Read the release notes + +For information about changes contained with this release, please read the [2.3.0 Release Notes](http://guides.spreecommerce.com/release_notes/spree_2_3_0.html). + +## Verify that everything is OK + +Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see. diff --git a/guides/content/feed.atom b/guides/content/feed.atom new file mode 100644 index 00000000000..d04f5aae553 --- /dev/null +++ b/guides/content/feed.atom @@ -0,0 +1,9 @@ +--- +title: GitHub API Changes +is_hidden: true +author_name: technoweenie +author_uri: https://github.com/technoweenie +layout: false +--- + +<%= atom_feed :limit => 30, :articles => api_changes %> diff --git a/guides/content/index.md b/guides/content/index.md new file mode 100644 index 00000000000..a96228177d3 --- /dev/null +++ b/guides/content/index.md @@ -0,0 +1,3 @@ +--- +title: "Spree Documentation" +--- diff --git a/guides/content/misc/robots.txt b/guides/content/misc/robots.txt new file mode 100644 index 00000000000..df8a95915a8 --- /dev/null +++ b/guides/content/misc/robots.txt @@ -0,0 +1,7 @@ +User-agent: * + +<% if ENV['EDGE_GUIDES'] == 'true' %> +Disallow: / +<% else %> +Disallow: /legacy +<% end %> \ No newline at end of file diff --git a/guides/content/release_notes/0_10_0.md b/guides/content/release_notes/0_10_0.md new file mode 100644 index 00000000000..24494bac807 --- /dev/null +++ b/guides/content/release_notes/0_10_0.md @@ -0,0 +1,443 @@ +--- +title: Spree 0.10.0 +section: version +--- + +# Upgrade Notes + +## General upgrade process + +### Back up your database and code + +Always advisable! + +### Perform the standard upgrade command +```bash +spree —update +``` + +### Remove obsolete initializers + +```bash +rm -rf config/initializers/compass.rb +``` + +h5. Remove defunct contents of public dirs + +```bash +rm -rf public/javascripts/ +rm -rf public/stylesheets/ +rm -rf public/images/``` + +### Take note of existing payment gateway settings + +The new payment gateway implementation will remove existing settings, +and these need to be added again using the new interface. + +### Run the migrations +```bash +rake db:migrate``` + +### Configure a payment method + +See the [additional +information](#improvementsto-payment-gateway-configuration) later in +the +release notes. + +### Deprecation Warnings + +The newer version of Rails used by Spree generates a lot of deprecation +warnings. You may see the following message in your console: + +```bash +DEPRECATION: require "activerecord" is deprecated and will be removed +in Rails 3. Use require "active_record" instead. +``` + +Remove all instances of `require 'activerecord'` from your Rakefiles + +#### API Changes + +##### Change to `taxonomies` variable + +`Taxonomies` used to be set in most shared views. Now, it is only set +after calling `get_taxonomies` (inherited from Spree's base +controller). + +### Spree Base Controller and Layouts + +`Spree::BaseController` inherits directly from `ActionController::Base`, +rather than from `ApplicationController` (which itself is now an empty +class to help interaction with other Rails apps). +If you used `app/views/layout/application.html.erb` in an extension. +e.g. + +```bash +Spree::BaseController.class_eval { layout 'application' } +``` + +… then you will need to rename it to `spree_application.html.erb` and +update the occurrences. + +### Adding admin tabs changed + +`extension_tabs` is no longer used, instead use theme "hooks":hooks.html. + +### Theme Support + +Spree now has basic support for theming. Themes in Spree are implemented as "extensions":extensions.html. All of the default views in Spree have now been abstracted into a default theme. You can override all or parts of the default theme in a new theme. Themes have their own generator and are created as follows: + +```bash +script/generate theme foo +``` + +You can read more about themes in the very extensive [customization guide](http://guides.spreecommerce.com/legacy/0-11-x/customization_overview.html) + +*** +Don't panic if you have already customized Spree views in your site extension for a previous release of Spree. These view customizations should continue to work as long as they are loaded after the default theme extension. +*** + +### Named Scopes and Product Groups + +In various applications, we need to create lists of products according to various criteria, e.g. all products in taxon X, or all products costing less than $20, or composite criteria like all products costing more than $100 that have brand Y. Spree provides several so-called named scopes, which provide filtering by various aspects of the basic data, some of which can be chained or composed together for more complex filters. + +Named scopes can also be combined. The following chain of scopes lists the products that are active and with a price between $18 and $20. + +```bash +Product.active.price_between(18,20) +``` + +Product groups allow for the defining and naming groups of products for various purposes in an application. For example, you can define a group called Ruby products which contains everything with Ruby in the product name, or another group called Ruby clothing which is the sub-list of the above limited to items in the Clothing taxon. Product group definitions can also take price ranges, product properties, and variant options into account. It is possible to add your own filters by defining new named scopes too. + +Please see the documentaiton for a more complete explanation of [named scopes and product groups](http://guides.spreecommerce.com/legacy/0-11-x/scopes_and_groups.html). + + +### Improvements to Payment Gateway Configuration + +This release contains significant improvements to how payment gateways are configured. Gateways are no longer supported by database migrations, this scheme has been replaced by Active Record models that extend `Gateway`. The configuration of gateways is now done through standard Spree `preference configuration`. The [documentation](http://guides.spreecommerce.com/legacy/0-11-x/payment_gateways.html) has also been updated and contains a more detailed explanation. + +One major improvement is that it is now possible to configure multiple gateways for each of your Rails environments. Its also possible to use the live production server in development mode when previously, you were required to run in test mode. One unfortunate side effect of this improvement is that your existing gateway configuration information will be lost and you will need to reconfigure your gateway in the admin interface. + +!!! +You should make a note of your gateway configuration setting before upgrading since you will need to reconfigure your gateway when you're done. +!!! + +This approach to implementing and configuring gateways is extremely flexible. It makes it trivial to implement a new gateway that is already supported by Active Merchant. There are other useful benefits to this approach that a developer may be interested in knowing. + +#### Support of Non Active Merchant Gateways + +This architecture allows Spree to support gateways that are not officially supported by Active Merchant. Many times a new gateway is donated by someone in the community but its languishing in the queue waiting for someone to test and accept the patch. You have the option of taking that code (or writing your own from scratch) and implementing it within Spree. Instead of delegating to an Active Merchant class, you can simply implement that functionality yourself. You could also include the new gateway code from an Active Merchant fork inside your implementation and delegate the standard authorize, capture, etc operations to it. + +#### Ability to "Patch" Active Merchant Gateways + +We've noticed that sometimes it takes a while for a crucial Active Merchant patch to be applied. That's certainly understandable, the [Shopify](http://shopify.com) guys have a business to run and its probably not a high priority for them to make sure that the latest obscure gateway patch is applied in a timely fashion. Fortunately, the Spree approach to wrapping these gateways provides you with a convenient option. + +Lets say there is a bug with the +authorize+ method. You could simply provide an implementation of the gateway that has the patched version of the `authorize` method and then delegates to the Active Merchant class for everything else (since that works just fine.) + +#### Additional Functionality Beyond Active Merchant + +Another benefit of the architecture is that it makes it possible for Spree to provide additional common functionality that was not envisioned by Active Merchant. Specifically, it is possible to provide an abstraction for storing credit card profiles to be used with recurring payments. There's a good reason for Active Merchant to not care about this functionality. Its designed for people who just want to drop a single gateway provider into their application. Most programmers don't need three different gateways at once. Spree is a specialized use case. Its providing multiple gateways for you to choose from and so its desirable to have a standard method for operations such as this. + +*** +Recurring payments are not yet supported in Spree although there are plans to provide this in the near future. +*** + +### Multi Step Checkout + +#### Checkout Steps + +Spree has returned to a multi step checkout process. The following checkout steps are defined by default. + +* Registration (Optional) +* Address Information +* Delivery Options (Shipping Method) +* Payment +* Confirm + +There is also a default progress "train" which shows the current step and allows you to jump back to a previous step by clicking on it. + +!!! +If you have a site running on a previous verison of Spree, your checkout process will likely need to be upgraded. The good news is the new approach is much easier to customize. +!!! + +The checkout process is highly customizable - in fact, this is the reasoning behind moving away from the single step checkout. There is far less code hidden in javascript and each checkout step has its own partial. See the [checkout documentation](http://guides.spreecommerce.com/legacy/0-11-x/checkout.html) for mor information on how to customize the checkout. + +#### Countries Available for Shipping and Billing + +The mechanism for determining the list of billing and shipping countries has changed. Prior to this release, there was no way to limit the billing countries and shipping countries were limited by the countries included in the shipping zones that were configured. The new approach is to simply use all countries defined in the database by default. + +The list can be limited to a specific set of countries by configuring the new `:checkout_zone` preference and setting its value to the name of a [zone](http://guides.spreecommerce.com/legacy/0-11-x/zones.html) containing the countries you wish to use. This should handle most cases where the list of billing and shipping countries are the same. You can always customize the code via extension if this does not suit your needs. + +#### State Machine + +The Checkout model now has its own [state machine](https://github.com/pluginaweek/state_machine). This allows for easier customization of the checkout process. It is now much simpler to add or remove a step to the default checkout process. Here's an example which avoids the address step in checkout. + +```bash +class SiteExtension < Spree::Extension + def activate + # customize the checkout state machine + Checkout.state_machines[:state] = StateMachine::Machine.new(Checkout, :initial => 'payment') do + after_transition :to => 'complete', :do => :complete_order + before_transition :to => 'complete', :do => :process_payment + event :next do + transition :to => 'complete', :from => 'payment' + end + end + + # bypass creation of address objects in the checkouts controller (prevent validation errors) + CheckoutsController.class_eval do + def object + return `object if `object + `object = parent_object.checkout + unless params and params[:coupon_code] + +`object.creditcard ||= Creditcard.new(:month => Date.today.month, :year => Date.today.year) + end + `object + end + end + end +end +``` + +#### Controller Hooks + +The*CheckoutController+ now provides its own "hook mechanism" (not to be +confused with theme hooks) which allow for the developer to perform +additional logic (or to change the default) logic that is applied during +the edit and/or update operation for a particular step. The +`Spree::Checkout::Hooks` module provides this additional functionality +and makes use of methods provided by the `resource_controller` gem. +See the [checkout documentation](http://guides.spreecommerce.com/legacy/0-11-x/checkout.html#controller-logic) for +further details and examples. + +## Checkout Partials + +The default theme now contains several partials located within +`vendor/extensions/theme_default/app/views/checkouts`. Each checkout +step automatically renders the `edit.html.erb` view along with a +corresponding partial based on the state associated with the current +step. For example, in the delivery step the `_delivery.html.erb` +partial is used. + +## Javascript + +Spree no longer requires javascript for checkout but the user experience +will be slightly more pleasing if they have javascript enabled in their +browser. Spree automatically includes the `checkout.js` file located in +the default theme. This file can be replaced in its entirety through use +of a site extension. + +## Payment Profiles + +The default checkout process in Spree assumes a gateway that allows for +some form of third party support for payment profiles. An example of +such a service would be [Authorize.net +CIM](http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/). +Such a service allows for a secure and PCI compliant means of storing +the users credit card information. This allows merchants to issue +refunds to the credit card or to make changes to an existing order +without having to leave Spree and use the gateway provider's website. +More importantly, it allows us to have a final "confirmation" step +before the order is processed since the number is stored securely on the +payment step and can still be used to perform the standard +authorization/capture via the secure token provided by the gateway. + +Spree provides a wrapper around the standard active merchant API in +order to provide a common abstraction for dealing with payment profiles. +All `Gateway` classes now have a `payment_profiles_supported?` method +which indicates whether or not payment profiles are supported. If you +are adding Spree support to a `Gateway` you should also implement the +`create_profile` method. The following is an example of the +implementation of `create_profile` used in the `AuthorizeNetCim` class: + +```ruby +# Create a new CIM customer profile ready to accept a payment +def create_profile(creditcard, gateway_options) + if creditcard.gateway_customer_profile_id.nil? + profile_hash = create_customer_profile(creditcard, +gateway_options) + creditcard.update_attributes(:gateway_customer_profile_id =\> +profile_hash[:customer_profile_id], :gateway_payment_profile_id +=\> profile_hash[:customer_payment_profile_id]) + end +end``` + +!!! +Most gateways do not yet support payment profiles but the +default checkout process of Spree assumes that you have selected a +gateway that supports this feature. This allows users to enter credit +card information during the checkout withou having to store it in the +database. Spree has never stored credit card information in the database +but prior to the use of profiles, the only safe way to handle this was +to post the credit card information in the final step. It should be +possible to customize the checkout so that the credit card information +is entered on the final step and then you can authorize the card before +Spree automatically discards the number while saving the `Creditcard` +object. +!!! + +# Seed and Sample Data in Extensions + +Seed data is data that is needed by the application in order for it to +work properly. Seed data is not the same as sample data. Instead of +loading this type of data in a migration it is handled through the +standard rails task through `rake db:seed`. The rake task will first +load the seed data in the spree core (ex. `db/default/countries.yml`.) +Spree will then load any fixtures found in the `db/default` directory of +your extensions. If you wish to perform a seeding function other than +simply loading fixtures, you can still do so in your extension's +`db/seeds.rb` file. + +Sample data is data that is convenient to have when testing your code. +Its loaded with the `rake db:sample` task. The core sample data is +loaded first, followed by any fixtures contained in the `db/sample` +directory of your extensions. + +If you have fixtures in your extension with the same filename as those +found in the core, they will be loaded instead of the core version. This +applies to both sample and seed fixtures. This allows for fine grained +control over the sample and seed data. For example, you can create your +own custom sample order data in your site extension instead of relying +on the version provided by Spree. + +!!! +You should remove all `db:bootstrap` tasks from your +extensions. The new bootstrap functionality in the core will +automatically load any fixtures found in `db/sample` of your extension. +Failing to remove this task from your extension will result in an +attempt to create the fixtures twice. +!!! + +# RESTful API + +The REST API is designed to give developers a convenient way to access +data contained within Spree. With a standard read/write interface to +store data, it is now very simple to write third party applications (ex. +iPhone) that can talk to Spree. The API currently only supports a +limited number of resources. +The list will be expanded soon to cover additional resources. Adding +more resources is simply a matter of making the time for testing and +investigating possible security implications. +See the [REST API section](http://guides.spreecommerce.com/legacy/0-11-x/rest.html) for full details. + +# Inventory + +Inventory modeling has been modified to improve performance. Spree now +uses a hybrid approach where on-hand inventory is stored as a count in +`Variant#on_hand`, but back-ordered, sold or shipped products are +stored as individual `InventoryUnits` so they can be tracked. + +This improves the performance of stores with large inventories. When the +`on_hand` count is increased using `Variant#on_hand=`, Spree will +first fill back-orders, converting them to `InventoryItems`, then place +the remaining new inventory as a count on the `Variant` model. A +migration is in place that will convert on-hand `InventoryItems` to a +simple count during upgrade. Due to an issue with the sample data, demo +stores cannot be upgraded in this fashion and should be re-bootstrapped. + +# Miscellaneous improvements + +## Sample Product Images in Extensions + +For some time now you've been able to write sample data fixtures in +extensions +that will get run when you load sample data with the `rake db:bootstrap` +task. + +Now you can also add sample product image files in your extensions in +the +extensions own `lib/tasks/sample/products` directory. These images will +be +copied to the `public/assets/products` directory when the sample data is +loaded. + +*** +Additional information on the release can be found in the +`CHANGELOG` file as well as the [official ticket +system](http://railsdog.lighthouseapp.com/projects/31096-spree/milestones/45833-10). +*** + +## Ruby 1.9 Support + +Spree is now 100% Ruby 1.9 compatible. There are a few workarounds +needed to achieve this and those are consolidated in a custom +initializer appropriately named `workarounds_for_ruby19`. + +## Sales Overview + +The default admin screen now shows a series of tables and graphs related +to recent sales activity. By consulting this screen you can now see the +following information + +- Best Selling Products +- Top Grossing Products +- Best Selling Taxons +- Information on the Last 5 Orders +- Biggest Spenders +- Out of Stock Products +- Order Count by Day + +## Extension Load Order + +It is now recommended to define the extension load order outside of the +`environment.rb` file. This makes it easier for you to use the standard +`environment.rb` file that comes with Spree and thus easier to upgrade. +To define the extension load order inside of an initializer you can use +the following line of code: + +```bash +SPREE_EXTENSIONS_LOAD_ORDER = [:theme_default, :all, :site]``` + +## SEO Improvements + +Products and taxons are now available by a single URL only. Prior to +this release both of these URL's returned the same result: + +- http://localhost:3000/products/ruby-on-rails-ringer-t-shirt/ +- http://localhost:3000/products/ruby-on-rails-ringer-t-shirt + +Now we are returning a `301` redirect for the version of the URL without +the trailing '/' character. Some SEO experts seem to feel that +inconsistent links and [links without a trailing slash can be +penalized](http://www.ragepank.com/articles/68/that-trailing-slash-does-matter/) +We've been asked by one of our clients to fix this. We're passing on the +SEO improvements to you! + +## Multiple Forms of Payment + +Spree now supports multiple forms of payment. This support is in the +early stages but the basic build blocks are now present so that it +should be quite easy to allow additional forms of payment. More +documentation and improvements in this area are coming. + +## Refunds and Credits + +Spree now has explicit support for refunds and credits. More details to +follow. + +# Known Issues + +The [ticket +system](http://railsdog.lighthouseapp.com/projects/31096-spree/) lists +all known +outstanding issues with the Spree core. Some issues have a release +target (*milestone*) +attached: this is an indication of how soon an issue will be tackled. + +!!! +There are some problems which we have traced to other projects. +We list a few significant ones here. +!!! + +## Ruby 1.9 and Sqlite3 + +This combination doesn't work with Rails 2.3.5: the `change_column` +calls make all fields into `NOT NULL`. +See [the related +ticket](http://railsdog.lighthouseapp.com/projects/31096-spree/tickets/1265-sqlite3sqlexception-adjustmentsposition-may-not-be-null) +for more info. + +Workaround: apply the Rails patch by hand, or use MySQL instead if you +want to try Ruby1.9 diff --git a/guides/content/release_notes/0_30_0.md b/guides/content/release_notes/0_30_0.md new file mode 100644 index 00000000000..2573400c6d9 --- /dev/null +++ b/guides/content/release_notes/0_30_0.md @@ -0,0 +1,221 @@ +--- +title: Spree 0.30.0 +section: version +--- + +# Summary + +Spree 0.30.0 is the first official release to support Rails 3.x. It has +been several months in the making but we're finally here. Unfortunately +we haven't had the time to write up detailed release notes and the +documentation is still a work in progress. We'll try to mention the +highlights here and we'll continue to update the documentation in the +coming weeks. + +*** +We're always looking for more help with the Spree documentation. +If you'd like to offer assistance please contact us on the spree-user +mailing list and we can give you commit access to the +[spree-guides](https://github.com/spree/spree-guides) documentation +project. +*** + +# Rails Engines + +Spree is now heavily reliant upon the concept of Rails Engines. This +represents a significant architectural shift from previous versions of +Spree. This will likely be the most time consuming upgrade of Spree +you'll ever have to make. The change is the result of a major change in +Rails itself so the difficulties are unavoidable. The good news is that +Rails has adopted many of the ideas used in Spree (Engines are now +equivalent to Spree Extensions and visa versa.) This means that there is +very little non-standard Rails behavior left in Spree. + +## No More Site Extension + +Previous versions of Spree required a [site +extension](http://spreecommerce.com/legacy/0-30-x/extensions.html#thesiteextension) +in order to customize the look and feel of the site. One major +improvement in Spree is that this is no longer necessary. All of the +content that normally goes in your site extension can now be moved to to +*Rails.root*. + +## Extensions are Now Gems + +Extensions are now installed as Rubygems. They are also no longer +deployed to *vendor/extensions*. You need to add the required extensions +to you *Gemfile*. There is a comprehensive [Extension Guide](/developer/extensions_tutorial) in the +online documentation which can assist you. + +As of the time of this release there are only a limited number of +extensions that are currently compatible with Spree 0.30.x. It is +suggested that you check the [Extension +Registry](http://spreecommerce.com/extensions) for more information on +which extensions are 0.30.x compatible. Check back often because the +Spree core team will be working on updating the more critical ones +immediately after the release. + +*** +Its relatively easy to convert an existing extension into a gem. +Its suggested you find a 0.30.x compatible extension and study the +source code for a better idea on how to do this. +*** + +# Improvements to Payments + +Payments have been significantly improved in this version of Spree. One +of the most important changes is the addition of a [state +machine](https://github.com/pluginaweek/state_machine) for payments. +Payments that are submitted to a payment gateway for processing are in +the "processing state." This will help to prevent additional attempts to +process the payment through customer refreshing, etc. Failed payments +are also recorded and given a "failed" state. + +We have abandoned the concept of payment transactions and now record +most of the information directly in the payment record. When in comes +time to calculate the payment total, only payments in the "completed" +state are counted. + +# Simplification of Adjustments + +Adjustments have also been dramatically simplified. Instead of having +the concept of *Charge* and *Credit* we just have the single +*Adjustment*. What used to be called a *Credit* is now just a negative +adjustment. Adjustments also now have a *mandatory* attribute. When this +attribute is *true* the adjustment is always shown when displaying the +order total, even if the value is zero. All non-mandatory adjustments +are removed from the order if their value is ever equal to zero. + +*** +Mandatory adjustments make it easy to show $0 for tax or shipping +when those cases apply. The thinking is we don't want customers to +wonder what the shipping cost because its not present - better to show a +$0 value explicitly. +*** + +# New Promotion Functionality + +Promotion functionality in Spree has been greatly improved. There is a +new *spree_promo* gem which is included by default when you install +Spree. + +## Creating a Promotion + +A new promotion requires a *name* and *code* attribute. The *code* +attribute can be used by customers when checking out to "activate" a +particular promotion. + +*** +This is standard "coupon code" functionality but you're not +required to have customers enter codes in order to utilize promotions. +*** + +## Promotion Rules + +Once a new promotion is created you can create one or more rules for the +promotion. You can require that all rules for the promotion be satisfied +or just one of the rules. + +Each of the rules is based on a Ruby class that extends *PromotionRule*. +There are four built in rule types for Spree but others can be added via +extension or directly through your Spree application code. + +- **Item Total:** Limit to orders with an item total above a specified + amount +- **Product:** Limit to orders containing one or all of the specified + products +- **User:** Limit to orders made by specific users +- **First Order:** Limit to the first order by a user + +# No More "Vendor Mode" + +Spree is deployed as a Rubygem now so the previous system of different +"boot modes" has been simplified. Spree never needs to be deployed +inside of your application, even if you're using edge or a custom fork. +Thanks to Bundler you can reference any version of Spree source directly +via *Gemfile* and either a physical directory location or a git +repository location. + +*** +See the [Source Guide](http://guides.spreecommerce.com/legacy/0-30-x/source_code.html) for a complete +understanding of all the changes to the organization of the source code. +*** + +# Upgrading + +## Before You Upgrade + +### Upgrade to the Previous Version + +It is recommended that you upgrade to Spree 0.11.x (the previous latest +stable version) first. The upgrade process should go much smoother if +you upgrade incrementally. + +### Backup Your Database + +It is always recommended that you backup your database before upgrading. +You should also test the upgrade process locally and/or on a staging +server before attempting on your live production system. + +!!! +The Spree 0.30.0 upgrade will delete any in progress orders +which should generally considered to be a safe thing to do since these +are typically just abandoned orders. There are also non trivial changes +to payments and other tables. Hang on to your database backup until +you're sure the upgrade has gone smoothly. +!!! + +## Create a New Rails Application + +It is suggested that you create a brand new Rails 3.x application and +then make the necessary changes to that application. We'll briefly walk +you through the steps to do this. + +!!! +There have been major changes to how Rails applications (and +consequently Spree) are configured and initialized. You will have an +easier time if you start with a new Rails application and migrate your +stuff over to it rather than trying to make changes to an existing Spree +application so that its Rails 3 compliant. +!!! + +### Copy Your Legacy Files + +Spree no longer requires that you have a "site" extension. This means +that you should copy all of the files in *vendor/extensions/site* into +the *app* directory of your new Rails application. This includes the +contents of the *public* directory. + +### Add Spree to the *Gemfile* + +So now you have a new Rails 3.x application and you've moved over your +custom files. Its time to add the Spree gem into the mix. Edit your +*Gemfile* and add the following entry: + +```ruby +gem 'spree', '0.30.0' +``` + +Then install the Spree gem using the familiar Bundler approach: + +```bash +$ bundle install +``` + +## Upgrade Migrations and Assets + +The gems that comprise Spree contain various public assets (images, +stylesheets, etc.) as well as database migrations that are needed in +order to run Spree. There is a Rake tasks designed to copy over the +necessary files. + +```bash +$ bundle exec rake spree:install +``` + +Once the migrations are copied over you can migrate using the familiar +Rake task for this. + +```bash +$ bundle exec rake db:migrate +``` diff --git a/guides/content/release_notes/0_40_0.md b/guides/content/release_notes/0_40_0.md new file mode 100644 index 00000000000..ea5d62bf014 --- /dev/null +++ b/guides/content/release_notes/0_40_0.md @@ -0,0 +1,185 @@ +--- +title: Spree 0.40.0 +section: version +--- + +# Summary + +Spree 0.40.0 represents another step forward towards the eventual 1.0.0 +release. This version focuses heavily on authentication and +authorization. Most sites running 0.30.x will be able to upgrade with +very little difficulty. We're still working on identifying all of the +Spree extensions that run 0.40.x but its a fairly safe bet that any +extension running 0.30.x will work with this release. + +*** +We're always looking for more help with the Spree documentation. +If you'd like to offer assistance please contact us on the spree-user +mailing list and we can give you commit access to the documentation +project. +*** + +# Database Migrations + +There are several new database changes in the 0.40.0 release. You will +need to update your database migrations as follows: + +```bash +$ bundle exec rake spree:install:migrations +$ bundle exec rake db:migrate``` + +!!! +Always be sure to perform a complete database backup before +performing any operations. It is also suggested that you examine the new +migrations closely before running them so you are aware of what changes +are being made to your database. +!!! + +# Replacing Authlogic with Devise + +Spree has replaced the authlogic gem in favor of +[Devise](https://github.com/plataformatec/devise) for all authentication +methods. In part an effort to both simplify customization strategies, +due to devise's modular nature, and allow for much simpler creation of +extensions needing different authentication schemas and scopes. We have +made every effort to maintain the backward compatibility required to +upgrade an existing site. This means that naming conventions, and +routing have all remained intact where possible as well as the use of +deprecation notices. + +The database changes, described below, are made to offer as much +flexibility as Devise itself offers and enabling you to implement, +adjust the behavior, or remove features fairly effortlessly for those +familiar with Devise. + +## Miscellaneous Clean Up + +Some of the biggest changes to the Spree authentication process is +consolidation of most all the user management functions into the +*spree_auth* gem. Prior versions of Spree still had small bits of code +floating around in *spree_core* etc. So some files have simply been +moved to where they should have been all along. + +## Upgrading Existing Sites + +We have tried to minimize changes and when possible, naming conventions +have be maintained. The result is that only one controller has been +moved and renamed. Routing and the named route conventions having been +maintained as well. + +*** +The file *core/password_resets_controller.rb* has been renamed +and moved to *auth/user_password_resets_controller.rb* +*** + +We have already set up devise to handle the existing encryption scheme +that authlogic used so there is no need to make any changes and the +current users will work "out of the box". + +# Changes to the REST API + +Spree 0.40.x introduces several minor but important changes to the REST +API. If you are currently relying on the API you should be aware of a +few important changes. Please also consult the detailed [REST +Guide](http://guides.spreecommerce.com/legacy/0-40-x//rest.html) for more details. + +## New Authentication Mechanism + +The most significant change to the REST API is related to +authentication. The recent adoption of +[Devise](https://github.com/plataformatec/devise) for authentication in +general has resulted in new opportunities to improve authentication for +the API specifically. + +Prior to Spree 0.40.x the old method of authentication was to pass an +authentication token in the header. This involved using the specially +designated *X-SpreeAPIKey* header and passing a corresponding token +value. The new approach is to use standard *HTTP_AUTHORIZATION* which +is already nicely implemented by Devise. + +If you were using curl you could achieve this authentication as follows: + +```bash +curl ~~u V8WPYgRdSZN1mSQG17sK:x \ +http://example.com/api/orders.json``` + +Note that we are using the token as the "user name" and passing "x" as a +password here. There is nothing special about "x", its just a +placeholder since many HTTP Basic Authentication implementations require +a password to be submitted. In our case the token is sufficient so we +use a placeholder for the password. + +h4. Support for *.json* Suffix + +It is now recommended that you consider using a *.json* suffix in your +URL when communicating via the REST API. This is technically not a new +feature~~ it was always possible in older versions of the REST API. +We've updated the documentation to suggest this simpler approach (which +avoids the necessity of passing *Accept:application/json* in the +header.) + +```bash +curl -u V8WPYgRdSZN1mSQG17sK:x http://example.com/api/orders.json``` + +# Tokenized Permissions + +There are situations where it may be desirable to restrict access to a +particular resource without requiring a user to authenticate in order to +have that access. Spree allows so-called "guest checkouts" where users +just supply an email address and they're not required to create an +account. In these cases you still want to restrict access to that order +so only the original customer can see it. The solution is to use a +"tokenized" URL. + +```bash +http://example.com/orders/token/aidik313dsfs49d +``` + +Spree provides a *TokenizedPermission* model used to grant access to +various resources through a secure token. This model works in +conjunction with the *Spree::TokenResource* module which can be used to +add tokenized access functionality to any Spree resource. + +The *Order* model is one such model in Spree where this interface is +already in use. The following code snippet shows how to add this +functionality through the use of the *token_resource* declaration: + +```ruby +Order.class_eval do + token_resource +end +``` + +If we examine the default CanCan permissions for *Order* we can see how +tokens can be used to grant access in cases where the user is not +authenticated. + +```ruby +can :read, Order do |order, token| + order.user user || order.token && token order.token +end +can :update, Order do |order, token| + order.user user || order.token && token order.token +end +can :create, Order +``` + +This configuration states that in order to read or update an order, you +must be either authenticated as the correct user, or supply the correct +authorizing token. + +The final step is to ensure that the token is passed to CanCan when the +authorization is performed. Most controllers will do this automatically +if they declare *resource_controller*. + +```ruby +authorize! action, resource, session[:access_token] +``` + +*** +Since *OrdersController* does not implement *resource_controller* +this is done explicitly in the controller +*** + +For more information on tokenized permissions please read the detailed +[security guide](http://guides.spreecommerce.com/legacy/0-40-x/security.html#tokenized-permissions). diff --git a/guides/content/release_notes/0_50_0.md b/guides/content/release_notes/0_50_0.md new file mode 100644 index 00000000000..0a089be1a63 --- /dev/null +++ b/guides/content/release_notes/0_50_0.md @@ -0,0 +1,74 @@ +--- +title: Spree 0.50.0 +section: version +--- + +# Summary + +Spree 0.50.0 represents a minor update to the 0.40.x release. Several +important bugs in the 0.40.x release have been addressed. There are no +crucial security fixes in this release but you are still encouraged to +upgrade as soon as convenient. By making these small upgrades as they +are released you will only need to focus on minor changes to each point +release instead of a series of important changes covering several +releases. + +This is a another step forward towards the eventual 1.0.0 release. We're +still working on identifying all of the Spree extensions that run 0.50.x +but its a fairly safe bet that any extension running 0.40.x will work +with this release. Look for some major improvements to how extensions +are certified against Spree versions in the very near future. + +INFO: We're always looking for more help with the Spree documentation. +If you'd like to offer assistance please contact us on the spree-user +mailing list and we can give you commit access to the +[spree-guides](https://github.com/spree/spree-guides) documentation +project. + +# Database Migrations + +There is only one minor database changes in the 0.50.0 release. You will +need to update your database migrations as follows: + +```bash +$ bundle exec rake spree:install:migrations +$ bundle exec rake db:migrate``` + +!!! +Always be sure to perform a complete database backup before +performing any operations. It is also suggested that you examine the new +migrations closely before running them so you are aware of what changes +are being made to your database. +!!! + +# Significant Improvements in Test Coverage + +We have made drastic improvements to the level of test coverage. In +particular, there are tons of new Cucumber features that perform +automated testing in the browser for some of the most important +features. This doesn't impact store owners in any way, but better test +coverage means its safer to change the Spree code without breaking +things so its a step towards more stability. + +*** +See the Testing Guide for more details on testing in Spree. +*** + +# Replace search_logic gem with meta_search + +We have also replaced the +[search_logic](https://github.com/binarylogic/searchlogic) gem with +[meta_search](https://github.com/ernie/meta_search). This is another +one of those behind the scenes changes that you're not likely to notice. +Our reason for making the switch was that search_logic is not supported +for Rails3 and it took us several days to get it working for Spree +0.30.x. We're not anxious to support it any longer and this is part of +our ongoing effort to get to stable a stable release of Spree that is +easier to support. + +!!! +If you are using one of the search related extensions for Spree +you may experience compatibility issues. Please report any troubles you +have on the spree-user list and we'll see if we can help. Eventually +we'll revisit these extensions and verify their compatibility. +!!! \ No newline at end of file diff --git a/guides/content/release_notes/0_60_0.md b/guides/content/release_notes/0_60_0.md new file mode 100644 index 00000000000..788af88202e --- /dev/null +++ b/guides/content/release_notes/0_60_0.md @@ -0,0 +1,152 @@ +--- +title: Spree 0.60.0 +section: version +--- + +# Summary + +The 0.60.0 release contains a significant change to all controllers +within Spree as they have been refactored to remove the use of the +[resource_controller](https://github.com/jamesgolick/resource_controller) +gem. This release may have an impact on existing extensions or site +customizations that leverage some of resource_controllers features, so +it's important to read the details on it's removal below and review any +code which might be affected. + +While the removal of resource_controller is not most glamous or +exciting change, it's another significant stepping stone to our 1.0 +release. + +*** +We're always looking for more help with the Spree documentation. +If you'd like to offer assistance please contact us on the spree-user +mailing list and we can give you commit access to the +documentation project. +*** + +# Database Migrations + +There are no database migrations to worry about in this release +(assuming you are already running Spree 0.50.x). + +!!! +Always be sure to perform a complete database backup before +performing any operations. +!!! + +# Removal of resource_controller + +The resource_controller gem has been central to Spree's controllers +from one it's earliest releases and was responsible for some of Spree's +customizability features. It's removal has been discussed (endlessly) +and worked on for quite some time. It's original lack of Rails 3.0 +support delayed Spree's 0.30.0 release for sometime while we forked and +updated the gem to support Rails 3. However it was still felt that the +library was too overbearing for Spree's needs and added unneeded +complexity to it's controllers. + +Some earlier Spree releases removed its usage from the more complex +front-end controllers (like OrdersController and CheckoutController) and +this release extends this to all front-end controllers. + +## Supported Functionality + +The majority of backend (Admin) controllers now base off of +*Admin::ResourceController* class which provides a much simpler and +slimmed down version of resource_controllers existing features, while +attempting to maintain backwards compatibility for the majority commonly +used extension points. + +*Admin::ResourceController* provides several resource_controller style +features as follows. + +### Standard CRUD Methods + +There are basic implementations of all the standard CRUD methods: + +- *:index* +- *:show* +- *:create* +- *:read* +- *:update* +- *:destroy* + +### In Action Callbacks + +- *:update.before* +- *:create.before* +- *:update.after* +- etc. + +### URL Helpers + +- *new_object_url* +- *edit_object_url* +- *object_url* +- *collection_url* + +!!! +Use of these generic helpers is discouraged in favor of the +default Rails helper urls. +!!! + +### Instance Variables + +- *`object+ + * +`collection* + +!!! +Use of these variables is strongly discouraged. Use the actual +Rails standard variable names instead. +!!! + +## Unsupported Functionality + +*Admin::ResourceController* does **NOT** provide the following +resource_controller features: + +### Custom Responses + +- *create.wants.js* +- *update.wants.html* + +These have been replaced with a new method called *respond_override* + +*** +See the Customization guide for more details +on *respond_override* +*** + +### Deprecated Methods + +These methods are deprecated and should either be removed or replaced +with a custom *load_resource* method (see *Admin::ResourceController* +source for details). + +- *object* +- *collection methods* + +The following method for custom flash messages has been removed entirely +(with no replacement approach.) + +- *create.flash* + +Admin::ResourceController's use is encouraged in extensions and/or site +customizations that require basic CRUD admin controllers. It should not +be used on front-end or complex admin controllers, as you can see from +the current source even certain core admin controllers do not use it +(for example Admin::OrdersController). + +*** +While the use of resource_controller has been completely removed +from all of Spree's core controllers, the dependency on +resource_controller gem will remain for a while to allow extension +authors to update their projects. +*** + +# Miscellaneous Changes + +There are also a series of minor bug fixes and improvments. Please see +the [Github +compare](https://github.com/spree/spree/compare/v0.50.2...v0.60.0) for +more details. diff --git a/guides/content/release_notes/0_70_0.md b/guides/content/release_notes/0_70_0.md new file mode 100644 index 00000000000..619224d7075 --- /dev/null +++ b/guides/content/release_notes/0_70_0.md @@ -0,0 +1,449 @@ +--- +title: Spree 0.70.0 +section: version +--- + +# Summary + +This 0.70.0 release is the first Spree release to support Rails 3.1 and +contains several exciting new features including a complete overhaul of +the theming system and major improvements to the Promotions system. + +*** +We're always looking for more help with the Spree documentation. +If you'd like to offer assistance please contact us on the spree-user +mailing list and we can give you commit access to the documentation +project. +*** + +# Theming Improvments + +Theming support in this release has change significantly and all the new +features are covered in detail in the following guides: + +- [Customization Overview](http://guides.spreecommerce.com/legacy/0-70-x/customization.html) - provides a high level + introduction to all the customization and theming options now + provided by Spree. +- [View Customization](http://guides.spreecommerce.com/legacy/0-70-x/view_customization.html) - introduces and + explains how to use Deface to customize Spree's views. +- [Asset Customization](http://guides.spreecommerce.com/legacy/0-70-x/asset_customization.html) - explains how Spree + uses Rails' new asset pipeline, and how you can use it to customize + the stylesheets, javascripts and images provided by Spree. + +*** +While the upgrade instructions that follow briefly cover the new +asset pipeline features we suggest you review the guides above as part +of your upgrade preparation. +*** + +## Themes as engines + +In previous versions (0.11.x and earlier) Spree encouraged the approach +of bundling themes in their own extensions, with the advent of the asset +pipeline in Rails 3.1 and the upcoming Theme Editor we're bringing back +this approach as a suitable model for distributing and sharing themes. + +While the distinction between themes and extensions is covered in the +[Extensions & Themes](http://guides.spreecommerce.com/legacy/0-70-x/extensions.html) guide they are both just Rails +engines and can be treated as such. + +We've created two front-end themes to help show this new approach in +action: + +- [Spree Blue](https://github.com/spree/spree_blue_theme) - Recreates + the original "blue" theme of 0.60.x as a stand alone theme. + +- [Rails Dog Radio](https://github.com/spree/spree_rdr_theme) - This + recreates some of the aspects of the Rails Dog Radio demo + application for a default Spree application. + +Both themes can be installed by just adding a reference to the git +repository to your Gemfile, ie: + +```ruby +gem 'spree_blue_theme', :git => 'git://github.com/spree/spree_blue_theme.git' +``` + +# Experimental SCSS/SASS Theme + +LESS proves to be unpopular amongst Spree developers so it is decided to +retire LESS stylesheets of `spree_blue_theme` in favor for all-in-one +`screen.css`. + +Yet with the recently adopted SCSS/SASS in Rails 3.1, we believe this +technology could become de-facto standard for CSS someday. Thus, we +create the experimental SCSS/SASS version of +[spree_blue_sass_theme]("https://github.com/spree/spree_blue_sass_theme") +to collect feedbacks before we could come up with a decision to opt for +SCSS/SASS in future version of Spree. + +# The Asset Pipeline + +One of the more interesting new features included in Rails 3.1 is the +asset pipeline, and a lot of the customization improvements in Spree are +enabled by this feature. While the asset pipeline provides excellent +flexibility and improved organization for all of Spree's assets (images, +javascripts and stylesheets) it does add a significant overhead as all +asset requests are now being handled by Rails itself (and not being +handed off to the web server). + +This can be especially noticeable in development mode when using a +single process application server like Webrick. This release of Spree +includes a small tweak to the standard pre-compiling rake task that +allows pre-compiling of assets for development mode. + +## Pre-compiling in production + +Rails supports pre-compiling of assets which is intended to offload the +overhead of generating and serving assets from the application server in +production environments. + +Pre-compiling is not required for the asset pipeline to function +correctly in production, if you choose to not pre-compile Rails will +generate each asset only once and serve each subsequent request using +Rack::Cache. + +Rack::Cache is generally sufficient for lower traffic sites, but +pre-compiling will provide some additional speed increases by allowing +the web server to serve all assets, including gzipped versions of +javascript and css files. + +To pre-compile assets for production you would normally execute the +following rake task (on the production server). + +```bash +$ bundle exec rake assets:precompile +``` + +This would write all the assets to the `public/assets` directory while +including an MD5 fingerprint in the filename for added caching benefits. + +*** +In production all references to assets from views using +image_tag, asset_path, javascript_include_tag, etc. will +automatically include this fingerprint in the file name so the correct +version will be served. +*** + +## Pre-compiling for development + +Spree alters the behaviour of the precompile rake task as follows: + +```bash +$ bundle exec rake assets:precompile:nondigest``` + +It will still output the assets to `public/assets` but it will not +include the MD5 fingerprint in the filename, hence the files will be +served in development directly by the web server (and not processed by +Rails). + +!!! +Using the precompile rake task in development will prevent any +changes to asset files from being automatically included in when you +reload the page. You must re-run the precompile task for changes to +become available. +!!! + +Rail's also provides the following rake task that will delete the entire +`public/assets` directory, this can be helpful to clear out development +assets before committing. + +```bash +$ bundle exec rake assets:clean``` + +It might also be worthwhile to include the `public/assets` directory in +your `.gitignore` file. + +# Promotions + +Promotions have been extended well beyond their coupon roots to include +new `activator` support, which can now fire or activate a promotion for +a variety of different user triggered events, including: + +- Adding items to a cart +- Visiting specific pages +- Adjusting cart contents +- Using a coupon code + +Promotions also includes a new `Actions` feature which can perform an +action as a result of a promotion being actived, these actions currently +include creating an adjustment or adding additional items to a cart. + +For more details on these new promotions feature please refer to the +[Promotions](http://guides.spreecommerce.com/legacy/0-70-x/promotions.html) guide. + +# New Extension Generator + +There is a new extension generator available as part of this release. +The generator is utilized by a new executable contained within the gem. + +You can generate new extensions inside an existing rails application or +as a standalone git repository using the following command + +```bash +$ spree extension foofah +``` + +One of the most important advances in this new generator is that you can +now easily run specs for extensions in their own standalone repository. +You just need to create a test application (one time only) as a context +before running your specs. + +```bash +$ bundle exec rake test_app +$ bundle exec rspec spec +``` + +You can get more information on the extension generator in the [Creating +Extensions](http://guides.spreecommerce.com/legacy/0-70-x/creating_extensions.html) guide. + +*** +You must install the spree gem in order to use the new extension +generator. +*** + +# Upgrade Instructions + +These instructions are mainly concerned with upgrading Spree +applications, extension developers should jump straight to the "Asset +Customization"asset_customization.html and [View +Customization](http://guides.spreecommerce.com/legacy/0-70-x/view_customization.html) guides for details on the +changes that are required for extensions to fulling support 0.70.0. + +## Before you begin + +To prevent problems later please ensure that you've copied all the +migrations from your current version of Spree and all the extensions +that you are running. While this is normal practice when setting up +Spree and extensions any missing migrations will cause issues later in +the upgrade process. + +You can check that all Spree migrations are copied by running: + +```bash +$ bundle exec rake spree:install:migrations +``` + +Each extension will provide it's own rake task (or generator) for +copying migrations so please refer to the extensions README for +instructions. + +!!! +It's vital that you confirm this first before starting the +upgrade process as Rails 3.1 has altered how engine migrations are +handled and incorrectly copying migrations later could result in data +loss. +!!! + +## Changes required for Rails 3.1 + +Most of the changes required to upgrade a Spree application to 0.70.0 is +same for any Rails 3.0.x application upgrading to 3.1. + +### Gemfile changes + +You'll need to make several additions and changes to your Gemfile: + +- Update spree to 0.70.0 +- Update rails to 3.1.1 +- Update mysql2 to 0.3.6 - if your using it. +- Ensure the assets group is present: + +```ruby +group :assets do + gem 'sass-rails', "~> 3.1.1" + gem 'coffee-rails', "~> 3.1.1" + gem 'uglifier' +end +``` + +### Update config/boot.rb + +Rails 3.1 simplifies the `config/boot.rb` file significantly, so update +yours to match: + +```ruby +require 'rubygems' + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', *FILE*) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +``` + +### Enable the asset pipeline + +Add the following line to `config/application.rb`, this will enable the +Asset Pipeline feature (required by 0.70.0): + +```ruby +config.assets.enabled = true +``` + +### Remove deprecated configuration + +Remove the following line from `config/environments/development.rb`. + +```ruby +config.action_view.debug_rjs = true +``` + +### Retire lib/spree_site.rb + +The spree_site.rb file is no longer required so it's important to +migrate any existing code out of this file and into the correct +location. + +- Model, controller or helper evals should be relocated to a suitable + _decorator.rb file. +- Configuration settings should be moved to initializers. +- If you are activating custom Abilities or Calculators, you should + remove then for now. You can re-add them to config/application.rb + after you've ran the spree:site generator below. They will need to + be inside the `config.to_prepare` block. + +*** +The decorator initialization block will be automatically included +in config/application.rb for you, when you run the spree:site generator +below. +*** + +### Remove spree_site from config/application.rb + +Also ensure the following line is **not** present in +config/application.rb: + +```ruby +require 'spree_site' +``` + +## Update gems & generate Spree files + +Once you've completed all the Rails 3.1 steps above, you can now update +your dependencies and start generating the new asset pipeline +placeholders. + +### Install dependencies + +Install all the required gems, and lock your dependencies by running: + +```bash + $ bundle update``` + +### Generate & copy required files + +Running the `spree:site` generator will create the skeleton structure +for the asset pipeline, and also copy missing migrations. + +```bash + $ rails g spree:site``` + +After running the generator above, it's best to check to make sure you +do not have multiple copies of the following line, in +`config/application.rb`: + +```ruby +config.middleware.use "RedirectLegacyProductUrl" +config.middleware.use "SeoAssist" +``` + +### Update your database + +If you encounter any issues with this step please ensure you've +completed the [Before you +begin](#before-you-begin) section above. + +!!! +Make sure you've taken a backup of the database before +attempting this step. +!!! + +```bash +$ bundle exec rake db:migrate``` + +## Migrating your assets + +Cleaning up your `/public` directory is one major advantage of using +Rails 3.1. The main task required here is to separate your application +specific assets (javascript, stylesheets and images) from all of Spree's +(and all those belonging to all the extensions installed) which have +been mingled together in your public directory. + +### Asset Organization + +Assets should be placed inside your application in one of three +locations: `app/assets`, `lib/assets` or `vendor/assets`. + +`app/assets` is for assets that are owned by the application, such as +custom images, JavaScript files or stylesheets. + +`lib/assets` is for your own libraries' code that doesn't really fit +into the scope of the application or those libraries which are shared +across applications. + +`vendor/assets` is for assets that are owned by outside entities, such +as code for JavaScript plugins. + +*** +If you are using a library like Jammit solely for concatenating +and minifying your javascript and stylesheet files then we suggest you +remove it from your application as part of the Rails 3.1 upgrade. +*** + +For a full explanation of how Spree uses Rails' asset pipeline and how +to update your site to use these new features please refer to the [Asset +Customization](http://guides.spreecommerce.com/legacy/0-70-x/asset_customization.html) guide. + +### Cleaning up /public + +Once you've relocated all your applications assets, the only +remaining directories and files in `/public` should be images that +you've uploaded for your Products (or taxonomies). If you are using the +default Spree configuration these will be in `/public/assets`. + +!!! +Leaving any javascript, stylesheet or image files in the public +directory will override those provided by Spree or it's extensions. So +it's vital that you delete all remaining files from the `/public` +directory, EXCEPT YOUR PRODUCT IMAGES!. +!!! + +*** +If you are using S3 (or another cloud storage system) for your +Product images then your `/public` directory should be completly empty. +*** + +## Including old style theming hooks + +With the introduction of Deface the old style theming hooks have been +deprecated, the old hooks will continue to function after the upgrade as +they are automatically upgraded to Deface overrides, we strongly suggest +you upgrade them as soon as possible. + +To include your previously defined old style theming hooks from +`lib/site_hooks.rb` add the following to the bottom of +`config/application.rb` + +```ruby +require 'lib/site_hooks' +``` + +*** +The development log will include suggested replacement Deface +overrides anytime Rails encounters a old style hook call. These +suggestions are a best effort replacement, but might need some tweaks as +some elements have been moved while removing the old hook calls from +Spree's view files. +*** + +For more information about using Deface overrides, please refer to the +[View Customization](http://guides.spreecommerce.com/legacy/0-70-x/view_customization.html) guide. + +# New way to register Calculators + +Calculators no longer can be registered with *register* method. The +register method is refactored to take advantage of default Rails +application configuration *Rails.application.config.spree.calculators*. + +For more information about this new change, please refer to the +[Ajustments](http://guides.spreecommerce.com/legacy/0-70-x/adjustments.html) guide. diff --git a/guides/content/release_notes/0_9_0.md b/guides/content/release_notes/0_9_0.md new file mode 100644 index 00000000000..1f8b1e00b50 --- /dev/null +++ b/guides/content/release_notes/0_9_0.md @@ -0,0 +1,487 @@ +--- +title: Spree 0.9.0 +section: version +--- + +!!! +Some of the information here has been made redundant by later +changes. +!!! + +# Improved Layout Customization + +Work has been done to reduce the likelihood of new projects needing to +override the default Spree layout template *application.html.erb*. The +title, stylesheets, and logo now can all be customized without creating +your own copy of the layout. + +## New title methods + +There are some new methods for manipulating the page title: the *title* +and *set_title* helper methods in Spree::BaseController. + +Use *set_title* to set a page title either from a controller method, or +a view template. You can also override the *default_title* and *title* +methods in Spree::BaseController for further control. + +The *title* method is used in *application.html.erb* of the new release, +however if you are upgrading and want to take advantage, use this in +between your *<title>* tags of your layout template + + +And to set the title in a view template: + +## Customize default stylesheets + +*Spree::Config+ is a new config option for customizing the stylesheets +used by the default application layout. The value +of*Spree::Config[:stylesheets]+ is a comma-separated string of +stylesheet names without the file extensions. See the [customization +guide](http://guides.spreecommerce.com/legacy/0-11-x/customization_overview.html) for more information. + +If you are upgrading, to take advantage of this use the new +*stylesheet_tags* helper method. + +## Customize logo + +*Spree::Config+ is a new config option for customizing the logo image +path. + +If you are upgrading, take advantage of this by using the new*logo+ +helper method. + +# Polymorphic Calculators + +There has been significant refactoring to the implementation of +calculators. Calculators are now polymorphic and belong to *calculable*. +This will have a non trivial impact on your existing store +configuration. After upgrading to Spree 0.9.0 you are likely going to +have to make several manual adjustments to the existing tax and shipping +configurations. Ultimately we feel this is outweighed by the superior +design of the new calculator system which will allow for a more modular +design. + +!!! +Many of the existing calculator extensions are not yet updated +to support Spree 0.9.0. Please check the [extension +registry](http://spreecommerce.com/extensions) to see which versions are +supported. Our goal is to back port most of the useful calculators out +there shortly after the release. +!!! + +All calculators need to implement the following method + +```bash + def compute(something=nil) + … + end +``` + +The calculator is passed an optional "target" on which to base their +calculation. This method is expected to return a single numeric value +when the calculation is complete. A value of *nil* should be returned in +the event that a charge is not applicable. + +Since calculators are now instances of *ActiveRecord::Base* they can be +configured with preferences. Each instance of *ShippingMethod* is now +stored in the database along with the configured values for its +preferences. This allows the same calculator (ex. +*Calculator::FlatRate*) to be used with multiple *ShippingMethods*, and +yet each can be configured with different values (ex. different amounts +per calculator.) + +Calculators are configured using Spree's flexible [preference +system](http://guides.spreecommerce.com/legacy/0-11-x/preferences.html). Default values for the preferences are +configured through the class definition. For example, the flat rate +calculator class definition specifies an amount with a default value of +0. + +```bash + class Calculator::FlatRate < Calculator + preference :amount, :decimal, :default =\> 0 + … + end +``` + +Spree now contains a standard mechanism by which calculator preferences +can be edited. The screenshot below shows how the amounts for the flat +rate calculator are now editable directly in the admin interface. + +Calculators are now stored in a special *calculator* directory located +within *app/models*. There are several calculators included that meet +many of the standard store owner needs. Developers are encouraged to +write their own [extensions](http://guides.spreecommerce.com/legacy/0-11-x/extensions.html) to supply additional +functionality or to consider using a [third party +extension](http://spreecommerce.com/extensions) written by members of the Spree +community. + +Calculators need to be "registered" with Spree in order to be made +available in the admin interface for various configuration options. The +recommended approach for doing this is via an extension. Custom +calculators will typically be written as extensions so you need to add +some registration logic to the extension containing the calculator. This +will allow the calculator to do a one time registration during the +standard extension activation process. + +The *CalculatorExtenion* that is included in the Spree core is a good +example of how you can achieve this in your own custom extensions. + +```bash + def activate + [ + Calculator::FlatPercent, + Calculator::FlatRate, + Calculator::FlexiRate, + Calculator::PerItem, + Calculator::SalesTax, + Calculator::Vat, + ].each(&:register) + end``` + +This calls the *register* method on the calculators that we intend to +register. Spree provides a mechanism for extension authors to specify +the operations for which the calculator is intended. For example, a flat +rate calculator might be useful for all operations but another +calculator may be appropriate only for coupons and not shipping or +taxes. + +Models that are declared with *has_calculator* maintains their own set +of registered calculators. Currently this includes *Coupons*, +*ShippingMethods*, *ShippingRates* and *TaxRates*. The following example +shows how to configure a calculator to make it available for use with +*Coupons*. + +```bash + def self.register + super + Coupon.register_calculator(self) + end``` + +*** +Spree automatically configures your calculators for you when using +the basic install and/or third party extensions. This discussion is +intended to help developers and others interested in understanding the +design behind calculators. +*** + +Once your calculators have been registered correctly by your extensions, +then they will become available as options in the appropriate admin +screens. + + +# Simplified Tax Configuration + +There are also minor changes to how taxes are configured. You no longer +need to specify sales tax or VAT but you do need to choose a calculator +type. Tax rates are configured as preferences for the calculator itself. + +!!! +Your tax rates will be lost when you run the migrations. You +will have to recreate them manually in the admin interface. +!!! + +# Unified Adjustment Model + +Spree 0.9.0 provides a new flexible system of adjustments associated +with orders. The *orders* table no longer has separate columns for +*tax_total*, *ship_total*, etc. This information is now captured more +generically as an *Adjustment*. This allows a Spree application to add +more then one tax or shipping charge per order as well as to support new +types of charges that might be required. For instance, some products for +sale (like cell phones) require a separate activation fee. + +Adjustments come in two basic flavors: *Charges* and *Credits*. From an +implementation perspective, they are both modeled in a single database +table called *adjustments* and use the single table inheritance +mechanism of Rails. Charges add to the order total, and credits work in +the opposite direction. + +Orders have one or more adjustments associated with them and each +adjustment also belongs to an adjustment source. This allows charges and +credits to recalculate themselves when requested. Adjustments are always +recalculated before they are saved which includes every time and order +is updated. This provides a very flexible system by which an adjustment +can determine that it is no longer relevant based on changes in the +order. + +Consider a coupon that takes $5 off all orders over $20. If the order +exceeds the required amount during checkout the coupon will create the +proper adjustment. If the customer then decides to edit their cart +(before completing checkout) then you will want to make sure that the +coupon still qualifies. If the order total drops below the required +amount the source of the adjustment (in this case the coupon) will have +the ability to remove the adjustment based on its own internal logic. + +!!! +There are significant changes to the database to support the +new adjustment system. The migrations should update your legacy data and +create the necessary tax and shipping adjustments for existing orders +but you should backup your database before running. +!!! + +# Coupons and Discounts + +Spree now supports a flexible coupon system. Coupons in an online store +are virtual and can be thought of as "codes" that must be entered during +the checkout process. Coupons serve two important functions. First, they +determine whether or not they are eligible to be used for the offer in +question. Second, they calculate the actual credit/discount that should +be applied to the specific order (assuming that the eligibility +requirement is satisfied.) + +## Eligibility + +Coupon eligibility is completely customizable on a per coupon basis. +Eligibility is determined by the following factors. + +- **Start Date** - coupons can be configured to be invalid before a + specific date +- **Expiration Date** - coupons can be configured so that they are not + usable passed a certain date +- **Expiration Date** - coupons can be configured so that they are not + usable passed a certain date +- **Number of Uses** - coupons can be restricted to an arbitrary + number of uses (typically a single use if there's a limit at all) +- **Combination** - there is an option to restrict specific coupons so + that they cannot be combined with other coupons in the same order. + +Any other restriction on eligibility is intended to be provided by +custom calculators. The *compute* method has access to the complete +order (including shipping and other related information) and can simply +return *nil* if the coupon is not to be applied in a specific situation + +*** +The next version of Spree will also provide built in filtering for +coupons based on product properties and taxon information. This will +provide a standard way to restrict coupons to certain types of products. +As a workaround, you can accomplish this by hard coding restrictions in +your calculator. +*** + +## Discount Calculation + +The *create_discount* method in *Coupon* is responsible for the actual +calculation of the credit to be applied for the cooupon. By default, +Spree will not allow the credit amount to exceed the item total. The +credit adjustment associated with a coupon is subject to recalculation +based on changes in the order. This is no different then any other +adjustment to the order total (such as shipping or tax charges.) + +# RESTful Checkout + +There have been several minor but crucial changes to the checkout +process. The primary motivation for these changes was to improve +maintenance of the checkout process and to simplify checkout +customization. + +## Checkout Module has been Replaced by Controller + +Prior to the refactoring, much of the checkout logic was contained in +*lib/checkout.rb*. The idea was to isolate this logic from the +*OrdersController* and to make it easier to extend. In this release we +have just taken this another step further and made the checkout its own +resource. + +## Changes to the Checkout Partials + +The views have been shuffled around a bit due to this refactoring. This +shouldn't affect you too much unless you have an existing Spree +application in which you customized some of the checkout partials. For +instance, *app/views/orders/_billing.html.erb* has been moved to +*app/views/checkouts/_billing.html.erb*. So you may need to +rename your custom partials if you have any. + +## Additional Details + +For more detailed information on the nature of these changes, please see +the [relevant +commit](https://github.com/spree/spree/commit/ce1aad7bc25c15a794f8f5689efcdbf8c3311b7b) +in Github. + +# Variant Images + +Some changes have been made to allow you to attach images to both the +Product model and each individual variant for a product. The Images +administration has been relocated from the main product details form to +it's own tab accessible via the right hand side bar on the product +details screen. + +This new admin interface enables you to select from a drop-down list +which object (product or variant) the image represents. Note if a +product does not contain any variants then the drop-down is not +displayed to ensure that basic implementations are not cluttered with +unnecessary administration options. + +The front-end product details interface has also been updated to filter +the displayed images depending on which variant is selected, and the +cart view now displays the image of the selected variant. + +# Improvements to image handling + +We've upgraded the paperclip gem to take advantage of recent changes. +Paperclip is the library which handles creation of and access to the +various formats of image. On top of this, we're explicitly catching +errors in the image creation stage and returning these via the validation +mechanism - also adding a more meaningful message in the *errors* list. This will avoid the silent +failures that some people have experienced when they don't have image +magick installed correctly. + +Another change is to store the original image's width and height: this +info is sometimes useful when working with a set of images with different 'shapes', e.g. where your +images might all have a width of 240 but (minor) variation on height. +Knowing the height of the original allows you to calculate the max +height of your images and thus to create a suitable bounding box. + +Finally, note that the processing tools behind paperclip can do many +transformations on the images, such as cropping, color adjustment, … - and these can be requested by +passing the options to paperclip, or you can run the conversions on a batch of images in +advance of loading into Spree. Automatic cropping is particularly useful to make best use of screen +area. + +# Update to SearchLogic + +Spree now runs with version 2.1.13 of SearchLogic. It has meant some +minor recoding of how searches are set up and paginated, and allowed some of the existing forms to be +simplified (by taking advantage of new functionality) and opened the door to more sophistication in +selecting products, e.g. for handling faceted search or product groups. + +There's an overview of what the new SearchLogic offers on the +[Spree +blog](http://spreecommerce.com/blog/2009/07/30/updating-searchlogic/), +and full documentation +is at [rdoc.info](http://rdoc.info/projects/binarylogic/searchlogic). + +# Some new named scopes for products + +To make it easier to construct sets of products for various uses, we've +added some more named scopes whichhelp with taxon, property, and availability of option values. The first +kind (*taxons_id_in_tree* and +*taxons_id_in_tree_any*) allows restriction to a set of taxons and +their combined descendents. The property +scope *with_property* takes a property object (or its id - the +definition uses Rails' automatic conversion) +and an optional argument for uniquifying the table names in complex +queries, eg where you are filtering by +two distinct properties. This scope does not take a property value: the +design is that you add further +condition(s) on the value in a subsequent scope. It will handle cases +where the property is absent or null +for a product. There is a simpler scope *with_property_value* for +simpler cases. +The option type scope (*with_option*, with its prerequisite +*with_variant_options*) follows the pattern +of option type object or id, and an optional table name, and is intended +as a basis for further conditions +on the value of that option type. +See *lib/product_scopes.rb* for the definitions, and see +*lib/product_filters.rb* for examples of +their use. + +# Basic support for filtering of results + +It is often useful to cut down the results in a taxon via certain +criteria, such as products in a price +range or with certain properties, and sometimes you want a set of +restrictions selectable via checkboxes etc. +Using ideas from SearchLogic version 2, Spree now contains a basic +framework for this kind of filtering. +You can some basic filtering by visiting */products?taxon=1000* (unless +you have overridden the products +controller), where it allows you to select zero or more of a taxon's +children and to select some price ranges and product brands. + +File *lib/product_filters.rb* explains the mechanism in detail, with +several concrete examples. Basically, +a filter definition associates a named scope with a mapping of human +readable labels to internal labels. +The named scope should be defined to test the relevant product +attribute(s), and to convert a set of these +internal labels into tests on the attributes. For example, you may want +to filter by price range, so +should set up labels for price ranges like 0-20, 20-50, 50-100, 100 or +more; then define a named scope +which maps these into a combined test on the (master) price attribute of +products. + +The partial *app/views/shared/_filters.html.erb* displays a checkbox +interface for the filters +returned by the method *applicable_filters* for the selected taxon. +This method allows you to control +which filters are used for some taxon, eg a filter on fabric type may be +required for clothing taxons, +but not suitable for mugs etc. + +To use this framework, you should override and extend +*lib/product_filters.rb* and define a suitable +*applicable_filters* method for taxons. +The new named scopes (above) are useful building blocks for adding +application-specific filters. + +# Miscellaneous improvements + +## Default ship and bill addresses + +Spree now saves the last used bill and ship addresses for logged in +users and uses these as the defaults +in their next checkout. If the ship or bill addresses are edited in +checkout, then the old addresses are +left unchanged and new addresses saved as the defaults. This is a very +simple form of address book. + +## Extension initializers + +It is now possible to include initializers in your extensions. This +makes it a lot easier to +configure extensions and to make site-specific customizations, and to +keep them with the relevant +extension code. + +## Improved handling of requests for invalid objects + +If a method *object_missing* for a controller, Spree will pass all +requests for invalid objects to +this method. This provides an easy way for applications to add specific +handlers for invalid requests. +For example, you may wish to direct customers back to the front page. +If no method has been defined, Spree will use its default 404 response. + +## Reduced silent failures in checkout + +The checkout code is now more careful about returning and checking +results from key operations, and +a few more handlers for exceptions and invalid responses have been +added. In normal use these should +not occur, but they may sometimes occur if you have an error in your +database or configuration. + +## Improvements to Upgrade Process + +The *rake spree:upgrade* task has been eliminated. It turns out there +were some crucial flaws that caused issues when the older version of +Spree used a different version of Rails or a different version of +*upgrade.task* than the newer version of Spree. The rake task has been +replaced by a new gem command: + +```bash + spree —update``` + + +You can also use the abbreviated form: + +```bash + spree —u``` + +After installing a new version of the Spree gem, simply run either one +of these commands from inside *RAILS_ROOT* (your application directory) +and your application will be upgraded. + +The update process is also now less "destructive" than in previous +versions of Spree. Instead of silently replacing crucial files in your +application, Spree now checks the content of files it needs to replace, +and if the old version differs, it will be saved with a *\~* suffix. + +This makes it easier to see when and how some file has changed - which +is often useful if you need to update a customized version. The update +command will also no longer copy the *routes.rb* file - the original +version just loads the core Spree routes file, so has no need to change. +(Recall that you can define new routes in your extensions.) diff --git a/guides/content/release_notes/1_0_0.md b/guides/content/release_notes/1_0_0.md new file mode 100644 index 00000000000..31b976d667c --- /dev/null +++ b/guides/content/release_notes/1_0_0.md @@ -0,0 +1,503 @@ +--- +title: Spree 1.0.0 +section: version +--- + +# Summary + +This is the official 1.0 Release of Spree. This is a **major** release +for Spree, and so backwards compatibility with extensions and +applications is not guaranteed. Please consult the [extension +registry](http://spreecommerce.com/extensions) to see which extensions +are compatiable with this release. If your extension is not yet +compatible you should check back periodically since the community will +be upgrading various extensions over time. + +!!! +If you are upgrading from older versions of Spree you should +perform a complete backup of your database before attempting. It is also +recommended that you perform a test upgrade on a local development or +staging server before attempting in your production environment. +!!! + +# Namespacing + +A difficulty in previous versions of Spree was using it with existing +applications, as there may have been conflicting class names between the +Spree engines and the host application. For example, if the host +application had a *Product* class, then this would cause Spree's +*Product* class to not load and issues would be encountered. + +A major change within the 1.0 Release is the namespacing of all classes +within Spree. This change remedies the above problem in the cleanest +fashion possible. + +Classes such as *Product*, *Variant* and *ProductsController* are now +*Spree::Product*, *Spree::Variant* and *Spree::ProductsController*. +Other classes, such as *RedirectLegacyProductUrl*, have undergone one +more level of namespacing to more clearly represent what areas of Spree +they are from. This class is now called +*Spree::Core::RedirectLegacyProductUrl*. + +Constants such as *SpreeCore* and *SpreeAuth* are now *Spree::Core* and +*Spree::Auth* respectively. + +## Referencing Spree routes + +In previous versions of Spree, due to the lack of namespacing, it was +possible to reference routing helpers such as *product_url* as-is in +the controllers and views of your application and send them to the +*ProductsController* for Spree. + +Due to the namespacing changes, these references must now be called on +the *spree* routing proxy, so that Rails will route to Spree's +*product_url*, rather than a *potential* *product_url* within an +application. Routing helpers referencing Spree parts must now be written +like this: + +```ruby +spree.product_url +``` + +Conversely, to reference routes from the main application within a +controller, view or template from Spree, you must use the *main_app* +routing proxy like this: + +```ruby +main_app.root_url +``` + +If you encounter errors where routing methods you think should be there +are not available, ensure that you aren't trying to call a Spree route +from the main application within the proxy prefix, or a main application +route from Spree without the proxy as well. + +## Mounting the Spree engine + +When *rails g spree:install* is run inside an application, it will +install Spree, mounting the *Spree::Core::Engine* component by inserting +this line automatically into *config/routes.rb*: + +```ruby +mount Spree::Core::Engine, :at => "/" +``` + +By default, all Spree routes will be available at the root of your +domain. For example, if your domain is http://shop.com, Spree's +/products URL will be available at http://shop.com/products. + +You can customize this simply by changing the *:at* specification in +*config/routes.rb* to be something else. For example, if your domain is +http://bobsite.com and you would like Spree to be mounted at /shop, you +can write this: + +```ruby +mount Spree::Core::Engine, :at => "/shop" +``` + +The different parts of Spree (Auth, API, Dash & Promo) all extend the +Core's routes, and so they will be mounted as well if they are available +as gems. + +# Spree Analytics + +The admin dashboard has been replaced with Spree Analytics. This new +service will provide deep insight\ +into your store's ecommerce performance and sales conversion funnel. + +You will have to [register your +store](http://spreecommerce.com/stores/new) with Spree Commerce. Then +configure the Analytics Add On to generate your token. The token should +be entered on the Admin Overview page. + +The original dashboard has been extracted into the [spree_dash +gem](https://github.com/spree/spree_simple_dash) . + +# Command line tool + +We have moved the 'spree' command line tool to its own gem. This is the +new recommended way for adding Spree to an existing Rails application. +The tool will add the Spree gem, copy migrations, initializers and +generate sample data. + +To add Spree to a Rails application you do the following: + +```bash +$ gem install spree +$ rails new my_store +$ cd my_store +$ spree install``` + + +The extension generator has also been moved to this new tool. + +```bash +$ gem install spree +$ spree extension my_ext``` + +# Default Payment Gateways + +The new Spree Command Line Tool prompts you to install the default +gateways. This adds the +[spree\skrill](https://github.com/spree/spree_skrill) and +[spree_usa_epay](https://github.com/spree/spree_usa_epay) gems. These +are the Spree Commerce supported gateways for stores in the United +States (USA ePay) and Internationally (Skrill formally Moneybookers). + +```bash + $ rails new my_store + $ spree install my_store + Would you like to install the default gateways? (yes/no) [yes]``` + +We have moved all the gateways out of core (except bogus) to the [Spree +Gateway Gem](https://github.com/spree/spree_gateway). You can add this +gem to your Gemfile if you need one of those gateways. + +```ruby +gem 'spree' + +# add to your Gemfile after the Spree gem +gem 'spree_gateway' +``` + +The gateways available in the [Spree Gateway +Gem](https://github.com/spree/spree_gateway) are community supported. +These include Authorize.net, Stripe and Braintree and many other +contributed gateways. + +# Preferences + +We have refactored Spree Preferences to improve performance and +simplify code for applications and extensions. The previous interfaces have been\ +maintained so no code changes should be required. The underlying +classes have been completely rewritten. + +Please see the [Spree +blog](http://spreecommerce.com/blog/2011/12/08/spree-preferences-refactored) +for notes on this release. + +# Deprecated functions + +## Middleware + +The lines for middleware in *config/application.rb* within a host +application are now deprecated. When upgrading to Spree 1.0 you must +remove these two lines from *config/application.rb*: + +```ruby +config.middleware.use "SeoAssist" +config.middleware.use "RedirectLegacyProductUrl" +``` + +These two pieces of middleware are now automatically included by the +`Spree::Core::Engine`. + +## Product + +*master_price*, *master_price=*, *variants?*, *variant* are now +officially retired. Please use *Spree::Product#price*, +*Spree::Product#price=*, *Spree::Product#has_variants?* and +*Spree::Product#master* respectively instead. + +## Spree::Config[:stylesheets] + +`Spree::Config` and `stylesheet_tags` are removed in favor for the Rails +3.1 Asset Pipeline. See the [Asset +Customization](http://guides.spreecommerce.com/legacy/1-0-x/asset_customization.html) for more information. + +Extensions looking to add stylesheets to the application should do so +through the Asset Pipeline by making the extension an engine. + +## General deprecations + +- *Gateway.current* is now deprecated. Use + *order.payment_method.gateway* now. + [#747](https://github.com/spree/spree/pull/747) + +# Calculator + +## Calculator::PriceBucket is now renamed to Calculator::PriceSack + +The *PriceBucket* contains Bucket keyword that conflicts with *AWS::S3* +library which has caused few issues with Heroku deployment. If you used +this calculator in your application, then you will need to rename it to +*PriceSack*. + +# Taxation + +There have been several major changes to how Spree handles tax +calculations. If you are migrating from an older version of Spree your +previous tax configurations will not function properly and will need to +be reconfigured before you can resume processing orders. + +!!! +Be sure to backup your database before migrating. Your tax +configuration will likely break after upgrading. You have been warned. +!!! + +## Zone#match now only returns the best possible match. + +Previously the method would return an array of zones as long as the zone +included the address. Now only the narrowest match is returned. + +## New `Order#tax_zone` method + +Will return the zone to be used for computing tax on the order. Returns +the best possible zone match given the order address. In the absence of +an order address it will return the default tax zone (if one is +specified.) + +## Adjustments are now polymorphic + +Previously the `Adjustment` class belonged to just `Order`. Now the +`LineItem` class can have adjustments as well. This allows Spree to +store the amount of tax included in the price when prices include tax +(such as VAT case.) + +## New `Order#price_adjustments` method + +Convenience method for listing all of the price adjustments in the +order. Price adjustments are tax calculations equivalent to the amount +of tax included in a price. These adjustments are not counted against +the order total but are sometimes useful for reporting purposes. + +*** +You don't need to worry about price adjustments unless your prices +include tax (such as the case with Value Added Tax.) +*** + +## New `Order#price_adjustment_totals` method + +Convenient method for show the price adjustment totals grouped by +similar tax categories and rates. + +## Removed helpers and javascript related to VAT + +Prior to this version of Spree there were several helpers designed to +show prices including tax before Spree was changed so that prices were +expected to already include tax (when applicable.) We've removed a lot +of stuff related to the old (more complicated) way of doing things. + +!!! +One unfortunate byproduct of prices now including tax is that +you will need to change the prices on your products if you are in a +region that requires prices to include tax and you were not already +including the tax in your prices. +!!! + +## Removed sales tax and VAT calculators + +Both of these calculators have been replaced by the single calculator +`Calculator::DefaultTax`. + +## Tax rates can now be included in a product price + +There is now a boolean checkbox for indiciating if a tax rate is +included in the product price. The tax rate will only be considered as +part of the product price if the product has a matching tax category. +You can also have multiple tax rates with this designation. + +## New `TaxRate#adjust` method + +This method is responsible for calculating the price. This is basically +an internal change but some developers may be interested to know this. + +*** +Marking a tax rate as including price is the new way to handle +Value Added Tax (VAT) and other similar tax schemes. +*** + +# Zones + +There is one major change related to zones in this release. Zones can no +longer have zone members that are themselves a zone. All zone members +must now be a either a country or state. + +# Testing + +## The demise of Cucumber testing + +Cucumber is a great testing tool however it doesn't bring more values +for testing but overhead. It is decided to opt for a light-weight +practice of RSpec + Capybara. + +# Upgrading + +This section aims to walk you through upgrading to the newest version of +Spree. + +*** +This steps in this guide were written while upgrading from 0.70.x +to 1.0.0. Upgrading older versions of Spree may require some additional +steps. +*** + +## Upgrading the Spree Gem + +You will want to begin the update process by updating the Spree gem in +your Gemfile to reference version 1.0.0. + +```ruby +gem 'spree', '1.0.0' +``` + +Next, you will need to update this gem using this command: + +```bash +$ bundle update spree``` + +*** +If you run `bundle update` instead of `bundle update spree`, +you run the risk of having all your application dependencies updated to +their latest version. It is recommended to only update spree during the +upgrade process. +*** + +## Extensions + +Any Spree extensions being used will also need to be updated to a 1.0.0 +compatible version. If there is not a 1.0.0 compatible extensions +release yet, you will need to disable that extension in order to +continue the upgrade process. + +## Routes + +You will need to update your routes file in order for Spree's routes to +be correctly loaded. You will need to add `mount Spree::Core::Engine, +:at => '/'` as shown below. + +```ruby +#config/routes.rb +YourStore::Application.routes.draw do + mount Spree::Core::Engine, :at => '/' + + # your application's custom routes + … +end +``` + +If you're mounting Spree at the default root path, it is recommended to +place your application's custom routes beneath Spree's mounted routes as +shown in the above example. This will ensure you don't override any of +Spree's defined routes. + +You may choose to mount Spree at a custom location by changing the *:at* +option to something different, such as *:at => '/shop'*. + +## Update config/application.rb + +Remove the following two lines from **config/application.rb** in your +application: + +```ruby +config.middleware.use "SeoAssist" +config.middleware.use "RedirectLegacyProductUrl" +``` + +These two pieces of middleware are now automatically included by Spree. +If you have no desire to use these pieces of middleware, you can now +remove them by placing these two lines into your +**config/application.rb**: + +```ruby +config.middleware.delete "Spree::Core::Middleware::SeoAssist" +config.middleware.delete +"Spree::Core::Middleware::RedirectLegacyProductUrl" +``` + +## Migrations + +```bash +$ bundle exec rake railties:install:migrations``` + +Run the above command to copy over all the migrations from all the +engines included in your application. This may also include any +migrations from extensions or other engines. + +Then it is time to run any new migrations copied to your application. + +```bash +$ bundle exec rake db:migrate +``` + +## Asset Manifest Files + +Remove the line requiring spree_dash from +**app/assets/stylesheets/store/all.css**, +**app/assets/stylesheets/admin/all.css**, +**app/assets/javascripts/store/all.js**, and +**app/assets/javascripts/store/all.js** + +## Other Tips for Upgrading + +- If your application defines any class decorators, you will need to + update these files to decorate Spree's new namespace classes. This + means *Product* becomes *Spree::Product*, *Country* becomes + *Spree::Country*, and so on. +- Correct the paths to any templates you are overriding to include the + Spree namespace. Things such as **app/views/products/show.html.erb** + have now become **app/views/spree/products/show.html.erb**. + +# Bug fixes + +- Fixed issue caused by using *&:upcase* syntax inside the *tab* + helper provided by *Admin::NavigationHelper*. + [#693](https://github.com/spree/spree/pull/693) and + [#704](https://github.com/spree/spree/pull/704). +- Fixed issue where non-ASCII characters were not being correctly + titleized in the *tab* helper provided by *Admin::NavigationHelper*. + [#722](https://github.com/spree/spree/pull/722) +- When Thinking Sphinx was being used, a conflict would occur with its + *Scopes* module and the one inside Spree. + [#740](https://github.com/spree/spree/pull/740) +- Added *script/rails* to core to allow things such as *rails + generate* and *rails console* to be used. [commit + b0903ea](https://github.com/spree/spree/commit/b0903ea477b63bd36c9940b5e0386e29e55f6189) +- Performance improvements for the *best_selling_variants* and + *top_grossing_variants* methods in *Admin::OverviewController*. + [#718](https://github.com/spree/spree/pull/718) +- If an admin user already exists, *rake db:admin:create* will now ask + if you want to override. + [#752](https://github.com/spree/spree/pull/752) +- Making a request to a URL such as + */admin/products/non-existant/edit* no longer shows a status 500 + page. [#538](https://github.com/spree/spree/issues/538) +- *rails g spree:install* output is now not so excessive. [commit + ca4db30](https://github.com/spree/spree/commit/ca4db301e773da4ebc9d2a13e24c5d0e86dd0108) +- The *Spree::Core::Engine* is automatically mounted inside your + application when you run *rails g spree:install*. [commit + ba67b51](https://github.com/spree/spree/commit/ba67b514af41918bf892323c9fd685689c74667a) +- Product *on_hand* now takes all variants into account. + [#772](https://github.com/spree/spree/issues/772) +- The translation for "Listing Products" in admin/products now is more + easily translatable into different languages. [commit + c0d5cb5](https://github.com/spree/spree/commit/c0d5cb5316715ec8aa886fab5bc0820be616d302) +- Removed POSIX-only system calls, replaced with Ruby calls instead to + enable Windows compatibility. + [#711](https://github.com/spree/spree/issues/711) and [commit + ce00172](https://github.com/spree/spree/commit/ce001721a32dd84523d9504feec074db72ef3efb) +- Improved *bundle exec rake test_app* performance. [commit + 6a2d367](https://github.com/spree/spree/commit/ce001721a32dd84523d9504feec074db72ef3efb) +- Improved permalink code, removed reliance on the + *rd-find_by_param* gem. + [#444](https://github.com/spree/spree/issues/444) and + [#847](https://github.com/spree/spree/issues/847) +- Master variant is now deleted when a product is deleted. Performance + with this action has also been improved. + [#801](https://github.com/spree/spree/issues/801) +- An invalid coupon code on the payment screen will now show an error. + [#717](https://github.com/spree/spree/issues/717) +- Products are now restocked when an order is canceled, and unstocked + when the order is resumed. + [#729](https://github.com/spree/spree/issues/729) +- The *ffaker* gem is now used in favor of the *faker* gem. + [#826](https://github.com/spree/spree/issues/826) +- *Spree::Config.set* should no longer be used, please use + *Spree.config* with a block: [commit + 5590fb3](https://github.com/spree/spree/commit/5590fb3) + [#801](https://github.com/spree/spree/issues/801) +- Fix calculator dropdown bug for creating a shipping method in the + admin interface. [#825](https://github.com/spree/spree/issues/825) +- Fix escaping of *order_subtotal* in view. + [#852](https://github.com/spree/spree/issues/852) + diff --git a/guides/content/release_notes/1_1_0.md b/guides/content/release_notes/1_1_0.md new file mode 100644 index 00000000000..2ea953f6293 --- /dev/null +++ b/guides/content/release_notes/1_1_0.md @@ -0,0 +1,307 @@ +--- +title: Spree 1.1.0 +section: version +--- + +# Summary + +This is the official 1.1 Release of Spree. This is a minor release, and +so backwards compatibility with extensions and applications is mostly +guaranteed. There may still be some changes required for your extensions +or applications, and so please read the changelog below to know if you +are affected. + +If any particular extension your store uses is not yet compatible you +are encouraged to alert the Spree team about it by filing an issue on +that extension project if it's an official extension, or to submit a +patch to that project to upgrade compatibility. + +# Major changes (backwards incompatibility) + +## Support for Rails 3.2.x only + +Support for Rails 3.1.x is dropped. Rails 3.2.x offers performance boost +in development +mode and is the first-class supported platform for 1.1.x release cycles. +It is recommended +that you use the latest version 3.2.3. Please upgrade your Rails before +bumping Spree gem by modifying your Gemfile: + +```bash +gem 'rails', '3.2.3' + group :assets do + gem 'sass-rails', '~> 3.2.3' + gem 'coffee-rails', '~> 3.2.1' + gem 'uglifier', '>= 1.0.3' + gem 'jquery-rails' + end``` + +## ransack replaced meta_search + +`ransack` replaced `meta_search` as the primary object-based +searching mechanism. +Be warned that `ransack` is not fully backward-compatible with +`meta_search` query. +Make sure you port and test all `meta_search` queries to `ransack` +after upgrade. + +## spree_product_groups now as standalone extension + +Product Groups component has been extracted to a standalone spree +extension. It is recommended that if you are using this functionality to +add the new extension to your Gemfile: + +gem 'spree_product_groups', :git => +'git@github.com:spree/spree_product_groups.git' + +## Old Theme Hook + +`theme_support` files are now deprecated in favor of Deface. Make sure +you port +all your old style hooks to Deface. + +## Major rewrite of Creditcard model + +Prior to 1.1, the `Creditcard` model contained a lot of payment +processing code. This has [since been +moved](https://github.com/spree/spree/commit/0e684e01b5a15ec21b34263699004ebd78692f0d) +into the `Payment` model. If you have customized the +`Spree::Admin::PaymentsController` or depend on any of the payment +processing methods inside the `CreditCard` model such as `authorize`, +`credit`, or `void`, this change may affect you. + +## API rewrite + +The API component of Spree has undergone a major rewrite in order to +provide better support for applications wishing to interact with it +using tools such as Backbone, or for people wishing to just generally +access the individual components of Spree. + +This is currently a work-in-progress and we would appreciate feedback on +the API on the [mailing +list](https://groups.google.com/group/spree-user). + +As a part of this, the `spree/spree_api` assets are no longer +available, and so these should be removed from the `store/all.css`, +`store/all.js`, `store/admin/all.css` and `store/admin/all.js` assets +located in your application. + +# Minor changes + +## Introduce Spree::Product#master_images and Spree::Product#variant_images + +`Spree::Product#master_images` and its alias `Spree::Product#images` +only returns images belongs to master variant. + +`Spree::Product#variant_images` behaviour is changed, it is no longer +return only images belongs to master variants but now also include all +variants' images. + +## Stronger mass assignment protection + +As per the [Rails 3.2.3 release +notes](https://groups.google.com/forum/?fromgroups#!topic/rubyonrails-core/X-zNKaPOVJw) +, there is a stronger enforcement of attribute protection within Rails. +This **may** affect your Spree application, and if it does we would +advise [filing an issue](https://github.com/spree/spree/issues) so that +it can be promptly fixed. While the tests for Spree itself are +extensive, there may still be edge cases where your application goes +that we do not have covered. + +## Removed images association of `Spree::Product` + +`Spree::Product` association with `viewable` has been moved `master` +variant of +the product. The change is backward compatible and require no upgrade +modification. + +A call to the `images` method on a `Spree::Product` will return all the +images associated with all the variants of this product. If you want +just the `master` variant's images, use `product.master.images`. + +The version of Paperclip required by Spree 1.1 is now any version of the +2.7.x branch of Paperclip. + +## Clearer separation of Spree components + +It was [brought to our +attention](https://github.com/spree/spree/issues/1292) that the Core +component of Spree depended upon things from other components, primarily +Auth. The purpose of the Core component is that it should be usable in +complete isolation from all other components of Spree. This feature was +regressed during the 1..0 branch, but has [now been +fixed](https://github.com/spree/spree/issues/1296) in the 1.1 branch. + +From 1.1 onwards, you will once again be able to use the Core component +of Spree in isolation from the other components if you choose so. + +## Allow for easier Spree controller spec testing + +We have added a `Spree::Core::TestingSupport::ControllerRequests` module +to aid in the testing of Spree controllers within not only the Spree +components, but also within your application. [The documentation at the +top of this +module](https://github.com/spree/spree/blob/1-1-stable/core/lib/spree/core/testing_support/controller_requests.rb) +should adequately describe how this works. + +## Deprecated functions + +### Spree::Zone + +`Spree::Zone#in_zone?` is retired, please use `Spree::Zone#include?` +instead. + +### Spree::PaymentMethod + +`Spree::PaymentMethod.current` is retired, please use +`current_order.payment_method` instead. + +Additionally, `current_gateway` is also removed. + +### Spree::ProductsHelper + +`product_price` is retired, please use `number_to_currency` instead. + +### Spree::HookHelper + +`hook` is retired, please use Deface instead. + +### Spree::Variant + +`Spree::Variant.additional_fields` has been deprecated in favour of +using decorators and Deface. Please see +[#1406](https://github.com/spree/spree/issues/1406) for more +information. + +# Patches + +## Version bumps + +- Paperclip version has been bumped to 2.7.0. + [#1148](https://github.com/spree/spree/issues/1148) + + [#1152](https://github.com/spree/spree/issues/1152) +- Stringex version has been bumped to 1.3.2 to prevent 1.3.1 from + being used, as that release contained a bug. +- nested_set version has been bumped to 1.7.0. + [9b7eda3](https://github.com/spree/spree/commit/9b7eda361dcb001ffa5ad20cd124428d95da21d6) +- jquery-rails version has been bumped to \~> 2.0.0. + [d4b3d7](https://github.com/spree/spree/commit/d4b3d76491) +- deface version has been bumped to 0.8.0. + [e571ed](https://github.com/spree/spree/commit/e571edd86dfadab48e4243caf4fd850a3fd10553) +- highline version has been bumped to 1.6.11. + [45f5e2](https://github.com/spree/spree/commit/45f5e2) + +## Other fixes + +- Added `admin/orders/address_states.js` to precompile list. + [#754](https://github.com/spree/spree/issues/754) +- Added initializer to warn about orphaned preferences. [commit + 4f2669](https://github.com/spree/spree/commit/4f2669) +- Address.default will no longer provide a nil value if the default + country is deleted. + [#1142](https://github.com/spree/spree/issues/1142) + + [#997](https://github.com/spree/spree/issues/997) +- Fix undefined_method to_d for PriceSack Shipping. + [#1156](https://github.com/spree/spree/issues/1156) +- Fixed rounding calculation bug for VAT. + [#1128](https://github.com/spree/spree/issues/1128) + + [#1172](https://github.com/spree/spree/issues/1172) +- Allow `:error` key to be passed to `link_to_delete` .[#1169](https://github.com/spree/spree/issues/1169) +- Fix issue where assigning a price such as"$5" to a variant + caused it to set the price to 0. + [#1173](https://github.com/spree/spree/issues/1173) +- Product names longer than 50 characters are now truncated. + [#1171](https://github.com/spree/spree/issues/1171) +- Fix issue where preferences set to `false` were not being saved. + [#1177](https://github.com/spree/spree/issues/1177) +- Fix incorrect variable name in `script/rails` file inside extension + generator. [#1135](https://github.com/spree/spree/issues/1135) +- Acknowledge Spree's own locale settings before `Rails.application.config.i18n.default_locale` for Spree's + locale details. [#1184](https://github.com/spree/spree/issues/1184) +- Fix issue where preferences set to an empty string were not being + saved. [#1187](https://github.com/spree/spree/issues/1187) +- Set `default_url_options` in `mail_settings.rb` so that it doesn't + need to be set manually for each environment or mailer. + [#1188](https://github.com/spree/spree/issues/1188) +- Correctly fire events for content paths and actions. + [#1141](https://github.com/spree/spree/issues/1141) +- Allow preferences with a type of `:text`. +- Image settings (such as width & height) are now configurable via the + Admin interface. + [7d987fe](https://github.com/spree/spree/commit/7d987fe0e86d799d0896e123e638745201e7adb8) +- Fix bug where `Payment#build_source` would fail dependent on the + ordering of the hash passed in. + [#981](https://github.com/spree/spree/issues/981) +- Fix issue where states javascript include would be prefixed with + asset host when alternate asset host was configured. + [#1213](https://github.com/spree/spree/issues/1213) +- Fix issue where `Promotion#products` would return no products due + to incorrect class specification. + [#1237](https://github.com/spree/spree/issues/1237) +- Order email addresses are now validated with the Mail gem. + [#1238](https://github.com/spree/spree/issues/1238) +- Attempt to access `/admin/products/{id}` will now redirect to + `/admin/products/{id}`. + [#1239](https://github.com/spree/spree/issues/1239) +- Show 'N/A' for tax category on `/admin/tax_rates` if a tax rate + doesn't have a tax category. + [#535](https://github.com/spree/spree/issues/#535) +- Fix issue where incorrect order assignment was breaking return + authorization creation. + [#1107](https://github.com/spree/spree/issues/1107) + [#1109](https://github.com/spree/spree/issues/1109) + [#1149](https://github.com/spree/spree/issues/1149) +- Fix issue where under certain circumstances users were able to view + other people's order information. + [#1243](https://github.com/spree/spree/issues/1243) +- Fix issue where searching for orders by SKU was broken in admin + backend. [#1259](https://github.com/spree/spree/issues/1259) +- Logout when "Remember me" was checked for login now will actually + log a user out. [#1257](https://github.com/spree/spree/issues/1257) +- `Spree::UrlHelpers` has moved to `Spree::Core::UrlHelpers` + [3bf5df](https://github.com/spree/spree/commit/3bf5df57e3474322dc484eb57ca5ee9098bd9454) +- Preview buttons on the admin dashboard are now hidden once the + dashboard has been configured. + [#1271](https://github.com/spree/spree/issues/1271) +- Slightly alter permalink code so that it does not conflict on + similar names. Current permalinks will not be affected. + [#1254](https://github.com/spree/spree/issues/1254) +- Don't allow payments to be created in admin backend unless payment + methods have been defined. + [#1269](https://github.com/spree/spree/issues/1269) +- `Address#full_name` will now return a string with no extra spaces + around it. [#1298](https://github.com/spree/spree/issues/1298) +- Ensure `StatesController` always returns JS response. + [#1304](https://github.com/spree/spree/issues/1304) +- Fix issue where checkbox for "Use Billing Address" was not being + checked for an order in admin backend when it was in the frontend. + [#1290](https://github.com/spree/spree/issues/1290) +- Fix issue where a shipping method could not be updated. + [#1331](https://github.com/spree/spree/issues/1331) +- Allow layout to be customized based on a configuration setting. + [#1355](https://github.com/spree/spree/issues/1355) +- `Rails.application.config.assets.debug` is no longer hardcoded to + `false` in Spree. Set this variable at your discretion inside your + `config/application.rb` from now on. + [#1356](https://github.com/spree/spree/issues/1356) +- Added `gem_available?` method to `BaseHelper` to be able to check + if an extension is available. + [#1241](https://github.com/spree/spree/issues/1241) +- Fixed potential bug where `Activator.active` scope may have not been + returning activators that were currently active. + [#1343](https://github.com/spree/spree/issues/1343) +- Fix incorrect route issue when updating a return authorization. + [#1343](https://github.com/spree/spree/issues/1343) +- Fixed issue where "Add to Cart" button may not work on IE7. + [#1397](https://github.com/spree/spree/issues/1397) +- Fixed issue where non-price characters were being stripped from + prices. [#1392](https://github.com/spree/spree/issues/1392) + [#1400](https://github.com/spree/spree/issues/1400) +- Limit ProductsController#show to only show active products. + [#1390](https://github.com/spree/spree/issues/1390) +- Spree::Calculator::PerItem will now calculate per-item, rather than + per-line-item. This means that if you have a per-item calculator + costing $1 and 3 line items with quantity of 5 that would be $15, + rather than $3. + [#1414](https://github.com/spree/spree/issues/1414) + diff --git a/guides/content/release_notes/1_1_4.md b/guides/content/release_notes/1_1_4.md new file mode 100644 index 00000000000..0eb4a5db134 --- /dev/null +++ b/guides/content/release_notes/1_1_4.md @@ -0,0 +1,126 @@ +--- +title: Spree 1.1.4 +section: version +--- + +# Summary + +This is the official 1.1.4 Release of Spree. This is a trivial release, +and so backwards compatibility with extensions and applications is +guaranteed. + +This will be the final release of the 1.1.x branch for Spree. We would +recommend upgrading to 1.2.x as soon as possible. + +# Major fixes + +## Migrations + +This new version of Spree most likely contains new migrations. Please +install them and run them with this command: + +```bash +bundle exec rake railties:install:migrations +bundle exec rake db:migrate``` + +## Rails dependency upgraded + +Due to a [security +bug](http://weblog.rubyonrails.org/2012/11/12/ann-rails-3-2-9-has-been-released/) +within Rails, we have upgraded the dependency of Rails to be 3.2.9. You +will not be able to use Rails 3.2.8 with Spree 1.1.4. It's highly +encouraged you upgrade your Rails version. + +## JavaScript changes + +If you are upgrading from an older version of Spree 1.1, please make +sure that your JavaScript requires are correct within +`app/assets/javascripts`. + +Inside `app/assets/javascripts/admin/all.js`, the directives should be +this: + +```bash +//= require jquery +//= require jquery_ujs + +//= require admin/spree_core +//= require admin/spree_auth +//= require admin/spree_promo + +//= require_tree . +``` + +Inside `app/assets/javascripts/store/all.js`, the directives should be +this: + +```bash +// +//= require jquery +//= require jquery_ujs + +//= require store/spree_core +//= require store/spree_auth +//= require store/spree_promo + +//= require_tree . +``` + +These changes are important, as they fix [Issue +#1854](https://github.com/spree/spree/issues/1854). + +# Minor fixes + +- Ensure there is a valid state during the checkout. + [#1770](https://github.com/spree/spree/issues/1770) +- Use GET request for /user/logout. + [#1663](https://github.com/spree/spree/issues/1663) and + [#1812](https://github.com/spree/spree/issues/1812) +- Use product description if no meta description is provided. + [#1811](https://github.com/spree/spree/issues/1811) +- Don't update *product.count_on_hand* if track inventory labels is + disabled. [#1820](https://github.com/spree/spree/issues/1820) +- Set payment state to *credit_owed* if order is cancelled and + nothing is shipped. + [#1513](https://github.com/spree/spree/issues/1513) +- Added *ignore_types* option for *flash_messages* to ignore + specific types of flash messages. + [#1835](https://github.com/spree/spree/issues/1835) +- Added large image placeholder. + [#1828](https://github.com/spree/spree/issues/1828) +- Assets are no longer precompiled during the installation process. + [#1854](https://github.com/spree/spree/issues/1854) +- API responses now return the correct content type. + [#1866](https://github.com/spree/spree/issues/1866) +- I18n-ify order cancel, confirmation and shipment emails. + [#1884](https://github.com/spree/spree/issues/1884) +- Preferences are now checked for in database before falling back to + their defined-defaults. + [#1500](https://github.com/spree/spree/issues/1500) +- Deleted shipping methods are no longer visible in the admin backend. + [#1847](https://github.com/spree/spree/issues/1847) and + [#1967](https://github.com/spree/spree/issues/1967). +- Moved admin JS translations out to their own partial + (*core/app/views/admin/shared/_translations.html.erb*) + [#1906](https://github.com/spree/spree/issues/1906) +- Fixed error where admin page for tax rates would not load if a tax + rate didn't have a valid zone. + [#2019](https://github.com/spree/spree/issues/2019) +- Fixed incorrect PriceSack calculation + [#2055](https://github.com/spree/spree/issues/2055) +- InventoryUnit#restock_variant will no longer alter + *variant.count_on_hand* if inventory tracking is disabled. +- Orders with all return authorizations received are now marked as + returned. [#1714](https://github.com/spree/spree/issues/1714) and + [#2097](https://github.com/spree/spree/issues/2097) +- *Product.in_taxons* will now only return unique products. + [#1917](https://github.com/spree/spree/issues/1917) and + [#1974](https://github.com/spree/spree/issues/1974) and + [#1962](https://github.com/spree/spree/issues/1962) +- *Product.on_hand* no longer sums deleted variants. + [#2112](https://github.com/spree/spree/issues/2112) +- EXIF data is now stripped from uploaded JPEGs + [#2145](https://github.com/spree/spree/issues/2145) +- OrdersController#show now requires SSL, as it contains sensitive + data. [#2164](https://github.com/spree/spree/issues/2164). + diff --git a/guides/content/release_notes/1_2_0.md b/guides/content/release_notes/1_2_0.md new file mode 100644 index 00000000000..891432044b1 --- /dev/null +++ b/guides/content/release_notes/1_2_0.md @@ -0,0 +1,233 @@ +--- +title: Spree 1.2.0 +section: version +--- + +Spree 1.2.0 introduces some fairly major changes in the basic +architecture of Spree, as well as minor alterations and bugfixes. + +Due to the long development cycle of Spree 1.2 in parallel with +continuing development of the 1.1 branch, there may be features released +in 1.2 that are already present in 1.1. + +# Summary + +There were two major topics addressed within this release of Spree: +custom authentication and better checkout customization. + +The first was the ability to use Spree in conjunction with an +application that already provided its own way to authenticate users. Due +to how Spree was architected in the past, this was not as easy as it +could have been. In this release of Spree, the auth component of Spree +has been removed completely and placed into a separate extension called +[spree_auth_devise](https://github.com/spree/spree_auth_devise). If +you wish to continue using this component of Spree, you will need to +specify this extension as a dependency in your Gemfile. See [Issue +#1512](https://github.com/spree/spree/pull/1512) for more detail about +the customization. + +The checkout process has always been hard to customize within Spree, and +that has generated complaints in the past. We are pleased to report in +the 1.2 release of Spree that this has been substaintially easier with a +new checkout DSL that allows you to re-define the checkout steps in a +simple manner. For more information about this, please see [Issue +#1418](https://github.com/spree/spree/pull/1418) and [Issue +#1743](https://github.com/spree/spree/pull/1743). + +Along with these two major issues, there were also a ton of minor +improvements and bug fixes, explained in detail below. + +# Major changes (backwards incompatibility) + +## spree_auth removal + +Authentication is disabled by default within Spree as of this release, +with the application supposed to be providing its own authentication. If +you are upgrading an existing Spree installation or just want it to +work, you can achieve the behaviour of a 1.1 installation by adding +"spree_auth_devise" to your Gemfile. + +```ruby +gem 'spree_auth_devise', :git => +"git://github.com/spree/spree_auth_devise"``` + +For more information on how to customize authentication, please see the +[Authentication +guide](http://guides.spreecommerce.com/authentication.html). + +## State machine customizations + +Customizing the state machine within Spree now does not require you to +override the entire *state_machine* definition within Spree's *Order* +model. Instead, you are provided with the ability to define the "next" +events for *Order* objects like this: + +```ruby +Spree::Order.class_eval do + checkout_flow do + go_to_state :address + go_to_state :delivery + go_to_state :payment, :if => lambda { payment_required? } + go_to_state :confirm, :if => lambda { confirmation_required? } + go_to_state :complete + remove_transition :from => :delivery, :to => :confirm + end +end``` + +For more information about customizing the checkout process within +Spree, please see the [Checkout +guide](http://guides.spreecommerce.com/checkout.html). + +# Minor changes + +## has_role?, api_key and roles methods now namespaced + +On a usr object, the *has_role?* method is now called +*has_spree_role?*, the *api_key* method is called *spree_api_key* +and the *roles* association is now called *spree_roles*. This allows +for applications to define their own *has_role?*, *api_key* and +*roles* methods without them conflicting with the methods defined within +Spree. + +## Introduce Spree::Product#master_images and Spree::Product#variant_images + +*Spree::Product#master_images* and its alias *Spree::Product#images* +only returns images belongs to master variant. + +*Spree::Product#variant_images* behaviour is changed, it is no longer +return only images belongs to master variants but now also include all +variants' images. + +## Spree::Zone#country_list renamed to #zone_member_list + +Please be noted that the underlying logics remain intact. + +## Spree::Creditcard renamed to Spree::CreditCard + +All occurences of Creditcard are changed to CreditCard to better follow +Rails naming conventions. + +## Assert that ImageMagick is installed during Spree installation + +When installing Spree, if the ImageMagick library was not installed on +the system, then the *identify* command that Paperclip uses would fail +and sample product images would not appear. + +There is now a [check in +place](https://github.com/spree/spree/commit/a6deb62) to ensure that +ImageMagick is installed before Spree is. + +## Sass variables can now be overriden + +The variable definitions for the sass files of Spree have been moved to +a separate file within Core called +*app/assets/stylesheets/store/variables.css.scss*. You can override this +file within your application in order to re-define the colors and other +variables that Spree uses for its stylesheets. + +## Added support for serialized preferences + +Preferences can now be defined as serialized. See +[7415323](https://github.com/spree/spree/commit/7415323) for more +information. + +## New UI for defining taxons and option types for a product + +Rather than having the taxons and option types related to a product on +two completely separate pages, they are now included on the product edit +form. This functionality is provided by the Select2 JavaScript plugin, +and will fall back to a typical select box if JavaScript is not +available. + +## Using the Money gem to display currencies + +In earlier versions of Spree, we used *number_to_currency* to display +prices for products. This caused a problem when somebody selected a +different I18n locale, as the prices would be displayed in their +currency: 20 Japanese Yen, rather than 20 American Dollars, for +instance. + +To fix this problem, we're now parsing the prices through the Money gem +which will display prices consistently across all I18n locales. To now +change the currency for your site, go to Admin, then Configuration, then +General Settings. Changing the currency will only change the currency +symbol across all prices of your store. + +Note: After the 1.2.0 release, more options to format the currency +output have been introduced. Specifically the position of the currency +symbol which is fixed to :before in 1.2.0 can be adjusted. To achieve +this in the 1.2.0, you may wish to override the Spree::Money.to_s +function. + +# Tiny changes / bugfixes + +## General + +- Replaced uses of deprecated jQuery method live with on + ([273987d](https://github.com/spree/spree/commit/273987d)) + +## API + +- Added ability to make shipments ready and declare them shipped + ([e933f36](https://github.com/spree/spree/commit/e933f36)) +- Added payments and shipments information to order data returned from + the API ([8c2aaef](https://github.com/spree/spree/commit/8c2aaef)) +- Add ability to credit payments through the order + ([320599a](https://github.com/spree/spree/commit/320599a)) +- Added the ability to update an order + ([ab1e23b](https://github.com/spree/spree/commit/ab1e23b)) +- Added the ability to search for products + ([024b22a](https://github.com/spree/spree/commit/024b22a)) +- Added zones + ([b522703](https://github.com/spree/spree/commit/b522703) + [#1615](https://github.com/spree/spree/issues/1615)) +- Allow orders to be paginated + ([873e9f8](https://github.com/spree/spree/commit/873e9f8)) + +## Core + +- Added more products in sample data + ([6b66b3a](https://github.com/spree/spree/commit/6b66b3a)) +- Sample products now have product properties associated with them + ([c71ef3a](https://github.com/spree/spree/commit/c71ef3a)) +- Product images are now sorted by their position + ([5115377](https://github.com/spree/spree/commit/5115377)) +- The configuration menu/sidebar will now display on + */admin/shipping_categories/index* + ([459b5d0](https://github.com/spree/spree/commit/459b5d0)) +- Datepickers are now localized in the admin backend + ([e5f1680](https://github.com/spree/spree/commit/e5f1680)) +- You can now click on a product name in */admin/products/* list to go + to that product + ([f2e9cc3](https://github.com/spree/spree/commit/f2e9cc3)) +- Defining of Spree::Image helper methods is done on the fly now + ([ff0c837](https://github.com/spree/spree/commit/ff0c837)) +- Account for purchased units in stock validation + ([7c4cd77](https://github.com/spree/spree/commit/7c4cd77)) +- Sort all order adjustments by their created_at timestamp + ([aef2fd9](https://github.com/spree/spree/commit/aef2fd9)) +- Use *ransack* method on classes rather than *search*, as this may be + defined by an extension + ([4fabc52](https://github.com/spree/spree/commit/4fabc52)) +- Fix issue where redeeming a coupon code for a free product did not + apply the coupon + ([2459f75](https://github.com/spree/spree/commit/2459f75) + [#1589](https://github.com/spree/spree/issues/1589)) +- Specify class name for all *belongs_to* associations + ([94a6859](https://github.com/spree/spree/commit/94a6859)) +- Don't rename users table if *User* constant is defined + ([c77c822](https://github.com/spree/spree/commit/c77c822)) +- Removed all references to link_to_function throughout Spree, + replace with 100% JavaScript +- Moved *get_taxonomies* helper method out of + *Spree::Core::ControllerHelpers* into *Spree::ProductsHelper* + ([980348a](https://github.com/spree/spree/commit/980348a)) + +## Promo + +- Coupon code input is now displayed on the cart page + ([c030671](https://github.com/spree/spree/commit/c030671)) +- Only acknowledge coupon codes for promos that have the + 'spree.checkout.coupon_code_added' event + ([4158979](https://github.com/spree/spree/commit/4158979)) + diff --git a/guides/content/release_notes/1_2_2.md b/guides/content/release_notes/1_2_2.md new file mode 100644 index 00000000000..49893f87315 --- /dev/null +++ b/guides/content/release_notes/1_2_2.md @@ -0,0 +1,112 @@ +--- +title: Spree 1.2.2 +section: version +--- + +Spree 1.2.2 is the latest Spree release in the 1.2.x branch. This +release contains minor improvements and bug fixes. Compatibility with +extensions is mostly guaranteed, however there may be edge cases. If you +find one of these, please [file an +issue](https://github.com/spree/spree/blob/master/CONTRIBUTING.md). + +## Major changes + +### Migrations + +This new version of Spree contains new migrations. Please install them +and run them with this command: + +```bash +bundle exec rake railties:install:migrations +bundle exec rake db:migrate +``` + +### API changes + +We have changed some aspects of the API component in Spree. For a +detailed list of these changes, please refer to the [Changes page on our +API site](http://api.spreecommerce.com/changes/) + +## Other changes + +- Switched from using the `acts_as_nested_set` gem to using + `awesome_nested_set_gem`, which is an optimized version of the + same gem. [#1927](https://github.com/spree/spree/issues/1927) +- Renamed InventoryUnit.backorder to InventoryUnit.backordered + [commit](https://github.com/spree/spree/commit/6cc3da52daa3ef57423c0ddbeb4211980ea3103d) +- Fix issue with installer when running on `i386-mingw32` platform + PCs. [#1903](https://github.com/spree/spree/issues/1903) +- All adjustments, not just optional ones, are locked on an order + completion. + [commit](https://github.com/spree/spree/commit/1a9b25c0a4232f02f25ab0d7bc80250e045bf8fa) +- Fix issue where currently selected currency wasn't displayed + correctly on the "General Settings" page. + [commit](https://github.com/spree/spree/commit/a46455afd8e4691aaf789b4639da8967277f1916) +- Added `:currency_symbol_position` configuration option, to + configure if currency symbol goes before or after amount. + [commit](https://github.com/spree/spree/commit/575af696f39f9ea408fc9f4082bccff4e7fa4e05) + [#1911](https://github.com/spree/spree/issues/1911) +- Allow for calling `save_permalink` manually to recalculate a + permalink. [#1920](https://github.com/spree/spree/issues/1920) +- Disable double-clicking delete links on line items. + [#1934](https://github.com/spree/spree/issues/1934) +- Add `per_page` parameter for API orders. + [#1949](https://github.com/spree/spree/issues/1949) +- Fixed payment banner not being dismissed when asked. + [#1952](https://github.com/spree/spree/issues/1952) +- Prevent double-submit on checkout confirm step. + [commit](https://github.com/spree/spree/commit/84f91aa875d41fa1e77646c9cc25b321dab050cc) +- Allow an order with no shipments to be canceled. + [#1989](https://github.com/spree/spree/issues/1989) +- Added `Product#available?` + [#2002](https://github.com/spree/spree/issues/2002) +- Allow case-insensitive coupon codes + [#2009](https://github.com/spree/spree/issues/2009) and + [#2012](https://github.com/spree/spree/issues/2012) +- Fix problem with currencies whose sub-units are not in hundreds. + [#2030](https://github.com/spree/spree/issues/2030) +- Payments are no longer processed if `Order#payment_required?` + returns false. [#2028](https://github.com/spree/spree/issues/2028) +- Expired promotions are now excluded from + `Product#possible_promotions` + [#2058](https://github.com/spree/spree/issues/2058) +- Load `Spree::AuthenticationHelpers` during a `to_prepare` hook + [#2076](https://github.com/spree/spree/issues/2076) +- Updating a line item's quantity and then clicking the checkout + button will now persist the update. + [#2086](https://github.com/spree/spree/issues/2086) +- `with_option_value` and `taxons_name_eq` scopes on `Product` + will now return `Product` objects, rather than IDs. + [#2082](https://github.com/spree/spree/issues/2082) +- Searcher class instances in `HomeController`, `ProductsController` + and `TaxonsController` will now have access to the current user + object. [#2089](https://github.com/spree/spree/issues/2089) +- `spree_products.count_on_hand` and + `spree_variants.count_on_hand` columns can now be set to `NULL` + to indicate an infinite supply. + [#2096](https://github.com/spree/spree/issues/2096) +- Orders are marked "returned" if all return authorizations are + received [#1714](https://github.com/spree/spree/issues/1714) + [#2099](https://github.com/spree/spree/issues/2099) +- Unique products are returned from `Product.in_taxon` scope + [#1917](https://github.com/spree/spree/issues/1917) + [#1974](https://github.com/spree/spree/issues/1974) + [#1962](https://github.com/spree/spree/issues/1962) +- `Product.on_hand` scope no longer sums deleted variants. + [#2112](https://github.com/spree/spree/issues/2112) +- `Payment#capture` now does nothing for payments marked as + "completed" [#2119](https://github.com/spree/spree/issues/2119) +- I18nify "Order Adjustments" text + [#2123](https://github.com/spree/spree/issues/2123) +- EXIF data is now stripped from JPEGs + [#2142](https://github.com/spree/spree/issues/2142) + [#2145](https://github.com/spree/spree/issues/2145) +- Only eligible promotions are now included in `promo_total` on + `Order` objects. + [commit](https://github.com/spree/spree/commit/74a7914903b9d7dac77e0cbd38b1919fb3396254) +- Promotion usage count is now visible on a promotion's edit page. + [#2193](https://github.com/spree/spree/issues/2193) +- The user picker for the promotions backend is now functional again, + after being broken accidentally in the previous release. + [#1890](https://github.com/spree/spree/issues/1890) + diff --git a/guides/content/release_notes/1_3_0.md b/guides/content/release_notes/1_3_0.md new file mode 100644 index 00000000000..58ee25c7f9e --- /dev/null +++ b/guides/content/release_notes/1_3_0.md @@ -0,0 +1,170 @@ +--- +title: Spree 1.3.0 +section: version +--- + +Spree 1.3.0 is the first release of the 1.3.x branch of Spree. This release contains some major non-breaking changes, which are covered in the release notes below. + +Due to the long development cycle of Spree 1.3 in parallel with continuing development of the 1.1 branch, there may be bug fixes released in 1.3 that are already present in the latest release of 1.2. + +Here's a quick summary of the major features in this release: + +- Admin redesign +- Currency support for variants + +## Major changes + +### Admin redesign + +Alexey Topolyanskiy has done some amazing work performing a makeover for the admin backend for Spree, something that has been long overdue! + +![](../images/developer/new-admin-interface.png) + +### Currency support + +Thanks to work by Gregor MacDougall and the team at Free Running Technologies, Spree's Variant model now is able to keep track of a different price for different currencies. + +## Minor changes + +### Remove child node from API responses + +The API has previously returned data with a child node within its responses. Take this example from `/api/products`: + +```ruby +{ + [products]() [ + { + [product]() { + [id]() 1, + … + } + }] +} +``` + +This response will now be returned without the child nodes, like this: + +```ruby +{ + [products]() [ + { + [id]() 1, + … + } + ] +} +``` + +### API requests can now ask for different Rabl templates + +If you would like to make a request to the API use a different Rabl template, pass the template's name within the request as an `X-Spree-Template` header or *template* parameter, and Spree will automatically use that template to render the response. + +For instance, if you have a template at `app/views/spree/api/products/special_show.v1.rabl`, to render that template the `X-Spree-Template` header or `template` parameter would need to be simply "special_show". This will allow you to customize the responses from Spree's API extremely easily. + +### Jirafe false positive conversions + +We've had a number of reports of Jirafe false positive conversions within Spree +([#2273](https://github.com/spree/spree/issues/2273) +[#2211](https://github.com/spree/spree/issues/2211) and +[#2157](https://github.com/spree/spree/issues/2157)) + +This issue should now be fixed based on [this commit](https://github.com/spree/spree/commit/50bc65f78d07453fea85ae034748007946bd27bd) + +## Other changes + +- Fix issue where return authorization form would crash if a variant + had an ID + with a large value + [commit](https://github.com/spree/spree/commit/820a1c023d915f9d2c972c04c5641b5d823ab508) +- Don't process payments if payments are not required [#2025](https://github.com/spree/spree/issues/2025) +- Payments are now applied one at a time until the order total is met, + rather + than processing all payments at the same time. + [#1954](https://github.com/spree/spree/issues/1954) + [#2008](https://github.com/spree/spree/issues/2008) +- Exclude expired promotions from Product#possible_promotions + [#2058](https://github.com/spree/spree/issues/2058) +- Pass all changes to Variant#count_on_hand to Variant#on_hand= + to ensure + backorders are processed correct + [commit](https://github.com/spree/spree/commit/d6c1183095125a946e8f6f1078ce0ee7487687b9) +- Use select2 for properties and option types on prototype form to + display + options better. [#2077](https://github.com/spree/spree/issues/2077) +- Clicking 'Checkout' on the cart page will now update the order and + redirect to + the address form, rather than just redirecting to the address form. + [#2086](https://github.com/spree/spree/issues/2086) +- The searcher class now has access to the current user. + [#2089](https://github.com/spree/spree/issues) +- Allow anonymous requests to the API. + [commit](https://github.com/spree/spree/commit/456cadf5ff858ecac75646ca6b592be384a07396) +- Don't clear mail method or payment method passwords if they're not + included in + a request. [#2094](https://github.com/spree/spree/issues/2094) +- An order is marked as returned automatically if all return + authorizations are + received. [#1714](https://github.com/spree/spree/issues/1714) + [#2099](https://github.com/spree/spree/issues/2099) +- Added *on_demand* field for variants, indicating that the variant + is an "on + demand" item. [#1940](https://github.com/spree/spree/issues/1940) + [#2080](https://github.com/spree/spree/issues/2080) +- Product.in_taxons does not return duplicate products + [commit](https://github.com/spree/spree/commit/75fa3623b61e22fcde395b7f9900e23038361df9) +- Spree::Product.on_hand no longer sums with deleted variants + [#2112](https://github.com/spree/spree/issues/2112) +- Payment#capture! will no longer work on completed payments. + [#2119](https://github.com/spree/spree/issues/2119) +- Fix "Order adjustments" translation + [#2123](https://github.com/spree/spree/issues/2123) +- Order#create_tax_charge! is called whenever a line item is added + or removed + from an order. [#1418](https://github.com/spree/spree/issues/1418) +- Don't allow + void_transaction! to operate on a payment which is already void. + [#2119](https://github.com/spree/spree/issues/2119) +- Strip EXIF data from images [#2142](https://github.com/spree/spree/issues/2142) +- Display promotion usage data in admin +[#2193](https://github.com/spree/spree/issues/2193) +- Remove display_on option for Payment Methods. +[#1918](https://github.com/spree/spree/issues/1981) +- Add Order#variants, to get a list of variants associated with an order. +[#2195](https://github.com/spree/spree/issues/2195) +- Fix issue when trying to move taxon to the bottom of the tree +[#2180](https://github.com/spree/spree/issues/2180) +- Show only one validation message for an order's email if left blank on the +checkout [#2214](https://github.com/spree/spree/issues/2214) +- Taxonomies can now be reordered +[#2237](https://github.com/spree/spree/issues/2237) +- Order#merge no longer uses Order#add_variant. For an +explanation, [see this +commit](https://github.com/spree/spree/commit/8569ed5d98e354285ad6ccbd366444fd31e773f8) +- Orders with promotions that "zero" the order total will no longer + skip + delivery step if that step is required. + [#2191](https://github.com/spree/spree/issues/2191) +- Jirafe analytics can now be edited after registration + [#2238](https://github.com/spree/spree/issues) +- awesome_nested_set version has been bumped to 2.1.5 + [commit](https://github.com/spree/spree/commit/3bdd22fedda456308f20f0817155590fab231e96) +- Order details page no longer errors if a payment's credit card type + is blank + [#2282](https://github.com/spree/spree/issues/2282) +- No longer transition to complete if payment is required and there + are payments + due. + [commit](https://github.com/spree/spree/commit/8639bbcc3b1909a339b0a60da239a49b95baa760) +- Refactored preference fetching from the preference store + [commit](https://github.com/spree/spree/commit/bfcb5b29b3e29c3d451b14ab39e2b502ea93f6a4) +- Order#checkout_steps will now always include the "Complete" step. + [commit](https://github.com/spree/spree/commit/227f86ff57735e0e0637a0896006ff79fe8e0a6d) +- Allow "first order for user" promotion to work with guest users as + well + [#2306](https://github.com/spree/spree/issues/2306) +- Always show "resend" (email confirmation) button when viewing an + order in + admin backend. [#2318](https://github.com/spree/spree/issues/2318) +- Made sure that shipment for resumed order can be set to "ready" + [#2317](https://github.com/spree/spree/issues/2317) + diff --git a/guides/content/release_notes/2_0_0.md b/guides/content/release_notes/2_0_0.md new file mode 100644 index 00000000000..bd5b362fa5a --- /dev/null +++ b/guides/content/release_notes/2_0_0.md @@ -0,0 +1,319 @@ +--- +title: Spree 2.0.0 +section: version +--- + +## Major/new features + +### General + +#### Removing support for Ruby 1.8.7 + +Support for Ruby 1.8.7 is going away in this major release. If you are still using +1.8.7, it is time to upgrade. +[Ruby 1.8.7 is End of Life'd at the end of June](https://blog.engineyard.com/2012/ruby-1-8-7-and-ree-end-of-life) + +Upgrading to Ruby 1.9.3 or higher is highly encouraged. Spree 2.0 and above supports Ruby 2. + +#### Splitting up core + +A lot of people have requested the ability to use either the backend or the +frontend separately from the other. We did a lot of work toward this goal as part of +<%= issue 2225 %> and now Spree is split up +into the following components: + +* API +* **Backend** +* Core +* Dash +* **Frontend** +* Sample + +The **Backend** component provides the admin interface for Spree and the +**Frontend** component provides the frontend user-facaing checkout interface. These +components were extracted out of Core to allow for users of Spree to override the frontend +or backend functionality of Spree as they choose. Core now contains just the very basic +needs for Spree. + +Along with this work, the Promo engine has now been merged with Spree core. We +saw that there was a lot of hackery going on to get promos to work with Core, +and a lot of stores want promos anyway, and so merging them made sense. + +Additionally, as part of this work, the spree_core assets have been renamed. +In `store/all.css` and `store/all.js`, you will need to rename the references +from `spree_core` to `spree_frontend`. Similarly to this, in `admin/all.css` +and `admin/all.js`, you will need to rename the references from `spree_core` +to `spree_backend`. + +#### Split shipments + +Complex Spree stores require sophisticated shipping and warehouse logic that Spree hasn't had a general solution for until now. Split shipments in Spree allows for multiple shipments per order and for those shipments to be shipped from multiple locations. + +There are 4 main components that make up split shipments: Stock Locations, Stock Items, Stock Movements and Stock Transfers. + +##### Stock locations + +Stock locations are the locations where your inventory is shipped from. Each stock location can have many stock items. When creating a new stock location, stock items for that location are automatically created for each variant in your store. + +Having multiple stock locations allows for more robust shipping options. For example, if an item in an order is out of stock at the location of the other items in a order, a new shipment may be created if that item is found to be in stock at another location. + +You are also able to create and manage orders that have items from multiple locations by using the improved admin interface. + +##### Stock items + +Stock Items represent the inventory at a stock location for a specific variant. Stock item count on hand can be increased or decreased by creating stock movements. Because these are created automatically for each location you create, there is no need to manually manage these. + +##### Stock movements + +Stock movements allow you to manage the inventory of a stock item for a stock location. Stock movements are created in the admin interface by first navigating to the product you want to manage. Then, follow the Stock Management link in the sidebar. + +For more information on split shipments and how they pertain to inventory management, read the [Inventory Guide](http://edgeguides.spreecommerce.com/developer/inventory.html). + +For more information on the classes introduced by split shipments and how to work with them programmatically, see the [Shipments Guide](http://edgeguides.spreecommerce.com/developer/shipments.html#split-shipments). + +##### Stock transfers + +Stock transfers allow you to move inventory in bulk from one stock location to another stock location. + +Stock transfers generally consist of a source location, a destination location, one or more variants and an optional reference number. Stock transfers can also be used as a way to track new stock, in which case only a stock location destination and variant are required. + +#### I18n + +Spree 2.0 now comes with namespaced translations so that translations in your application +will no longer conflict with those within Spree. It's recommended that if you have extension +that uses Spree to move its translations into the Spree namespace to avoid the same problem. + +Translations within Spree views should now use the `Spree.t` helper, rather than the `t` +helper so that they are namespaced correctly. + +### API + +#### New API endpoints + +API clients can now manage the following resources through the API: + +* Option Types +* Option Values +* Inventory Units +* Shipments +* Stock Items +* Stock Locations +* Stock Movements + +The documentation for these endpoints hasn't been written yet, but will be shortly. + +#### Instance level permissions + +The API now can enforce instance-level permissions on objects. This means that +some users would be able to access a single item within a resource, rather than +an "all or none" approach to the API. + +<%= commit "548dc0c58e4400501bc67cddea942fda1c7dbad3" %> + +#### Custom API templates + +If you wish to use a custom template for an API response you can do this by +passing in a `template` parameter to API requests. + +[Read the documentation for more +information](http://edgeguides.spreecommerce.com/api/summary.html#customizing-responses). + +### Core + +#### Adjustment state changes + +Adjustments can now be open, closed or finalized, allowing for a more flexible + adjustments system. An 'open' adjustment can be modified, whereas a 'closed' + adjustment cannot. Finalized adjustments are never altered. + +<%= commit "43a3cca49180b1572e41bc3638d3ca0f0e9116d9" %> + +#### OrderPopulator + +Order population responsibility has been moved out to its own class. This has +been done so that the API, Core and any other extensions that wish to use the +order population logic have an easy way to do so. + +See <%= issue 2341 %> and <%= commit "432d129c86e03597347cd223507d9386e9613d62" %> for more information. + +#### CouponApplicator + +Coupon application responsibility has been moved out to its own class too. This +has been done so that the API, Core and any other extensions that wish to apply +coupons have an easy way to do so. + +<%= commit "8ac9ac1c56fe1e471dd5b0124edbe383a8c70c48" %> + +#### ProductDuplicator + +Product duplication code has been moved out to its own class as well. + +<%= issue 2641 %> + +#### New helpers to modify checkout flow steps + +To add or remove steps to the checkout flow, you can now use the `insert_checkout_step` +and `remove_checkout_step` helpers respectively. This patch has been backported +to 1-3-stable as well, and will be available in Spree releases >= 1.3.3. + +The `insert_checkout_step` takes a `before` or `after` option to determine where to +insert the step: + +```ruby +insert_checkout_step :new_step, :before => :address +# or +insert_checkout_step :new_step, :after => :address``` + +The `remove_checkout_step` will remove just one checkout step at a time: + +```ruby +remove_checkout_step :address +remove_checkout_step :delivery``` + +### Dash + +## Minor changes + +### API + +#### CheckoutsController + +The Spree API now has support for "checking out" an order. This API +functionality allows an existing order to be updated and advanced until +it is in the complete state. + +For instance, if you have an order in the "confirm" state that you would +like to advance to the "complete" state, make the following request: + +```bash +PUT /api/checkouts/ORDER_NUMBER``` + +For more information on using the new CheckoutsController, please see +the [Checkouts API Documentation](/api/checkouts). + +#### Versioned templates + +API responses can now be versioned by the [versioncake](https://github.com/bwillis/versioncake) gem. While this is not used in Spree at the moment, it is future-proofing the API. + +### Core + +#### Auto-rotation of images + +Images will now be auto-rotated to their correct orientation based on EXIF data from the original image. All EXIF data is then stripped from the image, resulting in a smaller final image size. + +<%= issue 2338 %> + +#### Sample data + +The sample data now exists within straight Ruby code. The previous YAML-backed +configuration was confusing and led to invalid data being inserted for sample data. + +<%= commit "cc2f55a27a154c0bf9d67e1dbef3c4761c68f8b8" %> + +#### Unique payment identifier + +Some payment gateways require payments to have a unique identifier. To solve this problem in Spree, each payment now has an `identifier` attribute which is generated when the payment is created. + +<%= issue 1998 %> +<%= commit "b543fd105c2d511cdc98f27223cd0f5b1f663e72" %> + +#### Removal of `CheckoutController#state_callback` + +The `state_callback` method in `CheckoutController` has been removed. Instead of this method, please use transition callbacks on the `Order.state_machine` instance instead. + +#### Tracking URL for shipments + +Shipping methods now have the ability to have tracking URLs. These can be used to track the shipments on external shipping providers' websites. + +<%= issue 2644 %> + +#### Mailers now can take IDs or model objects + +To help with potential background processing of mailers, all mailer actions can now take the ID of their respective object, or the object itself. + +<%= issue 2808 %> + +#### SSLRequirement deprecated in favour of ForceSSL + +Spree will now use the `config.force_ssl` setting of Rails to determine whether or not to use SSL. + +<%= issue 2410 %> + +#### MailMethod model no longer exists + +The `MailMethod` model no longer exists. Instead, it is a class and all configuration is now done through that class or through `Spree::Config` settings. + +<%= issue 2643 %> + +## Trivial changes + +Some of these changes may have made it into 1-3-stable or 1-2-stable as well. You may wish to check that branch for commits with the same message to make sure of this. + +* `ShippingMethod` labels can now be overriden by overriding the `adjustment_label` method. <%= issue(2222) %> +* Promotion rules that respond to `#products` will have their products considered in promotion adjustments. <%= issue 2363 %> +* Taxons and products are now joined with a `Classification` model. <%= issue 2532 %> <%= commit "0c594923a457d2f6050c936498d31df312d6153a" %> +* Fix call to `select_month` and `select_year` <%= issue 2259 %> +* Fix issue where `params[:keyword]`, not `params[:keywords]` was acknowledged on taxons/show.html.erb. <%= issue 2258 %> <%= issue 2270 %> +* Allow overriding of a shipping address's label <%= issue 2222 %> +* Guard against false positive Jirafe conversions <%= issue 2273 %> <%= issue 2211 %> <%= issue 2157 %> +* Add first_name and last_name aliases for Addresses <%= commit "ad119f9e21af9ba6f6ada96944fc758a6144c61c" %> +* Escape JavaScript within ecommerce tracking code <%= issue 2289 %> +* Slight refactoring of how preferences are fetched from preference store <%= commit "bddc49a5cf261ca9fcee45e4234bdb076eaa3feb" %> +* Fix issue where "New State" button on country page would not link to correct country. <%= issue 2281 %> +* Order#checkout_steps will now always include complete <%= commit "5110e12127840fb9b2e22f8b4f4c4fbee594de23" %> +* Fix issue where `flash[:commerce_tracking]` was hanging around too long. <%= issue 2287 %> +* Remove colons from translations in mail templates <%= issue 2278 %> +* A payment method can now control its `auto_capture?` method. <%= issue 2304 %> +* Added a preference to display a product without a price. <%= issue 2302 %> +* Improve 'out of stock' error message. <%= issue 1821 %> +* Track IP addresses for orders, for payment gateway reasons. <%= issue 2216 %> <%= issue 2257 %> +* Use protocol-specific URL for font. <%= issue 2316 %> +* First order promotion will now work with guest users. <%= issue 2306 %> +* Always show resend button on admin order page. <%= issue 2318 %> +* Allow a shipment to be made 'ready' once order has been made 'resumed' <%= issue 2317 %> +* PerItem calculator no longer fails if a rule doesn't respond to the `products` method. <%= issue 2322 %> +* `attachment_url` can now be configured from the admin backend. <%= issue 2344 %> +* Scale and precision have been corrected in split_prices_from_variants migration. <%= issue 2358 %> +* Localize error message for email validator. <%= issue 2364 %> <%= issue 2729 %> <%= issue 2730 %> +* Remove duplicate thumbnails on products/show. <%= issue 2361 %> +* Use line item price, not variant price, in return authorization price calculation JS. <%= issue 2342 %> +* Allow for a product's description to be put to the page raw <%= issue 2323 %> <%= issue 2874 %> <%= commit "30fdf083" %>. See also <%= commit "b616d84a78e426bdeb3e8e6251aaa9ce8757996d" %> and <%= issue 2518 %>. +* Promotions can now apply to orders which were created before the promotion. <%= issue 2388 %> <%= issue 2395 %> +* Allow Address#phone validation to be overridden. <%= issue 2394 %> +* Sort properties by alphabetical order on prototype form. <%= issue 2389 %> +* Introduce datepicker_field_value for displaying datepicker field values. <%= issue 2405 %> +* The 'New Product' button now appears on the edit product page. <%= issue 2407 %> +* option values in `Variant#options_text` are now sorted in a predictable order. <%= issue 2432 %> +* link_to_cart can now be used outside of Spree contexts. <%= issue 2441 %> +* Promotion adjustments will now be removed on orders which are not complete when the promotion is deleted. <%= issue 1046 %> <%= issue 2453 %> +* Adjustments are now displayed on the orders/show template. <%= issue 2449 %> +* Variant images are displayed in place of product images in admin/images <%= issue 2228 %> +* Product properties are now sortable. <%= issue 2464 %> +* Returned items can now be re-shipped. <%= issue 2475 %> +* LogEntry records are now saved for failed payments <%= issue 1767 %> +* Show full address in order confirmation <%= issue 2136 %> <%= issue 2511 %> +* Retrieve a list of variants from an order by calling `Order#variants`. <%= issue 2195 %> +* Fix group_by_products_id sometimes not being available as a scope. <%= issue 1247 %> +* Allow `meta_description` on `Spree::Product` to be as long as `text` will allow, rather than `string`. <%= issue 2611 %> +* Fix currency display issues when using Euro. <%= issue 2634 %> +* Fix issue where orders would become "locked" when initial payment had failed. <%= issue 2616 %> <%= issue 2570 %> <%= issue 2678 %> <%= issue 2585 %> <%= issue 2652 %> +* Check for unprocessed payments before transitioning to complete state <%= issue 2694 %> +* Added check to prevent skipping of checkout steps. <%= issue 2280 %> +* Improve the look of the coupon code on the checkout. <%= issue 2720 %> +* Admin tabs are now only displayed if user is authorized to see them. <%= issue 2626 %> +* Reduce minimum characters required for variant autocomplete <%= issue 2774 %> +* Added helper methods to shipment to calculate shipment item and total costs <%= issue 2843 %> +* Allow product scopes to be added to from an extension <%= issue 2608 %> +* Fix routing error caused by routing-filter and previous ghetto implementation of taxon autocomplete. <%= issue 2248 %> +* ActionMailer settings will no longer be re-configured if they're already set. <%= issue 2855 %> +* Fix issue where tax calculator computed taxes incorrectly for non-VAT taxes. <%= issue 2870 %> +* The coupon code field is now hidden when there are no possible coupon codes. <%= issue 2835 %> +* The `position` attribute for variants is now computed once the variant is saved. <%= issue 2744 %> +* Account for situation where `current_order` might return `nil` in an `OrdersController#update` call. <%= issue 2750 %> +* Fix case where ActiveMerchant would incorrectly process currencies without 2 decimal places. <%= issue 2930 %> +* Add US military states to default states. <%= issue 2769 %> +* Allow for specially overridden attributes for line items in the API. <%= issue 2916 %> +* Transition order as far to complete as it will go after editing customer details. <%= issue 2950 %> <%= issue 2433 %> +* Fix issue where going back a step in a cart could cause a `undefined method run_callbacks` error to be raised. <%= issue 2959 %> <%= issue 2921 %> +* Use DISTINCT ON to make product `in_taxon` scope really distinct in PostgreSQL <%= issue 2851 %> +* Run order update hooks when order is finalized too <%= issue 2986 %> diff --git a/guides/content/release_notes/2_1_0.md b/guides/content/release_notes/2_1_0.md new file mode 100644 index 00000000000..63847045323 --- /dev/null +++ b/guides/content/release_notes/2_1_0.md @@ -0,0 +1,289 @@ +--- +title: Spree 2.1.0 +section: version +--- + +## Major/new features + +### Rails 4 compatibility + +Spree 2.1.0 is the first Spree release which is Rails 4 compatible. Go ahead, try it out! + +### Breaking API changes + +Spree's API component has undergone some work to make it easier to build JavaScript-backed frontends. For example, our [experimental Spree+Marionette project](https://github.com/radar/spree-marionette). + +As a result, we have altered some parts of the API to make this process easier. Please check the API changelog below to see if anything in there affects you. + +### Better Spree PayPal Express extension + +We now have a [better Spree PayPal Express](https://github.com/radar/better_spree_paypal_express) extension which is fully compatible with this release. If you are looking for PayPal Express Checkout integration for your new Spree store, check out this extension. + +## API + +* The Products API endpoint now returns an additional key called `shipping_category_id`, and also requires `shipping_category_id` on create. + + *Jeff Dutil* + +* The Products API endpoint now returns an additional key called `display_price`, which is the proper rendering of the price of a product. + + *Ryan Bigg* + +* The Images API's `attachment_url` key has been removed in favour of keys that reflect the current image styles available in the application, such as `mini_url` and `product_url`. Use these now to references images. + + *Ryan Bigg* + +* Fix issue where calling OrdersController#update with line item parameters would *always* create new line items, rather than updating existing ones. + + *Ryan Bigg* + +* The Orders API endpoint now returns an additional key called `display_item_total`, which is the proper rendering of the total line item price of an order. + + *Ryan Bigg* + +* Include a `per_page` key in Products API end response so that libraries like jQuery.simplePagination can use this to display a pagination element on the page. + + *Ryan Bigg* + +* Line item responses now contain `single_display_amount` and `display_amount` for "pretty" versions of the single and total amount for a line item, as well as a `total` node which is an "ugly" version of the total amount of a line item. + + *Ryan Bigg* + +* /api/orders endpoints now accept a `?order_token` parameter which should be the order's token. This can be used to authorize actions on an order without having to pass in an API key. + + *Ryan Bigg* + +* Requests to POST /api/line_items will now update existing line items. For example if you have a line item with a variant ID=2 and quantity=10 and you attempt to create a new line item for the same variant with a quantity of 5, the existing line item's quantity will be updated to 15. Previously, a new line item would erroneously be created. + + *Ryan Bigg* + +* /api/countries now will a 304 response if no country has been changed since the last request. + + *Ryan Bigg* + +* The Shipments API no longer returns inventory units. Instead, it will return manifest objects. This is necessary due to the split shipments changes brought in by Spree 2. + + *Ryan Bigg* + +* Checkouts API's update action will now correctly process line item attributes (either `line_items` or `line_item_attributes`) + + *Ryan Bigg* + +* The structure of shipments data in the API has changed. Shipments can now have many shipping methods, shipping rates (which in turn have many zones and shipping categories), as well as a new key called "manifest" which returns the list of items contained within just this shipment for the order. + + *Ryan Bigg* + +* Address responses now contain a `full_name` attribute. + + *Ryan Bigg* + +* Shipments responses now contain a `selected_shipping_rate` key, so that you don't have to sort through the list of `shipping_rates` to get the selected one. + + *Ryan Bigg* + +* Checkouts API now correctly processes incoming payment data during the payment step. + + *Ryan Bigg* + +* Fix issue where `set_current_order` before filter would be called when CheckoutsController actions were run, causing the order object to be deleted. #3306 + + *Ryan Bigg* + +* An order can no longer transition past the "cart" state without first having a line item. #3312 + + *Ryan Bigg* + +* Attributes other than "quantity" and "variant_id" will be added to a line item when creating along with an order. #3404 + + *Alex Marles & Ryan Bigg* + +* Requests to POST /api/line_items will now update existing line items. For example if you have a line item with a variant ID=2 and quantity=10 and you attempt to create a new line item for the same variant with a quantity of 5, the existing line item's quantity will be updated to 15. Previously, a new line item would erroneously be created. + + * Ryan Bigg + +* Checkouts API's update action will now correctly process line item attributes (either `line_items` or `line_item_attributes`) + + * Ryan Bigg + +* Taxon attributes from `/api/taxons` are now returned within `taxons` subkey. Before: + +```json +[{ name: 'Ruby' ... }] +``` + +Now: + +```json +{ taxons: [{ name: 'Ruby' }]} +``` + + * Ryan Bigg + +## Backend + + +* layouts/admin.html.erb was broken into partials for each section. e.g. + header, menu, submenu, sidebar. Extensions should update their deface + overrides accordingly + + *Washington Luiz* + +* No longer requires all jquery ui modules. Extensions should include the + ones they need on their own manifest file. #3237 + + *Washington Luiz* + +* Symbolize attachment style keys on ImageSettingController otherwise users + would get *undefined method `processors' for "48x48>":String>* since + paperclip can't handle key strings. #3069 #3080 + + *Washington Luiz* + +* Split line items across shipments. Use this to move line items between + existing shipments or to create a new shipment on an order from existing + line items. + + *John Dyer* + +* Fixed display of "Total" price for a line item on a shipment. #3135 + + *John Dyer* + +* Fixed issue where selecting an existing user in the customer details step would not associate them with an order. + + *Ryan Bigg and dan-ding* + +* We now use [jQuery.payment](https://stripe.com/blog/jquery-payment) (from Stripe) to provide slightly better formatting on credit card number, expiry and CVV fields. + + *Ryan Bigg* + +* "Infinite scrolling" now implemented for products taxon search to prevent loading all taxons at once. Only 50 taxons are loaded at a time now. + + *Ryan Bigg* + +## Cmd + +No changes. + +## Core + + +* Product requires `shipping_category_id` on create #3188. + + *Jeff Dutil* + +* No longer set ActiveRecord::Base.include_root_in_json = true during install. + Originally set to false back in 2011 according to convention. After + https://groups.google.com/forum/#!topic/spree-user/D9dZQayC4z, it + was changed. Applications should now decide their own setting for this value. + + *Weston Platter* + +* Change `order.promotion_credit_exists?` api. Now it receives an adjustment + originator (PromotionAction instance) instead of a promotion. Allowing + multiple adjustments being created for the same promotion as the current + PromotionAction / Promotion api suggests #3262 + +* Remove after_save callback for stock items backorders processing and + fixes count on hand updates when there are backordered units #3066 + + *Washington Luiz* + +* InventoryUnit#backordered_for_stock_item no longer returns readonly objects + neither return an ActiveRecored::Association. It returns only an array of + writable backordered units for a given stock item #3066 + + *Washington Luiz* + +* Scope shipping rates as per shipping method display_on #3119 + e.g. Shipping methods set to back_end only should not be displayed on frontend too + + *Washington Luiz* + +* Add `propagate_all_variants` attribute to StockLocation. It controls + whether a stock items should be created fot the stock location every time + a variant or a stock location is created + + *Washington Luiz* + +* Add `backorderable_default` attribute to StockLocation. It sets the + backorderable attribute of each new stock item + + *Washington Luiz* + +* Removed `t()` override in `Spree::BaseHelper`. #3083 + + *Washington Luiz* + +* Improve performance of `Order#payment_required?` by not updating the totals every time. #3040 #3086 + + *Washington Luiz* + +* Fixed the FlexiRate Calculator for cases when max_items is set. #3159 + + *Dana Jones* + +* Translation for admin tabs are now located under the `spree.admin.tab` key. Previously, they were on the top-level, which led to conflicts when users wanted to override view translations, like this: + +```yml +en: + spree: + orders: + show: + thank_you: "Thanks, buddy!" +``` + +See #3133 for more information. + + * Ryan Bigg* + +* CreditCard model now validates that the card is not expired. + + *Ryan Bigg* + +* Payment model will now no longer provide a vague error message for when the source is invalid. Instead, it will provide error messages like "Credit Card Number can't be blank" + + *Ryan Bigg* + +* Calling #destroy on any PaymentMethod, Product, TaxCategory, TaxRate or Variant object will now no longer delete that object. Instead, the `deleted_at` attribute on that object will be set to the current time. Attempting to find that object again using something such as `Spree::Product.find(1)` will fail because there is now a default scope to only find *non*-deleted records on these models. To remove this scope, use `Spree::Product.unscoped.find(1)`. #3321 + + *Ryan Bigg* + +* Removed `variants_including_master_and_deleted`, in favour of using the Paranoia gem. This scope would now be achieved using `variants_including_master.with_deleted`. + + *Ryan Bigg* + +* You can now find the total amount on hand of a variant by calling `Variant#total_on_hand`. #3427 + + *Ruben Ascencio* + +* Tax categories are now stored on line items. This should make tax calculations slightly faster. #3481 + + *Ryan Bigg* + +* `update_attribute(s)_without_callbacks` have gone away, in favour of `update_column(s)` + + *Ryan Bigg* + +## Frontend + +* Fix issue where "Use Billing Address" checkbox was unticked when certain + browsers autocompleted the checkout form. #3068 #3085 + + *Washington Luiz* + +* Switch to new Google Analytics analytics.js SDK from ga.js SDK for custom dimensions & metrics. + + *Jeff Dutil* + +* We now use [jQuery.payment](https://stripe.com/blog/jquery-payment) (from Stripe) to provide slightly better formatting on credit card number, expiry and CVV fields. + + *Ryan Bigg* + +## Spree::ActiveShipping + +* Origin address fields (e.g., origin_country) have been removed from the Spree::ActiveShipping preferences. + + *Ryan Bigg* + + diff --git a/guides/content/release_notes/2_2_0.md b/guides/content/release_notes/2_2_0.md new file mode 100644 index 00000000000..fbb0266ac7a --- /dev/null +++ b/guides/content/release_notes/2_2_0.md @@ -0,0 +1,222 @@ +--- +title: Spree 2.2.0 +section: version +--- + +## Major/new features + +### Adjustments Refactoring + +The adjustments system in Spree has undergone a large portion of work. Adjustments (typically originating from promotions and taxes) can now be applied at a line item, shipment or order level. + +**This system has been designed to be backwards-compatible with older versions of Spree, so that an upgrade path is relatively easy. If you encounter any issues during an upgrade, please [file an issue](https://github.com/spree/spree/issues/new).** + +Along with this, taxes are now split into two groupings: "additional" and "included". Additional taxes are those which increase the price of the item they're attached to. Included taxes are those which are already included in the cost of the item. It is still necessary to track these included taxes due to tax reporting requirements in many countries. + +Shipments no longer have a linked adjustment. Instead, the shipment itself has a "cost" attribute which is used in the calculation of shipping costs for an order. + +Also worth noting is that the number of callbacks triggered when any aspect of an order is updated has been greatly reduced, which should lead up to speed-ups in stores. An example of this would be in prior versions of Spree, an order would trigger an update on all its adjustments when it updated. With the new system, only line items or shipments that change will have their adjustments updated. + +For more information about this, [Ryan Bigg wrote up a long explanation about it](http://ryanbigg.com/2013/09/order-adjustments/), and there is further discussion on #3567. + +### Fragment caching + +In certain places in the frontend, the following changes have been applied: + +* Fragment caching for each product. +* Fragment caching for the lists of products in home/index and products/index. +* Fragment caching for a taxon's children. + +This can lead to significant speedups in the frontend of a Spree store. + +See more about this in [this comment on spree/spree#2913](https://github.com/spree/spree/issues/2913#issuecomment-34946007). + + +### Asset renaming + +An issue was brought up in #4050 where a user showed us that a `require_tree` use inside `app/assets` would also require the Spree assets that were placed in `app/assets/store` and app/assets/admin` respectively. This would happen in areas of the application where Spree wasn't even used. + +To fix this bug, we have moved the location of the assets to `vendor/assets`. Frontend's assets are now placed in `vendor/assets/spree/frontend` and Backend's are in `vendor/assets/spree/backend`. + +Similar changes to this have also been made to extensions, where their assets are now placed in `app/assets/spree/[extension_name]`. Ultimately, these changes fix the bug and now we're using the same names to refer to the same components (store -> frontend, admin -> backend) on assets as we do internally to Spree. + +You will need to manually rename asset requires within your application: + +* `admin/spree_backend` => `spree/backend` +* `store/spree_frontend` => `spree/frontend` + +### Risk analysis + +The AVS and CVV response codes for payments are now checked to determine the possibility that an order is considered risky. If the order is considered risky, then it will transition to a 'considered risky' state upon finalize rather than 'complete'. The order must be approved in the admin backend in order for it to proceed to the 'complete' state. + +Stores may choose to override `Order#is_risky` to implement their own risk analysis for orders. + +See issues #4021 and #4298 for further information. + + +### Paperclip settings have been removed + +The ability to configure Paperclip settings for `Spree::Image` has been removed from Spree. The alternative to this is to configure the Paperclip settings for `Spree::Image` in an initializer: + + + Paperclip::Attachment.default_options[:s3_protocol] = "https" + Spree::Image.attachment_definitions[:attachment][:styles] = "" + Spree::Image.attachment_definitions[:attachment][:path] = "" + +These settings are for the Paperclip gem, and hence more information about them can be found in [Paperclip's documentation](http://rubydoc.info/gems/paperclip/Paperclip/ClassMethods). + +You may wish to use S3, in which case you can configure it using code [like this Gist](https://gist.github.com/radar/e414c49579b393e4aafe). + +## Minor changes + +### Core + + +* Switched to using friendly_id for permalink generation. This meant that we needed to rename `Spree::Product`'s `permalink` field to `slug`. + + Ryan Bigg + +* Add a `name` column to spree_payments. That should hold the *Name on card* + option in payment checkout step. + + Washington Luiz + +* Associate line item and inventory units for better extensibility with + product assemblies. Migration was added to set line_item_id for existing + inventory units. + +* A *channel* column was added to the spree_orders table. Users can set + it when importing orders from other stores. e.g. amazon + + Washington Luiz + +* Introduce `Core::UserAddress` module. Once included on the store user class + the user address can be rememembered on checkout + + Washington Luiz + +* Added tax_category to variants, to allow for different variants of a product to have different tax categories. #3946 + + Peter Rhoades + +* Removed `Spree::Activator`. Promotions are now activated using the `Spree::PromotionHandler` classes. + + Ryan Bigg + +* Promotion#event_name attribute has been removed. A promotion's event now depends on the fields that are filled out during its creation. + + Ryan Bigg + +* Simplified OrderPopulator to take only a variant_id and a quantity, rather than a confusing hash of product/variant ids. + + Ryan Bigg + +* lib/ is no longer in autoload paths. You'll have to manually require what + you need in that dir. See https://github.com/spree/spree/commit/b3834a1542e350034c1e9c5a8b13c00b2415e63b + +* Introduce Spree::Core::MailMethod to manage mail settings at each delivery. + This allows changes to mail settings to be applied without a server restart. + See https://github.com/spree/spree/commit/95df1aa7832912f73e34302d31b0abbbea3af709 + + John Hawthorn + +* Create Spree::Migrations to warn about missing migrations. See #4080 + + Washington Luiz + +* Variant#in_stock? now no longer takes a quantity. Call can_supply? instead. + see #4279 + + Ryan Bigg / Peter Berkenbosch + +* PromotionRule#activator_id column has been renamed to promotion_id. + + Ryan Bigg + +### API + +* Api requires authentication by default now + + Peter Berkenbosch + +* Improve products_controller #create and #update for better support to create + and update variants, option types and option values. + See #4172 and #4240 + + Bruno Buccolo / Washington Luiz / John Dyer + +* ApiHelpers attributes can now be extended without overriding instance + methods. By using the same approach in PermittedAttributes. e.g. + + Spree::Api::ApiHelpers.order_attributes.push :locked_at + + Washington Luiz + +* Admin users can set the order channel when importing orders. By sing the + channel attribute on Order model + + Washington Luiz + +* Cached products/show template, which can lead to drastically (65x) faster loading times on product requests. 806319709c4ce9a3d0026e00ec2d07372f51cdb8 + + Ryan Bigg + +* The parts that make up an order's response from /api/orders/:num are cached, which can lead to a 5x improvement of speed for this API endpoint. 80ffb1e739606ac02ac86336ac13a51583bcc225 + + Ryan Bigg + +* Cached variant objects which can lead to slightly faster loading times (4x) for each variant. + + Ryan Bigg + +* Added a route to allow for /api/variants/:id requests + + Ryan Bigg + +* Products response now contains a master variant separately from all the other variants. Previously all variants were grouped together. + + Ryan Bigg + +* Added API endpoint to retrieve a user's orders: /api/orders/mine. #4022 + + Richard Nuno + +* Order token can now be passed as a header: `X-Spree-Order-Token`. #4148 + + Lucjan Suski + +### Frontend + +* Payment step displays a name input so that users can enter *Name on card* + Previously we had a first_name and last_name hidden input instead. + + Washington Luiz + +* Checkout now may remember user address + + Washington Luiz + +### Backend + +* Don't serve JS to non XHR requests. Prevents sentive data leaking. Thanks to + Egor Homakov for pointing that out in Spree codebase. + See http://homakov.blogspot.com.br/2013/05/do-not-use-rjs-like-techniques.html + for details. + +* 'Only show completed orders' checkbox status will now persist when paging through orders. + + * darbs + Ryan Bigg + +* Implemented a basic Risk Assessment feature in Spree Backend. Viewing any Order's edit page now shows the following, with a status indicator: + + Payments; link_to new log feature (ie. Number of multiple failed authorization requests) + AVS response (ie. Billing address not matching credit card) + CVV response (ie. code not matching) + + * Ben Radler (aka lordnibbler) + +* Moved 'Taxonomies' out from under 'Configuration' menu. It now is a sub-menu item on the products. + + * Ryan Bigg + + diff --git a/guides/content/release_notes/2_3_0.md b/guides/content/release_notes/2_3_0.md new file mode 100644 index 00000000000..d1a64aa45d8 --- /dev/null +++ b/guides/content/release_notes/2_3_0.md @@ -0,0 +1,246 @@ +--- +title: Spree 2.3.0 +section: version +--- + +## Major/new features + +### Rails 4.1 support + +Rails 4.1 is now supported by Spree 2.3. If you wish to use Rails 4.1, Spree 2.3 is the release for you. + +### Preferences serialized on records + +Preferences are now stored on their records, rather than being stored in `spree_preferences`. This means that to fetch a preference for say, a calculator, one query needs to be done to the database for that row, as that row has the `preferences` column which contains all preferences. + +Previously, there would be a single DB call for the record itself, and then any number of database calls thereafter to fetch the required preference values for that record. What happens now is that there's only one database call, which means there should be some minor speedups. + +### Better multi-store support + +A `Spree::Store` model for basic multi-store/multi-domain support has been added. + +This provides a basic framework for multi-store/multi-domain, based on the +spree-multi-domain extension. Some existing configuration has been moved to +this model, so that they can have different values depending on the site +being served: + +* `Spree::Config[:site_name]` is moved to `name` +* `Spree::Config[:site_url]` is moved to `url` +* `Spree::Config[:default_meta_description]` is moved to `meta_description` +* `Spree::Config[:default_meta_keywords]` is moved to `meta_keywords` +* `Spree::Config[:default_seo_title]` is moved to `seo_title` + +A migration will move existing configuration onto a new default store. + +A new `ControllerHelpers::Store` concern provides a `current_store` helper +to fetch a helper based on the request's domain. + +### Better guest user tracking + +Now we are using a signed cookie to store the guests unique token +in the browser. This allows customers who close their browser to +continue their shopping when they visit again. More importantly +it allows you as a store owner to uniquely identify your guests orders. +Since we set `cookies.signed[:guest_token]` whenever a vistor comes +you may also use this cookie token on other objects than just orders. +For instance if a guest user wants to favorite a product you can +assign the `cookies.signed[:guest_token]` value to a token field on your +favorites model. Which will then allow you to analyze the orders and +favorites this user has placed before which is useful for recommendations. + +## Core + +* Drop first_name and last_name fields from spree_credit_cards. Add + first_name & last_name methods for now to keep ActiveMerchant happy. + + Jordan Brough + +* Replaced cookies.signed[:order_id] with cookies.signed[:guest_token]. + + Now we are using a signed cookie to store the guests unique token + in the browser. This allows customers who close their browser to + continue their shopping when they visit again. More importantly + it allows you as a store owner to uniquely identify your guests orders. + Since we set cookies.signed[:guest_token] whenever a vistor comes + you may also use this cookie token on other objects than just orders. + For instance if a guest user wants to favorite a product you can + assign the cookies.signed[:guest_token] value to a token field on your + favorites model. Which will then allow you to analyze the orders and + favorites this user has placed before which is useful for recommendations. + + Jeff Dutil + +* Order#token is no longer fetched from another table. + + Both Spree::Core::TokenResource and Spree::TokenizedPermission are deprecated. + Order#token value is now persisted into spree_orders.guest_token. Main motivation + here is save a few extra queries when creating an order. The TokenResource + module was being of no use in spree core. + + NOTE: Watch out for the possible expensive migration that come along with this + + Washington L Braga Jr + +* Replaced session[:order_id] usage with cookies.signed[:order_id]. + + Now we are using a signed cookie to store the order id on a guests + browser client. This allows customers who close their browser to + continue their shopping when they visit again. + Fixes #4319 + + Jeff Dutil + + +* Order#process_payments! no longer raises. Gateways must raise on failing authorizations. + + Now it's a Gateway or PaymentMethod responsability to raise a custom + exception any time an authorization fails so that it can be rescued + during checkout and proper action taken. + +* Assign request headers env to Payment when creating it via checkout. + + This might come in handy for some gateways, e.g. Adyen, actions that require + data such as user agent and accept header to create user profiles. Previously + we had no way to access the request headers from within a gateway class + +* More accurate and simpler Order#payment_state options. + + Balance Due. Paid. Credit Owed. Failed. These are the only possible values + for order payment_state now. The previous `pending` state has been dropped + and order updater logic greatly improved as it now mostly consider total + values rather than doing last payment state checks. + + Huge thanks to dan-ding. See https://github.com/spree/spree/issues/4605 + +* Config settings related to mail have been removed. This includes + `enable_mail_delivery`, `mail_bcc`, `intercept_email`, + `override_actionmailer_config`, `mail_host`, `mail_domain`, `mail_port`, + `secure_connection_type`, `mail_auth_type`, `smtp_username`, and + `smtp_password`. + + These should instead be [configured on actionmailer directly](http://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Configuration+options). + The existing functionality can also be used by including the [spree_mail_settings](https://github.com/spree-contrib/spree_mail_settings) gem. + + John Hawthorn + +* refactor the api to use a general importer in `lib/spree/importer/order.rb` + + Peter Berkenbosch + +* Ensure transition to payment processing state happens outside transaction. + + Chris Salzberg + +* Increase the precision of the amount/price columns in order for support other currencies. See https://github.com/spree/spree/issues/4657 + + Gonzalo Moreno + +* Preferences on models are now stored in a serialized `preferences` column instead of the `Spree::Preferences` table. + + `Spree::Preferences` are still used for configuration (like `Spree::Config`). + For models with preferences (`Calculator`, `PromotionRule`, and + `PaymentMethod` in spree core) they are now serialized using + `ActiveRecord::Base.serialize`, storing the preferences as YAML in the + `preferences` column. + + ``` + > c = Spree::Calculator.first + => #5, :currency=>"USD"}> + > c.preferred_amount + => 5 + > c.preferred_amount = 10 + => 10 + > c + => #10, :currency=>"USD"}> + ``` + + John Hawthorn + +* Add Spree::Store model for basic multi-store/multi-domain support + + This provides a basic framework for multi-store/multi-domain, based on the + spree-multi-domain extension. Some existing configuration has been moved to + this model, so that they can have different values depending on the site + being served: + + * `Spree::Config[:site_name]` is moved to `name` + * `Spree::Config[:site_url]` is moved to `url` + * `Spree::Config[:default_meta_description]` is moved to `meta_description` + * `Spree::Config[:default_meta_keywords]` is moved to `meta_keywords` + * `Spree::Config[:default_seo_title]` is moved to `seo_title` + + A migration will move existing configuration onto a new default store. + + A new `ControllerHelpers::Store` concern provides a `current_store` helper + to fetch a helper based on the request's domain. + + Jeff Dutil, Clarke Brunsdon, and John Hawthorn + +## API + +* Support existing credit card feature on checkout. + + Checkouts_controller#update now uses the same Order::Checkout#update_from_params + from spree frontend which help us to remove a lot of duplicated logic. As a + result of that `payment_source` params must be sent now outsite the `order` key. + + Before you'd send a request like this: + + ```ruby + api_put :update, :id => order.to_param, :order_token => order.guest_token, + :order => { + :payments_attributes => [{ :payment_method_id => @payment_method.id.to_s }], + :payment_source => { @payment_method.id.to_s => { name: "Spree" } } + } + ``` + + Now it should look like this: + + ```ruby + api_put :update, :id => order.to_param, :order_token => order.guest_token, + :order => { + :payments_attributes => [{ :payment_method_id => @payment_method.id.to_s }] + }, + :payment_source => { + @payment_method.id.to_s => { name: "Spree" } + } + ``` + + Josh Hepworth and Washington + +* api/orders/show now display credit cards as source under payment + + Washington Luiz + +* refactor the api to use a general importer in core gem. + + Peter Berkenbosch + +* Shipment manifests viewed within the context of an order no longer return variant info. The line items for the order already contains this information. #4498 + + * Ryan Bigg + +## Frontend + +* The api key that was previously placed in the dom for ajax requests has been + removed since the api now uses the session to authenticate the user. + +* Mostly inspired by Jeff Squires' extension spree_reuse_credit card, checkout + now can remember user credit card info. Make sure your user model responds + to a `payment_sources` method and customers will be able to reuse their + credit card info. + + Washington Luiz + +* Use settings from current_store instead of Spree::Config + + Jeff Dutil, John Hawthorn, and Washington Luiz + +## Backend + +* The api key that was previously placed in the dom for ajax requests has been + removed since the api now uses the session to authenticate the user. diff --git a/guides/content/release_notes/2_4_0.md b/guides/content/release_notes/2_4_0.md new file mode 100644 index 00000000000..915b0d97b28 --- /dev/null +++ b/guides/content/release_notes/2_4_0.md @@ -0,0 +1,132 @@ +--- +title: Spree 2.4.0 +section: version +--- + +## Major/new features + +### Return Authorization Rewrite + +Return Authorizations have received a major rewrite, and can be read more about in the updated [Returning Orders](http://guides.spreecommerce.com/user/returning_orders.html) guide. + +The previous return authorization code has been extracted into an extension if you would like to continue using it: +[https://github.com/spree-contrib/spree_legacy_return_authorizations](https://github.com/spree-contrib/spree_legacy_return_authorizations) + +### HTML Email Templates + +There are now default HTML email templates, which use [Zurb Ink](http://zurb.com/ink/templates.php) and [Premailer-Rails](https://github.com/fphilipe/premailer-rails) for responsive styling. + +### Guides moved into core repository + +The previous spree-guides repository has been merged into the [Spree Core](https://github.com/spree/spree) repository. Any documentation updates to this website should now be submitted with pull requests to [https://github.com/spree/spree](https://github.com/spree/spree) + +## Upgrade Tips + +### Read the upgrade guide + +For information about upgrading a basic spree store, please read the [2.3 to 2.4 upgrade guide](http://guides.spreecommerce.com/developer/upgrades/two-dot-three-to-two-dot-four.html). + +### Other Gotchas + +If you have implemented your own authentication system instead of using spree_auth_devise, +or you have implemented your own adjustment source please ensure you change your +concerns to the new namespace: + +```ruby +Spree::Core::AdjustmentSource => Spree::AdjustmentSource +Spree::Core::CalculatedAdjustments => Spree::CalculatedAdjustments +Spree::Core::UserAddress => Spree::UserAddress +Spree::Core::UserPaymentSource => Spree::UserPaymentSource +``` + +Also please review each of the noteable changes, and ensure your customizations +or extensions are not effected. If you are effected by a change, and have any +of your own tips please submit a PR to help the next person! + +## Full Changelog + +You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/2-3-stable...2-4-stable) + +## Noteable Changes +* The admin order creation/edit process now closely matches the frontend by allowing a separate cart and shipments page. + + Tyler Smart (tesserakt) + + +* Add authorize_payments! and capture_payments! to the public interface of orders. + + Richard Wilson + +* Spree no longer holds aws-sdk as a core dependency. In case you use it + you need to add it to your Gemfile. See paperclip README for reference on + scenarios where this is needed https://github.com/thoughtbot/paperclip/tree/v4.1.1#understanding-storage + + Washigton L Braga Jr + +* Added Spree::Config.auto_capture_on_dispatch that when set to true will + cause shipments to advance to ready state upon successfully authorizing + payment for the order. As each shipment is marked shipped the + shipment's total will be captured from the authorization. Fixes #4727 + + Jeff Dutil + +* Added `actionable?` for Spree::Promotion::Rule. `actionable?` defines + if a promotion action can be applied to a specific line item. This + can be used to customize which line items can accept a promotion + action by defining its logic within the promotion rule rather than + relying on Spree's default behaviour. Fixes #5036 + + Gregor MacDougall + +* Refactored Stock::Coordinator to optionally accept a list of inventory units + for an order so that shipments can be created for an order that do not comprise + only of the order's line items. + + Andrew Thal + +* Default ship and bill addresses are now saved and restored in callbacks. This + makes the default address functionality available to orders driven through + frontend, backend and API without duplicating the code. + + Magnus von Koeller + +* When a user successfully uses a credit card to pay for an order, that card + becomes the default credit card for that user. On future orders, we automatically + add that default card as a payment when the order reaches the payment step. + + Magnus von Koeller + +* Provided hooks for extensions to seamlessly integrate with the order population workflow. + Extensions make use of the convention of passing parameters during the 'add to cart' + action https://github.com/spree/spree/blob/master/core/app/models/spree/order_populator.rb#L12 + with a prefix like [:options][:voucher_attributes] (in the case of the spree_vouchers + extension). The extension then provides some methods named according to what was passed in + like: + + https://github.com/spree-contrib/spree_vouchers/blob/master/app/models/spree/order_decorator.rb#L51 + + to determine if these possible line item customizations affect the line item equality condition and + + https://github.com/spree-contrib/spree_vouchers/blob/master/app/models/spree/variant_decorator.rb#L3 + + to adjust a line item's price if necessary. + + https://github.com/spree/spree/blob/master/core/app/models/spree/order_contents.rb#L70 + shows how we expect inbound parameters (such as the voucher_attributes) to be saved in a + nested_attributes fashion. + + Jeff Squires + +* Rename *_filter callbacks to *_action callbacks. + + Masahiro Saito + +* Move some modules to model's concerns directory. + We move modules Spree::Core::AdjustmentSource, Spree::Core::CalculatedAdjustments, Spree::Core::UserAddress + and Spree::Core::UserPaymentSource. Fixes #5264. + + Masahiro Saito + +* Enable default html email templates with Zurb Ink, and PreMailer for Rails. + + Ben Morgan & Jeff Dutil diff --git a/guides/content/release_notes/3_0_0.md b/guides/content/release_notes/3_0_0.md new file mode 100644 index 00000000000..0ce702afef5 --- /dev/null +++ b/guides/content/release_notes/3_0_0.md @@ -0,0 +1,92 @@ +--- +title: Spree 3.0.0 +section: version +--- + +## Major/New Features + +### Bootstrap Backend & Frontend + +The most visible change will be the use of Bootstrap to replace the previous +Skeleton framework. We've redesigned the frontend, and backend while keeping +much of the previous structure to the site. We hope this will make upgrades +slightly easier to port to the new Bootstrap markup. Now that we've updated +the default css/js framework we're hoping to begin implementing usability +improvements & improved mobile support. + +### Rails 4.2 Support + +We've added Rails 4.2 support laying the groundwork for further improvements. +Rails 4.2 comes with ActiveJob a background job API, which we will leverage in +the future to send long running tasks to Sidekiq, DelayedJob or Resque if you +set them up. We've also already updated mailers to use the deliver later +functionality so you can send your confirmation emails in the background. + +### Google Analytics Enhanced Ecommerce + +With the change to [enhanced ecommerce](https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce) tracking you should make sure to [upgrade your Google Analytics account to Universal Analytics](https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#overview). If you use Spree's default google analytics implementation you should be fine, but if you've customized these make sure to update to [Universal Analytics](https://developers.google.com/analytics/devguides/collection/analyticsjs/). + +## Upgrade Tips + +### Read Rails 4.2 Release Notes & Upgrade Guide + +[Release Notes](http://edgeguides.rubyonrails.org/4_2_release_notes.html) + +[Upgrade Guide](http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2) + +### Read Spree Upgrade Guide + +For information about upgrading a basic spree store, please read the [2.4 to 3.0 upgrade guide](http://guides.spreecommerce.com/developer/upgrades/two-dot-four-to-three-dot-zero.html). + +### Other Gotchas + +#### SSL + +SSLRequirement controller concern was removed in favor of using Rails built in [ForceSSL](http://api.rubyonrails.org/classes/ActionController/ForceSSL/ClassMethods.html). + +It is recommended that you enable config.force_ssl = true within your production.rb file which will enforce SSL on every page. + +If you were previously using ssl_required in any controllers you should now use force_ssl at the controller level if you're not going to use force_ssl at the application level. + +#### PaymentMethod's and Tracker's are no longer based on environment. + +Previously payment methods and google analytics trackers could be assigned an environment, +such as, production/staging/development etc.. This is no longer the case. If you previously +relied on importing data from production to a development or staging environment you should +ensure to sanitize and/or update these credentials to prevent submitting payments or analytics +information to your production account credentials. + +We recommend that you begin to [manage your credentials with environment variables](http://www.gotealeaf.com/blog/managing-environment-configuration-variables-in-rails) instead. + +#### Noteable Changes + +Also please review each of the noteable changes, and ensure your customizations +or extensions are not effected. If you are affected by a change, and have any +of your own tips please submit a PR to help the next person! + +## Full Changelog + +You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/2-4-stable...3-0-stable). + +## Noteable Changes + +* Switched to Bootstrap. + + Jeff Dutil & [Other Contributors](https://github.com/200Creative/spree_bootstrap_frontend/graphs/contributors) + +* Moved Core's helpers into Frontend. + + Jeff Dutil + +* Use Google Analytics Enhanced ecommerce. + + https://github.com/erikaxel + +* PaymentMethod's and Tracker's are no longer based on environment. + + Clarke Brunsdon + +* Removed promo code field from cart page. This prevents issues with promos + attempting to be used before the order is ready. e.g. Free Shipping before shipments. + + Jeff Dutil diff --git a/guides/content/release_notes/index.md b/guides/content/release_notes/index.md new file mode 100644 index 00000000000..f6e03e84d98 --- /dev/null +++ b/guides/content/release_notes/index.md @@ -0,0 +1,12 @@ +--- +title: Spree +section: version +--- + +## Release Notes + +Each major new release of Spree has an accompanying set of release notes. The purpose of these notes is to provide a high level overview of what has changed since the previous version of Spree. + +If you are upgrading to a new version of Spree that is several versions ahead of your current version, it is suggested that you update one version at a time and follow the release notes associated with each update. + +You can find the detailed release notes for each version of Spree using the table of contents on the right hand side of this page. diff --git a/guides/content/user/config/configuring_analytics.md b/guides/content/user/config/configuring_analytics.md new file mode 100644 index 00000000000..318912f2ab6 --- /dev/null +++ b/guides/content/user/config/configuring_analytics.md @@ -0,0 +1,11 @@ +--- +title: Analytics Tracker +--- + +## Introduction + +Understanding your site's visitor traffic patterns are important for planning your company's marketing and growth strategies. + +### Google Analytics + +Spree's Admin Interface makes it easy for you to add the robust Google Analytics toolset to your site. [This blog entry](http://spreecommerce.com/blog/ecommerce-tracking-in-spree) covers all of the intricacies with registering a Google Analytics account, adding the tracker to your site, and testing the functionality. \ No newline at end of file diff --git a/guides/content/user/config/configuring_general_settings.md b/guides/content/user/config/configuring_general_settings.md new file mode 100644 index 00000000000..f9269f3ac01 --- /dev/null +++ b/guides/content/user/config/configuring_general_settings.md @@ -0,0 +1,85 @@ +--- +title: General Settings +--- + +## Introduction + +The General Settings section is where you will make site-wide settings about things like your store's name, security, and currency display. You can access this area by going to your Admin Interface, clicking "Configuration", then clicking the "General Settings link." + +![General Settings Configuration](/images/user/config/general_settings.jpg) + +Each setting on the page is covered below. + +### Site Name + +The Site Name is what is set in the `` tag of your website. It renders in your browser's title bar on every page of the public-facing area of the site. + +![Site Name in Title](/images/user/config/site_name_in_title.jpg) + +### Default SEO Title + +"SEO" stands for "Search Engine Optimization". It is a way to improve your store's visibility in search results. If you assign a value to the Default SEO Title field, it would override what you had set for the "Site Name" field. + +![SEO Title Override](/images/user/config/seo_title_override.jpg) + +### Default Meta Keywords + +Meta keywords give search engines more information about your store. Use them to supply a list of the words and phrases that are most relevant to the products you offer. These keywords show up in the header of your site. The header is not visible to the casual site visitor, but it does inform your rankings with web browsers. + +*** +For more information about Search Engine Optimization, try reading the [Google Webmaster Tools topic](https://support.google.com/webmasters/answer/35291?hl=en) on the subject. +*** + +### Default Meta Description + +Whereas meta keywords constitutes a comma-separated list of words and phrases, the meta description is a fuller, prose description of what your store is and does. The phrasing you use can help distinguish you from any other e-commerce websites offering products similar to yours. + +### Site URL + +The site's URL is the website address for your store, such as "http://myawesomestore.com". This address is used when your application sends out confirmation emails to customers about their purchases. + +### Security Settings + +Three of the four checkboxes in the "Security Settings" section of your General Settings cover which modes in which [SSL (Secure Sockets Layer)](http://en.wikipedia.org/wiki/Secure_Socket_Layer) can be used on your website. SSL is the way data is encrypted and sent securely through the Internet from the user to the server on which your store resides. + +By default, your store will use SSL only in production and staging. Production mode is commonly referred to as "live" mode - real data, real users, real transactions. Staging mode is similar to dress rehearsal - real data, possibly real users, fake transactions. + +Development mode is the mode your site's programmer is in as he works on your site. This is typically not deployed anywhere that real users could get to it, so SSL is usually not needed. + +Testing mode involves no outside user input at all; it is how your programmer tests functionality in an automated way before the application meets actual end users. + +If you're not sure how or whether to use SSL, ask your site's developer for guidance. + +The fourth checkbox - "Check for Spree Alerts" - will disable the polling and display of important security and release announcements from Spree Commerce, Inc. These alerts appear on the Admin Interface pages, and may be dismissed after you have read them. + +## Currency Settings + +The remaining settings all cover how currency is rendered in your store. + +![Currency Settings](/images/user/config/currency_settings.jpg) + +### Display Currency + +If you check this option, the three-letter symbol for the currency of your store is rendered next to each price. + +![Show Currency](/images/user/config/show_currency.jpg) + +### Hide Cents + +Checking this option renders all prices in your store to whole-dollar amounts. The system will not round up to the nearest dollar; it will simply drop anything after the decimal. + +### Choose Currency + +From this drop-down menu, select the currency of your store. Default is United States Dollars (USD). + +### Currency Symbol Location + +You can elect to have the currency symbol (if applicable) appear either before or after the amount. Default for USD is to have the "$" sign appear before the amount. + +### Currency Decimal Mark + +This is where you input what will separate whole amounts from partial amounts (cents). Default is to use a period (".") but you can change it to any character, symbol, or string that you want. + +### Currency Thousands Separator + +The default setting is ",", which takes a price of $1999.00 and renders it as "$1,999.00" (assuming you left the other settings at default). You can leave the thousands separator blank to have your store render the price as "$1999.00", or change it to anything you like. \ No newline at end of file diff --git a/guides/content/user/config/configuring_geography.md b/guides/content/user/config/configuring_geography.md new file mode 100644 index 00000000000..e14f5f5eb06 --- /dev/null +++ b/guides/content/user/config/configuring_geography.md @@ -0,0 +1,75 @@ +--- +title: Zones, Countries, and States +--- + +## Introduction + +Your Spree store allows you to make decisions about which parts of the world you will sell products to, and how those areas are grouped into geographical regions for the convenience of setting [shipping](shipments) and [taxation](taxation) policies. This is accomplished through the use of: + +* [zones](#zones) +* [countries](#countries), and +* [states](#states) + +### Zones + +Within a Spree store, zones are geographical groupings - collections of either states or countries. You can read all about zones in the [zones guide](zones), including how to [create zones](#zones#creating-a-zone), how to [add members to a zone](zones#adding-members-to-a-zone), and how to [remove members from a zone](zones#removing-members-from-a-zone). + +### Countries + +If you pre-loaded the seed data into your Spree store, then you already have several countries configured. You may want to edit items in this list based on your needs. To access the Countries list, go to your Admin Interface, click "Configuration", then click "Countries". + +![Countries List](/images/user/config/countries.jpg) + +#### Editing a Country + +![Edit Country Icon](/images/user/config/edit_country_icon.jpg) + +To edit a country, click the "Edit" icon next to the country. + +![Editing Country Form](/images/user/config/editing_country.jpg) + +On this page, you can input the country's name, its [ISO Name](https://www.iso.org/obp/ui/#search), and whether or not a state name is required at the time of checkout for orders either billed to or shipped to an address in this country. Click "Update" to save any changes. + +### States + +A Spree store pre-loaded with seed data already has all of the states in the US configured for it. + +![US States](/images/user/config/us_states_list.jpg) + +You can edit, remove, or add states to your store to suit your needs. + +#### Editing a State + +To edit an existing store, click the "Edit" icon next to its name in the list. + +![Edit State Icon](/images/user/config/edit_state_icon.jpg) + +You can change the name and abbreviation for the state. Click "Update" to save your changes. + +![Editing State](/images/user/config/editing_state.jpg) + +#### Removing a State + +To remove a state from your store, click the "Delete" icon next to its name in the list. + +![Deleting State Icon](/images/user/config/edit_state_icon.jpg) + +Click "OK" to confirm the deletion. + +#### Adding a State + +To add a state to your store, first select the country the state belongs to from the "Country" drop-down list. + +![Select a Country From the List](/images/user/config/countries_drop_down.jpg) + +Next, click the "New State" button. A data entry form appears. Enter the name and abbreviation for the new state, and click "Create". + +![New State Form](/images/user/config/new_state_form.jpg) + +The new state is created, and you can now edit or delete it like the other states. + +![State Added to List](/images/user/config/state_added.jpg) + +*** +Don't forget to add new states and countries to your store's [zones](zones), so the system can accurately calculate tax and shipping options. +*** \ No newline at end of file diff --git a/guides/content/user/config/configuring_inventory.md b/guides/content/user/config/configuring_inventory.md new file mode 100644 index 00000000000..a73c7db4a56 --- /dev/null +++ b/guides/content/user/config/configuring_inventory.md @@ -0,0 +1,79 @@ +--- +title: Inventory Settings +--- + +## Introduction + +The Spree store gives you a great deal of leverage in managing your business' inventory. You can set up multiple [stock locations](#stock-locations), each of which represents a physical location at which you store your products for delivery to customers. As you add new products and make sales, [stock movements](#stock-movements) are recorded. You can receive stock from a supplier, and even move products from one stock location to another by recording [stock transfers](#stock-transfers). All of this helps to keep your inventorying manageable and current. + +### Stock Locations + +To reach the Stock Locations management panel, go to your Admin Interface, click "Configuration", then click "Stock Locations". Your store should already have at least one default stock location. If you keep all of your inventory in one place, this may be all you need. + +#### Create a New Stock Location + +To add a stock location to your store, click the "New Stock Location" button. + +![New Stock Location](/images/user/config/new_stock_location.jpg) + +Here, you can input everything of relevance about your stock location: name, address, and phone are the most obvious. The three checkboxes on the right-hand side merit more explanation: + +* **Active** - Denotes whether the stock location is currently in operation and serving inventory for orders. +* **Backorderable Default** - Controls whether inventory items at this location can be backordered when they run out. You can still change this on an item-by-item basis as needed. +* **Propagate All Variants** - Checking this option when you create a new stock location will loop through all of the products you already have in your store, and create an entry for each one at your new location, with a starting inventory amount of 0. + +Input the values for all of the fields, and click "Create" to add your new stock location. + +#### Edit a Stock Location + +To edit a stock location, click the "Edit" icon next to it in the Stock Locations list. + +![Edit Stock Location Icon](/images/user/config/edit_stock_location_icon.jpg) + +Make the desired changes in the form and click "Update". + +#### Delete a Stock Location + +To remove a stock location, click the "Delete" icon next to it in the Stock Locations list. + +![Delete Stock Location Icon](/images/user/config/delete_stock_location_icon.jpg) + +Click "OK" to confirm the deletion. + +### Stock Movements + +Notice the "Stock Movements" link on the Stock Locations list. + +![Stock Movements Link](/images/user/config/stock_movements_link.jpg) + +Clicking this link will show you all of the stock movements that have taken place for this stock location, both positive and negative. + +Stock movements are actions that happen automatically through the normal management and functioning of your store. You do not have to (and in fact, can not) manually manipulate them. This is just a way for you to see which things are moving in and out of a particular stock location. + +### Stock Transfers + +If you have more than one stock location, your Spree store offers you a way to record the movement on inventory from one location to another: the stock transfer. + +To create a new stock transfer, go to your Admin Interface, click "Configuration", then "Stock Transfers", then click the "New Stock Transfer" button. + +![New Stock Transfer](/images/user/config/new_stock_transfer.jpg) + +You can enter an optional Reference Number - this could correlate to a PO number, a transfer request number, a tracking number, or any other identifier you wish to use. + +Next, select your Source and Destination stock locations. If you are receiving stock from a supplier, check the "Receive Stock" checkbox and the "Source" drop-down box will be hidden. + +Select a product variant from the "Variant" drop-down list and enter the quantity of that product being transferred. Click the "Add" button. + +![Stock Transfer Readied](/images/user/config/stock_transfer.jpg) + +*** +If you try to transfer an item that you do not have in stock at your Source location, the Spree system will record a stock transfer with a quantity of 0. +*** + +The new stock transfer is readied. Once you have added all of the items you want to transfer, click the "Transfer Stock" button. + +![Stock Transfer Complete](/images/user/config/stock_transfer_complete.jpg) + +Now when you look at the [Stock Movements](#stock-movements) for each of the stock locations, you see that there are two new entries that correspond to the stock transfer, both with a system-assigned "Action" number (actually, the id for the stock transfer). + +![Resulting Stock Movements](/images/user/config/resulting_stock_movements.jpg) \ No newline at end of file diff --git a/guides/content/user/config/configuring_mail_methods.md b/guides/content/user/config/configuring_mail_methods.md new file mode 100644 index 00000000000..41f12027b36 --- /dev/null +++ b/guides/content/user/config/configuring_mail_methods.md @@ -0,0 +1,33 @@ +--- +title: Mail Methods +--- + +<%= warning "Spree mail settings has been extracted out into a gem in favor of generic action mailer settings." %> + +## Introduction + +As this has been extracted, please be sure to add [spree_mail_settings](https://github.com/spree-contrib/spree_mail_settings) to your Gemfile before proceeding if you desire this behavior. + +The configurable components of your Spree site are managed in the Mail Method Settings panel. You can reach this by going first to the Admin Interface, clicking "Configuration" and then "Mail Method Settings". + +![Mail Method Settings](/images/user/config/mail_method_settings.jpg) + +### Enable Mail Delivery + +Checking the "Enable Mail Delivery" option will cause all of the confirmation and notification emails the Spree shopping cart system generates to be sent. You may want to disable this option if you want to test other functionality of the store without sending bogus emails. + +### Send Mails As + +Set this to the email address you want to use as the "From" line on emails that are auto-generated by your store. + +### Send Copy of All Mails To + +You may want to keep track of the emails your store sends, especially if you are newly launching your e-commerce business. If so, you can configure the system to send a copy of all confirmation and notification emails to the email address you input for this setting. + +### Intercept Email Address + +Setting this option causes any notification emails to be re-routed to the email address you declare. + +### SMTP Settings + +The SMTP Mail Method settings allow you to fully configure your Spree store's server to send out email messages via [SMTP - Simple Mail Transfer Protocol](http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol). A full explanation of how SMTP works is beyond the scope of this user guide, but any changes you or your site's developer need to make can be done through this area of your Admin Interface. diff --git a/guides/content/user/config/configuring_shipping.md b/guides/content/user/config/configuring_shipping.md new file mode 100644 index 00000000000..65d0ecbd7d3 --- /dev/null +++ b/guides/content/user/config/configuring_shipping.md @@ -0,0 +1,13 @@ +--- +title: Shipping Settings +--- + +## Introduction + +$$$ +Explain how shipping methods and shipping categories relate to one another. +$$$ + +# Shipping Methods + +# Shipping Categories diff --git a/guides/content/user/config/configuring_taxes.md b/guides/content/user/config/configuring_taxes.md new file mode 100644 index 00000000000..b6e15995139 --- /dev/null +++ b/guides/content/user/config/configuring_taxes.md @@ -0,0 +1,69 @@ +--- +title: Taxes +--- + +## Introduction + +Taxation, as you undoubtedly already know, is a very complicated topic. It can be challenging to manage taxation settings in an e-commerce store - particularly when you sell a variety of types of goods - but the Spree shopping cart system gives you simple, yet effective tools to manage it with ease. + +There are a few concepts you need to understand in order to configure your site adequately: + +* [Tax Categories](#tax-categories) +* [Zones](#zones) +* [Tax Rates](#tax-rates) + +## Tax Categories + +Tax Categories is Spree's way of grouping products into those which are taxed in the same way. This is behind-the-scenes functionality; the customer never sees the category a product is in. They only see the amount they will be charged based on their order's delivery address. + +To access your store's existing Tax Categories, go to your Admin Interface, click "Configuration" then "Tax Categories". + +![Tax Categories](/images/user/config/tax_categories.jpg) + +You can edit existing Tax Categories by clicking the "Edit" icon next to each in the list. + +![Edit Tax Category Link](/images/user/config/edit_tax_category_link.jpg) + +You can also remove a Tax Category by clicking the "Delete" icon next to the category, then clicking "OK" to confirm. + +![Delete Tax Category Link](/images/user/config/delete_tax_category_link.jpg) + +To create a new Tax Category, click the "New Tax Category" button. + +![New Tax Category Form](/images/user/config/new_tax_category_form.jpg) + +You supply a name, an optional description, and whether or not this is the default tax category for this store. + +Each product in your store will need a tax category assigned to it to accurately calculate the tax due on an order. Any product that does not have a tax category assigned will be put in the default tax category. If there is no default tax category set, the item will be treated as non-taxable. + +## Zones + +In addition to a product's tax category, the zone an order is being shipped to will play a role in determining the tax amount. You can read more about how zones work in the [Zones guide](zones). + +## Tax Rates + +Tax rates are how it all comes together. A product with a given [Tax Category](#tax-categories), being shipped to a particular [Zone](#zones), will accrue tax charges based on the relevant tax rate that you create. + +To add a new Tax Rate, go to your Admin Interface. Click "Configuration" then "Tax Rates". + +![Tax Rates](/images/user/config/tax_rates.jpg) + +Here, you can see all of your existing tax rates and how they are configured. To create a new tax rate, click the "New Tax Rate" button. + +![New Tax Rate](/images/user/config/new_tax_rate.jpg) + +* **Name** - Give your new tax rate a meaningful name (like "Taxable US Goods", for example) +* **Zone** - You'll need to make separate tax rates for each zone you serve, per category. Suppose you have a "Clothing" tax category, and you sell to both the US and Europe. You'll need to make two different tax rates - one for the US zone, and one for the European zone. +* **Rate** - The actual percentage amount you are charging. 8% would be expressed as .08 in this field. +* **Tax Category** - The [tax category](#tax-categories) that relates to this tax rate. +* **Included in Price** - Check this box if you have already added the cost of tax into the price of the items. +* **Show Rate in Label** - When this box is checked, order summaries will include the tax rate, not just the tax amount. +* **Calculator** - By default, Spree uses the Default Tax calculator (a simple tax rate times item price adjusted for any promotions) calculation to determine tax. If you need something more specific or customized than this, it can be done - you'll just need to work with your development team to make it happen. + +## Tax Settings + +Finally, European stores will benefit from the Tax Settings page. + +![Tax Settings](/images/user/config/tax_settings.jpg) + +When this option is checked, your Spree site will take its default [tax category](#tax_categories), find the corresponding [tax rate](#tax-rate), and multiply it times the shipping rate for each available [shipping method](shipping_methods) offered to a customer during checkout. \ No newline at end of file diff --git a/guides/content/user/config/configuring_taxonomies.md b/guides/content/user/config/configuring_taxonomies.md new file mode 100644 index 00000000000..71c172e58c9 --- /dev/null +++ b/guides/content/user/config/configuring_taxonomies.md @@ -0,0 +1,109 @@ +--- +title: Taxonomies +--- + +## Introduction + +Taxonomies are the Spree system's approach to category trees. The heading of a tree is called a _Taxonomy_. Any child branches are called _Taxons_. Taxons themselves can have their own branches. So you could have something like the following categories structure: + +![Taxonomy Tree](/images/user/config/taxonomy_tree.jpg) + +In this example, "Categories" is the name of the Taxonomy. It has three child branches - "Luggage", "Housewares", and "Clothing". The last two branches each have three of their own child branches. Thus, "Clothing" is a child taxon to "Categories" and a parent taxon to "Men's". + +To reach the Taxonomies list, first go to your Admin Interface, then click "Configurations" and "Taxonomies". + +### Create a New Taxonomy + +To create a new taxonomy, click the "New Taxonomy" button. Enter a name for the taxonomy and click "Create". + +![New Taxonomy](/images/user/config/new_taxonomy.jpg) + +You can then [add child taxons](#adding-a-taxon-to-a-taxonomy) to your new taxonomy. + +### Edit a Taxonomy + +To edit an existing taxonomy, click the "Edit" icon next to the name in the Taxonomies list. + +![Edit Taxonomy Icon](/images/user/config/edit_taxonomy_icon.jpg) + +Here, you can change the name of the Taxonomy. You can also [reorder the child taxons](#reorder-a-taxon), [delete a taxon](#delete-a-taxon), [add a new taxon](#adding-a-taxon-to-a-taxonomy), or [edit a taxon](#edit-a-taxon). Make your changes, then click the Update button. + +![Edit Taxonomy](/images/user/config/edit_taxonomy.jpg) + +### Delete a Taxonomy + +![Delete Taxonomy Icon](/images/user/config/delete_taxonomy_icon.jpg) + +To delete a taxonomy, click the "Delete" icon next to the name in the Taxonomies list. Click "OK" to confirm. + +### Adding a Taxon to a Taxonomy + +Once you have created a taxonomy, you may want to add child taxons to it. Do do this, right-click the name of the Taxonomy, and click "Add". + +![Add Taxon to Taxonomy](/images/user/config/add_taxon_to_taxonomy.jpg) + +This will cause a new input field to open up, with "New node" in it. Replace this text with the name of your new taxon, and hit Enter. You'll now see the child tax in the taxonomy tree. + +![New Taxon](/images/user/config/new_taxon.jpg) + +Click "Update" to save your addition. + +### Adding a Taxon to Another Taxon + +If your site needs sub-trees, just add taxons to other taxons. To do so, right-click the name of what will become the parent taxon, and click Add. + +![Add Taxon to Another Taxon](/images/user/config/add_taxon_to_taxon.jpg) + +Enter the name of the child taxon and click enter. Repeat this process for any sub-trees you need. + +![Complex Taxonomy Tree](/images/user/config/complex_taxonomy_tree.jpg) + +Remember to save your changes by clicking the "Update" button after you have added any taxons. + +*** +When you navigate away from your taxonomy's page, then navigate back to it, sub-trees will be collapsed by default. To see child taxons, just click the arrow next to the parent taxon. +*** + +### Reorder a Taxon + +Taxons are displayed in the order you add them by default. To reorder them, just drag and drop them to their correct location in the tree. + +Let's assume, for example, that we want the "Children's" taxon to be listed first, above "Women's" and "Men's". Just drag and drop the taxon to its new location. + +![Reordering Taxons](/images/user/config/reorder_taxons.jpg) + +You can even drag a parent taxon into the tree of a different parent taxon, merging it into the second taxon's sub-tree. + +![Parent-to-Parent Taxon Merge](/images/user/config/parent_into_parent_taxon_merge.jpg) + +### Edit a Taxon + +To edit a taxon's name, just right-click it and click "Edit". + +![Edit Taxon Form](/images/user/config/edit_taxon.jpg) + +Here, you can edit several aspects of the taxon: + +* **Name** - A required field for all taxons. The name determines what the user will see when they look at this product in your store. +* **Permalink** - The end of the URL a user goes to, in order to see all products associated with this taxon. This field is also required, and a value is automatically generated for you when you create the taxon. Be careful with arbitrarily changing the permalink - if you have two taxons with the same permalink you will run into issues. +* **Icon** - This option is currently not functional. +* **Meta Title** - Overrides the store's setting for page title when a user visits the taxon's page on the front end of the website. +* **Meta Description** - Overrides the store's setting for meta description when a user visits the taxon's page on the front end of the website. +* **Meta Keywords** - Overrides the store's setting for meta keywords when a user visits the taxon's page on the front end of the website. +* **Description** - This option is currently not functional. + +Remember to click "Update" after you make your changes. + +### Delete a Taxon + +To delete a taxon, right-click it in the taxonomy tree and click "Remove". + +![Remove a Taxon](/images/user/config/remove_taxon.jpg) + +Click "OK" to confirm. + +### Associating Products with Taxons + +To associate a product with one or more taxons, go to the Admin Interface, and click the "Products" tab. Locate the product you want to edit, and click its "Edit" icon. Select the taxons for the product in the Taxons field. + +![Add Taxons to a Product](/images/user/config/add_taxons_to_product.jpg) \ No newline at end of file diff --git a/guides/content/user/configuration.md b/guides/content/user/configuration.md new file mode 100644 index 00000000000..f248d45a365 --- /dev/null +++ b/guides/content/user/configuration.md @@ -0,0 +1,9 @@ +--- +title: Configuration +--- + +## Configuration + +The Configuration page of the Admin Interface is that area of your store where you implement decisions about how you want your store to be set up. This is where you decide which shipping methods you offer, which categories you assign to product, how you want currency displayed, and dozens of other settings. + +The guides in this section will walk you through making all of those configuration decisions, and show you how to customize your Spree store to best fit your particular needs. diff --git a/guides/content/user/extensions.md b/guides/content/user/extensions.md new file mode 100644 index 00000000000..1d192f4ad88 --- /dev/null +++ b/guides/content/user/extensions.md @@ -0,0 +1,35 @@ +--- +title: Common Add-Ons +--- + +## Introduction + +Spree is a powerful e-commerce platform in its own right. Right out of the gate, it offers you a lot of functionality that can be customized to fit your store's particular needs. + +Some customization scenarios are so common that developers have bundled them into [extensions](/developer/extensions_tutorial) - modules which can be added on to your store to extend its functionality. Listed below are some of the most commonly-used extensions, each with a short description of its use. Consider asking your developer(s) to add one or more to your store. To see a complete list of Spree extensions, browse our [extension library](http://spreecommerce.com/extensions). + +All of these extensions are open source - meaning they, like Spree itself, are offered free of charge. It also means they require support from the community contributors to keep them current and documented. + +## better_spree_paypal_express +[better_spree_paypal_express](https://github.com/spree-contrib/better_spree_paypal_express) allows your store to accept payments for orders using [Paypal](https://www.paypal.com/). Paypal has over 130 million active registered accounts, and supports more than 25 currencies, making its use mission-critical for many e-commerce stores. + +## spree_gateway +[spree_gateway](https://github.com/spree/spree_gateway) is a collection of payment gateways that your store can use for processing payments. It supports dozens of both [direct payment gateways](https://github.com/Shopify/active_merchant/blob/master/README.md#supported-direct-payment-gateways) and [offsite payment gateways](https://github.com/Shopify/active_merchant/blob/master/README.md#supported-offsite-payment-gateways). + +## spree_wishlist +Consumers of e-commerce have come to expect Wish List functionality on most stores. With the [spree_wishlist](https://github.com/spree/spree_wishlist) extension, you can easily add this functionality to your own stores. This should help to drive up sales at your store, widen your customer base, and improve customer satisfaction. + +## spree_email_to_friend +If a potential customer spots a product in your store that they want to tell a friend about, you want to make it as easy as possible for them to do so. That's where the [spree_email_to_friend](https://github.com/spree/spree_email_to_friend) extension comes in. + +## spree_reviews +[spree_reviews](https://github.com/spree/spree_reviews) enables logged-in users to submit ratings and reviews for the products in your store. You can then moderate these reviews as necessary. + +## spree_social +Use of the [spree_social](https://github.com/spree/spree_social) extension gives your store all of the usual tie-ins with widely-used social networking sites, including Twitter, Facebook, Github, and Google. + +## spree_multi_currency +[spree_multi_currency](https://github.com/spree/spree_multi_currency) gives you the UI (user interface) and behind-the-scenes functionality to allow for multiple currencies within your Spree store. + +## spree-multi-domain +Have you ever wanted to have multiple stores, all managed in one central location? Then [spree-multi-domain](https://github.com/spree/spree-multi-domain) is what you need! This allows you to have several domain names all pointed to the same Spree application, but appearing to be completely separate entities. Your multiple domains can each have their own stylesheet and layout, and products can be assigned to one or more stores. diff --git a/guides/content/user/index.md b/guides/content/user/index.md new file mode 100644 index 00000000000..d77f98c8b58 --- /dev/null +++ b/guides/content/user/index.md @@ -0,0 +1,11 @@ +--- +title: Spree User Documentation +--- + +## Spree User Documentation + +Welcome to the Spree User Guides! This documentation is intended for business owners and site administrators of Spree e-commerce sites. Everything you need to know to configure and manage your Spree store can be found here. + +Should you find any errors in these guides, or topics you wish to see covered, please let us know by joining, then sending an email to the [Spree user mailing list](http://groups.google.com/group/spree-user). + +If you are a Spree developer, you may find the [Developer Guides](\developer/index) to be of benefit to you, though we strongly urge you to read through both sets of guides. \ No newline at end of file diff --git a/guides/content/user/orders.md b/guides/content/user/orders.md new file mode 100644 index 00000000000..2611bcb52fd --- /dev/null +++ b/guides/content/user/orders.md @@ -0,0 +1,16 @@ +--- +title: Orders +--- + +## Orders + +Much of your time administering your Spree store will be spent manipulating customer orders - processing payments, issuing refunds, confirming shipments, etc. + +In these guides, you will learn how to: + +* [Process orders](processing_orders) (capture payments, record shipments) +* [Manually enter an order](entering_orders) +* [Edit an order](editing_orders) +* [Understand the states an order goes through](order_states) +* [Process the return of an order](returning_orders) +* [Search through your orders](searching_orders) \ No newline at end of file diff --git a/guides/content/user/orders/editing_orders.md b/guides/content/user/orders/editing_orders.md new file mode 100644 index 00000000000..b3b94b19159 --- /dev/null +++ b/guides/content/user/orders/editing_orders.md @@ -0,0 +1,32 @@ +--- +title: Editing an Order +--- + +## Introduction + +There will come times when you need to edit orders that are placed in your store. Some examples: + +* A customer may call you to adjust the quantity of items they want to purchase. +* You may sell out of an item and need to remove it from an item altogether. +* You may need to change the shipping being charged on an order. +* The customer holds a store credit you need to manually apply to their order. + +## Editing an Order + +First, go to your Admin Interface. Click the "Orders" tab, and [locate the order](searching_orders) you want to change. + +![Edit Order Link](/images/user/orders/edit_order_link.jpg) + +This will bring up the order edit page: + +![Order Edit Page](/images/user/orders/order_edit.jpg) + +You can change any of the following components of an order from here: + +* [The types and quantities of products](entering_orders#add-products) +* [Shipping method](entering_orders#shipments) +* Tracking details for shipments +* [Customer information](entering_orders#customer-details) +* [Adjustments](entering_orders#adjustments) +* [Payment information](entering_orders#payments) +* [Return authorizations](returning_orders) \ No newline at end of file diff --git a/guides/content/user/orders/entering_orders.md b/guides/content/user/orders/entering_orders.md new file mode 100644 index 00000000000..35519e64198 --- /dev/null +++ b/guides/content/user/orders/entering_orders.md @@ -0,0 +1,136 @@ +--- +title: Manual Order Entry +--- + +## Introduction + +An order can be created one of two ways: + +1. An order is generated when a customer purchases an item from your store. +2. An order can be created manually from the Admin Interface for your store. + +This guide covers how to create a manual order from the Admin Interface. + +## Add Products + +To create a new manual order, go into the Admin Interface, click the "Orders" tab, and click the "New Order" button. + +![Create New Order](/images/user/orders/create_new_order.jpg) + +Type the name of the product you would like to add to the order in the search field. A list of matching product and variant combinations will return in the drop-down menu. Select the product/variant option you want to add to the order. + +![Create New Order](/images/user/orders/order_product_search.jpg) + +The system will tell you how many of that product/variant you currently have on hand. Enter the quantity to add to the new order, and click the "Add" icon next to the item. + +![Product Added to Order](/images/user/orders/order_product_added.jpg) + +The system creates the order and shows you the line items in it. + +![Manual Order With Product](/images/user/orders/manual_order_with_product.jpg) + +Follow the same steps to add more products to the order. + +## Customer Details + +Click the "Customer Details" link. You can either select a name from the "Customer Search" field if the customer has ordered from you before, or you can enter the customer's email address in the "Email" field of the "Account" section. The setting for "Guest Checkout" will automatically change accordingly. + +Enter the customer's billing address and the shipping address for the order. You can click the "USE BILLING ADDRESS" checkbox to use the same address for both. If you do so, the shipping address fields will become invisible. + +![Enter Customer Details](/images/user/orders/order_customer_details.jpg) + +Click the "Update" button. + +## Shipments + +After you input the customer information, you will want to choose a shipping method. To do so, click the "Order Details" link in the "Order Information" section of the page. + +![Order Details Link](/images/user/orders/order_details_link.jpg) + +The default shipping method for your store (if you have one) should already be assigned to this order. Depending on the items you added and the location you're shipping to, there could be additional methods available. You may also have shipping methods that are only available for your site's administrator to assign (in-store pickup, for example). + +Click the "Edit" link next to the order's shipping line. + +![Edit Order Shipping Info](/images/user/orders/edit_shipping_on_order_link.jpg) + +Click the "Shipping Method" drop-down menu, and make your selection. + +![Edit Shipping Options](/images/user/orders/edit_shipping_options.jpg) + +Click the "Save" icon to confirm your change. Your Spree site will re-calculate the shipping, any relevant adjustments, and total for your order. + +## Adjustments + +The Spree shopping cart will automatically add the cost of the Shipping Method to your order as an adjustment - you can change or remove this. + +### Editing Adjustments + +To edit an existing order adjustment, just click the "Adjustments" link in the order summary, then click the "Edit" icon next to the adjustment in the Adjustments list. + +![Edit Adjustment Icon](/images/user/orders/edit_adjustment_icon.jpg) + +### Deleting Adjustments + +To remove an adjustment, click the "Delete" icon next to it in the Adjustments list. + +![Delete Adjustment Icon](/images/user/orders/delete_adjustment_icon.jpg) + +Confirm the deletion by clicking "OK". + +### Opening and Closing Adjustments + +Some types of adjustments - tax and shipping, for example - may re-calculate as the order changes, new products are added to it, etc. If you want to be sure that the amount of an adjustment will remain the same, you can lock it in place. This is also known as closing the adjustment. + +*** +Closed adjustments can be re-opened and changed, up to the moment when the order is shipped. At that point, the adjustment is finalized and cannot be changed. +*** + +To lock/close an individual adjustment, click the closed-lock icon at the end of the adjustment row. + +![Close Adjustment Icon](/images/user/orders/close_adjustment_icon.jpg) + +Doing so will change the state of the adjustment to "closed", and that change will then be reflected in the adjustments list. + +![Closed Adjustment](/images/user/orders/closed_adjustment.jpg) + +To open an individual adjustment, click the open-lock icon at the end of the adjustment row. + +![Open Adjustment Icon](/images/user/orders/open_adjustment_icon.jpg) + +If you want to open or close all of the adjustments on an order, just click the "Open All Adjustments" or "Close All Adjustments" buttons on the Adjustments list. + +![Mass Open and Close Adjustments](/images/user/orders/mass_open_close_adjustments.jpg) + +### Adding Adjustments + +You can also add further adjustments - positive or negative - to the order to account for things like handling charges, store credits, etc. To add a new adjustment, click the "New Adjustment" button. + +![New Adjustment Button](/images/user/orders/new_adjustment_button.jpg) + +You need only enter the "Amount" (positive for a charge on the order; negative for a credit) and a "Description", then click the "Continue" button. + +![New Adjustment Form](/images/user/orders/new_adjustment_form.jpg) + +For a fuller understanding of adjustments, please read the [Developer Adjustments Guide](/developer/adjustments). + +Once you have finished all of the changes you want to the order's Adjustments, click "Continue". + +## Payments + +If you are manually entering this order, it is presumed that you have received payment either in person, on the phone, or through some other non-website means. You can manually enter payment using any of your site's configured [payment methods](payment_methods). + +Just click the "Payments" link in the "Order Information" section. + +![Payments Link](/images/user/orders/payments_link.jpg) + +This form is pretty self-explanatory; you enter the amount of the payment, the method, and the credit card information (if applicable). + +One thing to note is that you can enter multiple payments on the same order. This is useful if, for example, a customer wants to pay part of their order in cash and put the rest on a credit card. In that case, all you have to do is create the first payment for the Cash amount, check the "Cash" payment method (be sure you have it configured in your store first!), and click Update. + +Then, click the "New Payment" link to enter the information for the credit card portion of the payment. + +![New Payment Method Link](/images/user/orders/new_payment_method_link.jpg) + +Don't forget that you will need to [capture the payment](payment_states#authorize-vs-capture) on the credit card (unless your store is set up to automatically authorize and capture a payment when it is made). + +For more on payments, be sure to read both the [Payment Methods](payment_methods) and [Payment States](payment_states) guides. \ No newline at end of file diff --git a/guides/content/user/orders/order_states.md b/guides/content/user/orders/order_states.md new file mode 100644 index 00000000000..9991259c612 --- /dev/null +++ b/guides/content/user/orders/order_states.md @@ -0,0 +1,22 @@ +--- +title: Order States +--- + +## Introduction + +A new order is initiated when a customer places a product in their shopping cart. The order then passes through several states before it is considered `complete`. The order states are listed below. An order cannot continue to the next state until the previous state has been successfully satisfied. For example, an order cannot proceed to the `delivery` state until the customer has provided their billing and shipping address for the order during the `address` state. + +## Order States + +The states that an order passes through are as follows: + +* `cart` - One or more products have been added to the shopping cart. +* `address` - The store is ready to receive the billing and shipping address information for the order. +* `delivery` - The store is ready to receive the shipping method for the order. +* `payment` - The store is ready to receive the payment information for the order. +* `confirm` - The order is ready for a final review by the customer before being processed. +* `complete` - The order has successfully completed all of the previous states and is now being processed. + +*** +The states described above are the default settings for a Spree store. You can customize the order states to suit your needs utilizing our API. This includes adding, removing, or changing the order of certain states. Customization details are provided in the [Checkout Flow API Guide](/developer/checkout.html#checkout-customization). +*** diff --git a/guides/content/user/orders/processing_orders.md b/guides/content/user/orders/processing_orders.md new file mode 100644 index 00000000000..916b7b9306d --- /dev/null +++ b/guides/content/user/orders/processing_orders.md @@ -0,0 +1,50 @@ +--- +title: Processing Orders +--- + +## Introduction + +Once an order comes into your store - whether it is entered by a customer through the website frontend or you [manually enter it yourself](entering_orders) through the admin backend - it needs to be processed. That means these steps need to be taken: + +1. Verify that the products are, in fact, in stock and shippable. +2. Process the payment(s) on the order. +3. Package and ship the order. +4. Record the shipment information on the order. + +Steps 1 and 3 you would obviously do physically at your stocking location(s). This guide covers how you use your Spree store to manage steps 2 and 4. + +## Processing Payments + +You have received an order in your store - hooray! Either the items are all in stock, or you have adjusted the order to include only those items you can sell to the customer. + +Now you need to process the payment on the order. You know best how your store processes things like checks, money orders, and cash, so this guide will focus on processing credit card payments. + +![Order to Process](/images/user/orders/order_to_process.jpg) + +Pictured above is an order that is ripe for processing. The current order State is "Complete", meaning that all of the information the customer needs to provide is present. The Payment State is "Balance Due", meaning that there is an unpaid balance on the order. The Shipment State is "Pending", because you can't ship an order before you collect payment on it. + +If you click either the Order Number or the "Edit" icon, you will open the order in Edit mode. Instead, click the "Balance Due" payment state to open the order's payment info page. + +![Payment to Process](/images/user/orders/payment_to_process.jpg) + +As you can see above, we have only a single payment to process, for the full amount ($22.59). Processing this payment is literally a click away - just click on the "Capture" icon next to the payment. + +If the payment is processed successfully, the page will re-load, showing you that the payment state has progressed from "Pending" to "Completed". + +![Completed Payment](/images/user/orders/completed_payment.jpg) + +That's it! Now you can prepare your packages for shipment, then update the order with the shipment information. + +## Processing Shipments + +Now, when you visit the Order Summary, you can see that there is a new "Ship" button in the middle of the page. + +![Ship Button](/images/user/orders/ship_it.jpg) + +Clicking this button moves the Shipment State from "Ready" to "Shipped". + +![Order Marked Shipped](/images/user/orders/order_shipped.jpg) + +The only thing that remains to do now is to click the "Edit" icon next to the tracking details line, and provide tracking info for the package(s). Click the "Save" icon to record this information. + +![Input Tracking Info](/images/user/orders/tracking_input.jpg) \ No newline at end of file diff --git a/guides/content/user/orders/returning_orders.md b/guides/content/user/orders/returning_orders.md new file mode 100644 index 00000000000..91d9a633491 --- /dev/null +++ b/guides/content/user/orders/returning_orders.md @@ -0,0 +1,45 @@ +--- +title: Returns +--- + +## Introduction + +Returns are a reality of doing business for most e-commerce sites. A customer may find that the item they ordered doesn't fit, or that it doesn't fit their needs. The product may be damaged in shipping. There are many reasons why a customer could choose to return an item they purchased in your store. This guide covers how you, as the site administrator, issue RMAs (Return Merchandise Authorizations) and process returns. + +## Creating RMAs for Returns + +You can only create RMAs for orders that have already been shipped. That makes sense, as you wouldn't authorize a return for something you haven't sent out yet. + +![Return Authorizations Link](/images/user/orders/return_authorizations_link.png) + +To create an RMA for a shipped order, click the order's "Return Authorizations" link, then click the "New Return Authorization" button. The form that opens up enables you to select which items will be authorized to be returned, and issue an RMA for the corresponding amount. + +![RMA Form](/images/user/orders/rma_form.png) + +To use it, just select each line item to be returned, and either a reimbursement type or exchange item. Selecting the "Original" reimbursement type will refund a user back to their original payment method when the items are returned and approved. Selecting an exchange item will create a new shipment to ship the exchange item to the customer. The form will automatically calculate the RMA value based on the sale price of the item(s), but you will have to confirm the amount when the reimbursement is issued. This gives you a chance to adjust for handling fees, restocking fees, damages, etc. + +Input the reason and any memo notes for the return, and select the [Stock Location](stock_locations) the item is coming back to. Click the "Create" button. + +Now you just need to wait for the package to be received at your location. + +## Processing Returns + +Once you receive a return package, you need to create a "Customer Return". To do so, go to the order in question and click "Customer Returns". Click the "New Customer Return" button. + +![Receive RMA Button](/images/user/orders/customer_return_link.png) + +Select which of the authorized return items were received, and to which [Stock Location](stock_locations). Once done click the "Create" button. + +![Receive RMA Button](/images/user/orders/customer_return_form.png) + +The return items are marked as accepted, and now you can create a reimbursement for the $22.99 you owe the customer. + +![RMA Received](/images/user/orders/create_reimbursement_button.png) + +The reimbursement form will be populated according to your original reimbursement or exchange selections chosen during the return authorization form. You may override the selected reimbursement type or exchange item now if you would like, otherwise click the "Reimburse" button to create the refund. + +![Issue a Reimbursement](/images/user/orders/reimbursement_form.png) + +Your return-processing is complete! As you can see there will now be a $22.99 refund issued to the original credit card. + +![Reimbursement Complete](/images/user/orders/reimbursement_complete.png) diff --git a/guides/content/user/orders/searching_orders.md b/guides/content/user/orders/searching_orders.md new file mode 100644 index 00000000000..b31cd6945ae --- /dev/null +++ b/guides/content/user/orders/searching_orders.md @@ -0,0 +1,72 @@ +--- +title: Searching Orders +section: searching_orders +--- + +When you click the **Orders** tab on the Admin Interface, you are instantly presented with a summary of the most recent orders your store has received. + +![Initial List of Orders](/images/user/orders/list_of_orders.jpg) + +The list shows you the following information about each order: + +* **Completed At** - The date on which the user finalized their order. +* **Number** - The Spree-generated order number. +* **State** - The current state of the order. You can learn more about [order states in another guide](order_states.md). +* **Payment State** - Spree tracks the state of an order's payment separately from the state of the order itself. As payment is received, the state of the order progresses. +* **Shipment State** - Having the Shipment State pictured separately lets you quickly see which orders are paid and need to be packed and shipped, improving your store's workflow. +* **Customer Email** +* **Total** - This amount includes item totals, tax, shipping, and any promotions or adjustments made to the order. + +Next to each row is an "Edit" icon. Clicking this icon allows you to [make changes to an order](editing.md). + +# Filtering Results + +You may not always want to see all of the most recent orders - the Spree default. You may want to view only those orders that you need to pack and ship, or only those from a particular customer. Spree gives you the flexibility to quickly find only those orders you need. + +![Order Filter Options](/images/user/orders/filter_options.jpg) + +You can choose one or more of the following options to narrow your order search, then click the **Filter Results** button to update the results. + +## Date Range + +You can input a **Start** and/or **Stop** date. If you enter both, the results shown will be all orders that fall on or between those dates. + +If you input only a **Start** date, you will get all orders placed on or after that date. + +If you input only a **Stop** date, the results will include all orders placed up to and on that date. + +## Status + +You can restrict orders to only those with a particular status. Available status options include: + +* **cart** - Customer has added items to a shopping cart, but has not yet checked out. +* **address** - Customer has entered the checkout process, but has not yet completed input of shipping and/or billing address(es). +* **delivery** - Customer has completed entry of addresses, but has not yet completed selection of delivery method(s). +* **payment** - Customer has entered addresses and chosen a delivery method, but still needs to enter a payment method. +* **confirm** - All required information has been entered; customer just needs to confirm the order. +* **complete** - All required information is present, customer has confirmed the order, payment has not yet been received or processed. +* **canceled** - Either customer or store admin has chosen to cancel the order. +* **awaiting return** - Customer has elected to return products, but they have not yet been received. +* **return** - A return has been processed. +* **resumed** - A formerly canceled order has been reactivated. + +## Order Number + +Spree generates a unique order number for each order when the first item is added to a shopping cart. Order numbers begin with the letter R, followed by 9 random numbers. If you are searching for a particular order, you can just input the entire order number and that order is all that will be returned. + +## Email + +At this time, the filter does not allow you to search for only part of an email address. If you want to find all orders from `jane_doe@example.com`, you will have to use the full address. Inputting only "jane_doe" will result in a pop-up alert to enter a valid email address. + +## Name + +The **First Name Begins With** and **Last Name Begins With** fields will let you filter order results based on the *billing address*, not on the shipping address. You can use any number of letters, from just an initial to the full first and/or last name. + +## Complete + +By default, the filter restricts results to only orders that have reached the `complete` order state. To remove this restriction, uncheck the box that is marked **Only Show Complete Orders**. + +## Unfulfilled + +If you only want to review orders that have not been shipped, you can check the box marked **Show Only Unfulfilled Orders**. + diff --git a/guides/content/user/overview.md b/guides/content/user/overview.md new file mode 100644 index 00000000000..48f37392773 --- /dev/null +++ b/guides/content/user/overview.md @@ -0,0 +1,127 @@ +--- +title: Understanding the Overview Page +--- + +The main purpose of the Overview/Dashboard page is to let you see - at a glance - the most important information about your store's sales. + +$$$ +Replace some of these screenshots with ones from a store with actual data. +$$$ + +*** +The terms "Dashboard" and "Overview page" are used interchangeably. Both refer to the page that is displayed when you visit http://yourwebsite.com/admin. +*** + +# Making Selections + +There are a variety of ways to easily impact the information that is displayed to you on the Overview page. You can change the Dashboard locale (and, thus, the language); the date range for the orders and carts summarized; and which store's data is showcased. You can even choose which reports you see based on your own needs. + +## Selecting a Locale + +## Selecting the Time Frame + +By default, the Overview page shows you data about your store from the last 7 days, and compares those figures to data from the previous 7 days. + +![Date Range Selector](/images/user/jirafe/date_range_selector.jpg) + +You can choose to view data from any of the following: + +* Today +* Yesterday +* Last 7 Days (default) +* Last 30 Days +* A specific date range + +You can choose among several types of comparisons between the data currently shown and previous sales data. You can choose to compare to: + +* The Period Before +* The Day Before +* The Week Before +* The Month Before +* The Year Before + +Or you can turn off data comparison altogether, by clicking the green "ON" button next to "Compare To". + +## Selecting a Store + +$$$ +I really don't know what this drop-down is supposed to do. Someone else will have to edify. +$$$ + +## Refreshing Data + +The Dashboard can give you an up-to-date summary of your store's data by simply clicking the Refresh data button. + +![Refresh Button](/images/user/jirafe/refresh_dashboard_button.jpg) + +# Sales Figures + +Spree makes it simple for you to get quick, aggregate figures on your store sales based on a number of metrics. + +$$$ +Can someone fact-check these descriptions? +$$$ + +## Revenue + +The **Revenue** figure includes all income, including product sales, tax, shipping, and other charges. + +## Orders + +The **Orders** figure gives the total number of finalized orders for the period shown. + +## Visits + +The **Visits** figure shows the total number of website visits your website has received for the period shown. It can be useful to gauge the effectiveness of a recent marketing campaign. + +## Conversion Rate + +**Conversion Rate** reflects the proportion of website visitors that made a purchase, expressed as a percentage. + +## Average Order Value + +Dividing the Revenue by the Orders for the time period shown gives you the **Average Order Value** or AOV. + +## Revenue Per Visit + +The **Revenue Per Visit** is calculated by dividing the total Revenue by the number of Visits for the time period shown. + +## Customizing + +While the reports listed above are the ones shown on the Dashboard by default, you can easily remove the ones that are not as valuable to you, and/or add additional built-in reports. + +![Report Customizer Menu](/images/user/jirafe/report_customizer.jpg) + +As you can see, there are additional built-in reports, including: + +* Total Carts +* Abandoned Carts +* Abandon Rate +* Abandoned Revenue +* Abandoned Average Order Value + +Clicking on the star next to the name of any of these reports adds it to what is shown when you click the Overview tab. You can also click on the name of the report to see a more detailed breakdown of the aggregate information. For example, here is the detailed metrics for a sample store's Conversion Rate: + +![Conversion Rate Details](/images/user/jirafe/conversion_rate_detail.jpg) + +# The Purchase Funnel + +$$$ +Do these figures change when you select a new time frame, or do they reflect just the previous 7 days? +$$$ + +Information displayed in the Purchase Funnel section gives you a window into the shopping tendencies of your site's visitors. Using beautifully-rendered pie chart graphics, you can see the total numbers of users who: + +* **Visited** - Should be the same as the number shown on the *Visits* report. +* **Stayed** - Remained on the site for longer than [TODO] Longer than what? +* **Shopped** - Looked at individual products. +* **Added to Cart** - Selected one or more products to add to their cart. +* **Purchased** - Completed an order. + +# Abandoned Carts + +Data in the Abandoned Carts table shows you details about how many users added items to their carts, but did not complete a purchase within the time frame you selected. It also helpfully shows you a comparison of the current time period with the previous time period. + +Data shown includes the number of carts that had items added to them (Total Carts), the number of carts that did not result in sales (Abandoned), and the Abandon Rate (abandoned carts divided by total carts). + +Furthermore, the graphs sum the total amount of revenue that your store would have received had the orders completed (Abandoned Revenue) and the Average Order Value of the abandoned orders (calculated as abandoned revenue divided by abandoned carts). \ No newline at end of file diff --git a/guides/content/user/payments.md b/guides/content/user/payments.md new file mode 100644 index 00000000000..85b9e2d41b5 --- /dev/null +++ b/guides/content/user/payments.md @@ -0,0 +1,9 @@ +--- +title: Payments +--- + +## Payments + +Processing payments for orders is a very important component of your Spree store. The flexibility of this system allows you to add, remove, or change [methods of payment](payment_methods) to suit your needs, as well as to use almost any [payment gateway](payment_methods#add-a-supported-gateway) you prefer. + +The guides in this section will also help you to understand the [states an order goes through](payment_states) from the time the first item is added to the cart, to the time you close out the fulfillment process. \ No newline at end of file diff --git a/guides/content/user/payments/payment_methods.md b/guides/content/user/payments/payment_methods.md new file mode 100644 index 00000000000..eea03541778 --- /dev/null +++ b/guides/content/user/payments/payment_methods.md @@ -0,0 +1,91 @@ +--- +title: Payment Methods +--- + +## Introduction + +Payment methods represent the different payment options available to customers during the checkout process on an e-commerce store. Spree supports many types of payment methods, including both online and offline options. This guide describes how to add payment methods to your Spree store. + +## Terminology + +Let's begin by explaining the difference between a Payment Gateway and a Merchant Account. + +**Payment Gateway** - A payment gateway is a service that authorizes credit card payments, processes them securely, and deposits the funds into your bank account. A payment gateway performs the same functions as a credit card swipe machine at a restaurant or retail store, it just performs these functions for purchases made online instead of in person. + +**Merchant Account** - A merchant account is a type of bank account that allows you to accept credit card payments online. If you have a retail business and already accept credit card payments, then more than likely you have a merchant account. When you start to sell products online, you may need to call your bank and ask that they set you up with an _Internet_ merchant account. An Internet merchant account allows you to accept payments online without having the customer's credit card physically in front of you. + +## Evaluating Payment Gateways + +When researching payment gateway options, you may find that they offer an all-in-one solution that includes both the gateway and the merchant account. This is just something to be aware of and to evaluate if it makes sense for your store. Payment gateways also charge a fee for their services. Here are a few of the fees you might come across when evaluating providers: + +**Setup Fee** - A one-time charge to set up your payment gateway account. + +**Recurring Fixed Monthly Fees** - A fixed monthly fee that a payment gateway provider charges for access to their services and reports. Some gateways break this charge down further into a monthly Gateway Fee and a Statement Fee. + +**Transaction Fees** - A charge for each purchase made on your e-commerce store. The pricing structure for these fees differ per gateway. A popular structure is to charge a percentage of the purchase price plus a flat fee. For example, 2.9% of the purchase price plus $0.30 per transaction. + +## Add a Payment Method + +Spree enables you to utilize the payment method of choice for your e-commerce store. We have two [preferred payment gateway partners](http://spreecommerce.com/products/payment_processing) and a long [list](https://github.com/Shopify/active_merchant#supported-direct-payment-gateways) of payment gateways that are supported by default in Spree. We also enable you to add a custom payment gateway, as well as offer offline payment options such as checks and purchase orders. + +### Add a Supported Gateway + +Read through the following explanatory text to add one of the supported payment gateways as a payment method on your store. + +#### Select Provider + +To configure one of the supported payment gateways, you must first install the [Spree_Gateway](https://github.com/spree/spree_gateway) extension on your store. More than likely, you will want to ask someone from your technical team to do this. Once this extension has been installed, you can configure one of the supported gateways in the Admin Interface by clicking the "Configuration" tab and then clicking the "New Payment Method" button. + +![New Payment Method Form](/images/user/payments/new_payment_method.jpg) + +If you installed the [Spree_Gateway](https://github.com/spree/spree_gateway) extension, you will see a long list of gateways in the "Provider" drop down menu. Select the one that you would like to add. + +![Select Payment Gateway Provider](/images/user/payments/add_payment_provider.jpg) + +#### Environment + +Choose the environment where you would like to enable the payment method. The choices are: + +* **Development** - Used by developers when they are testing a Spree store on their local machine. +* **Production** - Select if you want the payment gateway to appear on the customer-facing area of your store. +* **Test** - Used by developers who are testing their Spree store, typically with our [test suite](/developer/testing.html). + +#### Display + +Select whether you want the payment method to appear on the Frontend or the Backend of your store, or both. + +The Frontend is the customer-facing area of your store, meaning that the payment method will display as a payment option to your customers during the checkout step. + +The Backend is the Admin Interface for your store. Site administrators typically select this option when they want to make a payment option available to their internal staff but not to their end customers. For example, you might want to offer purchase orders as a payment option to customers on a one-off basis, but only if they contact one of your customer service representatives via email or telephone. + +#### Active + +Select "Yes" if you want the payment method to be active on your store. Select "No" if you want to create the payment method, but not present it on your store until a later point. + +#### Name + +Give the payment method a name. The value you enter will appear on the customer-facing area of your store, on the Payment page as seen below: + +![Payment Method Name](/images/user/payments/payment_method_name.jpg) + +#### Description + +Add a description for the payment method. This field is optional and is only displayed to internal users and not to customers. + +Click "Update" once you've input the desired settings for your new payment method. + +### Add a Non-Supported Gateway + +It is possible to add a new payment gateway that is not included on the supported by default gateway [list](https://github.com/Shopify/active_merchant#supported-direct-payment-gateways), but doing so is outside the scope of this tutorial. Please consult with your development team if you need this functionality. + +## Edit a Payment Method + +To edit the configuration settings for an existing payment method, go to the Admin Interface, click the "Configuration" tab, and then click the "Payment Methods" link. Find the payment method that you would like to edit on the list that appears. Click the "Edit" icon next to the payment method to edit its settings. + +![Edit Payment Method](/images/user/payments/edit_payment_method.jpg) + +Make the desired changes to the payment method settings and then click "Update" to save them. + +## Processing Payments + +Processing orders and the payments associated with them are covered in detail in the [Processing Orders guide](processing_orders). \ No newline at end of file diff --git a/guides/content/user/payments/payment_states.md b/guides/content/user/payments/payment_states.md new file mode 100644 index 00000000000..f877832d54b --- /dev/null +++ b/guides/content/user/payments/payment_states.md @@ -0,0 +1,50 @@ +--- +title: Payment States +--- + +## Introduction + +When an order is initiated for a customer purchase a payment is created in the Spree system. A payment goes through various states while being processed. + +## Payment States + +The possible payment states are: + +* **Checkout** - The checkout has not been completed. +* **Processing** - The payment is being processed. +* **Pending** - The payment has been processed but is not yet complete (ex. authorized but not captured). +* **Failed** - The payment was rejected (ex. credit card was declined). +* **Void** - The payment should not be applied against the order. +* **Completed** - The payment is completed. Only payments in this state count against the order total. + +A payment does not necessarily go through each of these states in sequential order as illustrated below: + +![Payments Flow](/images/developer/core/payment_flow.jpg) + +You can determine the payment state for a particular order by going to the Admin Interface and clicking on the "Orders" tab. Find the order you want to look up and click on it. Then click on the "Payments" link. + +![Payment Look Up](/images/user/payments/payments_look_up.jpg) + +The details for the payment will appear. The "Payment State" column will display one of the possible payment states listed above. + +![Payment Details](/images/user/payments/payment_details.jpg) + +## Authorize vs Capture + +Authorizing a payment is the process of confirming the availability of funds for a transaction with the purchaser's credit card company. Capturing a payment is the process of telling the credit card company that you would like to get paid for the transaction amount. Typically, this two step process of first authorizing the payment and then capturing the payment is used by online retailers to delay charging the customer until the product(s) purchased are fulfilled (shipped). + +By default, Spree automatically handles authorizing the payment for a transaction. For capturing payments, we give you the choice of auto-capturing the payment or manually capturing the payment via the Admin Interface. If you like, you can read further [documentation about auto-capturing payments](/developer/payments#auto-capturing). + +Note: Not all payment gateways allow for the two step *authorize and then capture* payment process. If this functionality is required for your store, please confirm with your payment gateway that they can support this process. + +# Capture a Payment via the Admin + +To capture a payment using the Admin Interface, click on the "Orders" tab. Find the order you want to look up and click on it. Then click on the "Payments" link. The order details will appear. Click on the "Capture" icon to initiate the capture process. + +![Capture a Payment](/images/user/payments/payment_capture.jpg) + +## Void a Payment + +To void a payment, go to the Admin Interface. click on the "Orders" tab. Find the order you want to look up and click on it. Then click on the "Payments" link. The order details will appear. Click on the "Void" icon to void the transaction. + +![Void a Payment](/images/user/payments/payment_void.jpg) diff --git a/guides/content/user/products.md b/guides/content/user/products.md new file mode 100644 index 00000000000..926d959857f --- /dev/null +++ b/guides/content/user/products.md @@ -0,0 +1,21 @@ +--- +title: Products +--- + +## Products + +Products are at the core of any e-commerce site. Selling them is the whole reason behind opening a store in the first place. + +From your store's Admin Interface, you can manage all of the common tasks associated with managing your products. To reach the Admin Interface, first log into your store with your admin user account, then go to the `/admin` directory of your site. Click the "Products" tab. + +![Products Admin](/images/user/products/products_admin.jpg) + +From here you can: + +* [Create New Products](creating_products) +* [Delete Existing Products](deleting_products) +* [Edit Existing Products](editing_products) +* [Clone Existing Products](cloning_products) +* [Search Existing Products](searching_products) + +In addition, you can set up new [Product Option Types and Values](product_options); add, edit, and remove [Product Properties](product_properties); and work with [Product Prototypes](product_prototypes). \ No newline at end of file diff --git a/guides/content/user/products/cloning_products.md b/guides/content/user/products/cloning_products.md new file mode 100644 index 00000000000..7c05b1a07b5 --- /dev/null +++ b/guides/content/user/products/cloning_products.md @@ -0,0 +1,26 @@ +--- +title: Cloning Products +section: cloning_products +--- + +## Introduction + +To clone a product in your store, go into the Admin Interface and click the "Products" tab. A list of your store's product inventory will appear. Find the product that you would like to clone and click the "Clone" icon to make a copy of it. + +![Cloning a Product](/images/user/products/clone_product.jpg) + +This will create a copy of the product and it will now appear in your product inventory list as "COPY OF" with the product name appended afterward. A new permalink and SKU will also be created for the product. + +![Cloned Product Example](/images/user/products/example_cloned_product.jpg) + +## Editing + +You may want to change the name of the product to remove the "COPY OF" text and to rename the permalink and SKU so it matches your product name. + +!!! +Be very careful in assigning arbitrary permalinks, so that you don't accidentally assign two products the same permalink. +!!! + +You can edit the information associated with a cloned product just like you would for any other product. To do this, click on the product from your inventory list. In the "Name" field, delete the "COPY OF" text. To modify the permalink name, enter your desired value in the "Permalink" field. To modify the product SKU, enter your desired value in the "SKU" field. Once you've made your changes, scroll to the bottom of the page and click "Update". + +![Editing a Cloned Product](/images/user/products/edit_cloned_product.jpg) \ No newline at end of file diff --git a/guides/content/user/products/creating_products.md b/guides/content/user/products/creating_products.md new file mode 100644 index 00000000000..85b4cf2e403 --- /dev/null +++ b/guides/content/user/products/creating_products.md @@ -0,0 +1,134 @@ +--- +title: Creating a New Product +section: creating_products +--- + +## Introduction + +To create a new product for your store, go into the Admin Interface, click the "Products" tab, and click the "New Product" button. + +![New Product Entry Form](/images/user/products/new_product_entry_form.jpg) + +The two mandatory fields ("Name" and "Master Price") are denoted with an asterisk (*) next to the label. You can leave SKU blank. If you don't add a value for "Available On" the product will not be shown in your store. + +*** +[Prototypes](product_prototypes) are a more complex topic, and are covered in their own guide. +*** + +## Product Details + +After you click the "Create" button, the Spree application brings you to a more detailed product entry page, where you can input more information about your new product. + +![Product Edit Form](/images/user/products/product_edit_form.jpg) + +* **Name** - This field will either be blank, or the same as what you entered on the initial page. You can change this field whenever you like. +* **Permalink** - The permalink is automatically created by the application for you when the product is first saved, and is based on the product's name. This is what is appended to the end of a URL when someone visits the page for a particular product. You can change the permalink, but should exercise extreme caution in doing so to avoid naming collisions with other products in your database. +* **Description** - This is where you will provide a detailed description of the product and its features. The application gives you plenty of room to be thorough. +* **Master Price** - For now, just think about the Master Price as the price you charge someone to buy the item. Later in this guide, you will learn more about variants and how they impact a product's actual price. +* **Cost Price** - What the item costs you, the seller, to purchase or produce. +* **Cost Currency** - It may be that the currency used when you purchased the product is not the same as that you use in your store. Spree makes these conversions for you - just enter the code for the currency used in acquiring your inventory. +* **Available On** - This field will either be blank, or the same as what you entered on the initial page. You can change this field whenever you like. +* **SKU** - This field will either be blank, or the same as what you entered on the initial page. You can change this field whenever you like. +* **Weight** - The product's weight in ounces. May be used to calculate shipping cost. +* **Height** - The product's height in inches. May be used to calculate shipping cost. +* **Width** - The product's width in inches. May be used to calculate shipping cost. +* **Depth** - The product's depth or breadth in inches. May be used to calculate shipping cost. +* **Shipping Categories** - You will learn about setting up Shipping Categories in the [Shipping Categories](shipping_categories). +* **Tax Category** - You will learn about setting up Tax Categories in the [Taxes Guide](configuring_taxes). +* **Taxons** - Taxons are basically like categories. You will learn more about them in the [Taxonomies Guide](configuring_taxonomies). +* **Option Types** - You can select any number of Options to associate your new product with. You'll learn more about Options in the [Options Guide](product_options). +* **Meta Keywords** - These words are appended to the website's keywords you established in the [Site Settings](configuring_general_settings) and can help improve your site's search engine ratings, bringing you more web traffic. They should be words that are key to your new product. +* **Meta Description** - The summary that someone sees when your product's page is returned in a web search. It should be descriptive but not overly verbose. + +## Images + +A store whose products had no images to look at would be pretty boring, and probably not garner a lot of sales. It would be very time-consuming to have to upload, crop, resize, and associate several photos to each product, if you had to do so manually. Luckily, Spree makes maintaining images of your products quick and painless. + +Just click the "Images" link under "Product Details" on the right-hand side of the screen. Any images that you may have already uploaded will be previewed for you. To add a new image for your product, click the "New Image" button. + +![New Product Image Form](/images/user/products/new_image_form.jpg) + +Select the Image file, and enter the Alternative Text for the image. Alternative Text is what appears when someone has their browser's image-rendering turned off, as with certain types of screen readers. + +You have the option to associate a photo only with a particular "Variant" (again, more on Variants later in this guide), or with all of the product's Variants. + +When you click Update, not only is the product photo uploaded, it is automatically resized and cropped to fit your store's requirements, and it is associated with the correct versions of your product. + +## Understanding Variants + +Suppose that in your store, you sell drink tumblers. All of the tumblers are made by the same manufacturer and have the same basic product specifications (materials used, machine washability rating, etc.), but your inventory includes several options: + +* **Size** - You carry both Medium and Large tumblers +* **Decorative Wrap** - Your tumblers come with the option of several kinds of decorative plastic wraps: Stars, Owls, Pink Paisley, Purple Paisley, or Skulls. +* **Lid Color** - The tumblers also come with with an assortment of lids to match the decorative wrap - the Star tumblers have Blue lids, the Owls have Orange lids, the Pink Paisley have Pink lids, the Purple Paisley have White lids, and the Skulls can be purchased with White *or* Black lids. + +Given this inventory, you will need to create a Drink Tumbler _Product_, with three _Option Types_, the corresponding _Option Values_, and twelve _Variants_: + +Size | Wrap | Lid Color +|-----|------|---------| +Large | Stars | Blue +Small | Stars | Blue +Large | Owls | Orange +Small | Owls | Orange +Large | Pink Paisley | Pink +Small | Pink Paisley | Pink +Large | Purple Paisley | White +Small | Purple Paisley | White +Large | Skulls | White +Large | Skulls | Black +Small | Skulls | White +Small | Skulls | Black + +The _Option Types_ you would create for this inventory are - Size, Wrap, and Lid Color - with the corresponding _Option Values_ below. + +Option Type | Option Values +|-----------|--------------| +Size | Large, Small +Wrap | Stars, Owls, Pink Paisley, Purple Paisley, Skulls +Lid Color | Blue, Orange, Pink, White, Black + +Read the [Product Options Guide](product_options) for directions on creating Option Types and Option Values. You must establish your Option Types and Option Values before you can set up your Variants. Don't forget to associate the Option Types with the Tumbler product so they'll be available to you when you make your Variants. + +### Creating Variants + +Now that you have set up the appropriate options for your Product's Variants and associated those options with the product, you can create the Variants themselves. + +Let's create the large, star-wrapped, blue-lidded tumbler Variant as an example. You can then use the same approach to creating all of the other Variants we mentioned earlier. + +On your tumbler product edit page, click the "Variants" link. Click the "New Variant" button. + +![New Product Variant](/images/user/products/new_variant.jpg) + +Select the appropriate values for the Option Types. As you can see, you also have the choice to enter values for this particular Variant that may be different from what you input on the Product's main page. Let's raise the price on our Variant to $20. Click the "Create" button. + +![Variants List](/images/user/products/variants_list.jpg) + +## Product Properties + +You can set as many individual product properties as you like. These include things like the item's country of manufacture, material(s) used, design style, etc. Typically, these are characteristics that do not change across variants of a product. + +You can read much more in-depth information about this feature in the [Product Properties Guide](product_properties). + +## Stock Management + +As of version 2.0 of Spree, you now have much more granular control over how inventory is tracked through your store. You will learn more about stock locations in the [Stock Locations Guide](stock_locations), but for now it's enough to understand that you enter the number of each product variant that you have at each of your individual stock locations. + +Let's assume that you have two stock locations - your main New York warehouse and your satellite Detroit warehouse. Refer to the instructions on creating stock locations in the [Stock Locations Guide](stock_locations#create-a-new-stock-location) to add your warehouses. + +Now, go back to the Tumblers product page, and click the "Stock Management" link. + +![Stock Management Page](/images/user/products/stock_management.jpg) + +For this guide, let's say we want to say that we have 7 of our tumbler variant in the New York warehouse, and 3 in Detroit. To accomplish this, change the quantity to 7, select "New York Warehouse" from the "Stock Location" drop-down list, and select "large-blue-stars" from the "Variant" drop-down list. Click the "Add Stock button". + +The "Stock Location Info" table will update, showing you that there are 7 of these items in the New York warehouse. Repeat these steps, adding 3 tumblers from the Detroit warehouse. + +![Stock Location Info](/images/user/products/stock_location_info.jpg) + +Your Stock Location Info table should now look like the one pictured above. + +*** +"Backorderable" may or may not be checked for your individual Stock Locations, depending on how you configured them. Each Stock Location has defaults for this value, but you can change it on a variant-by-variant basis in this dialog. +*** + +You should be sure to read the [Stock Locations](configuring_inventory.html#stock-locations) and [Stock Movements](configuring_inventory.html#stock-movements) guides for further information on managing your store's inventory. diff --git a/guides/content/user/products/deleting_products.md b/guides/content/user/products/deleting_products.md new file mode 100644 index 00000000000..7deda1ce30a --- /dev/null +++ b/guides/content/user/products/deleting_products.md @@ -0,0 +1,26 @@ +--- +title: Deleting Products +section: deleting_products +--- + +## Introduction + +To delete a product in your store, go into the Admin Interface and click the "Products" tab. A list of your store's product inventory will appear. Find the product that you would like to delete and click the "Delete" icon on the right to remove it from your store. + +![Deleting a Product](/images/user/products/delete_products_icon.jpg) + +A message will appear asking you to confirm that you want to delete the product. Click "OK". + +## Viewing + +Deleted products no longer appear in the customer-facing area of your store. However, they do remain in your product database. To view products that have been deleted from your store, go to the "Products" tab in the Admin Interface and find the "Search" section. Do not enter anything in the "Name" or "SKU" fields. Check the "Show Deleted" box and click "Search". The search results will return a list of your entire product inventory including active and deleted products. + +![Search for Deleted Products](/images/user/products/show_deleted_products.jpg) + +## Re-activating + +To re-activate a deleted product, find the product in your inventory following the steps [above](#viewing). Deleted products will only have the "Clone" icon next to them, whereas active products will have the "Edit", "Clone", and "Delete" icons next to them. Click the "Clone" icon to the right of the deleted product. + +![Clone a Deleted Product](/images/user/products/clone_deleted_product.jpg) + +This will create a copy of the product, and it will now appear in your product inventory list as "COPY OF" with the product name appended afterward. A new permalink and SKU will also be created for the product. Follow the instructions from the [Cloning guide](cloning_products) to modify the information for this product. \ No newline at end of file diff --git a/guides/content/user/products/editing_products.md b/guides/content/user/products/editing_products.md new file mode 100644 index 00000000000..cc7db57b804 --- /dev/null +++ b/guides/content/user/products/editing_products.md @@ -0,0 +1,15 @@ +--- +title: Editing Products +--- + +## Introduction + +You will often need to modify the products in your store - prices may fluctuate, descriptions may change, you may take new photographs of your inventory. Spree makes it quick and easy to make such changes. + +## Editing Products + +To make changes to your product, go to your Admin Interface, click the "Products" tab, and click the "Edit" icon next to the product. + +![Edit Product Link](/images/user/products/edit_product_link.jpg) + +The edit form is the exact same one you use when [creating products](creating_products). See that guide for explanation of any of the fields when editing your product. \ No newline at end of file diff --git a/guides/content/user/products/product_options.md b/guides/content/user/products/product_options.md new file mode 100644 index 00000000000..9f013cb40b5 --- /dev/null +++ b/guides/content/user/products/product_options.md @@ -0,0 +1,45 @@ +--- +title: Product Options +--- + +## Option Types and Option Values + +Option Types are a way to help distinguish products in your store from one another. They are particularly useful when you have many products that are basically of the same general category (Tshirts or mugs, for example), but with characteristics that can vary, such as color, size, or logo. + +For each Option Type, you will need to create one or more corresponding Option Values. If you create a "Size" Option Type, then you would need Option Values for it like "Small", "Medium", and "Large". + +### Creating Option Types and Option Values + +Option Types and Option Values are created at the store level, not the product level. This means that you only have to create each Option Type and Option Value once. Once an Option Type and Option Value is created it can be associated with any product in your store. To create an Option Type, click "Products", then "Option Types", then "New Option Type". + +![New Option Type](/images/user/products/new_option_type.jpg) + +You are required to fill in two fields: "Name" and "Presentation". You will see this same pattern several places in the Admin Interface. "Name" generally is the short term (usually one or two words) for the option you want to store. "Presentation" is the wordier, more descriptive term that gives your site's visitors a little more detail. + +*** +NOTE: Sometimes the term "Display" is used instead of "Presentation" to indicate what is shown to the user on the Product Variant's page. +*** + +For our first Option Type - Size - enter "Size" for the Name and "Size of the Tumbler" as the Presentation. Click "Update". + +When the screen refreshes, you see that Spree has helpfully provided you with a blank row in which you can enter your first Option Value for the new Option Type. + +![New Option Value](/images/user/products/new_option_value.jpg) + +We're going to need two Option Values (Large and Small) for the Size Option Value, so go ahead and click the "Add Option Value" button. This gives you two blank rows to work with. + +"Name" is easy - "Large" for the first, and "Small" for the second. Let's input "24-ounce cup" in the "Display" field for the Large Option Value and "16-ounce cup" for the Small Option Value. + +![Completed Option Values](/images/user/products/large_small_option_values.jpg) + +When you click "Update", Spree saves the two new Option Values, associates them with the Size Option Type, and takes you to the list of all Option Types. + +### Associating Option Values with a Product + +Our Spree application now knows that we have an Option Type with corresponding Option Values,but it doesn't know which of our products should have those Option Types. We have to explicitly tell it about those associations. We can do so either when we create a new Product (if the options have already been created), or when we edit an existing product. + +At the bottom of the Product edit form is a text box labeled "Option Types". When you click in this box, a drop-down appears with all of the Option Types you have defined for your store. All you have to do is click one or more of them to associate them with your Product. + +![Option Types Dropdown List](/images/user/products/option_types_dropdown.jpg) + +Don't forget to click "Update" to save your changes. \ No newline at end of file diff --git a/guides/content/user/products/product_properties.md b/guides/content/user/products/product_properties.md new file mode 100644 index 00000000000..acabb45d081 --- /dev/null +++ b/guides/content/user/products/product_properties.md @@ -0,0 +1,34 @@ +--- +title: Product Properties +--- + +## Product Properties + +Depending on the nature of your store and the products you sell, you may want to add "Properties" to your product descriptions. Properties are typically used to provide additional information about a product to help the customer make a better purchase decision. Here is an example of how a product's properties would display on the customer-facing area of a store: + +![New Product Variant](/images/user/products/properties_example.jpg) + +### Adding a Product Property + +Follow these steps to add a product Property. In this example, we are going to add a Property called "Country of Origin" with a value of "USA". + +1. Click the "Products" tab in your Admin Interface. +2. Click "Properties". +3. Click the "New Property" button. +4. Enter values for the "Name" and "Presentation" fields, such as "Origin" and "Country of Origin", respectively. +5. Click the "Create" button. +6. Navigate to the edit page for one of the products in your store. +7. Click the "Product Properties" link. +8. Click in the empty text box field under "Property" and start typing the name of the property you want to use: "Origin". After you type a few letters, the property name will display, and you can click it to select it. +9. Enter a country name for the "Value" field, such as "USA". +10. Click "Update". + +Now, when you navigate to the product's page in your store, you will see the new Country of Origin property in the "Properties" list. + +![Properties List](/images/user/products/properties_list.jpg) + +*** +You can add as many "Product Properties" to an individual "Product" as you like - just use the "Add Product Properties" button on the Product Properties page for an individual product. +*** + +You can also add "Product Properties" on the fly as you're editing a "Product" - you don't have to specify them ahead of time. Just be cautious of defining too many similar properties ("Origin", "Country Origin", "Country of Origin"). It's best to re-use existing properties wherever you can. \ No newline at end of file diff --git a/guides/content/user/products/product_prototypes.md b/guides/content/user/products/product_prototypes.md new file mode 100644 index 00000000000..667751e1893 --- /dev/null +++ b/guides/content/user/products/product_prototypes.md @@ -0,0 +1,47 @@ +--- +title: Prototypes +--- + +## Introduction + +A Prototype is like a Product blueprint, useful for helping you add a group of similar new products to your store more quickly. The general procedure is that you create a Prototype which is associated with certain [Option Types](product_options) and [Properties](product_properties); then you create products based on that Prototype, and only need to fill in the values for those Option Types and Properties. + +Imagine that you've just received a new shipment of picture frames from your supplier. Your new stock encompasses a variety of brands, sizes, colors, and materials, but they are all basically the same type of product. This is a prime use case for prototypes. + +*** +This guide presumes you have already created the [Option Types](product_options) and [Properties](product_properties) you need for your new prototype. If you haven't, you should do that first before proceeding. +*** + +### Creating a Prototype + +To create a prototype, go to the Admin Interface and click "Products", then "Prototypes". Click the "New Prototype" button. + +![New Prototype Form](/images/user/products/new_prototype.jpg) + +Input a value for the "Name" field (such as "Picture Frames"), and choose the properties and options you want to associate with this type of product. + +![Filled-In Prototype Form](/images/user/products/picture_frame_prototype.jpg) + +Click the "Create" button. You should now see your new prototype in the "Prototypes" list. + +![Prototypes List](/images/user/products/prototypes.jpg) + +# Using a Prototype to Create Products + +To create a new product based on the new prototype, click "Products" from the Admin Interface, then click the "New Product" button. Select "Picture Frames" from the "Prototypes" drop-down menu. + +![Product From Prototype](/images/user/products/product_from_prototype.jpg) + +When you do so, the Spree system shows you values for both of the Option Types you entered, so that it can automatically create [Product Variants](creating_products#understanding-variants) for you for each of them. + +Let's create the Product and all Variants for the fictional "Hinkledink Picture Frame" product. Input the product's Name, SKU, a Master Price (remember, you can change this for each variant), and make sure to set the Available On date to today, so it will show up in your store. Check the boxes for the options this particular product has, and click "Create". + +*** +Clicking the box next to an Option Type title will automatically check all of its Option Values for you. +*** + +![Prototype Option Types](/images/user/products/prototype_product_with_options.jpg) + +Proceed with [creating the product](creating-product) as you would normally, adding any missing fields not supplied by the prototype. + +Be sure to update each of the Variants with corresponding images, SKUs, and - if applicable - correct pricing. \ No newline at end of file diff --git a/guides/content/user/products/searching_products.md b/guides/content/user/products/searching_products.md new file mode 100644 index 00000000000..e80809a0d82 --- /dev/null +++ b/guides/content/user/products/searching_products.md @@ -0,0 +1,5 @@ +--- +title: Searching Products +--- + +## Searching Products \ No newline at end of file diff --git a/guides/content/user/promotions.md b/guides/content/user/promotions.md new file mode 100644 index 00000000000..4ae54190808 --- /dev/null +++ b/guides/content/user/promotions.md @@ -0,0 +1,125 @@ +--- +title: Promotions +--- + +## Introduction + +The Spree cart's promotions functionality allows you to offer coupons and discount to your site's users, based on the conditions you choose. This guide will explain to you all of the options at hand. + +To reach the Promotions pane, go to your Admin Interface and click the "Promotions" tab. + +## Creating a New Promotion + +To create a new promotion, click the "New Promotion" button. + +![New Promotion](/images/user/promotions/new_promotion.jpg) + +The page that renders allows you to set several standard options that apply to all promotions. Each is explained below. + +Option | Description +|---|---| +Name | The name you assign for the promotion. +Event Name | This is what must happen before the system will check to see if the promotion will apply to the order. Options are: **Add to cart** (any time an item is added to the cart), **Order contents changed** (an item is added to or removed from an order, or a quantity for an item in the order changes), **User signup** (a store visitor creates an account on the site), **Coupon code added** (a store visitor inputs a coupon code at checkout. The code has to match what you input for the code value if you select this option), or **Visit static content page** (a store visitor visits a path that you declare. This is often used to ensure that a customer has reviewed your store's policies or has been exposed to some other content that is important to your business model.) +Advertise | Checking this box will make the promotion visible to site visitors as they shop your store. +Description | A more thorough explanation of the promotion. The customer will be able to see this description at checkout. +Usage Limit | The maximum total number of times the promotion can be used in your store across all users. If you don't input a value for this setting, the promotion can be used an unlimited number of times. Beneath this input field is a "Current Usage" counter, which is useful later when you're editing a promotion and need to know how many redemptions the promotion has had. +Starts At | The date the promotion becomes valid. +Expires At | The date after which the promotion is invalid. + +When you enter values for these fields and click "Create", a new screen is rendered, giving you access to even more options for fine-tuning your promotion. + +### Rules + +Rules represent the factors that must be met for a promotion to be applicable to an order. You can set one or more rules for a single promotion. When you set multiple rules, you have the option of either requiring that all of the rules must be met for the promotion to apply, or allowing a promotion to apply to an order if even one of the rules is met. + +There are five types of rules. You can only add one rule of each type to a single promotion. Each is explained in detail below. + +![Rules Options](/images/user/promotions/rules_options.jpg) + +#### Item Total + +When you select "Item total" from the "Add Rule of Type" drop-down menu and click "Add", you are declaring an Item Total rule. + +![Item Total Rule](/images/user/promotions/item_total_rule.jpg) + +You can then set the parameters for this type of rule. Specifically, you can establish whether an order's items must be **greater than** or **equal to or greater than** the amount you set. Click "Update". + +*** +To remove a rule from a promotion, click the trash can icon next to it. + +![Delete Rule Icon](/images/user/promotions/delete_rule_icon.jpg) +*** + +#### Products + +Using a rule of this type means the order must contain **at least one** or **all** of the products you declare. + +![Products Rule](/images/user/promotions/products_rule.jpg) + +To create this kind of rule, just select "Product(s)" from the "Add Rule of Type" drop-down menu and click "Add". Start typing in the name of the product(s) you want to apply discounts to into the "Choose Products" box. Click on the correct variants. Choose either "at least one" or "all" from the selection box, and click "Update". + +#### User + +You can use the User rule type to restrict a promotion to only those customers you declare. To create this type of rule, select "User" from the "Add Rule of Type" drop-down menu and click "Add". Start typing in the name or email address of the user(s) you want to offer this promotion to. As the correct users are offered, click them to add them to the list. Click "Update". + +![User Rule](/images/user/promotions/user_rule.jpg) + +#### First Order + +Select "First order" from the "Add Rule of Type" drop-down menu and click "Add" then "Update" to add a rule of this type to your promotion. This rule will restrict the promotion to only those customers ordering from you for the first time. + +![First Order Rule](/images/user/promotions/first_order_rule.jpg) + +#### User Logged In + +Add a rule of this type to restrict the promotion only to logged-in users. Select "User Logged In" from the "Add Rule of Type" drop-down list, click "Add", then click "Update". + +![Logged In Rule](/images/user/promotions/logged_in_rule.jpg) + +### Actions + +Whereas [Rules](#rules) establish whether a promotion applies or not, Actions determine what happens when a promotion does apply to an order. There are two types of actions: [create adjustments](#create-adjustments) and [create line items](#create-line-items). + +#### Create Adjustments + +When you select "Create adjustment" from the "Add Action of Type" drop-down menu and click "Add", the system presents you with several calculator options. These are the same as the options you read about in the [calculators guide](calculators), except that instead of a [price sack calculator](calculators#price-sack), there are two additional calculators: percent per item and free shipping. + +![Create Adjustments Action Calculators](/images/user/promotions/create_adjustment.jpg) + +By default, when you add a new "Create adjustment" calculator it sets it to a "Flat percent" calculator. You can change this by selecting the new calculator type from the "Calculator" drop-down menu, but you will need to click the "Update" button to get that calculator's specific additional required fields to display. + +Each calculator has its own set of required additional information fields. + +Calculator Type | Additional Data Required +|---|---| +Flat Percent | Percentage amount +Flat Rate | Amount of discount, and currency +Flexible Rate | The cost of the first item, the cost of each additional item, the maximum number of items included in the promotion, and the currency +Percent Per Item | Percentage amount +Free Shipping | No additional info required + +Enter all required information for your calculator type, then click "Update". + +#### Create Line Items + +This action type is a way of automatically adding items to an order when a promotion applies to an order. To add this action to your promotion, select "Create line items" from the "Add Action of Type" drop-down menu and click "Add". + +![Create Line Item Action](/images/user/promotions/create_line_item.jpg) + +Select the quantity and variant you want automatically added to the customer's order from the product drop-down menu. Click "Update". + +!!! +Product variants added through Line Item Action Promotions will be priced as usual. If your intention is to add a free product, you should do both a Line Item action to add the product, and an Adjustment action to discount the cost of that variant. +!!! + +## Editing a Promotion + +To edit a promotion, first go to the Promotions list (from the Admin Interface, click "Promotions"). Click the "Edit" icon next to the promotion. + +![Edit Promotion Icon](/images/user/promotions/edit_promotion_icon.jpg) + +## Removing a Promotion + +To remove a promotion, click the "Delete" icon next to the promotion in the Promotions list. + +![Delete Promotion Icon](/images/user/promotions/delete_promotion_icon.jpg) \ No newline at end of file diff --git a/guides/content/user/reports.md b/guides/content/user/reports.md new file mode 100644 index 00000000000..ab6edaf7b9b --- /dev/null +++ b/guides/content/user/reports.md @@ -0,0 +1,17 @@ +--- +title: Reports +--- + +## Introduction + +Within the Admin Interface is a "Reports" tab. Information within this tab helps you understand how your store's income is apportioned. + +### Sales Total + +From the Listing Reports page, click the "Sales Total" link. Here you, input a date range by selecting a Start date and an End date, then clicking "Search". + +![Sales Total Dates](/images/user/sales_total_dates.jpg) + +The resulting report will show you - for each type of currency you accept - what your orders' item total, adjustment total, and sales total was. + +![Sales Total Report](/images/user/sales_total_report.jpg) \ No newline at end of file diff --git a/guides/content/user/shipments.md b/guides/content/user/shipments.md new file mode 100644 index 00000000000..a2246248b3a --- /dev/null +++ b/guides/content/user/shipments.md @@ -0,0 +1,16 @@ +--- +title: Shipments +--- + +## Shipments + +Spree uses a very flexible and effective system to calculate shipping. This set of guides explains how Spree renders shipping options to your customers at checkout, how it calculates expected costs, and how you can configure your store with your own shipping options to fit your needs. + +To properly leverage Spree’s shipping system’s flexibility you must understand a few key concepts: + +* [Shipping Categories](shipping_categories) +* [Zones](zones) +* [Calculators](calculators) (to determine shipping rates) +* [Shipping Methods](shipping_methods) + +Let's begin by understanding what [Shipping Categories](shipping_categories) are and how you can use them to differentiate products in your store. \ No newline at end of file diff --git a/guides/content/user/shipments/calculators.md b/guides/content/user/shipments/calculators.md new file mode 100644 index 00000000000..3c7f422ea37 --- /dev/null +++ b/guides/content/user/shipments/calculators.md @@ -0,0 +1,76 @@ +--- +title: Calculators +--- + +## Calculators + +A Calculator is the component of the Spree shipping system responsible for calculating the shipping price for each available [Shipping Method](shipping_methods). + +Spree ships with 5 default calculators: + +* [Flat rate (per order)](#flat-rate-per-order) +* [Flat rate (per item)](#flat-rate-per-item) +* [Flat percent](#flat-percent) +* [Flexible rate](#flexible-rate) +* [Price sack](#price-sack) + +### Flat Rate (per order) + +The Flat Rate (per order) calculator allows you to charge the same shipping price per order regardless of the number of items in the order. You define the flat rate charged per order at the shipping method level. + +For example, if you have two shipping methods defined for your store ("UPS 1-Day" and "UPS 2-Day"), and have selected "Flat rate" as the calculator type for each, you could charge a $15 flat rate shipping cost for the UPS 1-Day orders and a $10 flat rate shipping cost for the UPS 2-Day orders. + +### Flat Rate (per item) + +The Flat Rate (per item/product) calculator allows you to determine the shipping costs based on the number of items in the order. + +For example, if there are 4 items in an order and the flat rate per item amount is set to $10 then the total shipping costs for the order would be $40. + +### Flat Percent + +The Flat Percent calculator allows you to calculate shipping costs as a percent of the total amount charged for the order. The amount is calculated as follows: + +```ruby +[item total] x [flat percentage]``` + +For example, if an order had an item total of $31 and the calculator was configured to have a flat percent amount of 10, the shipping cost would be $3.10, because $31 x 10% = $3.10. + +### Flexible Rate + +The Flexible Rate calculator is typically used for promotional discounts when you want to give a specific discount for the first product, and then subsequent discounts for other products, up to a certain amount. + +The Flexible Rate calculator takes four inputs: + +* First Item Cost: the amount of shipping charged for the first item in the order. +* Additional Item Cost: the amount of shipping charged for items beyond the first item. +* Max Items: the maximum number of items on which shipping will be calculated. +* Currency: defaults to the currency you have configured for your store. + +For example, if you set First Item Cost to $10, Additional Item Cost to $5, and Max Items to 4, you could be charging $10 for the first item, $5 for the next 3 items, and $0 for items beyond the first 4. Thus, an order with 1 item would have a shipping cost of $10. An order with two items would cost $15 to ship, and an order of 7 items would cost $25 to ship. + +### Price Sack + +The Price Sack calculator is a way to offer discount shipping to orders over a certain dollar amount. The Price Sack calculator takes four inputs: + +* Minimal Amount +* Normal Amount +* Discount Amount +* Currency (defaults to the currency you have configured for your store) + +Any order whose subtotal is under is less than what you set for Minimal Amount would be charged a shipping cost of Normal Amount. Orders whose subtotals are equal to or greater than the Minimal Amount would be charged the Discount Amount. + +For example, suppose you create a shipping calculator with these settings: + +* Minimal Amount - $50 +* Normal Amount - $15 +* Discount Amount - $5 + +A customer whose order subtotal equals $35 would be offered a shipping cost of $15 using this shipping method. A different customer whose order subtotal equals $55 would be offered a shipping cost of only $5. + +### Custom Calculators + +You can define your own calculator if you have more complex needs. In that case, check out the [Calculators Guide](../developer/calculators.html). + +## Next Step + +If you have followed this guide series [from the beginning](shipments), your store is now stocked with [shipping categories](shipping_categories), [geographical shipping zones](zones), and calculators. The final step is to pull it all together into [shipping methods](shipping_methods), from which your customers can choose at checkout. \ No newline at end of file diff --git a/guides/content/user/shipments/shipping_categories.md b/guides/content/user/shipments/shipping_categories.md new file mode 100644 index 00000000000..4b232097f6a --- /dev/null +++ b/guides/content/user/shipments/shipping_categories.md @@ -0,0 +1,31 @@ +--- +title: Shipping Categories +--- + +## Shipping Categories + +Shipping Categories are used to address special shipping needs for one or more of your products. The most common use for shipping categories is when certain products cannot be shipped in the same box. This is often due to a size or material constraint. + +For example, if a customer purchases a jump rope and a treadmill from an online exercise equipment store, the treadmill would be considered an over-sized item by most shipping carriers and would require special shipping arrangements. The jump rope could be sent via standard shipping. + +To handle this use case in Spree you would define a "Default" shipping category for the jump rope and any other products that can use standard shipping methods, and an "Over-sized" shipping category for extremely large items like the treadmill. You would then assign the "Over-sized" shipping category to your treadmill product and the "Default" shipping category to your jump rope product. + +During checkout, the shipping categories assigned to the products in your customer's order will be a key factor in determining which shipping methods and costs your Spree store offers to your customer at checkout. + +### Creating a Shipping Category + +To create a new shipping category, go to the Admin Interface, click the "Configuration" tab, click the "Shipping Categories" link, and then click the "New Shipping Category" button. Enter a name for your new shipping category and click the "Create" button. + +![New Shipping Category](/images/user/shipments/new_shipping_category.jpg) + +### Adding a Shipping Category to a Product + +Once you've created your shipping categories you can assign the appropriate category to each of your products. To associate a shipping category with a product, go to the Admin Interface, and click the "Products" tab. Then, click on the product that you would like to edit from the list that appears. + +Once you are in edit mode for the product, select the shipping category you want to assign to the product from the "Shipping Categories" drop-down menu, and click "Update". + +![Select Shipping Category](/images/user/shipments/select_shipping_category.jpg) + +## Next Step + +Now that you understand how Shipping Categories work, let's move on to the next piece of the Spree shipping system - shipping [zones](zones). \ No newline at end of file diff --git a/guides/content/user/shipments/shipping_methods.md b/guides/content/user/shipments/shipping_methods.md new file mode 100644 index 00000000000..9dec00fe9f6 --- /dev/null +++ b/guides/content/user/shipments/shipping_methods.md @@ -0,0 +1,93 @@ +--- +title: Shipping Methods +--- + +## Shipping Methods + +Now that you have set up all of the pieces you need, it's time to put them together into the shipping options that the customer sees when they reach checkout. These options are called Shipping Methods - they are the carriers and services used to send your products. + +### Adding a Shipping Method + +To add a new shipping method to your store, go to the Admin Interface and click "Configuration", then "Shipping Methods". Click the "New Shipping Method" button to open the New Shipping Method form. + +![New Shipping Method](/images/user/shipments/new_shipping_method.jpg) + +#### Name + +Enter a name for the shipping method. This is the exact wording that the customer will see at checkout. This should include both the carrier (USPS, UPS, Fedex, DHL, etc.) as well as the service type (First Class Mail, Overnight, Ground, etc.) So it would be very common to need several shipping methods for your store, for example: + +* USPS First Class +* USPS First Class International +* USPS Priority +* USPS MediaMail +* UPS Two-Day +* UPS Ground +* Fedex Overnight + +Remember that you will need to associate one or more [zones](#zones) with each shipping method in order for it to appear as an option at checkout. + +#### Display + +From the "Display" drop-down box, choose whether you want to have the option display only on the backend, the frontend, or both. + +Shipping methods that are displayed on the frontend can be chosen by your store's customers at checkout time, as long as the products in the order can be shipped by that carrier and the shipping address is one the carrier serves. + +If a shipping method is available only on backend, then only your store's administrators can assign it to an order. Some examples of cases where you might want to use a backend-only shipping method: + +* You sell handmade wind chimes. You want to offer a "Pick-up in Store" option, but only to certain customers. +* With your online produce market you provide personal delivery of goods, but only to your best local customers. +* Yours is a photography studio. You usually sell prints that physical delivery, but for some clients you are willing to send electronic media that they can print themselves. + +#### Tracking URL + +You can optionally input a tracking URL for your new shipping method. This allows customers to track the progress of their package from your [Stock Location](stock_locations) to the order's shipping address. The string ":tracking" will be replaced with the tracking number you input once you actually process the order. + +You may need to check with the shipping carrier to see if they have a Shipping Confirmation URL that customers can use for this service. Some [commonly-used tracking URLs](http://verysimple.com/2011/07/06/ups-tracking-url/) are available online. + +!!! +Please note that Spree Commerce, Inc. makes no claims of warranty or accuracy for the information presented on third-party websites. We strongly urge you to verify the information independently before you put it into production on your store. +!!! + +#### Categories + +Some shipping methods may only apply to certain types of products in your store, regardless of where those items are being shipped. You may only want to send over-sized items via UPS Ground, for example, and not via USPS Priority. The options shown in the "Categories" section correspond to the [Shipping Categories](shipping_categories) you set up in an earlier section of this guide series. + +![Shipping Method Categories](/images/user/shipments/shipping_method_categories.jpg) + +Check the boxes next to the categories you want served by your new shipping method. + +#### Zones + +In [a previous step to this guide](zones) you learned about how to set up geographical zones for your store. Within the form's "Zones" section, you need to specify which zones are served by this shipping method. The "EU_VAT" (European Value-Added Tax) zone could be served by USPS First Class International, but could _not_ be served by USPS Priority Mail. + +![Shipping Method Zones](/images/user/shipments/shipping_method_zones.jpg) + +Check the boxes next to any zones you want served by this shipping method. + +#### Calculator + +Each shipping method is associated with one [Calculator](calculators). You can choose one of the built-in Spree calculators, or one you made yourself. + +![Shipping Method Calculator](/images/user/shipments/shipping_method_calculator.jpg) + +Once you've made your calculator selection, click the "Create" button to finalize your new shipping method. The screen will refresh with one or more fields you'll use to set the parameters of your calculator. For example, creating a shipping method with a flat percent calculator will produce a screen like this: + +![Shipping Method Flat Percent](/images/user/shipments/shipping_method_flat_percent.jpg) + +If necessary, you can re-read the [Calculators](calculators) portion of this guide series to better understand the options. Click the "Update" button, and your shipping method is now complete! + +### Editing a Shipping Method + +To edit an existing method, go to the Admin Interface and click "Configuration", then "Shipping Methods". Click the "Edit" icon next to any of the shipping methods in the list. + +![Edit Shipping Method](/images/user/shipments/edit_shipping_method.jpg) + +The form and all options that come up are the same as those you used in creating your shipping methods. + +### Deleting a Shipping Method + +To delete a shipping method, go to the Admin Interface and click "Configuration", then "Shipping Methods". Click the "Delete" icon next to any of the shipping methods in the list. + +![Delete Shipping Method](/images/user/shipments/delete_shipping_method.jpg) + +Confirm that you want to delete the shipping method by clicking "OK". \ No newline at end of file diff --git a/guides/content/user/shipments/zones.md b/guides/content/user/shipments/zones.md new file mode 100644 index 00000000000..7094fb9ba1e --- /dev/null +++ b/guides/content/user/shipments/zones.md @@ -0,0 +1,39 @@ +--- +title: Zones +--- + +## Zones + +Zones serve as a way to define shipping rules for a particular geographic area. A zone is made up of a set of either countries or states. Zones are used within Spree to define the rules for a [Shipping Method](shipping_methods). + +Each shipping method can be assigned to only one zone. For example, if one of the shipping methods for your store is UPS Ground (a US-only shipping carrier), then the zone for that shipping method should be defined as the United States. + +When the customer enters their shipping address during checkout, Spree uses that information to determine which zone the order is being delivered to, and only presents the shipping methods to the customer that are defined for that zone. + +### Creating a Zone + +To create a new zone, go to the Admin Interface, click the "Configuration" tab, click the "Zones" link, and then click the "New Zone" button. Enter a name and description for your new zone. Decide if you want it to be the default zone selected for the purposes of calculating sales tax. Choose whether you want the zone to be country-based or state-based. Click the "Create" button once complete. + +![New Zone](/images/user/shipments/new_zone.jpg) + +### Adding Members to a Zone + +Once you have a zone set up, you can associate either countries or states with it. To do this, go back to the Zones list (from the Admin Interface, click "Configuration", then "Zones"). Click the "Edit" icon next to the zone you just created. Click the "Add Country" or "Add State" button. + +![Edit Zone Form](/images/user/shipments/edit_zone.jpg) + +Choose a country or state from the drop-down box and click the "Add Country" or "Add State" button. Follow the same steps to add additional countries or states for the Zone. + +![Add Multiple Members](/images/user/shipments/add_multi_to_zone.jpg) + +Click "Update" once complete. + +### Removing Members From a Zone + +It is easy to remove a state or country from one of your zones. Just go to your Admin Interface and click "Configuration", then "Zones". Click the "Edit" icon next to the zone you want to change. To remove a member of the zone, just click the X icon below its name. + +![Remove a Zone Member](/images/user/shipments/remove_zone_member.jpg) + +## Next Step + +Once you have set up all of the shipping zones you need, it's time to move on to the next Spree shipping component: [Calculators](calculators). \ No newline at end of file diff --git a/guides/layouts/_changes.html b/guides/layouts/_changes.html new file mode 100644 index 00000000000..1afd6e7e6ac --- /dev/null +++ b/guides/layouts/_changes.html @@ -0,0 +1,6 @@ +<% @changes.each do |article| %> +<div class="change" id="<%= article.path %>"> + <%= render '_meta', :item => article %> + <%= article.compiled_content %> +</div> +<% end %> diff --git a/guides/layouts/_edge_badge.html b/guides/layouts/_edge_badge.html new file mode 100644 index 00000000000..f15af374544 --- /dev/null +++ b/guides/layouts/_edge_badge.html @@ -0,0 +1,5 @@ +<% if ENV['EDGE_GUIDES'] == 'true' %> +<div> + <img src="../images/edge_badge.png" alt="edge-badge" id="edge-badge" /> +</div> +<% end %> \ No newline at end of file diff --git a/guides/layouts/_footer.html b/guides/layouts/_footer.html new file mode 100644 index 00000000000..0d89d1d8567 --- /dev/null +++ b/guides/layouts/_footer.html @@ -0,0 +1,80 @@ +<footer id="main-footer"> + + <div class="footer-top"> + <div class="container"> + <div id="link-blocks" class="row"> + + <div class="link-block three columns"> + <h3 class="block-title">Product</h3> + <ul> + <li><a href='http://spreecommerce.com/storefront'>Platform</a></li> + <li><a href="https://wombat.co" target="_blank">Wombat</a></li> + <li><a href='http://spreecommerce.com/training_and_support'>Support</a></li> + <li><a href='http://spreecommerce.com/privacy_policy'>Privacy Policy</a></li> + <li><a href='http://spreecommerce.com/terms_of_service'>Terms of Service</a></li> + </ul> + </div> + + <div class="link-block three columns"> + <h3 class="block-title">Developers</h3> + <ul> + <li><a href="http://spreecommerce.com/storefront">Overview</a></li> + <li><a href="http://guides.spreecommerce.com/developer">Documenation</a></li> + <li><a href="http://spreecommerce.com/storefront">Community</a></li> + <li><a href="http://spreecommerce.com/license">License</a></li> + </ul> + </div> + <div class="link-block three columns"> + <h3 class="block-title">Partners</h3> + <ul> + <li><a href="http://spreecommerce.com/solution_partners">Pro Services</a></li> + <li><a href="http://spreecommerce.com/training_and_support">Training</a></li> + <li><a href="http://spreecommerce.com/become_a_partner">Partnership Program</a></li> + <li><a href="http://spreecommerce.com/partners/payments">Payments</a></li> + </ul> + </div> + + <div class="link-block four columns"> + <h3 class="block-title">About Spree Commerce</h3> + <p> + Spree Commerce is an automated enterprise solution built specifically for ecommerce. We effectively manage your operations so you can focus on serving your customers and growing your business. + </p> + </div> + + <div class="link-block three columns"> + <h3 class="block-title">Take it for a test drive</h3> + <p> + You can even create your own personal store. + </p> + <a href="http://spreecommerce.com/demo" target="_blank" class="button"><i class="icon-right"></i> Try The Demo</a> + </div> + + </div> + </div> + </div> + + + <div class="footer-bottom"> + <div class="container"> + <div class="row"> + + <div class="copyright ten columns"> + <p> + Spree, Spree Commerce and “Behind the Best Storefronts” are all trademarks of Spree Commerce Inc. + </p> + </div> + + <div class="social-icons six columns"> + <ul class="inline"> + <li><a href="https://github.com/spree/spree" target="_blank"><i class="icon-github-circled"></i></a></li> + <li><a href="https://twitter.com/spreecommerce" target="_blank"><i class="icon-twitter-circled"></i></a></li> + <li><a href="https://www.facebook.com/spreecommerce" target="_blank"><i class="icon-facebook-circled"></i></a></li> + <li><a href="https://plus.google.com/110792218022940311363" target="_blank"><i class="icon-gplus-circled"></i></a></li> + </ul> + </div> + + </div> + </div> + </div> +</footer> +<%= render '_google_analytics' %> diff --git a/guides/layouts/_github.html b/guides/layouts/_github.html new file mode 100644 index 00000000000..6ac4eb598dd --- /dev/null +++ b/guides/layouts/_github.html @@ -0,0 +1,10 @@ +<aside id="sidebar" class="four columns"> + + <div id="github"> + <h1><i class="icon-github-circled"></i> Latest Changes</h1> + <ul id="commits"> + + </ul> + </div> + +</aside> \ No newline at end of file diff --git a/guides/layouts/_google_analytics.html b/guides/layouts/_google_analytics.html new file mode 100644 index 00000000000..aed99c5b55c --- /dev/null +++ b/guides/layouts/_google_analytics.html @@ -0,0 +1,14 @@ +<!-- google analytics --> +<script type="text/javascript"> + + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-3914566-1']); + _gaq.push(['_trackPageview']); + + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + +</script> \ No newline at end of file diff --git a/guides/layouts/_head.html b/guides/layouts/_head.html new file mode 100644 index 00000000000..0b517e0a91a --- /dev/null +++ b/guides/layouts/_head.html @@ -0,0 +1,49 @@ +<meta charset="utf-8"/> +<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> +<% if @item && @item[:description] %> +<meta name="description" content="<%= @item[:description] %>"/> +<% end %> +<meta name="viewport" content="width=device-width, initial-scale=1"/> +<meta name="section" content="<%= @item[:section] %>" /> + +<link href="/favicon.ico" rel="shortcut icon" type="image/x-icon" /> + +<!--[if lt IE 9]> + <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> +<![endif]--> + +<link href="/shared/css/icons.css" media="screen" rel="stylesheet" type="text/css"> +<link href="/shared/css/icons-codes.css" media="screen" rel="stylesheet" type="text/css"> + +<!--[if IE 7]> + <link href="/shared/css/icons-ie7.css" media="screen" rel="stylesheet" type="text/css"> + <link href="/shared/css/icons-ie7-codes.css" media="screen" rel="stylesheet" type="text/css"> +<![endif]--> + +<link href="/shared/css/skeleton.css" media="screen" rel="stylesheet" type="text/css"> +<link href='//fonts.googleapis.com/css?family=Bree+Serif' rel='stylesheet' type='text/css'> +<link href="/shared/css/documentation.css" media="screen" rel="stylesheet" type="text/css"> +<link rel="stylesheet" type="text/css" href="/assets/stylesheets/guides.css" media="screen" /> + +<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> +<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-migrate/1.1.1/jquery-migrate.min.js"></script> +<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.3.1/jquery.cookie.min.js"></script> +<script src="/assets/javascripts/css_browser_selector_dev.js" type="text/javascript"></script> +<script src="/assets/javascripts/jquery.toc.js" type="text/javascript"></script> +<!-- <script src="/assets/javascripts/waypoints.js" type="text/javascript"></script> +<script src="/assets/javascripts/waypoints-sticky.js" type="text/javascript"></script> --> +<script src="/assets/javascripts/documentation.js" type="text/javascript"></script> + +<script type="text/javascript"> +$(function(){ + $.ajax({ + url: document.location.protocol + '//munchkin.marketo.net/munchkin.js', + dataType: 'script', + cache: true, + success: function() { Munchkin.init('893-PDJ-826'); } + }); +}); +</script> +<script type="text/javascript" src="//use.typekit.net/tcx0cap.js"></script> +<script type="text/javascript">try{Typekit.load();}catch(e){}</script> + \ No newline at end of file diff --git a/guides/layouts/_main_menu.html b/guides/layouts/_main_menu.html new file mode 100644 index 00000000000..8c3708482bb --- /dev/null +++ b/guides/layouts/_main_menu.html @@ -0,0 +1,10 @@ +<nav id="main-menu" class="eleven columns"> + <ul class="inline"> + <li><a href="../">Home</a></li> + <li><a href="../user/">User</a></li> + <li><a href="../developer/">Developer</a></li> + <li><a href="../api/">API</a></li> + <li><a href="https://wombat.co" target="_blank">Wombat</a></li> + <li><a href="../release_notes/">Release Notes</a></li> + </ul> +</nav> diff --git a/guides/layouts/_main_menu_home.html b/guides/layouts/_main_menu_home.html new file mode 100644 index 00000000000..4349ac897cc --- /dev/null +++ b/guides/layouts/_main_menu_home.html @@ -0,0 +1,10 @@ +<nav id="main-menu" class="eleven columns"> + <ul class="inline"> + <li><a href="./">Home</a></li> + <li><a href="user/">User</a></li> + <li><a href="developer/">Developer</a></li> + <li><a href="api/">API</a></li> + <li><a href="https://wombat.co" target="_blank">Wombat</a></li> + <li><a href="release_notes/">Release Notes</a></li> + </ul> +</nav> diff --git a/guides/layouts/_meta.html b/guides/layouts/_meta.html new file mode 100644 index 00000000000..12ca53188c0 --- /dev/null +++ b/guides/layouts/_meta.html @@ -0,0 +1,18 @@ +<h2 class="title"> + <a href="<%= @item.path %>"><%= @item[:title] %></a> +</h2> + +<div class="meta"> + <div class="who_when"> + <%= gravatar_for(@item[:author_name]) %> + <span class="author vcard fn"> + <a href="https://github.com/<%= @item[:author_name] %>"><%= @item[:author_name] %></a> + </span> + <span class="published"> + <%= post_date @item %> + <% if version = @item[:api_version] %> + / Version: <a href="/changes/<%= version %>"><%= version %></a> + <% end %> + </span> + </div> +</div> diff --git a/guides/layouts/_spree_conf_widget.html b/guides/layouts/_spree_conf_widget.html new file mode 100644 index 00000000000..c6a8426d206 --- /dev/null +++ b/guides/layouts/_spree_conf_widget.html @@ -0,0 +1,145 @@ +<div id="share-window"> + <a class="close-widget" href="#">x</a> + + <img src="/images/spreeconf-nyc-2014-badge.jpg" alt="Spree Conf NYC 2014"> + + <h4 class="title">Want to learn more about Spree?</h4> + <p> + Early Bird is 50% Off (Ends 12/31) + </p> + <a href="http://spreeconf.com" target="_blank" class="button">Learn More</a> +</div> + +<style> + #share-window * { + font-family: 'futura-pt', serif !important; + color: #fff; + } + + #share-window img { + position: absolute; + left: 0; + z-index: 0; + } + + #share-window { + position: fixed; + bottom: -450px; + margin-left: 720px; + width: 250px; + height: 400px; + background-color: #0a0a0a; + text-align: center; + z-index: 1; + line-height: 28px; + } + + #share-window .close-widget { + position: absolute; + right: 10px; + top: 5px; + font-size: 30px; + color: white; + text-decoration: none; + z-index: 2; + } + + #share-window h1, #share-window h4 { + font-weight: normal; + margin-bottom: 0; + position: absolute; + z-index: 2; + } + + #share-window h1 { + font-size: 28px; + line-height: 35px; + position: absolute; + z-index: 2; + } + + #share-window a.button { + border: 3px solid white; + position: absolute; + top: 320px; + color: #FFF; + padding: 10px 30px 8px; + font-size: 16px; + text-decoration: none; + text-transform: uppercase; + background-color: transparent; + width: 200px; + left: 25px; + border-radius: 0; + } + + #share-window a.button:hover { + background-color: white; + color: #0a0a0a !important; + } + + #share-window p, #share-window p strong { + margin-top: 10px; + color: #D96657; + margin-bottom: 0; + } + + #share-window h2, #share-window h3 { + margin: 0; + border: none; + line-height: 28px; + } + + #share-window h3 { + margin-top: 0; + padding-top: 0; + } + + #share-window h4 { + font-size: 16px; + padding-top: 0; + margin-bottom: 10px; + margin-top: 0; + font-weight: 500; + text-transform: uppercase; + top: 260px; + line-height: 25px; + } +</style> + +<script> + $(function(){ + + var sc_banner_state = $.cookie('sc_nyc_2014_banner_state'); + + if (new Date().valueOf() < new Date('2014/02/27').valueOf()) { + if(sc_banner_state != 'closed') { + var widget = $("#share-window"); + + $("#share-window a.close-widget").click(function(e){ + e.preventDefault(); + widget.animate({ + bottom: "-400px", + opacity: 0 + }, 1000); + $.cookie('sc_nyc_2014_banner_state', 'closed', { path: '/' }); + }); + + $(window).scroll(function(){ + if($.browser.webkit) { + var scroll_top = $('body').scrollTop(); + } + else { + var scroll_top = document.documentElement.scrollTop; + } + if(scroll_top > $(window).height()){ + widget.animate({ + bottom: "-5px" + }); + $(window).unbind('scroll'); + } + }); + } + } + }); +</script> diff --git a/guides/layouts/api.html b/guides/layouts/api.html new file mode 100644 index 00000000000..326dd029c55 --- /dev/null +++ b/guides/layouts/api.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<!--[if lt IE 7]> <html class="ie lt-ie9 lt-ie8 lt-ie7"> <![endif]--> +<!--[if IE 7]> <html class="ie lt-ie9 lt-ie8"> <![endif]--> +<!--[if IE 8]> <html class="ie lt-ie9"> <![endif]--> +<!--[if IE 9]> <html class="ie ie9"> <![endif]--> +<!--[if gt IE 9]><!--> <html lang="en" > <!--<![endif]--> +<head> + <title><%= @item[:title]%> - Storefront API | Spree Commerce + + <%= render '_head' %> + + + + + + + <%= render '_edge_badge' %> + + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          + + <%= render 'api/_sidebar' %> + +
          + + <%= render '_footer' %> diff --git a/guides/layouts/api/_sidebar.html b/guides/layouts/api/_sidebar.html new file mode 100644 index 00000000000..f05319a9033 --- /dev/null +++ b/guides/layouts/api/_sidebar.html @@ -0,0 +1,562 @@ + diff --git a/guides/layouts/api/default.html b/guides/layouts/api/default.html new file mode 100644 index 00000000000..13110ea396b --- /dev/null +++ b/guides/layouts/api/default.html @@ -0,0 +1,58 @@ + + + + + + + + <%= @item[:title] %> - Storefront API | Spree Commerce + + <%= render '_head' %> + + + + <%= render '_edge_badge' %> + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          + + <%= render "api/_sidebar" %> +
          + + <%= render '_footer' %> + + diff --git a/guides/layouts/changes.html b/guides/layouts/changes.html new file mode 100644 index 00000000000..710fd394ac7 --- /dev/null +++ b/guides/layouts/changes.html @@ -0,0 +1,5 @@ +
          +<%= render '_meta', :item => @item %> + +<%= yield %> +
          diff --git a/guides/layouts/default.html b/guides/layouts/default.html new file mode 100644 index 00000000000..cd61259c320 --- /dev/null +++ b/guides/layouts/default.html @@ -0,0 +1,95 @@ + + + + + + + + <%= @item[:title] %> | Spree Commerce + + <%= render '_head' %> + + + + + <% if ENV['EDGE_GUIDES'] == 'true' %> +
          + edge-badge +
          + <% end %> + + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          +
          + + <%= render '_footer' %> + + diff --git a/guides/layouts/developer.html b/guides/layouts/developer.html new file mode 100644 index 00000000000..d375488914d --- /dev/null +++ b/guides/layouts/developer.html @@ -0,0 +1,58 @@ + + + + + + + + <%= @item[:title] %> | Spree Commerce + + <%= render '_head' %> + + + + <%= render '_edge_badge' %> + + +
          +
          + +
          +
          + <%= yield %> +
          +
          +
          + + <%= render 'developer/_sidebar' %> +
          + + <%= render '_footer' %> + + + diff --git a/guides/layouts/developer/_sidebar.html b/guides/layouts/developer/_sidebar.html new file mode 100644 index 00000000000..4d8b2d79a69 --- /dev/null +++ b/guides/layouts/developer/_sidebar.html @@ -0,0 +1,118 @@ + diff --git a/guides/layouts/developer/default.html b/guides/layouts/developer/default.html new file mode 100644 index 00000000000..4dc4b25d398 --- /dev/null +++ b/guides/layouts/developer/default.html @@ -0,0 +1,58 @@ + + + + + + + + <%= @item[:title] %> - Developer Guide | Spree Commerce + + <%= render '_head' %> + + + + <%= render '_edge_badge' %> + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          + + <%= render "developer/_sidebar" %> +
          + + <%= render '_footer' %> + + diff --git a/guides/layouts/release_notes.html b/guides/layouts/release_notes.html new file mode 100644 index 00000000000..495330f275b --- /dev/null +++ b/guides/layouts/release_notes.html @@ -0,0 +1,107 @@ + + + + + + + + <%= @item[:title] %> | Spree Commerce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%= render '_edge_badge' %> + + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          + + <%= render 'integration/_sidebar' %> + <%= render '_spree_conf_widget' %> + +
          + + <%= render '_footer' %> + + diff --git a/guides/layouts/release_notes/_sidebar.html b/guides/layouts/release_notes/_sidebar.html new file mode 100644 index 00000000000..f354a48d9b7 --- /dev/null +++ b/guides/layouts/release_notes/_sidebar.html @@ -0,0 +1,32 @@ + diff --git a/guides/layouts/release_notes/default.html b/guides/layouts/release_notes/default.html new file mode 100644 index 00000000000..239cfa37b6f --- /dev/null +++ b/guides/layouts/release_notes/default.html @@ -0,0 +1,58 @@ + + + + + + + + <%= @item[:title] %> - Release Notes | Spree Commerce + + <%= render '_head' %> + + + + <%= render '_edge_badge' %> + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          + + <%= render "release_notes/_sidebar" %> +
          + + <%= render '_footer' %> + + diff --git a/guides/layouts/user.html b/guides/layouts/user.html new file mode 100644 index 00000000000..873b2973a1b --- /dev/null +++ b/guides/layouts/user.html @@ -0,0 +1,66 @@ + + + + + + + + <%= @item[:title] %> | Spree Commerce + + <%= render '_head' %> + + + + + + <%= render '_edge_badge' %> + + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          + + <%= render 'user/_sidebar' %> + +
          + + <%= render '_footer' %> + + diff --git a/guides/layouts/user/_sidebar.html b/guides/layouts/user/_sidebar.html new file mode 100644 index 00000000000..007bc1d3a33 --- /dev/null +++ b/guides/layouts/user/_sidebar.html @@ -0,0 +1,219 @@ + diff --git a/guides/layouts/user/default.html b/guides/layouts/user/default.html new file mode 100644 index 00000000000..4322ac9d5a8 --- /dev/null +++ b/guides/layouts/user/default.html @@ -0,0 +1,58 @@ + + + + + + + + <%= @item[:title] %> - User Guide | Spree Commerce + + <%= render '_head' %> + + + + <%= render '_edge_badge' %> + + +
          +
          + +
          +
          + <%= yield %> +
          +
          + +
          + + <%= render "user/_sidebar" %> +
          + + <%= render '_footer' %> + + diff --git a/guides/lib/changes_helper.rb b/guides/lib/changes_helper.rb new file mode 100644 index 00000000000..d26a8ad5710 --- /dev/null +++ b/guides/lib/changes_helper.rb @@ -0,0 +1,89 @@ +module ChangesHelper + MimeFormat = "application/vnd.github.%s+json".freeze + # Public: Filters the change items out. If a version is given, show only the + # items related to that version. + # + # version - Optional String version key. + # + # Returns an Array of the first 30 Nanoc::Item objects, sorted in reverse + # chronological order. + def api_changes(version = nil) + changes = @items.select { |item| item[:kind] == 'change' } + if version + version_s = version.to_s + changes.select { |item| item[:api_version] == version_s } + else + changes + end.sort! do |x, y| + attribute_to_time(y[:created_at]) <=> attribute_to_time(x[:created_at]) + end.first(30) + end + + # Public + def current_api + @current_api ||= (api_versions[-2] || api_versions.first).first + end + + # Public + def upcoming_api + @upcoming_api ||= begin + version, date = api_versions.last + version unless date + end + end + + # Public + def current_api?(version) + @api_current_checks ||= {} + if @api_current_checks.key?(version) + @api_current_checks[version] + end + + @api_current_checks[version] = version == current_api + end + + # Public + def no_current_api_versions?(*versions) + versions.none? { |v| current_api?(v) } + end + + # Public + def api_released_at(version) + @api_releases ||= {} + if @api_releases.key?(version) + @api_releases[version] + end + + @api_releases[version] = begin + pair = api_versions.detect do |(name, date)| + name == version + end + pair ? pair[1] : nil + end + end + + # Public + def api_mimetype_listing(version) + version_s = version.to_s + mime = mimetype_for version_s + if time = api_released_at(version_s) + mime << " (" + mime << "Current, " if current_api?(version_s) + mime << strftime(time) + mime << ")" + else + mime + end + end + + # Internal + def mimetype_for(version) + MimeFormat % version.to_s + end + + # Internal + def api_versions + @api_versions ||= Array(@site.config[:api_versions]) + end +end + diff --git a/guides/lib/default.rb b/guides/lib/default.rb new file mode 100644 index 00000000000..28ed20f6c5a --- /dev/null +++ b/guides/lib/default.rb @@ -0,0 +1,5 @@ +include Nanoc::Helpers::Blogging +include Nanoc::Helpers::Breadcrumbs +include Nanoc::Helpers::Rendering +include Nanoc::Helpers::LinkTo +include ChangesHelper diff --git a/guides/lib/filters/add_toc.rb b/guides/lib/filters/add_toc.rb new file mode 100644 index 00000000000..de945b645d4 --- /dev/null +++ b/guides/lib/filters/add_toc.rb @@ -0,0 +1,41 @@ +class AddTOCFilter < Nanoc::Filter + + identifier :add_toc + + def run(content, params={}) + content.gsub('{{TOC}}') do + # Find all top-level sections + doc = Nokogiri::HTML(content) + headers = [] + doc.css("#main-content").css("h2, h3").each do |header_tag| + header = { :title => header_tag.inner_html, :id => header_tag['id'] } + if header_tag.name == 'h2' + headers << header + @current_header = header + else + @current_header[:subs] ||= [] + @current_header[:subs] << { :title => header_tag.inner_html, :id => header_tag['id'] } + end + end + + # Build table of contents + res = '
            ' + headers.each do |header| + res << %[
          1. #{header[:title]}] + if header[:subs] + res << "
              " + header[:subs].each do |header| + res << %[
            1. #{header[:title]}] + end + res << "
            " + end + res << "
          2. " + end + res << '
          ' + + res + end + end + +end + diff --git a/guides/lib/filters/fenced_code_blocks.rb b/guides/lib/filters/fenced_code_blocks.rb new file mode 100644 index 00000000000..add35a4a429 --- /dev/null +++ b/guides/lib/filters/fenced_code_blocks.rb @@ -0,0 +1,14 @@ +class FencedCodeBlocks < Nanoc::Filter + + identifier :fenced_code_blocks + + def run(content, params={}) + content = content.gsub(/^```\s?(.*?)\n(.*?)```/m) do + "~~~ #{$1}\n" + + "#{$2}\n" + + "~~~" + end + content + end +end + diff --git a/guides/lib/filters/info_boxes.rb b/guides/lib/filters/info_boxes.rb new file mode 100644 index 00000000000..5818df0e8d1 --- /dev/null +++ b/guides/lib/filters/info_boxes.rb @@ -0,0 +1,42 @@ +require 'kramdown' + +class ParseInfoBoxes < Nanoc::Filter + + identifier :parse_info_boxes + + def run(content, params={}) + content = content.gsub(/^!!!\n(.*?)!!!/m) do + generate_div("warning", $1) + end + + content = content.gsub(/^\*\*\*\n(.*?)\*\*\*/m) do + generate_div("note", $1) + end + + content = content.gsub(/^\+\+\+\n(.*?)\+\+\+/m) do + generate_div("github", $1) + end + + content = content.gsub(/^\$\$\$\n(.*?)\$\$\$/m) do + "

          **************** TODO ****************

          " + $1 + "

          **************************************

          " + end + + # add filename headers to code blocks + content = content.gsub(/^---(.*?)---/m) do + "
          " + $1 + "
          " + end + + content + end + + private + + def generate_div(klass, content) + %{
          #{parse_inner_content(content)}
          } + end + + def parse_inner_content(content) + Kramdown::Document.new(content).to_html + end +end + diff --git a/guides/lib/filters/pretty_urls.rb b/guides/lib/filters/pretty_urls.rb new file mode 100644 index 00000000000..fb3a2b71b2b --- /dev/null +++ b/guides/lib/filters/pretty_urls.rb @@ -0,0 +1,23 @@ +require 'nanoc' + +class PrettyUrls < Nanoc::Filter + + identifier :pretty_urls + + def run(content, params={}) + + # Allows us to use pretty urls in markdown and add .html for local viewing + + # TODO: Don't add .html when deploying to server - just use for local development since nanoc server + # is not smart enough to understand absence of .html and still route to the correct file + + # [Hubspot Integration](hubspot_integration#foo) => [Hubspot Integration](hubspot_integration.html#foo) + #content = content.gsub /\[(.+)\]\(([^#]+)(#\S*)?\)/ do + # [Hubspot Integration](hubspot_integration) => [Hubspot Integration](hubspot_integration.html) + content = content.gsub /\[([^\]]*)\]\(([^[#\)]\.]+)(#\S*)?\)/ do + "[#{$1}](#{$2}.html#{$3})" + end + + content + end +end \ No newline at end of file diff --git a/guides/lib/resources.rb b/guides/lib/resources.rb new file mode 100644 index 00000000000..2d5f4c58ad2 --- /dev/null +++ b/guides/lib/resources.rb @@ -0,0 +1,1052 @@ +require 'pp' +require 'yajl/json_gem' +require 'stringio' +require 'cgi' + +module Spree + module Resources + module Helpers + STATUSES ||= { + 200 => '200 OK', + 201 => '201 Created', + 202 => '202 Accepted', + 204 => '204 No Content', + 301 => '301 Moved Permanently', + 302 => '302 Found', + 307 => '307 Temporary Redirect', + 304 => '304 Not Modified', + 401 => '401 Unauthorized', + 403 => '403 Forbidden', + 404 => '404 Not Found', + 409 => '409 Conflict', + 422 => '422 Unprocessable Entity', + 500 => '500 Server Error' + } + + DefaultTimeFormat ||= "%B %-d, %Y".freeze + + def post_date(item) + strftime item[:created_at] + end + + def strftime(time, format = DefaultTimeFormat) + attribute_to_time(time).strftime(format) + end + + def gravatar_for(login) + %() % gravatar_url_for(login) + end + + def gravatar_url_for(login) + # TODO: Fix this. + return "" + md5 = AUTHORS[login.to_sym] + default = "https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" + "https://secure.gravatar.com/avatar/%s?s=20&d=%s" % + [md5, default] + end + + def headers(status, head = {}) + css_class = (status == 201 || status == 204 || status == 404) ? 'headers no-response' : 'headers' + lines = ["Status: #{STATUSES[status]}"] + + %(
          #{lines * "\n"}
          \n) + end + + def json(key) + hash = case key + when Hash + h = {} + key.each { |k, v| h[k.to_s] = v } + h + when Array + key + else Resources.const_get(key.to_s.upcase) + end + + hash = yield hash if block_given? + + %(
          ) +
          +          JSON.pretty_generate(hash) + "
          " + end + + def ruby(&block) + %(
          ) +
          +        block.call + "
          " + end + + def link_to(text, link, anchor=nil) + if link.is_a?(Symbol) + url = LINKS[link] + raise "No link found for #{link}" unless url + else + url = link + end + if anchor + "#{text}" + else + "#{text}" + end + end + + LINKS ||= {} + LINKS[:core] = "/developer/" + LINKS[:products] = LINKS[:core] + "products" + LINKS[:variants] = LINKS[:products] + "#variants" + LINKS[:prices] = LINKS[:core] + "#prices" + LINKS[:orders] = LINKS[:core] + "orders" + LINKS[:line_items] = LINKS[:orders] + "#line-items" + LINKS[:adjustments] = LINKS[:core] + "adjustments" + LINKS[:payments] = LINKS[:core] + "payments" + LINKS[:calculators] = LINKS[:core] + "calculators" + LINKS[:taxation] = LINKS[:core] + "taxation" + LINKS[:shipping] = LINKS[:core] + "shipments" + LINKS[:addresses] = LINKS[:core] + "addresses" + LINKS[:zones] = LINKS[:addresses] + "#zones" + LINKS[:promotions] = LINKS[:core] + "promotions" + LINKS[:activators] = LINKS[:core] + "activators" + LINKS[:preferences] = LINKS[:core] + "preferences" + LINKS[:checkout] = LINKS[:core] + "checkout" + LINKS[:extensions] = LINKS[:core] + "extensions_tutorial" + + def warning(message) + %(
          ) + message + %(
          ) + end + + def admin_only + warning("This action is only accessible by an admin user.") + end + + def not_found + headers(404) + json(:error => "The resource you were looking for could not be found.") + end + + def authorization_failure + headers(401) + json(:error => "You are not authorized to perform that action.") + end + + def invalid_resource + headers(422) + json(:error => "Invalid object.") + end + + def invalid_token + headers(401) + json(:error => "Token is invalid.") + end + + def store_not_found + headers(404) + json(:error => "Store not found.") + end + + def text_html(response, status, head = {}) + hs = headers(status, head.merge('Content-Type' => 'text/html')) + res = CGI.escapeHTML(response) + hs + %(
          ) + res + "
          " + end + + # Used in the release notes to stop RSI + def issue(num) + "##{num}" + end + + def commit(sha) + "commit #{sha[0..7]}" + end + end + + USER ||= + { + "id"=>1, + "email"=>"spree@example.com", + "login"=>"spree@example.com", + "spree_api_key"=>nil, + "created_at"=>"Fri, 01 Feb 2013 20:38:57 UTC +00:00", + "updated_at"=>"Fri, 01 Feb 2013 20:38:57 UTC +00:00" + } + + UPDATED_USER ||= USER.merge({"spree_api_key" => "A13adsfq234", + "updated_at" => "Fri, 01 Feb 2013 20:40:57 UTC +00:00"}) + + IMAGE ||= + {"id"=>1, + "position"=>1, + "attachment_content_type"=>"image/jpg", + "attachment_file_name"=>"ror_tote.jpeg", + "type"=>"Spree::Image", + "attachment_updated_at"=>nil, + "attachment_width"=>360, + "attachment_height"=>360, + "alt"=>nil, + "viewable_type"=>"Spree::Variant", + "viewable_id"=>1, + "mini_url"=>"/spree/products/1/mini/file.png?1370533476", + "small_url"=>"/spree/products/1/small/file.png?1370533476", + "product_url"=>"/spree/products/1/product/file.png?1370533476", + "large_url"=>"/spree/products/1/large/file.png?1370533476"} + + OPTION_VALUE ||= + { + "id"=>1, + "name"=>"Small", + "presentation"=>"S", + "option_type_name"=>"tshirt-size", + "option_type_id"=>1 + } + + OPTION_TYPE ||= + { + "id" => 1, + "name" => "tshirt-size", + "presentation" => "Size", + "position" => 1 + } + + VARIANT ||= + { + "id"=>1, + "name"=>"Ruby on Rails Tote", + "sku"=>"ROR-00011", + "price"=>"15.99", + "display_price"=>"$15.99", + "weight"=>nil, + "height"=>nil, + "width"=>nil, + "depth"=>nil, + "is_master"=>true, + "cost_price"=>"13.0", + "permalink"=>"ruby-on-rails-tote", + "description"=>"A text description of the product.", + "options_text"=> "(Size: small, Colour: red)", + "in_stock" => true, + "option_values"=> [OPTION_VALUE], + "images"=> [IMAGE] + } + + PRODUCT_PROPERTY ||= + { + "id"=>1, + "product_id"=>1, + "property_id"=>1, + "value"=>"Tote", + "property_name"=>"bag_type" + } + + MESSAGE ||= { + 'message' => 'some:event', + 'message_id' => ':guid', + 'payload' => {} + } + + SYNC_MESSAGE_RESPONSE ||= { + 'message_id' => ':guid', + 'messages' => [], + 'events' => [] + } + + ASYNC_MESSAGE_RESPONSE ||= { + 'message_id' => ':guid', + 'delay' => 6000, + 'update_url' => 'http://example.com/poll' + } + + UPDATE_REQUEST ||= { + 'message_id' => ':guid' + } + + NEW_PRODUCT_EVENT ||= + { + "event" => 'product:new', + "event_id" => '510bfe8e7575e41e41000017', + "payload" => { + "id"=>1, + "name"=>"Example product", + "description"=> "Description", + "price"=>"15.99", + "available_on"=>"2012-10-17T03:43:57Z", + "permalink"=>"ruby-on-rails-tote", + "count_on_hand"=>10, + "meta_description"=>nil, + "meta_keywords"=>nil } + } + + NEW_PRODUCT_EVENT_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000004', + "result" => 'OK', + "details" => { + "message" => "Product Added" + } + } + + NEW_PRODUCT_PUSH ||= + { + "message"=> 'product:new', + "payload" => { + "id"=>1123, + "name"=>"Example product", + "description"=> "Description", + "price"=>"15.99", + "available_on"=>"2012-10-17T03:43:57Z", + "permalink"=>"ruby-on-rails-tote", + "meta_description"=>nil, + "meta_keywords"=>nil + } + } + + temp ||= NEW_PRODUCT_PUSH.clone + temp['message_id'] = 'guid' + NEW_PRODUCT_PUSH_RESPONSE ||= temp + + UPDATE_PRODUCT_EVENT ||= + { + "event" => 'product:update', + "event_id" => '510bfe8e7575e41e41000017', + "payload" => { + "id"=>1, + "name"=>"Example product", + "description"=> "Description", + "price"=>"15.99", + "available_on"=>"2012-10-17T03:43:57Z", + "permalink"=>"ruby-on-rails-tote", + "count_on_hand"=>10, + "meta_description"=>nil, + "meta_keywords"=>nil } + } + + UPDATE_PRODUCT_EVENT_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000004', + "result" => 'OK', + "details" => { + "message" => "Product Updated" + } + } + + PRODUCT ||= + { + "id"=>1, + "name"=>"Example product", + "description"=> "Description", + "price"=>"15.99", + "display_price"=>"$15.99", + "available_on"=>"2012-10-17T03:43:57Z", + "permalink"=>"ruby-on-rails-tote", + "meta_description"=>nil, + "meta_keywords"=>nil, + "taxon_ids" => [1,2,3], + "shipping_category_id" => 1, + "has_variants" => true, + "master" => VARIANT.merge("is_master" => true), + "variants" => [VARIANT.merge("is_master" => false)], + "product_properties"=> [PRODUCT_PROPERTY], + "option_types" => [OPTION_TYPE] + } + + PAYMENT_METHOD ||= + { + "id"=>732545999, + "name"=>"Check", + "description"=>"Pay by check.", + "method_type"=>"check" + } + + + ORDER_PAYMENT ||= + { + "id"=>1, + "amount"=>"10.00", + "state"=>"checkout", + "payment_method_id"=>1, + "payment_method" => PAYMENT_METHOD + } + + NEW_PAYMENT_EVENT ||= + { + "event" => 'payment:new', + "event_id" => '510bfe8e7575e41e41000017', + "payload" => { + "id"=>1, + "amount"=>"10.00", + "state"=>"checkout", + "payment_method_id"=>1 } + } + + NEW_PAYMENT_EVENT_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000004', + "result" => 'OK', + "details" => { + "message" => "Payment Received" + } + } + + line_item_variant = VARIANT + + LINE_ITEM ||= + { + "id"=>1, + "quantity"=>2, + "price"=>"19.99", + "single_display_amount"=> "$19.99", + "total"=> "39.99", + "display_total"=> "$39.99", + "variant_id"=>1, + "variant" => line_item_variant + } + + LINE_ITEM2 ||= + { + "id"=>2, + "quantity"=>2, + "price"=>"19.99", + "single_display_amount"=> "$19.99", + "total"=> "39.99", + "display_total"=> "$39.99", + "variant_id"=>2, + "variant" => line_item_variant + } + + ADJUSTMENT ||= + { + "id" => 1073043775, + "source_type" => "Spree::Order", + "source_id" => 1, + "adjustable_type" => "Spree::Order", + "adjustable_id" => 1, + "originator_type" => "Spree::PromotionAction", + "originator_id" => 1, + "amount" => "-12.0", + "display_amount" => "-$12.00", + "label" => "Promotion (test)", + "mandatory" => false, + "locked" => false, + "eligible" => true, + "created_at" => "2012-10-24T01:02:25Z", + "updated_at" => "2012-10-24T01:02:25Z" + } + + PAYMENT ||= + { + "id"=>1, + "source_type"=>"Spree::CreditCard", + "source_id"=>1, + "amount"=>"10.00", + "payment_method_id"=>1, + "response_code"=>"12345", + "state"=>"checkout", + "avs_response"=>nil, + "created_at"=>"2012-10-24T23:26:23Z", + "updated_at"=>"2012-10-24T23:26:23Z" + } + + SHIPPING_METHOD ||= + { + "name" => "UPS Ground", + "zone_id" => 1, + "shipping_category_id" => 1 + } + + SHIPPING_RATE ||= + { + "id"=>1, + "name"=>"UPS Ground (USD)", + "cost"=>5, + "selected"=>true, + "shipment_id"=>1, + "shipping_method_id"=>5 + } + + MANIFEST ||= + { + "quantity"=>1, + "states"=>{"on_hand"=>1}, + "variant" => VARIANT + } + + INVENTORY_UNIT ||= + { + "id"=>1, + "lock_version"=>1, + "state"=>"on_hand", + "variant_id"=>10, + "shipment_id"=>1, + "return_authorization_id"=>nil + } + + SHIPMENT ||= + { + "id"=>1, + "tracking"=>nil, + "number"=>"H123456789", + "cost"=>"5.0", + "shipped_at"=>nil, + "state"=>"pending", + "order_id"=>"R1234567", + "stock_location_name"=>"NY Warehouse", + "shipping_rates"=>[SHIPPING_RATE], + "shipping_method"=> SHIPPING_METHOD, + "manifest"=>[MANIFEST] + } + + ORDER ||= + { + "id"=>1, + "number"=>"R335381310", + "item_total"=>"100.0", + "display_item_total"=>"$100.00", + "total"=>"100.0", + "display_total"=>"$100.00", + "state"=>"cart", + "adjustment_total"=>"-12.0", + "user_id"=>nil, + "created_at"=>"2012-10-24T01:02:25Z", + "updated_at"=>"2012-10-24T01:02:25Z", + "completed_at"=>nil, + "payment_total"=>"0.0", + "shipment_state"=>nil, + "payment_state"=>nil, + "email"=>nil, + "special_instructions"=>nil, + "total_quantity"=>1, + "token"=> "abcdef123456", + "line_items"=>[], + "adjustments"=>[], + "payments"=>[], + "shipments"=>[] + } + + NEW_ORDER ||= { + "id"=>1, + "number"=>"R335381310", + "item_total"=>"100.0", + "display_item_total"=>"$100.00", + "total"=>"0.0", + "state"=>"cart", + "adjustment_total"=>"-0.0", + "user_id"=>nil, + "created_at"=>"2012-10-24T01:02:25Z", + "updated_at"=>"2012-10-24T01:02:25Z", + "completed_at"=>nil, + "payment_total"=>"0.0", + "shipment_state"=>nil, + "payment_state"=>nil, + "email"=>nil, + "special_instructions"=>nil, + "total_quantity"=>1, + "token"=> "abcdef123456", + "line_items"=>[], + "adjustments"=>[], + "payments"=>[], + "shipments"=>[] + } + + NEW_ORDER_WITH_LINE_ITEMS ||= NEW_ORDER.merge({ + "line_items" => [LINE_ITEM] + }) + + ORDER_FAILED_TRANSITION ||= { + "error" => "The order could not be transitioned. Please fix the errors and try again.", + "errors" => { :email => ["can't be blank"] } + } + + temp = SHIPMENT.merge({ + "tracking" => "UPS1234566", + "shipped_at" => Time.now.to_s, + "state" => "shipped" + }) + temp.delete('shipping_method') + temp.delete('id') + PUSH_SHIPMENT_CONFIRMATION ||= temp + + PUSH_SHIPMENT_RESPONSE ||= { + 'event_id' => 'guid', + 'result' => 'accepted', + 'payload' => PUSH_SHIPMENT_CONFIRMATION + } + + READY_SHIPMENT ||= SHIPMENT.merge({"state" => "ready_to_ship"}) + + SHIPPED_SHIPMENT ||= SHIPMENT.merge({"state" => "shipped"}) + + ORDER_SHOW ||= ORDER.merge({ + "line_items" => [LINE_ITEM], + "payments" => [], + "adjustments" => [] + + }) + + ORDER_SHOW2 ||= ORDER.merge({ + "line_items" => [LINE_ITEM2], + "payments" => [PAYMENT], + "shipments" => [SHIPMENT], + "adjustments" => [ADJUSTMENT] + + }) + + EVENT ||= { + "event" => 'event:name', + "event_id" => 'guid', + "payload" => { + "order" => "..." + } + } + + EVENT_RESPONSE ||= { + "event_id" => 'guid', + "result" => 'ok', + "details" => { + "message" => "..." + } + } + + NEW_ORDER_EVENT ||= { + "event" => 'order:new', + "event_id" => '510bfe8e7575e41e41000001', + "payload" => { + "order" => ORDER_SHOW + } + } + + NEW_ORDER_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000001', + "result" => 'ok', + "details" => { + "message" => "Order sent to warehouse" + } + } + + UPDATED_ORDER_EVENT ||= { + "event" => 'order:updated', + "event_id" => '510bfe8e7575e41e41000002', + "payload" => { + "order" => ORDER_SHOW2 + } + } + + UPDATED_ORDER_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000002', + "result" => 'ok', + "details" => { + "message" => "Update sent to warehouse" + } + } + + CANCELLED_ORDER_EVENT ||= { + "event" => 'order:cancelled', + "event_id" => '510bfe8e7575e41e41000003', + "payload" => { + "order" => ORDER + } + } + + CANCELLED_ORDER_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000003', + "result" => 'ok', + "details" => { + "message" => "Order cancellation sent to warehouse" + } + } + + NEW_USER_EVENT ||= { + "event" => 'create:user', + "event_id" => '510bfe8e7575e41e41000017', + "payload" => { + "user" => USER + } + } + + NEW_USER_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000017', + "result" => 'ok', + "details" => { + "message" => "User Account Created" + } + } + + UPDATED_USER_EVENT ||= { + "event" => 'update:event', + "event_id" => '510bfe8e7575e41e41000018', + "payload" => { + "user" => UPDATED_USER + } + } + + UPDATED_USER_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000018', + "result" => 'ok', + "details" => { + "message" => "User Account Updated" + } + } + SHIPMENT_READY_EVENT ||= { + "event" => 'shipment:ready', + "event_id" => '510bfe8e7575e41e41000004', + "payload" => { + "shipment" => READY_SHIPMENT + } + } + + SHIPMENT_READY_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000004', + "result" => 'ok', + "details" => { + "message" => "Shipment sent to warehouse for fulfillment" + } + } + + SHIPMENT_CONFIRMATION_EVENT ||= { + "event" => 'shipment:confirmed', + "event_id" => '510bfe8e7575e41e41000005', + "payload" => { + "shipment" => SHIPPED_SHIPMENT + } + } + + SHIPMENT_CONFIRMATION_RESPONSE ||= { + "event_id" => '510bfe8e7575e41e41000005', + "result" => 'ok', + "details" => { + "message" => "Shipping Confirmation email Sent" + } + } + + ORDER_SHOW_ADDRESS_STATE ||= ORDER.merge({ + "state" => "address", + "line_items" => [LINE_ITEM] + }) + + ORDER_SHOW_DELIVERY_STATE ||= ORDER.merge({ + "shipments"=>[SHIPMENT], + "state" => "delivery" + }) + + ORDER_SHOW_PAYMENT_STATE ||= ORDER.merge({ + "payment_methods" => [PAYMENT_METHOD], + "state" => "payment" + }) + + ORDER_SHOW_CONFIRM_STATE ||= ORDER.merge({ + "state" => "confirm" + }) + + ORDER_SHOW_COMPLETE_STATE ||= ORDER.merge({ + "state" => "complete" + }) + + ADDRESS_COUNTRY ||= + { + "id"=>1, + "iso_name"=>"UNITED STATES", + "iso"=>"US", + "iso3"=>"USA", + "name"=>"United States", + "numcode"=>1 + } + + ADDRESS_STATE ||= + { + "abbr"=>"NY", + "country_id"=>1, + "id"=>1, + "name"=>"New York" + } + + ADDRESS ||= + { + "id"=>1, + "firstname"=>"Spree", + "lastname"=>"Commerce", + "address1"=>"1 Someplace Lane", + "address2"=>"Suite 1", + "city"=>"Bethesda", + "zipcode"=>"16804", + "phone"=>"123.4567.890", + "company"=>nil, + "alternative_phone"=>nil, + "country_id"=>1, + "state_id"=>1, + "state_name"=>nil, + "country"=> ADDRESS_COUNTRY, + "state" => ADDRESS_STATE + } + + COUNTRY_STATE ||= { "state"=> ADDRESS_STATE } + + COUNTRY ||= + { + "id"=>1, + "iso_name"=>"UNITED STATES", + "iso"=>"US", + "iso3"=>"USA", + "name"=>"United States", + "numcode"=>1, + "states"=> [COUNTRY_STATE] + } + + STATE ||= + { + "abbr"=>"NY", + "country_id"=>1, + "id"=>1, + "name"=>"New York" + } + + TAXON ||= + { + "id"=>2, + "name"=>"Ruby on Rails", + "permalink"=>"brands/ruby-on-rails", + "position"=>1, + "parent_id"=>1, + "taxonomy_id"=>1 + } + + SECONDARY_TAXON ||= + { + "id"=>3, + "name"=>"T-Shirts", + "permalink"=>"brands/ruby-on-rails/t-shirts", + "position"=>1, + "parent_id"=>2, + "taxonomy_id"=>1 + } + + TAXON_WITH_CHILDREN ||= TAXON.merge(:taxons => [SECONDARY_TAXON]) + TAXON_WITHOUT_CHILDREN ||= TAXON.merge(:taxons => []) + + TAXONOMY ||= + { + "id"=>1, + "name"=>"Brand", + "root"=> TAXON_WITH_CHILDREN + } + + NEW_TAXONOMY ||= + { + "id" => 1, + "name" => "Brand", + "root" => TAXON_WITHOUT_CHILDREN + } + + ZONE_MEMBER ||= + { + "id"=>1, + "name"=>"United States", + "zoneable_type"=>"Spree::Country", + "zoneable_id"=>1, + } + + ZONE ||= + { + "id"=>1, + "name"=>"America", + "description"=>"The US", + "zone_members"=> [ZONE_MEMBER] + } + + RETURN_AUTHORIZATION ||= + { + "id"=>1, + "number"=>"12345", + "state"=>"authorized", + "amount"=> 14.22, + "order_id"=>14, + "reason"=>"Didn't fit", + "created_at"=>"2012-10-24T23:26:23Z", + "updated_at"=>"2012-10-24T23:26:23Z" + } + + STOCK_LOCATION ||= + { + "id"=>1, + "name"=>"default", + "address1"=>"7735 Old Georgetown Road", + "address2"=>"Suite 510", + "city"=>"Bethesda", + "state_id"=>26, + "country_id"=>49, + "zipcode"=>"20814", + "phone"=>"", + "active"=>true + } + + STOCK_ITEM ||= + { + "id"=>1, + "count_on_hand"=>10, + "backorderable"=>true, + "lock_version"=>1, + "stock_location_id"=>1, + "variant_id"=>1 + } + + STOCK_MOVEMENT ||= + { + "id"=>1, + "quantity"=>10, + "action"=>"received", + "stock_item_id"=>1 + } + + MESSAGE ||= + { + "message"=>"stock:change", + "payload"=>{ + "sku"=>"APC-00001", + "quantity"=>10 + } + } + + INTEGRATION ||= + { + "id"=> "5279322c84a816b42e000010", + "name"=> "dotcom", + "display"=> "Dotcom Distribution", + "description"=> "Order fulfillment and tracking using Dotcom Distribution", + "help"=> "http://guides.spreecommerce.com/integration/dotcom_integration.html", + "url"=> "http://ep-rlm.spree.fm", + "category"=> "distribution", + "services"=> [ + { + "name"=> "send_shipment", + "path"=> "/send_shipment", + "description"=> "Send shipment details to Dotcom on completion", + "requires"=> { + "parameters"=> [ + { + "name"=> "api_key", + "description"=> "Dotcom Distribution API key", + "data_type"=> "string" + }, + { + "name"=> "password", + "description"=> "Dotcom Distribution password", + "data_type"=> "string" + }, + { + "name"=> "shipping_lookup", + "description"=> "Spree to Dotcom Distribution mapping", + "data_type"=> "list" + } + ] + }, + "recommends"=> { + "messages"=> [ + "shipment=>ready" + ] + }, + "produces"=> {} + } + ], + "error_messages"=> [], + "icon_url"=> "dotcom.png", + "store_id"=> nil + } + + NOTIFICATION ||= + { + "id"=> "52794a8b84a816f2f5000241", + "store_id"=> "5279008084a81693d5000001", + "message_id"=> "52794a8a84a816f2f400022e", + "attributes"=> { + "_id"=> "52794a8b84a816f2f5000241", + "store_id"=> "5279008084a81693d5000001", + "message_id"=> "52794a8a84a816f2f400022e", + "subject"=> "Completed order poll", + "description"=> "Spree endpoint successfully polled for orders.", + "level"=> "info", + "occurrences"=> 1, + "logged_on"=> "2013-11-05T19:44:11Z", + "last_update"=> "2013-11-05T19:44:11Z", + "origin"=> "spree.order_poll", + "reference_id"=> nil, + "reference_token"=> nil, + "reference_type"=> nil + }, + "level"=> "info", + "subject"=> "Completed order poll", + "description"=> "Spree endpoint successfully polled for orders.", + "occurrences"=> 1, + "logged_on"=> "2013-11-05T19:44:11Z", + "last_update"=> "2013-11-05T19:44:11Z", + "reference_type"=> nil, + "reference_id"=> nil, + "reference_token"=> nil + } + + ERROR_NOTIFICATION ||= + { + "id"=> "52794a8b84a816f2f5000241", + "store_id"=> "5279008084a81693d5000001", + "message_id"=> "52794a8a84a816f2f400022e", + "attributes"=> { + "_id"=> "52794a8b84a816f2f5000241", + "store_id"=> "5279008084a81693d5000001", + "message_id"=> "52794a8a84a816f2f400022e", + "subject"=> "Could not complete order poll", + "description"=> "Spree endpoint failed while polling for orders.", + "level"=> "error", + "occurrences"=> 1, + "logged_on"=> "2013-11-05T19:44:11Z", + "last_update"=> "2013-11-05T19:44:11Z", + "origin"=> "spree.order_poll", + "reference_id"=> nil, + "reference_token"=> nil, + "reference_type"=> nil + }, + "level"=> "error", + "occurrences"=> 1, + "logged_on"=> "2013-11-05T19:44:11Z", + "last_update"=> "2013-11-05T19:44:11Z", + } + + INCOMING_QUEUE ||= + { + "id"=> "52811a8584a8169f7a000002", + "message"=> "stock:change", + "state"=> "pending", + "consumer"=> "", + "created_at"=> "2013-11-11T17:57:25Z", + "completed_at"=> nil, + "attempt_at"=> "2013-11-13T15:51:26Z", + "is_consumer_remote"=> false, + "destination_name"=> "", + "integration_icon_url"=> nil, + "locked_at"=> nil, + "source"=> nil, + "queue_name"=> "incoming" + } + + ACCEPTED_QUEUE ||= + { + "id"=> "52811a8584a8169f7a000002", + "message"=> "stock:change", + "state"=> "parked", + "consumer"=> "", + "created_at"=> "2013-11-11T17:57:25Z", + "completed_at"=> nil, + "attempt_at"=> "2013-11-13T15:51:26Z", + "is_consumer_remote"=> false, + "destination_name"=> "", + "integration_icon_url"=> nil, + "locked_at"=> nil, + "source"=> nil, + "queue_name"=> "accepted" + } + + ARCHIVED_QUEUE ||= + { + "id"=> "52811a8584a8169f7a000002", + "message"=> "stock:change", + "state"=> "completed", + "consumer"=> "", + "created_at"=> "2013-11-11T17:57:25Z", + "completed_at"=> nil, + "attempt_at"=> "2013-11-13T15:51:26Z", + "is_consumer_remote"=> false, + "destination_name"=> "", + "integration_icon_url"=> nil, + "locked_at"=> nil, + "source"=> nil, + "queue_name"=> "archived" + } + end +end + +include Spree::Resources::Helpers diff --git a/guides/lib/static.rb b/guides/lib/static.rb new file mode 100644 index 00000000000..5d091fa8595 --- /dev/null +++ b/guides/lib/static.rb @@ -0,0 +1,55 @@ +require 'digest/sha1' + +module Nanoc3::DataSources + + class Static < Nanoc3::DataSource + + identifier :static + + def items + # Get prefix + prefix = config[:prefix] || 'static' + + # Get all files under prefix dir + filenames = Dir[prefix + '/**/*'].select { |f| File.file?(f) } + + # Convert filenames to items + filenames.map do |filename| + attributes = { + :extension => File.extname(filename)[1..-1], + :filename => filename, + } + identifier = filename[(prefix.length+1)..-1] + '/' + + mtime = File.mtime(filename) + checksum = checksum_for(filename) + + Nanoc3::Item.new( + filename, + attributes, + identifier, + :binary => true, :mtime => mtime, :checksum => checksum + ) + end + end + + private + + # Returns a checksum of the given filenames + # TODO un-duplicate this somewhere + def checksum_for(*filenames) + filenames.flatten.map do |filename| + digest = Digest::SHA1.new + File.open(filename, 'r') do |io| + until io.eof + data = io.readpartial(2**10) + digest.update(data) + end + end + digest.hexdigest + end.join('-') + end + + end + +end diff --git a/guides/script/bootstrap b/guides/script/bootstrap new file mode 100755 index 00000000000..a7c34d210b8 --- /dev/null +++ b/guides/script/bootstrap @@ -0,0 +1,2 @@ +#!/bin/sh +exec $0.rb "$@" diff --git a/guides/script/bootstrap.rb b/guides/script/bootstrap.rb new file mode 100755 index 00000000000..009f0d0d373 --- /dev/null +++ b/guides/script/bootstrap.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby +#/ Usage: script/bootstrap [] +#/ Bootstraps the gem environment. +#/ +#/ Options are passed through to the bundle-install command. In most cases you +#/ won't need these. They're used primarily in production environments. +# +# ============================================================================= +# Uses bundler to install all gems specified in the Gemfile. +# +# show usage message with --help +if ARGV.include?('--help') + system "grep '^#/' <'#{__FILE__}' |cut -c4-" + exit 2 +end + +# go into the project root because it makes everything easier +root = File.expand_path('../..', __FILE__) +Dir.chdir(root) + +# bring in rubygems and make sure bundler is installed. +require 'rubygems' +begin + require 'bundler' +rescue LoadError => boom + warn "Bundler not found. Install it with `gem install bundler' and try again." + exit 1 +end + +# run bundle-install to install any missing gems +argv = ['--no-color', 'install'] +argv += ARGV +system("bundle", *argv) || begin + if $?.exitstatus == 127 + warn "bundle executable not found. Ensure bundler is installed (`gem " + + "install bundler`) and that the gem bin path is in your PATH" + end + exit($?.exitstatus) +end + diff --git a/guides/spec/filters/pretty_urls_spec.rb b/guides/spec/filters/pretty_urls_spec.rb new file mode 100644 index 00000000000..fea2e0a1feb --- /dev/null +++ b/guides/spec/filters/pretty_urls_spec.rb @@ -0,0 +1,22 @@ +require 'filters/pretty_urls' + +describe PrettyUrls do + subject { PrettyUrls.new } + context "correctly translating" do + it "[Foo Bar](foo_bar) => /foo_bar.html" do + subject.run("[Foo Bar](foo_bar)").should == "[Foo Bar](foo_bar.html)" + end + + it "[Foo Bar](foo_bar.html) => /foo_bar.html" do + subject.run("[Foo Bar](foo_bar.html)").should == "[Foo Bar](foo_bar.html)" + end + + it "[Foo Bar](foo_bar#buzz) => /foo_bar.html#buzz" do + subject.run("[Foo Bar](foo_bar#buzz)").should == "[Foo Bar](foo_bar.html#buzz)" + end + + it "[Foo Bar](foo_bar.html#buzz) => /foo_bar.html#buzz" do + subject.run("[Foo Bar](foo_bar.html#buzz)").should == "[Foo Bar](foo_bar.html#buzz)" + end + end +end \ No newline at end of file diff --git a/guides/spec/spec_helper.rb b/guides/spec/spec_helper.rb new file mode 100644 index 00000000000..41f026f470b --- /dev/null +++ b/guides/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'rspec' \ No newline at end of file diff --git a/guides/static/favicon.ico b/guides/static/favicon.ico new file mode 100644 index 00000000000..b5b89e35635 Binary files /dev/null and b/guides/static/favicon.ico differ diff --git a/guides/static/google04bf30ab420c4b3b.html b/guides/static/google04bf30ab420c4b3b.html new file mode 100644 index 00000000000..a7c604c5cc6 --- /dev/null +++ b/guides/static/google04bf30ab420c4b3b.html @@ -0,0 +1 @@ +google-site-verification: google04bf30ab420c4b3b.html \ No newline at end of file diff --git a/guides/static/images/background-v2.png b/guides/static/images/background-v2.png new file mode 100644 index 00000000000..33292d8b647 Binary files /dev/null and b/guides/static/images/background-v2.png differ diff --git a/guides/static/images/background-white.png b/guides/static/images/background-white.png new file mode 100644 index 00000000000..c981fb8b8eb Binary files /dev/null and b/guides/static/images/background-white.png differ diff --git a/guides/static/images/developer-banner.png b/guides/static/images/developer-banner.png new file mode 100644 index 00000000000..ee25f329b34 Binary files /dev/null and b/guides/static/images/developer-banner.png differ diff --git a/guides/static/images/developer-pattern.jpg b/guides/static/images/developer-pattern.jpg new file mode 100644 index 00000000000..38eda9d709d Binary files /dev/null and b/guides/static/images/developer-pattern.jpg differ diff --git a/guides/static/images/developer/change_ssl_setting.png b/guides/static/images/developer/change_ssl_setting.png new file mode 100644 index 00000000000..28d5f67cf6b Binary files /dev/null and b/guides/static/images/developer/change_ssl_setting.png differ diff --git a/guides/static/images/developer/core/new_stock_transfer.png b/guides/static/images/developer/core/new_stock_transfer.png new file mode 100644 index 00000000000..9d9db6caf00 Binary files /dev/null and b/guides/static/images/developer/core/new_stock_transfer.png differ diff --git a/guides/static/images/developer/core/payment_flow.graffle b/guides/static/images/developer/core/payment_flow.graffle new file mode 100644 index 00000000000..a2f8889aff0 Binary files /dev/null and b/guides/static/images/developer/core/payment_flow.graffle differ diff --git a/guides/static/images/developer/core/payment_flow.jpg b/guides/static/images/developer/core/payment_flow.jpg new file mode 100644 index 00000000000..d254d10e3a9 Binary files /dev/null and b/guides/static/images/developer/core/payment_flow.jpg differ diff --git a/guides/static/images/developer/core/shipment_flow.graffle b/guides/static/images/developer/core/shipment_flow.graffle new file mode 100644 index 00000000000..dca05e9ab4b Binary files /dev/null and b/guides/static/images/developer/core/shipment_flow.graffle differ diff --git a/guides/static/images/developer/core/shipment_flow.jpg b/guides/static/images/developer/core/shipment_flow.jpg new file mode 100644 index 00000000000..3ee0b063d0b Binary files /dev/null and b/guides/static/images/developer/core/shipment_flow.jpg differ diff --git a/guides/static/images/developer/core/split_shipments_checkout.png b/guides/static/images/developer/core/split_shipments_checkout.png new file mode 100644 index 00000000000..724801dabbc Binary files /dev/null and b/guides/static/images/developer/core/split_shipments_checkout.png differ diff --git a/guides/static/images/developer/core/stock_movements.png b/guides/static/images/developer/core/stock_movements.png new file mode 100644 index 00000000000..b825ef244f3 Binary files /dev/null and b/guides/static/images/developer/core/stock_movements.png differ diff --git a/guides/static/images/developer/core/stock_transfers.png b/guides/static/images/developer/core/stock_transfers.png new file mode 100644 index 00000000000..ef23e526283 Binary files /dev/null and b/guides/static/images/developer/core/stock_transfers.png differ diff --git a/guides/static/images/developer/mail_server_settings.png b/guides/static/images/developer/mail_server_settings.png new file mode 100644 index 00000000000..4c95e074894 Binary files /dev/null and b/guides/static/images/developer/mail_server_settings.png differ diff --git a/guides/static/images/developer/new-admin-interface.png b/guides/static/images/developer/new-admin-interface.png new file mode 100644 index 00000000000..ef22329136d Binary files /dev/null and b/guides/static/images/developer/new-admin-interface.png differ diff --git a/guides/static/images/developer/overview.png b/guides/static/images/developer/overview.png new file mode 100644 index 00000000000..a1c694db58d Binary files /dev/null and b/guides/static/images/developer/overview.png differ diff --git a/guides/static/images/developer/spree_welcome.png b/guides/static/images/developer/spree_welcome.png new file mode 100644 index 00000000000..3ce4a5e3542 Binary files /dev/null and b/guides/static/images/developer/spree_welcome.png differ diff --git a/guides/static/images/edge_badge.png b/guides/static/images/edge_badge.png new file mode 100644 index 00000000000..cddd46c4b84 Binary files /dev/null and b/guides/static/images/edge_badge.png differ diff --git a/guides/static/images/feed-icon-28x28.png b/guides/static/images/feed-icon-28x28.png new file mode 100755 index 00000000000..d64c669c758 Binary files /dev/null and b/guides/static/images/feed-icon-28x28.png differ diff --git a/guides/static/images/index-background.png b/guides/static/images/index-background.png new file mode 100644 index 00000000000..a8f2a852839 Binary files /dev/null and b/guides/static/images/index-background.png differ diff --git a/guides/static/images/integration/active_integrations.jpg b/guides/static/images/integration/active_integrations.jpg new file mode 100644 index 00000000000..ce0412d19cd Binary files /dev/null and b/guides/static/images/integration/active_integrations.jpg differ diff --git a/guides/static/images/integration/add_custom_integration.jpg b/guides/static/images/integration/add_custom_integration.jpg new file mode 100644 index 00000000000..84d6746354f Binary files /dev/null and b/guides/static/images/integration/add_custom_integration.jpg differ diff --git a/guides/static/images/integration/address_variety_select.jpg b/guides/static/images/integration/address_variety_select.jpg new file mode 100644 index 00000000000..fcd93499f97 Binary files /dev/null and b/guides/static/images/integration/address_variety_select.jpg differ diff --git a/guides/static/images/integration/choose_environment.jpg b/guides/static/images/integration/choose_environment.jpg new file mode 100644 index 00000000000..2b8c261e3b6 Binary files /dev/null and b/guides/static/images/integration/choose_environment.jpg differ diff --git a/guides/static/images/integration/company_field_checkout.jpg b/guides/static/images/integration/company_field_checkout.jpg new file mode 100644 index 00000000000..65a2956a905 Binary files /dev/null and b/guides/static/images/integration/company_field_checkout.jpg differ diff --git a/guides/static/images/integration/errors_custom_integration.jpg b/guides/static/images/integration/errors_custom_integration.jpg new file mode 100644 index 00000000000..729deb654d3 Binary files /dev/null and b/guides/static/images/integration/errors_custom_integration.jpg differ diff --git a/guides/static/images/integration/fetch_custom_integration.jpg b/guides/static/images/integration/fetch_custom_integration.jpg new file mode 100644 index 00000000000..67fe2696caf Binary files /dev/null and b/guides/static/images/integration/fetch_custom_integration.jpg differ diff --git a/guides/static/images/integration/inspect_message.jpg b/guides/static/images/integration/inspect_message.jpg new file mode 100644 index 00000000000..dff6cebbedd Binary files /dev/null and b/guides/static/images/integration/inspect_message.jpg differ diff --git a/guides/static/images/integration/inspect_notification.jpg b/guides/static/images/integration/inspect_notification.jpg new file mode 100644 index 00000000000..8b6095e31c4 Binary files /dev/null and b/guides/static/images/integration/inspect_notification.jpg differ diff --git a/guides/static/images/integration/integration_overview.jpg b/guides/static/images/integration/integration_overview.jpg new file mode 100644 index 00000000000..d5e5a8d4419 Binary files /dev/null and b/guides/static/images/integration/integration_overview.jpg differ diff --git a/guides/static/images/integration/integration_tab.jpg b/guides/static/images/integration/integration_tab.jpg new file mode 100644 index 00000000000..ba7350933e7 Binary files /dev/null and b/guides/static/images/integration/integration_tab.jpg differ diff --git a/guides/static/images/integration/integrator_login.jpg b/guides/static/images/integration/integrator_login.jpg new file mode 100644 index 00000000000..12351dbf602 Binary files /dev/null and b/guides/static/images/integration/integrator_login.jpg differ diff --git a/guides/static/images/integration/mandrill_config.jpg b/guides/static/images/integration/mandrill_config.jpg new file mode 100644 index 00000000000..67ad256adca Binary files /dev/null and b/guides/static/images/integration/mandrill_config.jpg differ diff --git a/guides/static/images/integration/message_delivery.gif b/guides/static/images/integration/message_delivery.gif new file mode 100644 index 00000000000..788a968d4bf Binary files /dev/null and b/guides/static/images/integration/message_delivery.gif differ diff --git a/guides/static/images/integration/message_flow.gif b/guides/static/images/integration/message_flow.gif new file mode 100644 index 00000000000..34ab7975da2 Binary files /dev/null and b/guides/static/images/integration/message_flow.gif differ diff --git a/guides/static/images/integration/message_icon_archive.png b/guides/static/images/integration/message_icon_archive.png new file mode 100644 index 00000000000..2ff445e6a52 Binary files /dev/null and b/guides/static/images/integration/message_icon_archive.png differ diff --git a/guides/static/images/integration/message_icon_retry.png b/guides/static/images/integration/message_icon_retry.png new file mode 100644 index 00000000000..3d35028503b Binary files /dev/null and b/guides/static/images/integration/message_icon_retry.png differ diff --git a/guides/static/images/integration/message_viewer_skipped_archived.png b/guides/static/images/integration/message_viewer_skipped_archived.png new file mode 100644 index 00000000000..d59858d70e6 Binary files /dev/null and b/guides/static/images/integration/message_viewer_skipped_archived.png differ diff --git a/guides/static/images/integration/notifications_listing.jpg b/guides/static/images/integration/notifications_listing.jpg new file mode 100644 index 00000000000..5f6328fc6fe Binary files /dev/null and b/guides/static/images/integration/notifications_listing.jpg differ diff --git a/guides/static/images/integration/pending_custom_integration.jpg b/guides/static/images/integration/pending_custom_integration.jpg new file mode 100644 index 00000000000..8a4bbb4a31d Binary files /dev/null and b/guides/static/images/integration/pending_custom_integration.jpg differ diff --git a/guides/static/images/integration/testing_tool_message.png b/guides/static/images/integration/testing_tool_message.png new file mode 100644 index 00000000000..294ea31c796 Binary files /dev/null and b/guides/static/images/integration/testing_tool_message.png differ diff --git a/guides/static/images/integration/testing_tool_tab.png b/guides/static/images/integration/testing_tool_tab.png new file mode 100644 index 00000000000..ad3f7b6acab Binary files /dev/null and b/guides/static/images/integration/testing_tool_tab.png differ diff --git a/guides/static/images/integration/user_api_key.jpg b/guides/static/images/integration/user_api_key.jpg new file mode 100644 index 00000000000..35de71a4572 Binary files /dev/null and b/guides/static/images/integration/user_api_key.jpg differ diff --git a/guides/static/images/integration/viewing_message_queue.jpg b/guides/static/images/integration/viewing_message_queue.jpg new file mode 100644 index 00000000000..758210051aa Binary files /dev/null and b/guides/static/images/integration/viewing_message_queue.jpg differ diff --git a/guides/static/images/integrator-banner.png b/guides/static/images/integrator-banner.png new file mode 100644 index 00000000000..792b11c079d Binary files /dev/null and b/guides/static/images/integrator-banner.png differ diff --git a/guides/static/images/integrator-banner.svg b/guides/static/images/integrator-banner.svg new file mode 100644 index 00000000000..2d8b0c5782c --- /dev/null +++ b/guides/static/images/integrator-banner.svg @@ -0,0 +1,251 @@ + + + integrator-banner + Created with Sketch (http://www.bohemiancoding.com/sketch) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HUB + + + END POINTS + + + THIRD-PARTY SERVICES + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + END POINTS + + + + + + + + + + + + + + + + + + + + + + + + + + + THIRD-PARTY SERVICES + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/guides/static/images/integrator-banner@2x.png b/guides/static/images/integrator-banner@2x.png new file mode 100644 index 00000000000..0bffc25de2a Binary files /dev/null and b/guides/static/images/integrator-banner@2x.png differ diff --git a/guides/static/images/logo.png b/guides/static/images/logo.png new file mode 100644 index 00000000000..3c92c237d44 Binary files /dev/null and b/guides/static/images/logo.png differ diff --git a/guides/static/images/nav-rule.png b/guides/static/images/nav-rule.png new file mode 100644 index 00000000000..909c5a85f1f Binary files /dev/null and b/guides/static/images/nav-rule.png differ diff --git a/guides/static/images/spreeconf-badge.jpg b/guides/static/images/spreeconf-badge.jpg new file mode 100644 index 00000000000..084fb773b49 Binary files /dev/null and b/guides/static/images/spreeconf-badge.jpg differ diff --git a/guides/static/images/spreeconf-nyc-2014-badge.jpg b/guides/static/images/spreeconf-nyc-2014-badge.jpg new file mode 100644 index 00000000000..7ed3f2a28bd Binary files /dev/null and b/guides/static/images/spreeconf-nyc-2014-badge.jpg differ diff --git a/guides/static/images/user-banner.png b/guides/static/images/user-banner.png new file mode 100644 index 00000000000..12dac94b9c5 Binary files /dev/null and b/guides/static/images/user-banner.png differ diff --git a/guides/static/images/user-pattern.jpg b/guides/static/images/user-pattern.jpg new file mode 100644 index 00000000000..8ca178c2335 Binary files /dev/null and b/guides/static/images/user-pattern.jpg differ diff --git a/guides/static/images/user/config/add_new_style.jpg b/guides/static/images/user/config/add_new_style.jpg new file mode 100644 index 00000000000..7d2cdc1a08f Binary files /dev/null and b/guides/static/images/user/config/add_new_style.jpg differ diff --git a/guides/static/images/user/config/add_taxon_to_taxon.jpg b/guides/static/images/user/config/add_taxon_to_taxon.jpg new file mode 100644 index 00000000000..e88dee62664 Binary files /dev/null and b/guides/static/images/user/config/add_taxon_to_taxon.jpg differ diff --git a/guides/static/images/user/config/add_taxon_to_taxonomy.jpg b/guides/static/images/user/config/add_taxon_to_taxonomy.jpg new file mode 100644 index 00000000000..d1da193cebe Binary files /dev/null and b/guides/static/images/user/config/add_taxon_to_taxonomy.jpg differ diff --git a/guides/static/images/user/config/add_taxons_to_product.jpg b/guides/static/images/user/config/add_taxons_to_product.jpg new file mode 100644 index 00000000000..e916bf71299 Binary files /dev/null and b/guides/static/images/user/config/add_taxons_to_product.jpg differ diff --git a/guides/static/images/user/config/amazon_access_keys.jpg b/guides/static/images/user/config/amazon_access_keys.jpg new file mode 100644 index 00000000000..bcf82f1fced Binary files /dev/null and b/guides/static/images/user/config/amazon_access_keys.jpg differ diff --git a/guides/static/images/user/config/amazons3.jpg b/guides/static/images/user/config/amazons3.jpg new file mode 100644 index 00000000000..b95d571f4c8 Binary files /dev/null and b/guides/static/images/user/config/amazons3.jpg differ diff --git a/guides/static/images/user/config/complex_taxonomy_tree.jpg b/guides/static/images/user/config/complex_taxonomy_tree.jpg new file mode 100644 index 00000000000..751a43c851b Binary files /dev/null and b/guides/static/images/user/config/complex_taxonomy_tree.jpg differ diff --git a/guides/static/images/user/config/countries.jpg b/guides/static/images/user/config/countries.jpg new file mode 100644 index 00000000000..c85f7017beb Binary files /dev/null and b/guides/static/images/user/config/countries.jpg differ diff --git a/guides/static/images/user/config/countries_drop_down.jpg b/guides/static/images/user/config/countries_drop_down.jpg new file mode 100644 index 00000000000..543e9f185d9 Binary files /dev/null and b/guides/static/images/user/config/countries_drop_down.jpg differ diff --git a/guides/static/images/user/config/currency_settings.jpg b/guides/static/images/user/config/currency_settings.jpg new file mode 100644 index 00000000000..f451e8d9ef8 Binary files /dev/null and b/guides/static/images/user/config/currency_settings.jpg differ diff --git a/guides/static/images/user/config/delete_stock_location_icon.jpg b/guides/static/images/user/config/delete_stock_location_icon.jpg new file mode 100644 index 00000000000..73ff9a3660e Binary files /dev/null and b/guides/static/images/user/config/delete_stock_location_icon.jpg differ diff --git a/guides/static/images/user/config/delete_style.jpg b/guides/static/images/user/config/delete_style.jpg new file mode 100644 index 00000000000..bbaaa738e04 Binary files /dev/null and b/guides/static/images/user/config/delete_style.jpg differ diff --git a/guides/static/images/user/config/delete_tax_category_link.jpg b/guides/static/images/user/config/delete_tax_category_link.jpg new file mode 100644 index 00000000000..17cd52bf311 Binary files /dev/null and b/guides/static/images/user/config/delete_tax_category_link.jpg differ diff --git a/guides/static/images/user/config/delete_taxonomy_icon.jpg b/guides/static/images/user/config/delete_taxonomy_icon.jpg new file mode 100644 index 00000000000..9c04b2df852 Binary files /dev/null and b/guides/static/images/user/config/delete_taxonomy_icon.jpg differ diff --git a/guides/static/images/user/config/deleting_state_icon.jpg b/guides/static/images/user/config/deleting_state_icon.jpg new file mode 100644 index 00000000000..9d7d1186c86 Binary files /dev/null and b/guides/static/images/user/config/deleting_state_icon.jpg differ diff --git a/guides/static/images/user/config/edit_country_icon.jpg b/guides/static/images/user/config/edit_country_icon.jpg new file mode 100644 index 00000000000..1a97e64e9c5 Binary files /dev/null and b/guides/static/images/user/config/edit_country_icon.jpg differ diff --git a/guides/static/images/user/config/edit_state_icon.jpg b/guides/static/images/user/config/edit_state_icon.jpg new file mode 100644 index 00000000000..01a6a83c0ad Binary files /dev/null and b/guides/static/images/user/config/edit_state_icon.jpg differ diff --git a/guides/static/images/user/config/edit_stock_location_icon.jpg b/guides/static/images/user/config/edit_stock_location_icon.jpg new file mode 100644 index 00000000000..83de1d4d104 Binary files /dev/null and b/guides/static/images/user/config/edit_stock_location_icon.jpg differ diff --git a/guides/static/images/user/config/edit_tax_category_link.jpg b/guides/static/images/user/config/edit_tax_category_link.jpg new file mode 100644 index 00000000000..362f5adce92 Binary files /dev/null and b/guides/static/images/user/config/edit_tax_category_link.jpg differ diff --git a/guides/static/images/user/config/edit_taxon.jpg b/guides/static/images/user/config/edit_taxon.jpg new file mode 100644 index 00000000000..8e22bae9350 Binary files /dev/null and b/guides/static/images/user/config/edit_taxon.jpg differ diff --git a/guides/static/images/user/config/edit_taxonomy.jpg b/guides/static/images/user/config/edit_taxonomy.jpg new file mode 100644 index 00000000000..39ef83b0d37 Binary files /dev/null and b/guides/static/images/user/config/edit_taxonomy.jpg differ diff --git a/guides/static/images/user/config/edit_taxonomy_icon.jpg b/guides/static/images/user/config/edit_taxonomy_icon.jpg new file mode 100644 index 00000000000..e4285429b26 Binary files /dev/null and b/guides/static/images/user/config/edit_taxonomy_icon.jpg differ diff --git a/guides/static/images/user/config/editing_country.jpg b/guides/static/images/user/config/editing_country.jpg new file mode 100644 index 00000000000..a2bf3a2d9ea Binary files /dev/null and b/guides/static/images/user/config/editing_country.jpg differ diff --git a/guides/static/images/user/config/editing_state.jpg b/guides/static/images/user/config/editing_state.jpg new file mode 100644 index 00000000000..dbb2536418e Binary files /dev/null and b/guides/static/images/user/config/editing_state.jpg differ diff --git a/guides/static/images/user/config/general_settings.jpg b/guides/static/images/user/config/general_settings.jpg new file mode 100644 index 00000000000..7110bf2039e Binary files /dev/null and b/guides/static/images/user/config/general_settings.jpg differ diff --git a/guides/static/images/user/config/image_settings.jpg b/guides/static/images/user/config/image_settings.jpg new file mode 100644 index 00000000000..36caca10ce4 Binary files /dev/null and b/guides/static/images/user/config/image_settings.jpg differ diff --git a/guides/static/images/user/config/mail_method_settings.jpg b/guides/static/images/user/config/mail_method_settings.jpg new file mode 100644 index 00000000000..c5271125c99 Binary files /dev/null and b/guides/static/images/user/config/mail_method_settings.jpg differ diff --git a/guides/static/images/user/config/new_node.jpg b/guides/static/images/user/config/new_node.jpg new file mode 100644 index 00000000000..8ec46ec0e80 Binary files /dev/null and b/guides/static/images/user/config/new_node.jpg differ diff --git a/guides/static/images/user/config/new_state_form.jpg b/guides/static/images/user/config/new_state_form.jpg new file mode 100644 index 00000000000..43db987c3c1 Binary files /dev/null and b/guides/static/images/user/config/new_state_form.jpg differ diff --git a/guides/static/images/user/config/new_stock_location.jpg b/guides/static/images/user/config/new_stock_location.jpg new file mode 100644 index 00000000000..0a896677abe Binary files /dev/null and b/guides/static/images/user/config/new_stock_location.jpg differ diff --git a/guides/static/images/user/config/new_stock_transfer.jpg b/guides/static/images/user/config/new_stock_transfer.jpg new file mode 100644 index 00000000000..09d3999498c Binary files /dev/null and b/guides/static/images/user/config/new_stock_transfer.jpg differ diff --git a/guides/static/images/user/config/new_tax_category_form.jpg b/guides/static/images/user/config/new_tax_category_form.jpg new file mode 100644 index 00000000000..97fc92f85f5 Binary files /dev/null and b/guides/static/images/user/config/new_tax_category_form.jpg differ diff --git a/guides/static/images/user/config/new_tax_rate.jpg b/guides/static/images/user/config/new_tax_rate.jpg new file mode 100644 index 00000000000..1b2b4ac32d0 Binary files /dev/null and b/guides/static/images/user/config/new_tax_rate.jpg differ diff --git a/guides/static/images/user/config/new_taxon.jpg b/guides/static/images/user/config/new_taxon.jpg new file mode 100644 index 00000000000..26b610cab94 Binary files /dev/null and b/guides/static/images/user/config/new_taxon.jpg differ diff --git a/guides/static/images/user/config/new_taxonomy.jpg b/guides/static/images/user/config/new_taxonomy.jpg new file mode 100644 index 00000000000..461700a7829 Binary files /dev/null and b/guides/static/images/user/config/new_taxonomy.jpg differ diff --git a/guides/static/images/user/config/parent_into_parent_taxon_merge.jpg b/guides/static/images/user/config/parent_into_parent_taxon_merge.jpg new file mode 100644 index 00000000000..9db96c103fa Binary files /dev/null and b/guides/static/images/user/config/parent_into_parent_taxon_merge.jpg differ diff --git a/guides/static/images/user/config/remove_taxon.jpg b/guides/static/images/user/config/remove_taxon.jpg new file mode 100644 index 00000000000..ef9b86b8252 Binary files /dev/null and b/guides/static/images/user/config/remove_taxon.jpg differ diff --git a/guides/static/images/user/config/reorder_taxons.jpg b/guides/static/images/user/config/reorder_taxons.jpg new file mode 100644 index 00000000000..d6dc17f2535 Binary files /dev/null and b/guides/static/images/user/config/reorder_taxons.jpg differ diff --git a/guides/static/images/user/config/resulting_stock_movements.jpg b/guides/static/images/user/config/resulting_stock_movements.jpg new file mode 100644 index 00000000000..dd5b258a053 Binary files /dev/null and b/guides/static/images/user/config/resulting_stock_movements.jpg differ diff --git a/guides/static/images/user/config/seo_title_override.jpg b/guides/static/images/user/config/seo_title_override.jpg new file mode 100644 index 00000000000..9d4a69040b9 Binary files /dev/null and b/guides/static/images/user/config/seo_title_override.jpg differ diff --git a/guides/static/images/user/config/show_currency.jpg b/guides/static/images/user/config/show_currency.jpg new file mode 100644 index 00000000000..f87e333c11a Binary files /dev/null and b/guides/static/images/user/config/show_currency.jpg differ diff --git a/guides/static/images/user/config/site_name_in_title.jpg b/guides/static/images/user/config/site_name_in_title.jpg new file mode 100644 index 00000000000..7e81f4c7a9d Binary files /dev/null and b/guides/static/images/user/config/site_name_in_title.jpg differ diff --git a/guides/static/images/user/config/state_added.jpg b/guides/static/images/user/config/state_added.jpg new file mode 100644 index 00000000000..dc94ec3c1fc Binary files /dev/null and b/guides/static/images/user/config/state_added.jpg differ diff --git a/guides/static/images/user/config/stock_movements_link.jpg b/guides/static/images/user/config/stock_movements_link.jpg new file mode 100644 index 00000000000..e2eea4bc40f Binary files /dev/null and b/guides/static/images/user/config/stock_movements_link.jpg differ diff --git a/guides/static/images/user/config/stock_transfer.jpg b/guides/static/images/user/config/stock_transfer.jpg new file mode 100644 index 00000000000..9580d99a81a Binary files /dev/null and b/guides/static/images/user/config/stock_transfer.jpg differ diff --git a/guides/static/images/user/config/stock_transfer_complete.jpg b/guides/static/images/user/config/stock_transfer_complete.jpg new file mode 100644 index 00000000000..b6c6474211f Binary files /dev/null and b/guides/static/images/user/config/stock_transfer_complete.jpg differ diff --git a/guides/static/images/user/config/tax_categories.jpg b/guides/static/images/user/config/tax_categories.jpg new file mode 100644 index 00000000000..a6242308414 Binary files /dev/null and b/guides/static/images/user/config/tax_categories.jpg differ diff --git a/guides/static/images/user/config/tax_rates.jpg b/guides/static/images/user/config/tax_rates.jpg new file mode 100644 index 00000000000..2ba045c45e2 Binary files /dev/null and b/guides/static/images/user/config/tax_rates.jpg differ diff --git a/guides/static/images/user/config/tax_settings.jpg b/guides/static/images/user/config/tax_settings.jpg new file mode 100644 index 00000000000..e3e16a803b4 Binary files /dev/null and b/guides/static/images/user/config/tax_settings.jpg differ diff --git a/guides/static/images/user/config/taxonomy_tree.jpg b/guides/static/images/user/config/taxonomy_tree.jpg new file mode 100644 index 00000000000..238dbf57178 Binary files /dev/null and b/guides/static/images/user/config/taxonomy_tree.jpg differ diff --git a/guides/static/images/user/config/us_states_list.jpg b/guides/static/images/user/config/us_states_list.jpg new file mode 100644 index 00000000000..42b55bdc1e0 Binary files /dev/null and b/guides/static/images/user/config/us_states_list.jpg differ diff --git a/guides/static/images/user/jirafe/conversion_rate_detail.jpg b/guides/static/images/user/jirafe/conversion_rate_detail.jpg new file mode 100644 index 00000000000..e23d7aa1325 Binary files /dev/null and b/guides/static/images/user/jirafe/conversion_rate_detail.jpg differ diff --git a/guides/static/images/user/jirafe/date_range_selector.jpg b/guides/static/images/user/jirafe/date_range_selector.jpg new file mode 100644 index 00000000000..35f4fbcf02e Binary files /dev/null and b/guides/static/images/user/jirafe/date_range_selector.jpg differ diff --git a/guides/static/images/user/jirafe/refresh_dashboard_button.jpg b/guides/static/images/user/jirafe/refresh_dashboard_button.jpg new file mode 100644 index 00000000000..debff7a90c2 Binary files /dev/null and b/guides/static/images/user/jirafe/refresh_dashboard_button.jpg differ diff --git a/guides/static/images/user/jirafe/report_customizer.jpg b/guides/static/images/user/jirafe/report_customizer.jpg new file mode 100644 index 00000000000..02e145af80e Binary files /dev/null and b/guides/static/images/user/jirafe/report_customizer.jpg differ diff --git a/guides/static/images/user/orders/close_adjustment_icon.jpg b/guides/static/images/user/orders/close_adjustment_icon.jpg new file mode 100644 index 00000000000..4ef602b03f5 Binary files /dev/null and b/guides/static/images/user/orders/close_adjustment_icon.jpg differ diff --git a/guides/static/images/user/orders/closed_adjustment.jpg b/guides/static/images/user/orders/closed_adjustment.jpg new file mode 100644 index 00000000000..e14285fd0a3 Binary files /dev/null and b/guides/static/images/user/orders/closed_adjustment.jpg differ diff --git a/guides/static/images/user/orders/completed_payment.jpg b/guides/static/images/user/orders/completed_payment.jpg new file mode 100644 index 00000000000..e275bba6ca3 Binary files /dev/null and b/guides/static/images/user/orders/completed_payment.jpg differ diff --git a/guides/static/images/user/orders/create_new_order.jpg b/guides/static/images/user/orders/create_new_order.jpg new file mode 100644 index 00000000000..f750f8bd796 Binary files /dev/null and b/guides/static/images/user/orders/create_new_order.jpg differ diff --git a/guides/static/images/user/orders/create_reimbursement_button.png b/guides/static/images/user/orders/create_reimbursement_button.png new file mode 100644 index 00000000000..4912b1682f3 Binary files /dev/null and b/guides/static/images/user/orders/create_reimbursement_button.png differ diff --git a/guides/static/images/user/orders/customer_return_form.png b/guides/static/images/user/orders/customer_return_form.png new file mode 100644 index 00000000000..e331806afc6 Binary files /dev/null and b/guides/static/images/user/orders/customer_return_form.png differ diff --git a/guides/static/images/user/orders/customer_return_link.png b/guides/static/images/user/orders/customer_return_link.png new file mode 100644 index 00000000000..0488912dfa7 Binary files /dev/null and b/guides/static/images/user/orders/customer_return_link.png differ diff --git a/guides/static/images/user/orders/customer_returns_link.png b/guides/static/images/user/orders/customer_returns_link.png new file mode 100644 index 00000000000..a0cbf3fc1a3 Binary files /dev/null and b/guides/static/images/user/orders/customer_returns_link.png differ diff --git a/guides/static/images/user/orders/delete_adjustment_icon.jpg b/guides/static/images/user/orders/delete_adjustment_icon.jpg new file mode 100644 index 00000000000..9ab4a212999 Binary files /dev/null and b/guides/static/images/user/orders/delete_adjustment_icon.jpg differ diff --git a/guides/static/images/user/orders/edit_adjustment_icon.jpg b/guides/static/images/user/orders/edit_adjustment_icon.jpg new file mode 100644 index 00000000000..06b0778388f Binary files /dev/null and b/guides/static/images/user/orders/edit_adjustment_icon.jpg differ diff --git a/guides/static/images/user/orders/edit_order_link.jpg b/guides/static/images/user/orders/edit_order_link.jpg new file mode 100644 index 00000000000..bf5bdf07a9b Binary files /dev/null and b/guides/static/images/user/orders/edit_order_link.jpg differ diff --git a/guides/static/images/user/orders/edit_shipping_on_order_link.jpg b/guides/static/images/user/orders/edit_shipping_on_order_link.jpg new file mode 100644 index 00000000000..09ef154d8fc Binary files /dev/null and b/guides/static/images/user/orders/edit_shipping_on_order_link.jpg differ diff --git a/guides/static/images/user/orders/edit_shipping_options.jpg b/guides/static/images/user/orders/edit_shipping_options.jpg new file mode 100644 index 00000000000..e78b1deacf2 Binary files /dev/null and b/guides/static/images/user/orders/edit_shipping_options.jpg differ diff --git a/guides/static/images/user/orders/filter_options.jpg b/guides/static/images/user/orders/filter_options.jpg new file mode 100644 index 00000000000..271e49c820a Binary files /dev/null and b/guides/static/images/user/orders/filter_options.jpg differ diff --git a/guides/static/images/user/orders/list_of_orders.jpg b/guides/static/images/user/orders/list_of_orders.jpg new file mode 100644 index 00000000000..3cbd467c1a5 Binary files /dev/null and b/guides/static/images/user/orders/list_of_orders.jpg differ diff --git a/guides/static/images/user/orders/manual_order_with_product.jpg b/guides/static/images/user/orders/manual_order_with_product.jpg new file mode 100644 index 00000000000..53d0d9ac0ed Binary files /dev/null and b/guides/static/images/user/orders/manual_order_with_product.jpg differ diff --git a/guides/static/images/user/orders/mass_open_close_adjustments.jpg b/guides/static/images/user/orders/mass_open_close_adjustments.jpg new file mode 100644 index 00000000000..c82063a8ee7 Binary files /dev/null and b/guides/static/images/user/orders/mass_open_close_adjustments.jpg differ diff --git a/guides/static/images/user/orders/new_adjustment_button.jpg b/guides/static/images/user/orders/new_adjustment_button.jpg new file mode 100644 index 00000000000..80d94558a95 Binary files /dev/null and b/guides/static/images/user/orders/new_adjustment_button.jpg differ diff --git a/guides/static/images/user/orders/new_adjustment_form.jpg b/guides/static/images/user/orders/new_adjustment_form.jpg new file mode 100644 index 00000000000..457e6b356f5 Binary files /dev/null and b/guides/static/images/user/orders/new_adjustment_form.jpg differ diff --git a/guides/static/images/user/orders/new_payment_method_link.jpg b/guides/static/images/user/orders/new_payment_method_link.jpg new file mode 100644 index 00000000000..a3574614aba Binary files /dev/null and b/guides/static/images/user/orders/new_payment_method_link.jpg differ diff --git a/guides/static/images/user/orders/open_adjustment_icon.jpg b/guides/static/images/user/orders/open_adjustment_icon.jpg new file mode 100644 index 00000000000..d0c5f684a6b Binary files /dev/null and b/guides/static/images/user/orders/open_adjustment_icon.jpg differ diff --git a/guides/static/images/user/orders/order_adjustments.jpg b/guides/static/images/user/orders/order_adjustments.jpg new file mode 100644 index 00000000000..c1024f65c73 Binary files /dev/null and b/guides/static/images/user/orders/order_adjustments.jpg differ diff --git a/guides/static/images/user/orders/order_customer_details.jpg b/guides/static/images/user/orders/order_customer_details.jpg new file mode 100644 index 00000000000..681c71aac1e Binary files /dev/null and b/guides/static/images/user/orders/order_customer_details.jpg differ diff --git a/guides/static/images/user/orders/order_details_link.jpg b/guides/static/images/user/orders/order_details_link.jpg new file mode 100644 index 00000000000..7a9a93e496f Binary files /dev/null and b/guides/static/images/user/orders/order_details_link.jpg differ diff --git a/guides/static/images/user/orders/order_edit.jpg b/guides/static/images/user/orders/order_edit.jpg new file mode 100644 index 00000000000..bc4ac70858c Binary files /dev/null and b/guides/static/images/user/orders/order_edit.jpg differ diff --git a/guides/static/images/user/orders/order_product_added.jpg b/guides/static/images/user/orders/order_product_added.jpg new file mode 100644 index 00000000000..3245f09d430 Binary files /dev/null and b/guides/static/images/user/orders/order_product_added.jpg differ diff --git a/guides/static/images/user/orders/order_product_search.jpg b/guides/static/images/user/orders/order_product_search.jpg new file mode 100644 index 00000000000..cb9f2eaaa27 Binary files /dev/null and b/guides/static/images/user/orders/order_product_search.jpg differ diff --git a/guides/static/images/user/orders/order_shipped.jpg b/guides/static/images/user/orders/order_shipped.jpg new file mode 100644 index 00000000000..d5ea0bb851a Binary files /dev/null and b/guides/static/images/user/orders/order_shipped.jpg differ diff --git a/guides/static/images/user/orders/order_to_process.jpg b/guides/static/images/user/orders/order_to_process.jpg new file mode 100644 index 00000000000..938d6e3d480 Binary files /dev/null and b/guides/static/images/user/orders/order_to_process.jpg differ diff --git a/guides/static/images/user/orders/payment_to_process.jpg b/guides/static/images/user/orders/payment_to_process.jpg new file mode 100644 index 00000000000..7de65ff795c Binary files /dev/null and b/guides/static/images/user/orders/payment_to_process.jpg differ diff --git a/guides/static/images/user/orders/payments_link.jpg b/guides/static/images/user/orders/payments_link.jpg new file mode 100644 index 00000000000..b599c79393e Binary files /dev/null and b/guides/static/images/user/orders/payments_link.jpg differ diff --git a/guides/static/images/user/orders/reimbursement_complete.png b/guides/static/images/user/orders/reimbursement_complete.png new file mode 100644 index 00000000000..e0b8a09341c Binary files /dev/null and b/guides/static/images/user/orders/reimbursement_complete.png differ diff --git a/guides/static/images/user/orders/reimbursement_form.png b/guides/static/images/user/orders/reimbursement_form.png new file mode 100644 index 00000000000..61106f39b79 Binary files /dev/null and b/guides/static/images/user/orders/reimbursement_form.png differ diff --git a/guides/static/images/user/orders/return_authorizations_link.png b/guides/static/images/user/orders/return_authorizations_link.png new file mode 100644 index 00000000000..5a4b37417fe Binary files /dev/null and b/guides/static/images/user/orders/return_authorizations_link.png differ diff --git a/guides/static/images/user/orders/rma_form.png b/guides/static/images/user/orders/rma_form.png new file mode 100644 index 00000000000..bbb9a2b7b5b Binary files /dev/null and b/guides/static/images/user/orders/rma_form.png differ diff --git a/guides/static/images/user/orders/select_shipping.jpg b/guides/static/images/user/orders/select_shipping.jpg new file mode 100644 index 00000000000..60a21e536e9 Binary files /dev/null and b/guides/static/images/user/orders/select_shipping.jpg differ diff --git a/guides/static/images/user/orders/ship_it.jpg b/guides/static/images/user/orders/ship_it.jpg new file mode 100644 index 00000000000..55fa345ebc8 Binary files /dev/null and b/guides/static/images/user/orders/ship_it.jpg differ diff --git a/guides/static/images/user/orders/tracking_input.jpg b/guides/static/images/user/orders/tracking_input.jpg new file mode 100644 index 00000000000..c136eebda70 Binary files /dev/null and b/guides/static/images/user/orders/tracking_input.jpg differ diff --git a/guides/static/images/user/payments/add_payment_provider.jpg b/guides/static/images/user/payments/add_payment_provider.jpg new file mode 100644 index 00000000000..10c3cb13867 Binary files /dev/null and b/guides/static/images/user/payments/add_payment_provider.jpg differ diff --git a/guides/static/images/user/payments/edit_payment_method.jpg b/guides/static/images/user/payments/edit_payment_method.jpg new file mode 100644 index 00000000000..75a188d19cc Binary files /dev/null and b/guides/static/images/user/payments/edit_payment_method.jpg differ diff --git a/guides/static/images/user/payments/new_payment_method.jpg b/guides/static/images/user/payments/new_payment_method.jpg new file mode 100644 index 00000000000..b215e80d783 Binary files /dev/null and b/guides/static/images/user/payments/new_payment_method.jpg differ diff --git a/guides/static/images/user/payments/payment_capture.jpg b/guides/static/images/user/payments/payment_capture.jpg new file mode 100644 index 00000000000..24ea49a110e Binary files /dev/null and b/guides/static/images/user/payments/payment_capture.jpg differ diff --git a/guides/static/images/user/payments/payment_details.jpg b/guides/static/images/user/payments/payment_details.jpg new file mode 100644 index 00000000000..a80562e422f Binary files /dev/null and b/guides/static/images/user/payments/payment_details.jpg differ diff --git a/guides/static/images/user/payments/payment_method_name.jpg b/guides/static/images/user/payments/payment_method_name.jpg new file mode 100644 index 00000000000..b23f1234e3a Binary files /dev/null and b/guides/static/images/user/payments/payment_method_name.jpg differ diff --git a/guides/static/images/user/payments/payment_void.jpg b/guides/static/images/user/payments/payment_void.jpg new file mode 100644 index 00000000000..b64dacd3c4a Binary files /dev/null and b/guides/static/images/user/payments/payment_void.jpg differ diff --git a/guides/static/images/user/payments/payments_look_up.jpg b/guides/static/images/user/payments/payments_look_up.jpg new file mode 100644 index 00000000000..a9a6bdbcda7 Binary files /dev/null and b/guides/static/images/user/payments/payments_look_up.jpg differ diff --git a/guides/static/images/user/products/clone_deleted_product.jpg b/guides/static/images/user/products/clone_deleted_product.jpg new file mode 100644 index 00000000000..5d2996487d9 Binary files /dev/null and b/guides/static/images/user/products/clone_deleted_product.jpg differ diff --git a/guides/static/images/user/products/clone_product.jpg b/guides/static/images/user/products/clone_product.jpg new file mode 100644 index 00000000000..cd9dfbaa234 Binary files /dev/null and b/guides/static/images/user/products/clone_product.jpg differ diff --git a/guides/static/images/user/products/delete_products_icon.jpg b/guides/static/images/user/products/delete_products_icon.jpg new file mode 100644 index 00000000000..beefa9f416f Binary files /dev/null and b/guides/static/images/user/products/delete_products_icon.jpg differ diff --git a/guides/static/images/user/products/edit_cloned_product.jpg b/guides/static/images/user/products/edit_cloned_product.jpg new file mode 100644 index 00000000000..a90a3e0409d Binary files /dev/null and b/guides/static/images/user/products/edit_cloned_product.jpg differ diff --git a/guides/static/images/user/products/edit_product_link.jpg b/guides/static/images/user/products/edit_product_link.jpg new file mode 100644 index 00000000000..386d33f68bf Binary files /dev/null and b/guides/static/images/user/products/edit_product_link.jpg differ diff --git a/guides/static/images/user/products/example_cloned_product.jpg b/guides/static/images/user/products/example_cloned_product.jpg new file mode 100644 index 00000000000..667e51ba5f7 Binary files /dev/null and b/guides/static/images/user/products/example_cloned_product.jpg differ diff --git a/guides/static/images/user/products/large_small_option_values.jpg b/guides/static/images/user/products/large_small_option_values.jpg new file mode 100644 index 00000000000..24d7256af45 Binary files /dev/null and b/guides/static/images/user/products/large_small_option_values.jpg differ diff --git a/guides/static/images/user/products/new_image_form.jpg b/guides/static/images/user/products/new_image_form.jpg new file mode 100644 index 00000000000..00937bae334 Binary files /dev/null and b/guides/static/images/user/products/new_image_form.jpg differ diff --git a/guides/static/images/user/products/new_option_type.jpg b/guides/static/images/user/products/new_option_type.jpg new file mode 100644 index 00000000000..c88c0c1dca3 Binary files /dev/null and b/guides/static/images/user/products/new_option_type.jpg differ diff --git a/guides/static/images/user/products/new_option_value.jpg b/guides/static/images/user/products/new_option_value.jpg new file mode 100644 index 00000000000..04af3fa418a Binary files /dev/null and b/guides/static/images/user/products/new_option_value.jpg differ diff --git a/guides/static/images/user/products/new_product_entry_form.jpg b/guides/static/images/user/products/new_product_entry_form.jpg new file mode 100644 index 00000000000..9f2b5e2cb58 Binary files /dev/null and b/guides/static/images/user/products/new_product_entry_form.jpg differ diff --git a/guides/static/images/user/products/new_prototype.jpg b/guides/static/images/user/products/new_prototype.jpg new file mode 100644 index 00000000000..92dec66936e Binary files /dev/null and b/guides/static/images/user/products/new_prototype.jpg differ diff --git a/guides/static/images/user/products/new_variant.jpg b/guides/static/images/user/products/new_variant.jpg new file mode 100644 index 00000000000..5dcea494cdb Binary files /dev/null and b/guides/static/images/user/products/new_variant.jpg differ diff --git a/guides/static/images/user/products/option_types_dropdown.jpg b/guides/static/images/user/products/option_types_dropdown.jpg new file mode 100644 index 00000000000..4c2186e2e82 Binary files /dev/null and b/guides/static/images/user/products/option_types_dropdown.jpg differ diff --git a/guides/static/images/user/products/picture_frame_prototype.jpg b/guides/static/images/user/products/picture_frame_prototype.jpg new file mode 100644 index 00000000000..6e4d20560d5 Binary files /dev/null and b/guides/static/images/user/products/picture_frame_prototype.jpg differ diff --git a/guides/static/images/user/products/product_edit_form.jpg b/guides/static/images/user/products/product_edit_form.jpg new file mode 100644 index 00000000000..ab4bd934070 Binary files /dev/null and b/guides/static/images/user/products/product_edit_form.jpg differ diff --git a/guides/static/images/user/products/product_from_prototype.jpg b/guides/static/images/user/products/product_from_prototype.jpg new file mode 100644 index 00000000000..4c8317fa00c Binary files /dev/null and b/guides/static/images/user/products/product_from_prototype.jpg differ diff --git a/guides/static/images/user/products/products_admin.jpg b/guides/static/images/user/products/products_admin.jpg new file mode 100644 index 00000000000..5e890242e6b Binary files /dev/null and b/guides/static/images/user/products/products_admin.jpg differ diff --git a/guides/static/images/user/products/properties_example.jpg b/guides/static/images/user/products/properties_example.jpg new file mode 100644 index 00000000000..5006a47ccfc Binary files /dev/null and b/guides/static/images/user/products/properties_example.jpg differ diff --git a/guides/static/images/user/products/properties_list.jpg b/guides/static/images/user/products/properties_list.jpg new file mode 100644 index 00000000000..f9390bbc519 Binary files /dev/null and b/guides/static/images/user/products/properties_list.jpg differ diff --git a/guides/static/images/user/products/prototype_product_with_options.jpg b/guides/static/images/user/products/prototype_product_with_options.jpg new file mode 100644 index 00000000000..ed9e08f58c5 Binary files /dev/null and b/guides/static/images/user/products/prototype_product_with_options.jpg differ diff --git a/guides/static/images/user/products/prototypes.jpg b/guides/static/images/user/products/prototypes.jpg new file mode 100644 index 00000000000..485a3557213 Binary files /dev/null and b/guides/static/images/user/products/prototypes.jpg differ diff --git a/guides/static/images/user/products/show_deleted_products.jpg b/guides/static/images/user/products/show_deleted_products.jpg new file mode 100644 index 00000000000..61b27e99c24 Binary files /dev/null and b/guides/static/images/user/products/show_deleted_products.jpg differ diff --git a/guides/static/images/user/products/stock_location_info.jpg b/guides/static/images/user/products/stock_location_info.jpg new file mode 100644 index 00000000000..ba1c1299243 Binary files /dev/null and b/guides/static/images/user/products/stock_location_info.jpg differ diff --git a/guides/static/images/user/products/stock_management.jpg b/guides/static/images/user/products/stock_management.jpg new file mode 100644 index 00000000000..6373eae6e89 Binary files /dev/null and b/guides/static/images/user/products/stock_management.jpg differ diff --git a/guides/static/images/user/products/variants_list.jpg b/guides/static/images/user/products/variants_list.jpg new file mode 100644 index 00000000000..30fd591bd7b Binary files /dev/null and b/guides/static/images/user/products/variants_list.jpg differ diff --git a/guides/static/images/user/promotions/create_adjustment.jpg b/guides/static/images/user/promotions/create_adjustment.jpg new file mode 100644 index 00000000000..bb3159ba350 Binary files /dev/null and b/guides/static/images/user/promotions/create_adjustment.jpg differ diff --git a/guides/static/images/user/promotions/create_line_item.jpg b/guides/static/images/user/promotions/create_line_item.jpg new file mode 100644 index 00000000000..9baef391c91 Binary files /dev/null and b/guides/static/images/user/promotions/create_line_item.jpg differ diff --git a/guides/static/images/user/promotions/delete_promotion_icon.jpg b/guides/static/images/user/promotions/delete_promotion_icon.jpg new file mode 100644 index 00000000000..3bcbc1cdf32 Binary files /dev/null and b/guides/static/images/user/promotions/delete_promotion_icon.jpg differ diff --git a/guides/static/images/user/promotions/delete_rule_icon.jpg b/guides/static/images/user/promotions/delete_rule_icon.jpg new file mode 100644 index 00000000000..53e38ca6ea3 Binary files /dev/null and b/guides/static/images/user/promotions/delete_rule_icon.jpg differ diff --git a/guides/static/images/user/promotions/edit_promotion_icon.jpg b/guides/static/images/user/promotions/edit_promotion_icon.jpg new file mode 100644 index 00000000000..2a250896384 Binary files /dev/null and b/guides/static/images/user/promotions/edit_promotion_icon.jpg differ diff --git a/guides/static/images/user/promotions/first_order_rule.jpg b/guides/static/images/user/promotions/first_order_rule.jpg new file mode 100644 index 00000000000..0489a03f43d Binary files /dev/null and b/guides/static/images/user/promotions/first_order_rule.jpg differ diff --git a/guides/static/images/user/promotions/item_total_rule.jpg b/guides/static/images/user/promotions/item_total_rule.jpg new file mode 100644 index 00000000000..e0eb135e2a7 Binary files /dev/null and b/guides/static/images/user/promotions/item_total_rule.jpg differ diff --git a/guides/static/images/user/promotions/logged_in_rule.jpg b/guides/static/images/user/promotions/logged_in_rule.jpg new file mode 100644 index 00000000000..f5783d51bda Binary files /dev/null and b/guides/static/images/user/promotions/logged_in_rule.jpg differ diff --git a/guides/static/images/user/promotions/new_promotion.jpg b/guides/static/images/user/promotions/new_promotion.jpg new file mode 100644 index 00000000000..634580e96df Binary files /dev/null and b/guides/static/images/user/promotions/new_promotion.jpg differ diff --git a/guides/static/images/user/promotions/products_rule.jpg b/guides/static/images/user/promotions/products_rule.jpg new file mode 100644 index 00000000000..ddfe2c65bdb Binary files /dev/null and b/guides/static/images/user/promotions/products_rule.jpg differ diff --git a/guides/static/images/user/promotions/rules_and_actions.jpg b/guides/static/images/user/promotions/rules_and_actions.jpg new file mode 100644 index 00000000000..4afae866fe3 Binary files /dev/null and b/guides/static/images/user/promotions/rules_and_actions.jpg differ diff --git a/guides/static/images/user/promotions/rules_options.jpg b/guides/static/images/user/promotions/rules_options.jpg new file mode 100644 index 00000000000..58850d3fa83 Binary files /dev/null and b/guides/static/images/user/promotions/rules_options.jpg differ diff --git a/guides/static/images/user/promotions/user_rule.jpg b/guides/static/images/user/promotions/user_rule.jpg new file mode 100644 index 00000000000..e275ded2b58 Binary files /dev/null and b/guides/static/images/user/promotions/user_rule.jpg differ diff --git a/guides/static/images/user/sales_total_dates.jpg b/guides/static/images/user/sales_total_dates.jpg new file mode 100644 index 00000000000..9a6d7a6a3b6 Binary files /dev/null and b/guides/static/images/user/sales_total_dates.jpg differ diff --git a/guides/static/images/user/sales_total_report.jpg b/guides/static/images/user/sales_total_report.jpg new file mode 100644 index 00000000000..d3614563a7c Binary files /dev/null and b/guides/static/images/user/sales_total_report.jpg differ diff --git a/guides/static/images/user/shipments/add_multi_to_zone.jpg b/guides/static/images/user/shipments/add_multi_to_zone.jpg new file mode 100644 index 00000000000..1b1b12d3564 Binary files /dev/null and b/guides/static/images/user/shipments/add_multi_to_zone.jpg differ diff --git a/guides/static/images/user/shipments/delete_shipping_method.jpg b/guides/static/images/user/shipments/delete_shipping_method.jpg new file mode 100644 index 00000000000..f8c7d89f4c1 Binary files /dev/null and b/guides/static/images/user/shipments/delete_shipping_method.jpg differ diff --git a/guides/static/images/user/shipments/edit_shipping_method.jpg b/guides/static/images/user/shipments/edit_shipping_method.jpg new file mode 100644 index 00000000000..db6c92878b7 Binary files /dev/null and b/guides/static/images/user/shipments/edit_shipping_method.jpg differ diff --git a/guides/static/images/user/shipments/edit_zone.jpg b/guides/static/images/user/shipments/edit_zone.jpg new file mode 100644 index 00000000000..4d6e4b7137c Binary files /dev/null and b/guides/static/images/user/shipments/edit_zone.jpg differ diff --git a/guides/static/images/user/shipments/new_shipping_category.jpg b/guides/static/images/user/shipments/new_shipping_category.jpg new file mode 100644 index 00000000000..f5c63a2f955 Binary files /dev/null and b/guides/static/images/user/shipments/new_shipping_category.jpg differ diff --git a/guides/static/images/user/shipments/new_shipping_method.jpg b/guides/static/images/user/shipments/new_shipping_method.jpg new file mode 100644 index 00000000000..4064804b703 Binary files /dev/null and b/guides/static/images/user/shipments/new_shipping_method.jpg differ diff --git a/guides/static/images/user/shipments/new_zone.jpg b/guides/static/images/user/shipments/new_zone.jpg new file mode 100644 index 00000000000..fb35749e789 Binary files /dev/null and b/guides/static/images/user/shipments/new_zone.jpg differ diff --git a/guides/static/images/user/shipments/remove_zone_member.jpg b/guides/static/images/user/shipments/remove_zone_member.jpg new file mode 100644 index 00000000000..0d752404284 Binary files /dev/null and b/guides/static/images/user/shipments/remove_zone_member.jpg differ diff --git a/guides/static/images/user/shipments/select_shipping_category.jpg b/guides/static/images/user/shipments/select_shipping_category.jpg new file mode 100644 index 00000000000..1b02ff2082c Binary files /dev/null and b/guides/static/images/user/shipments/select_shipping_category.jpg differ diff --git a/guides/static/images/user/shipments/shipping_method_calculator.jpg b/guides/static/images/user/shipments/shipping_method_calculator.jpg new file mode 100644 index 00000000000..be4458b0404 Binary files /dev/null and b/guides/static/images/user/shipments/shipping_method_calculator.jpg differ diff --git a/guides/static/images/user/shipments/shipping_method_categories.jpg b/guides/static/images/user/shipments/shipping_method_categories.jpg new file mode 100644 index 00000000000..a8d2b918d52 Binary files /dev/null and b/guides/static/images/user/shipments/shipping_method_categories.jpg differ diff --git a/guides/static/images/user/shipments/shipping_method_flat_percent.jpg b/guides/static/images/user/shipments/shipping_method_flat_percent.jpg new file mode 100644 index 00000000000..f544d552e3e Binary files /dev/null and b/guides/static/images/user/shipments/shipping_method_flat_percent.jpg differ diff --git a/guides/static/images/user/shipments/shipping_method_zones.jpg b/guides/static/images/user/shipments/shipping_method_zones.jpg new file mode 100644 index 00000000000..ff4923ed8d9 Binary files /dev/null and b/guides/static/images/user/shipments/shipping_method_zones.jpg differ diff --git a/guides/static/shared/.gitignore b/guides/static/shared/.gitignore new file mode 100644 index 00000000000..9bea4330f05 --- /dev/null +++ b/guides/static/shared/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/guides/static/shared/css/documentation.css b/guides/static/shared/css/documentation.css new file mode 100644 index 00000000000..43577f00c4c --- /dev/null +++ b/guides/static/shared/css/documentation.css @@ -0,0 +1,453 @@ +body { + font-family: 'Open Sans', sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 24px; + color: #416793; + margin: 0; +} + +img { + border: none; +} + +a { + text-decoration: none; + color: #82B245; + transition: color 0.2s; + -moz-transition: color 0.2s; /* Firefox 4 */ + -webkit-transition: color 0.2s; /* Safari and Chrome */ + -o-transition: color 0.2s; /* Opera */ +} + +h1, h2, h3, h4, h5, h6 { + color: #82B245; + font-weight: normal; + font-family: 'Bariol', 'Open Sans', 'Helvetica Neue', 'Helvetica', sans-serif; +} + +h1 { + font-size: 20px; +} + +h1, h2, h3 { + border-bottom: 1px solid #E6EEF7; + line-height: 2em; +} + +input[type="button"], input[type="submit"], +button, .button { + color: white !important; + text-transform: uppercase; + font-weight: 600 !important; + padding: 7px 15px; + background-color: #3678D5; + + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + + transition: background-color 1s; + -moz-transition: background-color 1s; /* Firefox 4 */ + -webkit-transition: background-color 1s; /* Safari and Chrome */ + -o-transition: background-color 1s; /* Opera */ +} + +input[type="button"]:hover, +input[type="submit"]:hover, +button:hover, .button:hover { + background-color: #228DF2; +} + +ul.inline { + list-style: none; +} + +ul.inline > li { + display: inline-block; +} + +code, dl dt { + font-family: 'Source Code Pro', monospace; + font-size: 14px; +} + +dt { + color: #73A7E9; +} + +dd { + margin-bottom: 15px; +} + +code { + padding: 3px 5px; + background-color: #EAF3FF; + color: #73A7E9; + + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; +} + +pre code { + padding: 0; + background-color: inherit; + color: #68A8EC; + + -webkit-border-radius: inherit; + -moz-border-radius: inherit; + -ms-border-radius: inherit; + -o-border-radius: inherit; + border-radius: inherit; +} + +pre.highlight { + padding: 15px; + background-color: #EAF3FF; + margin-top: 0; + + -webkit-border-bottom-right-radius: 3px; + -moz-border-bottom-right-radius: 3px; + -ms-border-bottom-right-radius: 3px; + -o-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + + -webkit-border-bottom-left-radius: 3px; + -moz-border-bottom-left-radius: 3px; + -ms-border-bottom-left-radius: 3px; + -o-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; +} + +pre.highlight code { + font-size: 12px; +} + +pre.headers { + padding: 5px 15px; + margin-bottom: 0; + background-color: #9DC5F3; + color: white; + text-transform: uppercase; + + -webkit-border-top-right-radius: 3px; + -moz-border-top-right-radius: 3px; + -ms-border-top-right-radius: 3px; + -o-border-top-right-radius: 3px; + border-top-right-radius: 3px; + + -webkit-border-top-left-radius: 3px; + -moz-border-top-left-radius: 3px; + -ms-border-top-left-radius: 3px; + -o-border-top-left-radius: 3px; + border-top-left-radius: 3px; +} +pre.headers code { + color: white; +} + +.js-guides { + display: none; +} + +.home #site-title { + position: absolute; + text-align: center; + width: 100%; + margin-top: 35px; +} +.home #get-started { + position: absolute; + z-index: 1; + text-transform: none; + font-size: 24px; + padding: 15px 30px; + border: 4px solid white; + font-weight: 400 !important; + background-color: #78AD2F; + top: 420px; + width: 200px; + text-align: center; + left: 50%; + margin-left: -100px; +} +.home #get-started:hover { + background-color: #8BC73A; +} +.home #site-title h1 { + font-size: 45px; +} +.home #site-title h2, .home #site-title h1 { + color: white; + border: none; + text-transform: uppercase; + letter-spacing: 14px; +} +.home #site-title h2 { + font-size: 25px; + letter-spacing: 21px; + padding-left: 12px; + margin-top: 10px; + font-weight: 400; +} + +#api-objects { + width: 100%; + height: 400px; + position: absolute; + background: url("/shared/images/api-objects-bg.png") no-repeat center; + z-index: 0; +} + +#subheader { + padding: 35px 0; + background: url("/shared/images/subheader_bg.png") repeat center top; +} +.home #subheader { + padding-bottom: 50px; +} + +#subheader .page-title { + border: none; + padding: 0; + margin: 0; + color: white; + text-transform: uppercase; + letter-spacing: 6px; + line-height: 1; +} + +.logo img { + margin-top: 40px; +} + +#main-menu { + margin-top: 20px; + text-align: right; +} + +#main-menu ul li a { + text-transform: uppercase; + display: inline-block; + padding: 0 20px; + color: #358EC7; +} +#main-menu ul li a:hover, +#main-menu ul li.active a { + color: #89B64A; +} +#main-menu ul li:last-child a { + padding-right: 0; +} +#main-menu ul li:first-child a { + padding-left: 0; +} + +#github ul { + list-style: none; + margin-left: 0; + padding-left: 0; + -webkit-padding-start: 0px; + border-left: 1px solid #E5EEF7; +} +#github ul li { + margin-bottom: 20px; + padding-left: 20px; + line-height: 18px; +} +#github ul li i.icon-dot { + position: absolute; + position: absolute; + margin-left: -31px; + font-size: 21px !important; +} +#github ul li a { + font-size: 12px; + font-weight: 400; + color: #6286B1; +} +#github ul li a:hover { + color: #78AD2F; +} +#github ul li .info { + margin-top: 5px; + font-size: 11px; + text-transform: uppercase; + color: #BAC3CF; +} + +#github ul li .info .commit-author { + margin-left: 10px; + text-transform: none; +} + +#sidebar-menu { + margin-top: 56px; +} +#sidebar-menu ul { + list-style: none; +} +#sidebar-menu ul ul { + padding-left: 0px; + -webkit-padding-start: 0px; +} +#sidebar-menu ul ul li a { + padding-left: 7px; +} + +#sidebar-menu ul ul ul { + display: none; +} +#sidebar-menu ul ul ul li a { + color: #6D8FB6; +} + +.js-guides li i { + cursor: pointer; +} +.js-guides li i:hover { + color: #89B64A; +} + +#sidebar-menu ul li a { + color: #416793; +} +#sidebar-menu ul li a:hover, +#sidebar-menu ul li.active a:hover { + color: #89B64A; +} + +#sidebar-menu h3 a { + font-size: 14px; + text-transform: uppercase; +} + +#sidebar-menu h3 a.active, #sidebar-menu h3 a.active-open { + color: #89B64A; +} + +#sidebar-menu ul li ul a { + font-size: 12px; +} + +#content { + padding: 30px 0; +} + +#main-footer { + margin-top: 20px; +} +#main-footer .row { + margin-bottom: 0; + font-size: 12px; + color: #6286B1; + padding-bottom: 20px; +} +#main-footer .block-title { + text-transform: uppercase; + border: none; + font-size: 12px; +} +#main-footer ul { + list-style: none; + margin: 0; + padding-left: 0; + -webkit-padding-start: 0px; +} +#main-footer a { + color: #6286B1; +} +#main-footer a:hover { + color: #89B64A; +} +.footer-top { + background-color: #EFF6FD; + border-top: 1px solid #D7E1EE; +} +.footer-bottom { + background-color: #3678D5; +} +.footer-bottom p { + color: white !important; + font-size: 12px; +} +#main-footer .social-icons { + font-size: 30px; + margin-top: 40px; + text-align: right; +} +#main-footer .social-icons i { + background-color: white; + margin-left: 10px; + padding-top: 1px; + text-align: center; + width: 30px; + height: 30px; + border: 3px solid white; + display: block; + box-sizing: content-box !important; + -webkit-box-sizing: content-box !important; + -moz-box-sizing: content-box !important; + + -webkit-border-radius: 30px; + -moz-border-radius: 30px; + -ms-border-radius: 30px; + -o-border-radius: 30px; + border-radius: 30px; +} +#main-footer .social-icons i.icon-github-circled { + color: #2C84C7; +} +#main-footer .social-icons i.icon-twitter-circled { + color: #009FD4; +} +#main-footer .social-icons i.icon-facebook-circled { + color: #325B9A; +} +#main-footer .social-icons i.icon-gplus-circled { + color: #E4532E; +} + +/* DEVELOPER GUIDES */ +.developer.home #subheader { + padding-top: 20px; +} +.developer.home #subheader .container { + background: url('../../images/developer-banner.png') no-repeat center; + min-height: 440px; +} +.developer #subheader { + background-image: url('../../images/developer-pattern.jpg'); +} + +/* INTEGRATOR GUIDES */ +.integration.home #subheader .container { + background: url('../../images/integrator-banner.png') no-repeat center 100px; + min-height: 440px; +} +.integration.home #subheader .container #site-title { + margin-top: 0; +} + +/* USER GUIDES */ +.user.home #subheader .container { + background: url('../../images/user-banner.png') no-repeat center top; + min-height: 440px; +} +.user.home #subheader .container #site-title { + margin-top: 100px; +} +.user.home #subheader .container #site-title h1 { + letter-spacing: 10px +} + +/* Browser fixes */ +.firefox #main-footer .social-icons i { padding-top: 0; padding-bottom: 1px } +.ie #main-footer .social-icons i { height: 28px; width: 29px } +.ie #main-footer .social-icons i.icon-github-circled { padding-right: 1px } +.ie #github ul li i.icon-dot { font-size: 20px !important; margin-left: -30px } diff --git a/guides/static/shared/css/icons-codes.css b/guides/static/shared/css/icons-codes.css new file mode 100644 index 00000000000..8077ac4a76e --- /dev/null +++ b/guides/static/shared/css/icons-codes.css @@ -0,0 +1,230 @@ +@charset "UTF-8"; + + +.icon-plus:before { content: '\2b'; } /* '+' */ +.icon-minus:before { content: '\2d'; } /* '-' */ +.icon-info:before { content: '\2139'; } /* 'ℹ' */ +.icon-left-thin:before { content: '\2190'; } /* '←' */ +.icon-up-thin:before { content: '\2191'; } /* '↑' */ +.icon-right-thin:before { content: '\2192'; } /* '→' */ +.icon-down-thin:before { content: '\2193'; } /* '↓' */ +.icon-level-up:before { content: '\21b0'; } /* '↰' */ +.icon-level-down:before { content: '\21b3'; } /* '↳' */ +.icon-switch:before { content: '\21c6'; } /* '⇆' */ +.icon-infinity:before { content: '\221e'; } /* '∞' */ +.icon-plus-squared:before { content: '\229e'; } /* '⊞' */ +.icon-minus-squared:before { content: '\229f'; } /* '⊟' */ +.icon-home:before { content: '\2302'; } /* '⌂' */ +.icon-keyboard:before { content: '\2328'; } /* '⌨' */ +.icon-erase:before { content: '\232b'; } /* '⌫' */ +.icon-pause:before { content: '\2389'; } /* '⎉' */ +.icon-fast-forward:before { content: '\23e9'; } /* '⏩' */ +.icon-fast-backward:before { content: '\23ea'; } /* '⏪' */ +.icon-to-end:before { content: '\23ed'; } /* '⏭' */ +.icon-to-start:before { content: '\23ee'; } /* '⏮' */ +.icon-hourglass:before { content: '\23f3'; } /* '⏳' */ +.icon-stop:before { content: '\25a0'; } /* '■' */ +.icon-up-dir:before { content: '\25b4'; } /* '▴' */ +.icon-play:before { content: '\25b6'; } /* '▶' */ +.icon-right-dir:before { content: '\25b8'; } /* '▸' */ +.icon-down-dir:before { content: '\25be'; } /* '▾' */ +.icon-left-dir:before { content: '\25c2'; } /* '◂' */ +.icon-adjust:before { content: '\25d1'; } /* '◑' */ +.icon-cloud:before { content: '\2601'; } /* '☁' */ +.icon-star:before { content: '\2605'; } /* '★' */ +.icon-star-empty:before { content: '\2606'; } /* '☆' */ +.icon-cup:before { content: '\2615'; } /* '☕' */ +.icon-menu:before { content: '\2630'; } /* '☰' */ +.icon-moon:before { content: '\263d'; } /* '☽' */ +.icon-heart-empty:before { content: '\2661'; } /* '♡' */ +.icon-heart:before { content: '\2665'; } /* '♥' */ +.icon-note:before { content: '\266a'; } /* '♪' */ +.icon-note-beamed:before { content: '\266b'; } /* '♫' */ +.icon-layout:before { content: '\268f'; } /* '⚏' */ +.icon-flag:before { content: '\2691'; } /* '⚑' */ +.icon-tools:before { content: '\2692'; } /* '⚒' */ +.icon-cog:before { content: '\2699'; } /* '⚙' */ +.icon-attention:before { content: '\26a0'; } /* '⚠' */ +.icon-flash:before { content: '\26a1'; } /* '⚡' */ +.icon-record:before { content: '\26ab'; } /* '⚫' */ +.icon-cloud-thunder:before { content: '\26c8'; } /* '⛈' */ +.icon-tape:before { content: '\2707'; } /* '✇' */ +.icon-flight:before { content: '\2708'; } /* '✈' */ +.icon-mail:before { content: '\2709'; } /* '✉' */ +.icon-pencil:before { content: '\270e'; } /* '✎' */ +.icon-feather:before { content: '\2712'; } /* '✒' */ +.icon-check:before { content: '\2713'; } /* '✓' */ +.icon-cancel:before { content: '\2715'; } /* '✕' */ +.icon-cancel-circled:before { content: '\2716'; } /* '✖' */ +.icon-cancel-squared:before { content: '\274e'; } /* '❎' */ +.icon-help:before { content: '\2753'; } /* '❓' */ +.icon-quote:before { content: '\275e'; } /* '❞' */ +.icon-plus-circled:before { content: '\2795'; } /* '➕' */ +.icon-minus-circled:before { content: '\2796'; } /* '➖' */ +.icon-right:before { content: '\27a1'; } /* '➡' */ +.icon-direction:before { content: '\27a2'; } /* '➢' */ +.icon-forward:before { content: '\27a6'; } /* '➦' */ +.icon-ccw:before { content: '\27f2'; } /* '⟲' */ +.icon-cw:before { content: '\27f3'; } /* '⟳' */ +.icon-left:before { content: '\2b05'; } /* '⬅' */ +.icon-up:before { content: '\2b06'; } /* '⬆' */ +.icon-down:before { content: '\2b07'; } /* '⬇' */ +.icon-list-add:before { content: '\e003'; } /* '' */ +.icon-list:before { content: '\e005'; } /* '' */ +.icon-left-bold:before { content: '\e4ad'; } /* '' */ +.icon-right-bold:before { content: '\e4ae'; } /* '' */ +.icon-up-bold:before { content: '\e4af'; } /* '' */ +.icon-down-bold:before { content: '\e4b0'; } /* '' */ +.icon-user-add:before { content: '\e700'; } /* '' */ +.icon-help-circled:before { content: '\e704'; } /* '' */ +.icon-info-circled:before { content: '\e705'; } /* '' */ +.icon-eye:before { content: '\e70a'; } /* '' */ +.icon-tag:before { content: '\e70c'; } /* '' */ +.icon-upload-cloud:before { content: '\e711'; } /* '' */ +.icon-reply:before { content: '\e712'; } /* '' */ +.icon-reply-all:before { content: '\e713'; } /* '' */ +.icon-code:before { content: '\e714'; } /* '' */ +.icon-export:before { content: '\e715'; } /* '' */ +.icon-print:before { content: '\e716'; } /* '' */ +.icon-retweet:before { content: '\e717'; } /* '' */ +.icon-comment:before { content: '\e718'; } /* '' */ +.icon-chat:before { content: '\e720'; } /* '' */ +.icon-vcard:before { content: '\e722'; } /* '' */ +.icon-address:before { content: '\e723'; } /* '' */ +.icon-location:before { content: '\e724'; } /* '' */ +.icon-map:before { content: '\e727'; } /* '' */ +.icon-compass:before { content: '\e728'; } /* '' */ +.icon-trash:before { content: '\e729'; } /* '' */ +.icon-doc:before { content: '\e730'; } /* '' */ +.icon-doc-text-inv:before { content: '\e731'; } /* '' */ +.icon-docs:before { content: '\e736'; } /* '' */ +.icon-doc-landscape:before { content: '\e737'; } /* '' */ +.icon-archive:before { content: '\e738'; } /* '' */ +.icon-rss:before { content: '\e73a'; } /* '' */ +.icon-share:before { content: '\e73c'; } /* '' */ +.icon-basket:before { content: '\e73d'; } /* '' */ +.icon-shareable:before { content: '\e73e'; } /* '' */ +.icon-login:before { content: '\e740'; } /* '' */ +.icon-logout:before { content: '\e741'; } /* '' */ +.icon-volume:before { content: '\e742'; } /* '' */ +.icon-resize-full:before { content: '\e744'; } /* '' */ +.icon-resize-small:before { content: '\e746'; } /* '' */ +.icon-popup:before { content: '\e74c'; } /* '' */ +.icon-publish:before { content: '\e74d'; } /* '' */ +.icon-window:before { content: '\e74e'; } /* '' */ +.icon-arrow-combo:before { content: '\e74f'; } /* '' */ +.icon-chart-pie:before { content: '\e751'; } /* '' */ +.icon-language:before { content: '\e752'; } /* '' */ +.icon-air:before { content: '\e753'; } /* '' */ +.icon-database:before { content: '\e754'; } /* '' */ +.icon-drive:before { content: '\e755'; } /* '' */ +.icon-bucket:before { content: '\e756'; } /* '' */ +.icon-thermometer:before { content: '\e757'; } /* '' */ +.icon-down-circled:before { content: '\e758'; } /* '' */ +.icon-left-circled:before { content: '\e759'; } /* '' */ +.icon-right-circled:before { content: '\e75a'; } /* '' */ +.icon-up-circled:before { content: '\e75b'; } /* '' */ +.icon-down-open:before { content: '\e75c'; } /* '' */ +.icon-left-open:before { content: '\e75d'; } /* '' */ +.icon-right-open:before { content: '\e75e'; } /* '' */ +.icon-up-open:before { content: '\e75f'; } /* '' */ +.icon-down-open-mini:before { content: '\e760'; } /* '' */ +.icon-left-open-mini:before { content: '\e761'; } /* '' */ +.icon-right-open-mini:before { content: '\e762'; } /* '' */ +.icon-up-open-mini:before { content: '\e763'; } /* '' */ +.icon-down-open-big:before { content: '\e764'; } /* '' */ +.icon-left-open-big:before { content: '\e765'; } /* '' */ +.icon-right-open-big:before { content: '\e766'; } /* '' */ +.icon-up-open-big:before { content: '\e767'; } /* '' */ +.icon-progress-0:before { content: '\e768'; } /* '' */ +.icon-progress-1:before { content: '\e769'; } /* '' */ +.icon-progress-2:before { content: '\e76a'; } /* '' */ +.icon-progress-3:before { content: '\e76b'; } /* '' */ +.icon-back-in-time:before { content: '\e771'; } /* '' */ +.icon-network:before { content: '\e776'; } /* '' */ +.icon-inbox:before { content: '\e777'; } /* '' */ +.icon-install:before { content: '\e778'; } /* '' */ +.icon-lifebuoy:before { content: '\e788'; } /* '' */ +.icon-mouse:before { content: '\e789'; } /* '' */ +.icon-dot:before { content: '\e78b'; } /* '' */ +.icon-dot-2:before { content: '\e78c'; } /* '' */ +.icon-dot-3:before { content: '\e78d'; } /* '' */ +.icon-suitcase:before { content: '\e78e'; } /* '' */ +.icon-flow-cascade:before { content: '\e790'; } /* '' */ +.icon-flow-branch:before { content: '\e791'; } /* '' */ +.icon-flow-tree:before { content: '\e792'; } /* '' */ +.icon-flow-line:before { content: '\e793'; } /* '' */ +.icon-flow-parallel:before { content: '\e794'; } /* '' */ +.icon-brush:before { content: '\e79a'; } /* '' */ +.icon-paper-plane:before { content: '\e79b'; } /* '' */ +.icon-magnet:before { content: '\e7a1'; } /* '' */ +.icon-gauge:before { content: '\e7a2'; } /* '' */ +.icon-github-circled:before { content: '\f301'; } /* '' */ +.icon-twitter-circled:before { content: '\f30a'; } /* '' */ +.icon-facebook-circled:before { content: '\f30d'; } /* '' */ +.icon-gplus-circled:before { content: '\f310'; } /* '' */ +.icon-picture:before { content: '🌄'; } /* '\1f304' */ +.icon-globe:before { content: '🌎'; } /* '\1f30e' */ +.icon-leaf:before { content: '🍂'; } /* '\1f342' */ +.icon-graduation-cap:before { content: '🎓'; } /* '\1f393' */ +.icon-mic:before { content: '🎤'; } /* '\1f3a4' */ +.icon-palette:before { content: '🎨'; } /* '\1f3a8' */ +.icon-ticket:before { content: '🎫'; } /* '\1f3ab' */ +.icon-video:before { content: '🎬'; } /* '\1f3ac' */ +.icon-target:before { content: '🎯'; } /* '\1f3af' */ +.icon-music:before { content: '🎵'; } /* '\1f3b5' */ +.icon-trophy:before { content: '🏆'; } /* '\1f3c6' */ +.icon-thumbs-up:before { content: '👍'; } /* '\1f44d' */ +.icon-thumbs-down:before { content: '👎'; } /* '\1f44e' */ +.icon-bag:before { content: '👜'; } /* '\1f45c' */ +.icon-user:before { content: '👤'; } /* '\1f464' */ +.icon-users:before { content: '👥'; } /* '\1f465' */ +.icon-lamp:before { content: '💡'; } /* '\1f4a1' */ +.icon-alert:before { content: '💥'; } /* '\1f4a5' */ +.icon-water:before { content: '💦'; } /* '\1f4a6' */ +.icon-droplet:before { content: '💧'; } /* '\1f4a7' */ +.icon-credit-card:before { content: '💳'; } /* '\1f4b3' */ +.icon-monitor:before { content: '💻'; } /* '\1f4bb' */ +.icon-briefcase:before { content: '💼'; } /* '\1f4bc' */ +.icon-floppy:before { content: '💾'; } /* '\1f4be' */ +.icon-cd:before { content: '💿'; } /* '\1f4bf' */ +.icon-folder:before { content: '📁'; } /* '\1f4c1' */ +.icon-doc-text:before { content: '\1f4c4'; } /* '\1f4c4' */ +.icon-calendar:before { content: '📅'; } /* '\1f4c5' */ +.icon-chart-line:before { content: '📈'; } /* '\1f4c8' */ +.icon-chart-bar:before { content: '📊'; } /* '\1f4ca' */ +.icon-clipboard:before { content: '📋'; } /* '\1f4cb' */ +.icon-attach:before { content: '📎'; } /* '\1f4ce' */ +.icon-bookmarks:before { content: '📑'; } /* '\1f4d1' */ +.icon-book:before { content: '📕'; } /* '\1f4d5' */ +.icon-book-open:before { content: '📖'; } /* '\1f4d6' */ +.icon-phone:before { content: '📞'; } /* '\1f4de' */ +.icon-megaphone:before { content: '📣'; } /* '\1f4e3' */ +.icon-upload:before { content: '📤'; } /* '\1f4e4' */ +.icon-download:before { content: '📥'; } /* '\1f4e5' */ +.icon-box:before { content: '📦'; } /* '\1f4e6' */ +.icon-newspaper:before { content: '📰'; } /* '\1f4f0' */ +.icon-mobile:before { content: '📱'; } /* '\1f4f1' */ +.icon-signal:before { content: '📶'; } /* '\1f4f6' */ +.icon-camera:before { content: '📷'; } /* '\1f4f7' */ +.icon-shuffle:before { content: '🔀'; } /* '\1f500' */ +.icon-loop:before { content: '🔁'; } /* '\1f501' */ +.icon-arrows-ccw:before { content: '🔄'; } /* '\1f504' */ +.icon-light-down:before { content: '🔅'; } /* '\1f505' */ +.icon-light-up:before { content: '🔆'; } /* '\1f506' */ +.icon-mute:before { content: '🔇'; } /* '\1f507' */ +.icon-sound:before { content: '🔊'; } /* '\1f50a' */ +.icon-battery:before { content: '🔋'; } /* '\1f50b' */ +.icon-search:before { content: '🔍'; } /* '\1f50d' */ +.icon-key:before { content: '🔑'; } /* '\1f511' */ +.icon-lock:before { content: '🔒'; } /* '\1f512' */ +.icon-lock-open:before { content: '🔓'; } /* '\1f513' */ +.icon-bell:before { content: '🔔'; } /* '\1f514' */ +.icon-bookmark:before { content: '🔖'; } /* '\1f516' */ +.icon-link:before { content: '🔗'; } /* '\1f517' */ +.icon-back:before { content: '🔙'; } /* '\1f519' */ +.icon-flashlight:before { content: '🔦'; } /* '\1f526' */ +.icon-chart-area:before { content: '🔾'; } /* '\1f53e' */ +.icon-clock:before { content: '🕔'; } /* '\1f554' */ +.icon-rocket:before { content: '🚀'; } /* '\1f680' */ +.icon-block:before { content: '🚫'; } /* '\1f6ab' */ diff --git a/guides/static/shared/css/icons-ie7-codes.css b/guides/static/shared/css/icons-ie7-codes.css new file mode 100644 index 00000000000..b881b24d209 --- /dev/null +++ b/guides/static/shared/css/icons-ie7-codes.css @@ -0,0 +1,228 @@ + +.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '+ '); } +.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '- '); } +.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = 'ℹ '); } +.icon-left-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '← '); } +.icon-up-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↑ '); } +.icon-right-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '→ '); } +.icon-down-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↓ '); } +.icon-level-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↰ '); } +.icon-level-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↳ '); } +.icon-switch { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⇆ '); } +.icon-infinity { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '∞ '); } +.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⊞ '); } +.icon-minus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⊟ '); } +.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⌂ '); } +.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⌨ '); } +.icon-erase { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⌫ '); } +.icon-pause { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⎉ '); } +.icon-fast-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏩ '); } +.icon-fast-backward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏪ '); } +.icon-to-end { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏭ '); } +.icon-to-start { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏮ '); } +.icon-hourglass { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏳ '); } +.icon-stop { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '■ '); } +.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▴ '); } +.icon-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▶ '); } +.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▸ '); } +.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▾ '); } +.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '◂ '); } +.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '◑ '); } +.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☁ '); } +.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '★ '); } +.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☆ '); } +.icon-cup { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☕ '); } +.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☰ '); } +.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☽ '); } +.icon-heart-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♡ '); } +.icon-heart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♥ '); } +.icon-note { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♪ '); } +.icon-note-beamed { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♫ '); } +.icon-layout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚏ '); } +.icon-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚑ '); } +.icon-tools { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚒ '); } +.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚙ '); } +.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚠ '); } +.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚡ '); } +.icon-record { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚫ '); } +.icon-cloud-thunder { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⛈ '); } +.icon-tape { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✇ '); } +.icon-flight { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✈ '); } +.icon-mail { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✉ '); } +.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✎ '); } +.icon-feather { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✒ '); } +.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✓ '); } +.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✕ '); } +.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✖ '); } +.icon-cancel-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '❎ '); } +.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '❓ '); } +.icon-quote { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '❞ '); } +.icon-plus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➕ '); } +.icon-minus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➖ '); } +.icon-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➡ '); } +.icon-direction { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➢ '); } +.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➦ '); } +.icon-ccw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⟲ '); } +.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⟳ '); } +.icon-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⬅ '); } +.icon-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⬆ '); } +.icon-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⬇ '); } +.icon-list-add { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-user-add { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-help-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-upload-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-code { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-export { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-vcard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-address { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-location { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-map { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-compass { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc-text-inv { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc-landscape { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-archive { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-rss { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-share { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-basket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-shareable { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-login { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-popup { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-publish { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-window { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-arrow-combo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-language { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-air { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-drive { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bucket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-thermometer { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-0 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-1 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-2 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-back-in-time { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-network { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-inbox { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-install { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lifebuoy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-mouse { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-dot { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-dot-2 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-dot-3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-suitcase { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-cascade { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-branch { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-tree { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-parallel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-paper-plane { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-magnet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-twitter-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-facebook-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gplus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🌄 '); } +.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🌎 '); } +.icon-leaf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🍂 '); } +.icon-graduation-cap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎓 '); } +.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎤 '); } +.icon-palette { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎨 '); } +.icon-ticket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎫 '); } +.icon-video { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎬 '); } +.icon-target { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎯 '); } +.icon-music { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎵 '); } +.icon-trophy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🏆 '); } +.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👍 '); } +.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👎 '); } +.icon-bag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👜 '); } +.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👤 '); } +.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👥 '); } +.icon-lamp { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💡 '); } +.icon-alert { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💥 '); } +.icon-water { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💦 '); } +.icon-droplet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💧 '); } +.icon-credit-card { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💳 '); } +.icon-monitor { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💻 '); } +.icon-briefcase { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💼 '); } +.icon-floppy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💾 '); } +.icon-cd { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💿 '); } +.icon-folder { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📁 '); } +.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📄 '); } +.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📅 '); } +.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📈 '); } +.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📊 '); } +.icon-clipboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📋 '); } +.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📎 '); } +.icon-bookmarks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📑 '); } +.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📕 '); } +.icon-book-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📖 '); } +.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📞 '); } +.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📣 '); } +.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📤 '); } +.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📥 '); } +.icon-box { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📦 '); } +.icon-newspaper { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📰 '); } +.icon-mobile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📱 '); } +.icon-signal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📶 '); } +.icon-camera { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📷 '); } +.icon-shuffle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔀 '); } +.icon-loop { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔁 '); } +.icon-arrows-ccw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔄 '); } +.icon-light-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔅 '); } +.icon-light-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔆 '); } +.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔇 '); } +.icon-sound { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔊 '); } +.icon-battery { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔋 '); } +.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔍 '); } +.icon-key { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔑 '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔒 '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔓 '); } +.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔔 '); } +.icon-bookmark { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔖 '); } +.icon-link { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔗 '); } +.icon-back { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔙 '); } +.icon-flashlight { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔦 '); } +.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔾 '); } +.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🕔 '); } +.icon-rocket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🚀 '); } +.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🚫 '); } \ No newline at end of file diff --git a/guides/static/shared/css/icons-ie7.css b/guides/static/shared/css/icons-ie7.css new file mode 100644 index 00000000000..159b65d6066 --- /dev/null +++ b/guides/static/shared/css/icons-ie7.css @@ -0,0 +1,238 @@ +[class^="icon-"], +[class*=" icon-"] { + font-family: 'icons'; + font-style: normal; + font-weight: normal; +/* fix buttons height */ + line-height: 1em; +/* you can be more comfortable with increased icons size */ +/* font-size: 120%; */ +} + +.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '+ '); } +.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '- '); } +.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = 'ℹ '); } +.icon-left-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '← '); } +.icon-up-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↑ '); } +.icon-right-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '→ '); } +.icon-down-thin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↓ '); } +.icon-level-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↰ '); } +.icon-level-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '↳ '); } +.icon-switch { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⇆ '); } +.icon-infinity { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '∞ '); } +.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⊞ '); } +.icon-minus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⊟ '); } +.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⌂ '); } +.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⌨ '); } +.icon-erase { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⌫ '); } +.icon-pause { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⎉ '); } +.icon-fast-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏩ '); } +.icon-fast-backward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏪ '); } +.icon-to-end { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏭ '); } +.icon-to-start { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏮ '); } +.icon-hourglass { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⏳ '); } +.icon-stop { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '■ '); } +.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▴ '); } +.icon-play { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▶ '); } +.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▸ '); } +.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '▾ '); } +.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '◂ '); } +.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '◑ '); } +.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☁ '); } +.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '★ '); } +.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☆ '); } +.icon-cup { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☕ '); } +.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☰ '); } +.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '☽ '); } +.icon-heart-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♡ '); } +.icon-heart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♥ '); } +.icon-note { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♪ '); } +.icon-note-beamed { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '♫ '); } +.icon-layout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚏ '); } +.icon-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚑ '); } +.icon-tools { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚒ '); } +.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚙ '); } +.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚠ '); } +.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚡ '); } +.icon-record { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⚫ '); } +.icon-cloud-thunder { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⛈ '); } +.icon-tape { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✇ '); } +.icon-flight { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✈ '); } +.icon-mail { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✉ '); } +.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✎ '); } +.icon-feather { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✒ '); } +.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✓ '); } +.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✕ '); } +.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '✖ '); } +.icon-cancel-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '❎ '); } +.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '❓ '); } +.icon-quote { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '❞ '); } +.icon-plus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➕ '); } +.icon-minus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➖ '); } +.icon-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➡ '); } +.icon-direction { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➢ '); } +.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '➦ '); } +.icon-ccw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⟲ '); } +.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⟳ '); } +.icon-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⬅ '); } +.icon-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⬆ '); } +.icon-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '⬇ '); } +.icon-list-add { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-bold { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-user-add { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-help-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-upload-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-code { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-export { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-vcard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-address { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-location { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-map { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-compass { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc-text-inv { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-doc-landscape { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-archive { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-rss { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-share { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-basket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-shareable { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-login { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-popup { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-publish { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-window { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-arrow-combo { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-language { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-air { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-drive { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-bucket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-thermometer { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open-mini { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-down-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-left-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-right-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-up-open-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-0 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-1 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-2 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-progress-3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-back-in-time { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-network { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-inbox { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-install { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lifebuoy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-mouse { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-dot { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-dot-2 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-dot-3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-suitcase { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-cascade { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-branch { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-tree { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-flow-parallel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-paper-plane { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-magnet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-twitter-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-facebook-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-gplus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🌄 '); } +.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🌎 '); } +.icon-leaf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🍂 '); } +.icon-graduation-cap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎓 '); } +.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎤 '); } +.icon-palette { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎨 '); } +.icon-ticket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎫 '); } +.icon-video { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎬 '); } +.icon-target { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎯 '); } +.icon-music { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🎵 '); } +.icon-trophy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🏆 '); } +.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👍 '); } +.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👎 '); } +.icon-bag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👜 '); } +.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👤 '); } +.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '👥 '); } +.icon-lamp { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💡 '); } +.icon-alert { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💥 '); } +.icon-water { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💦 '); } +.icon-droplet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💧 '); } +.icon-credit-card { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💳 '); } +.icon-monitor { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💻 '); } +.icon-briefcase { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💼 '); } +.icon-floppy { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💾 '); } +.icon-cd { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '💿 '); } +.icon-folder { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📁 '); } +.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📄 '); } +.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📅 '); } +.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📈 '); } +.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📊 '); } +.icon-clipboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📋 '); } +.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📎 '); } +.icon-bookmarks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📑 '); } +.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📕 '); } +.icon-book-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📖 '); } +.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📞 '); } +.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📣 '); } +.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📤 '); } +.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📥 '); } +.icon-box { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📦 '); } +.icon-newspaper { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📰 '); } +.icon-mobile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📱 '); } +.icon-signal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📶 '); } +.icon-camera { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '📷 '); } +.icon-shuffle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔀 '); } +.icon-loop { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔁 '); } +.icon-arrows-ccw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔄 '); } +.icon-light-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔅 '); } +.icon-light-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔆 '); } +.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔇 '); } +.icon-sound { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔊 '); } +.icon-battery { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔋 '); } +.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔍 '); } +.icon-key { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔑 '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔒 '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔓 '); } +.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔔 '); } +.icon-bookmark { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔖 '); } +.icon-link { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔗 '); } +.icon-back { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔙 '); } +.icon-flashlight { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔦 '); } +.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🔾 '); } +.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🕔 '); } +.icon-rocket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🚀 '); } +.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '🚫 '); } \ No newline at end of file diff --git a/guides/static/shared/css/icons.css b/guides/static/shared/css/icons.css new file mode 100644 index 00000000000..fdb7a8d7569 --- /dev/null +++ b/guides/static/shared/css/icons.css @@ -0,0 +1,255 @@ +@charset "UTF-8"; + +@font-face { + font-family: 'icons'; + src: url("../fonts/icons.eot"); + font-weight: normal; + font-style: normal; +} +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: 'icons'; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: 0.2em; + text-align: center; + opacity: 0.8; +/* Uncomment for 3D effect */ +/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +/* fix buttons height */ + line-height: 1em; +/* you can be more comfortable with increased icons size */ +/* font-size: 120%; */ +} + +.icon-plus:before { content: '\2b'; } /* '+' */ +.icon-minus:before { content: '\2d'; } /* '-' */ +.icon-info:before { content: '\2139'; } /* 'ℹ' */ +.icon-left-thin:before { content: '\2190'; } /* '←' */ +.icon-up-thin:before { content: '\2191'; } /* '↑' */ +.icon-right-thin:before { content: '\2192'; } /* '→' */ +.icon-down-thin:before { content: '\2193'; } /* '↓' */ +.icon-level-up:before { content: '\21b0'; } /* '↰' */ +.icon-level-down:before { content: '\21b3'; } /* '↳' */ +.icon-switch:before { content: '\21c6'; } /* '⇆' */ +.icon-infinity:before { content: '\221e'; } /* '∞' */ +.icon-plus-squared:before { content: '\229e'; } /* '⊞' */ +.icon-minus-squared:before { content: '\229f'; } /* '⊟' */ +.icon-home:before { content: '\2302'; } /* '⌂' */ +.icon-keyboard:before { content: '\2328'; } /* '⌨' */ +.icon-erase:before { content: '\232b'; } /* '⌫' */ +.icon-pause:before { content: '\2389'; } /* '⎉' */ +.icon-fast-forward:before { content: '\23e9'; } /* '⏩' */ +.icon-fast-backward:before { content: '\23ea'; } /* '⏪' */ +.icon-to-end:before { content: '\23ed'; } /* '⏭' */ +.icon-to-start:before { content: '\23ee'; } /* '⏮' */ +.icon-hourglass:before { content: '\23f3'; } /* '⏳' */ +.icon-stop:before { content: '\25a0'; } /* '■' */ +.icon-up-dir:before { content: '\25b4'; } /* '▴' */ +.icon-play:before { content: '\25b6'; } /* '▶' */ +.icon-right-dir:before { content: '\25b8'; } /* '▸' */ +.icon-down-dir:before { content: '\25be'; } /* '▾' */ +.icon-left-dir:before { content: '\25c2'; } /* '◂' */ +.icon-adjust:before { content: '\25d1'; } /* '◑' */ +.icon-cloud:before { content: '\2601'; } /* '☁' */ +.icon-star:before { content: '\2605'; } /* '★' */ +.icon-star-empty:before { content: '\2606'; } /* '☆' */ +.icon-cup:before { content: '\2615'; } /* '☕' */ +.icon-menu:before { content: '\2630'; } /* '☰' */ +.icon-moon:before { content: '\263d'; } /* '☽' */ +.icon-heart-empty:before { content: '\2661'; } /* '♡' */ +.icon-heart:before { content: '\2665'; } /* '♥' */ +.icon-note:before { content: '\266a'; } /* '♪' */ +.icon-note-beamed:before { content: '\266b'; } /* '♫' */ +.icon-layout:before { content: '\268f'; } /* '⚏' */ +.icon-flag:before { content: '\2691'; } /* '⚑' */ +.icon-tools:before { content: '\2692'; } /* '⚒' */ +.icon-cog:before { content: '\2699'; } /* '⚙' */ +.icon-attention:before { content: '\26a0'; } /* '⚠' */ +.icon-flash:before { content: '\26a1'; } /* '⚡' */ +.icon-record:before { content: '\26ab'; } /* '⚫' */ +.icon-cloud-thunder:before { content: '\26c8'; } /* '⛈' */ +.icon-tape:before { content: '\2707'; } /* '✇' */ +.icon-flight:before { content: '\2708'; } /* '✈' */ +.icon-mail:before { content: '\2709'; } /* '✉' */ +.icon-pencil:before { content: '\270e'; } /* '✎' */ +.icon-feather:before { content: '\2712'; } /* '✒' */ +.icon-check:before { content: '\2713'; } /* '✓' */ +.icon-cancel:before { content: '\2715'; } /* '✕' */ +.icon-cancel-circled:before { content: '\2716'; } /* '✖' */ +.icon-cancel-squared:before { content: '\274e'; } /* '❎' */ +.icon-help:before { content: '\2753'; } /* '❓' */ +.icon-quote:before { content: '\275e'; } /* '❞' */ +.icon-plus-circled:before { content: '\2795'; } /* '➕' */ +.icon-minus-circled:before { content: '\2796'; } /* '➖' */ +.icon-right:before { content: '\27a1'; } /* '➡' */ +.icon-direction:before { content: '\27a2'; } /* '➢' */ +.icon-forward:before { content: '\27a6'; } /* '➦' */ +.icon-ccw:before { content: '\27f2'; } /* '⟲' */ +.icon-cw:before { content: '\27f3'; } /* '⟳' */ +.icon-left:before { content: '\2b05'; } /* '⬅' */ +.icon-up:before { content: '\2b06'; } /* '⬆' */ +.icon-down:before { content: '\2b07'; } /* '⬇' */ +.icon-list-add:before { content: '\e003'; } /* '' */ +.icon-list:before { content: '\e005'; } /* '' */ +.icon-left-bold:before { content: '\e4ad'; } /* '' */ +.icon-right-bold:before { content: '\e4ae'; } /* '' */ +.icon-up-bold:before { content: '\e4af'; } /* '' */ +.icon-down-bold:before { content: '\e4b0'; } /* '' */ +.icon-user-add:before { content: '\e700'; } /* '' */ +.icon-help-circled:before { content: '\e704'; } /* '' */ +.icon-info-circled:before { content: '\e705'; } /* '' */ +.icon-eye:before { content: '\e70a'; } /* '' */ +.icon-tag:before { content: '\e70c'; } /* '' */ +.icon-upload-cloud:before { content: '\e711'; } /* '' */ +.icon-reply:before { content: '\e712'; } /* '' */ +.icon-reply-all:before { content: '\e713'; } /* '' */ +.icon-code:before { content: '\e714'; } /* '' */ +.icon-export:before { content: '\e715'; } /* '' */ +.icon-print:before { content: '\e716'; } /* '' */ +.icon-retweet:before { content: '\e717'; } /* '' */ +.icon-comment:before { content: '\e718'; } /* '' */ +.icon-chat:before { content: '\e720'; } /* '' */ +.icon-vcard:before { content: '\e722'; } /* '' */ +.icon-address:before { content: '\e723'; } /* '' */ +.icon-location:before { content: '\e724'; } /* '' */ +.icon-map:before { content: '\e727'; } /* '' */ +.icon-compass:before { content: '\e728'; } /* '' */ +.icon-trash:before { content: '\e729'; } /* '' */ +.icon-doc:before { content: '\e730'; } /* '' */ +.icon-doc-text-inv:before { content: '\e731'; } /* '' */ +.icon-docs:before { content: '\e736'; } /* '' */ +.icon-doc-landscape:before { content: '\e737'; } /* '' */ +.icon-archive:before { content: '\e738'; } /* '' */ +.icon-rss:before { content: '\e73a'; } /* '' */ +.icon-share:before { content: '\e73c'; } /* '' */ +.icon-basket:before { content: '\e73d'; } /* '' */ +.icon-shareable:before { content: '\e73e'; } /* '' */ +.icon-login:before { content: '\e740'; } /* '' */ +.icon-logout:before { content: '\e741'; } /* '' */ +.icon-volume:before { content: '\e742'; } /* '' */ +.icon-resize-full:before { content: '\e744'; } /* '' */ +.icon-resize-small:before { content: '\e746'; } /* '' */ +.icon-popup:before { content: '\e74c'; } /* '' */ +.icon-publish:before { content: '\e74d'; } /* '' */ +.icon-window:before { content: '\e74e'; } /* '' */ +.icon-arrow-combo:before { content: '\e74f'; } /* '' */ +.icon-chart-pie:before { content: '\e751'; } /* '' */ +.icon-language:before { content: '\e752'; } /* '' */ +.icon-air:before { content: '\e753'; } /* '' */ +.icon-database:before { content: '\e754'; } /* '' */ +.icon-drive:before { content: '\e755'; } /* '' */ +.icon-bucket:before { content: '\e756'; } /* '' */ +.icon-thermometer:before { content: '\e757'; } /* '' */ +.icon-down-circled:before { content: '\e758'; } /* '' */ +.icon-left-circled:before { content: '\e759'; } /* '' */ +.icon-right-circled:before { content: '\e75a'; } /* '' */ +.icon-up-circled:before { content: '\e75b'; } /* '' */ +.icon-down-open:before { content: '\e75c'; } /* '' */ +.icon-left-open:before { content: '\e75d'; } /* '' */ +.icon-right-open:before { content: '\e75e'; } /* '' */ +.icon-up-open:before { content: '\e75f'; } /* '' */ +.icon-down-open-mini:before { content: '\e760'; } /* '' */ +.icon-left-open-mini:before { content: '\e761'; } /* '' */ +.icon-right-open-mini:before { content: '\e762'; } /* '' */ +.icon-up-open-mini:before { content: '\e763'; } /* '' */ +.icon-down-open-big:before { content: '\e764'; } /* '' */ +.icon-left-open-big:before { content: '\e765'; } /* '' */ +.icon-right-open-big:before { content: '\e766'; } /* '' */ +.icon-up-open-big:before { content: '\e767'; } /* '' */ +.icon-progress-0:before { content: '\e768'; } /* '' */ +.icon-progress-1:before { content: '\e769'; } /* '' */ +.icon-progress-2:before { content: '\e76a'; } /* '' */ +.icon-progress-3:before { content: '\e76b'; } /* '' */ +.icon-back-in-time:before { content: '\e771'; } /* '' */ +.icon-network:before { content: '\e776'; } /* '' */ +.icon-inbox:before { content: '\e777'; } /* '' */ +.icon-install:before { content: '\e778'; } /* '' */ +.icon-lifebuoy:before { content: '\e788'; } /* '' */ +.icon-mouse:before { content: '\e789'; } /* '' */ +.icon-dot:before { content: '\e78b'; } /* '' */ +.icon-dot-2:before { content: '\e78c'; } /* '' */ +.icon-dot-3:before { content: '\e78d'; } /* '' */ +.icon-suitcase:before { content: '\e78e'; } /* '' */ +.icon-flow-cascade:before { content: '\e790'; } /* '' */ +.icon-flow-branch:before { content: '\e791'; } /* '' */ +.icon-flow-tree:before { content: '\e792'; } /* '' */ +.icon-flow-line:before { content: '\e793'; } /* '' */ +.icon-flow-parallel:before { content: '\e794'; } /* '' */ +.icon-brush:before { content: '\e79a'; } /* '' */ +.icon-paper-plane:before { content: '\e79b'; } /* '' */ +.icon-magnet:before { content: '\e7a1'; } /* '' */ +.icon-gauge:before { content: '\e7a2'; } /* '' */ +.icon-github-circled:before { content: '\f301'; } /* '' */ +.icon-twitter-circled:before { content: '\f30a'; } /* '' */ +.icon-facebook-circled:before { content: '\f30d'; } /* '' */ +.icon-gplus-circled:before { content: '\f310'; } /* '' */ +.icon-picture:before { content: '🌄'; } /* '\1f304' */ +.icon-globe:before { content: '🌎'; } /* '\1f30e' */ +.icon-leaf:before { content: '🍂'; } /* '\1f342' */ +.icon-graduation-cap:before { content: '🎓'; } /* '\1f393' */ +.icon-mic:before { content: '🎤'; } /* '\1f3a4' */ +.icon-palette:before { content: '🎨'; } /* '\1f3a8' */ +.icon-ticket:before { content: '🎫'; } /* '\1f3ab' */ +.icon-video:before { content: '🎬'; } /* '\1f3ac' */ +.icon-target:before { content: '🎯'; } /* '\1f3af' */ +.icon-music:before { content: '🎵'; } /* '\1f3b5' */ +.icon-trophy:before { content: '🏆'; } /* '\1f3c6' */ +.icon-thumbs-up:before { content: '👍'; } /* '\1f44d' */ +.icon-thumbs-down:before { content: '👎'; } /* '\1f44e' */ +.icon-bag:before { content: '👜'; } /* '\1f45c' */ +.icon-user:before { content: '👤'; } /* '\1f464' */ +.icon-users:before { content: '👥'; } /* '\1f465' */ +.icon-lamp:before { content: '💡'; } /* '\1f4a1' */ +.icon-alert:before { content: '💥'; } /* '\1f4a5' */ +.icon-water:before { content: '💦'; } /* '\1f4a6' */ +.icon-droplet:before { content: '💧'; } /* '\1f4a7' */ +.icon-credit-card:before { content: '💳'; } /* '\1f4b3' */ +.icon-monitor:before { content: '💻'; } /* '\1f4bb' */ +.icon-briefcase:before { content: '💼'; } /* '\1f4bc' */ +.icon-floppy:before { content: '💾'; } /* '\1f4be' */ +.icon-cd:before { content: '💿'; } /* '\1f4bf' */ +.icon-folder:before { content: '📁'; } /* '\1f4c1' */ +.icon-doc-text:before { content: '\1f4c4'; } /* '\1f4c4' */ +.icon-calendar:before { content: '📅'; } /* '\1f4c5' */ +.icon-chart-line:before { content: '📈'; } /* '\1f4c8' */ +.icon-chart-bar:before { content: '📊'; } /* '\1f4ca' */ +.icon-clipboard:before { content: '📋'; } /* '\1f4cb' */ +.icon-attach:before { content: '📎'; } /* '\1f4ce' */ +.icon-bookmarks:before { content: '📑'; } /* '\1f4d1' */ +.icon-book:before { content: '📕'; } /* '\1f4d5' */ +.icon-book-open:before { content: '📖'; } /* '\1f4d6' */ +.icon-phone:before { content: '📞'; } /* '\1f4de' */ +.icon-megaphone:before { content: '📣'; } /* '\1f4e3' */ +.icon-upload:before { content: '📤'; } /* '\1f4e4' */ +.icon-download:before { content: '📥'; } /* '\1f4e5' */ +.icon-box:before { content: '📦'; } /* '\1f4e6' */ +.icon-newspaper:before { content: '📰'; } /* '\1f4f0' */ +.icon-mobile:before { content: '📱'; } /* '\1f4f1' */ +.icon-signal:before { content: '📶'; } /* '\1f4f6' */ +.icon-camera:before { content: '📷'; } /* '\1f4f7' */ +.icon-shuffle:before { content: '🔀'; } /* '\1f500' */ +.icon-loop:before { content: '🔁'; } /* '\1f501' */ +.icon-arrows-ccw:before { content: '🔄'; } /* '\1f504' */ +.icon-light-down:before { content: '🔅'; } /* '\1f505' */ +.icon-light-up:before { content: '🔆'; } /* '\1f506' */ +.icon-mute:before { content: '🔇'; } /* '\1f507' */ +.icon-sound:before { content: '🔊'; } /* '\1f50a' */ +.icon-battery:before { content: '🔋'; } /* '\1f50b' */ +.icon-search:before { content: '🔍'; } /* '\1f50d' */ +.icon-key:before { content: '🔑'; } /* '\1f511' */ +.icon-lock:before { content: '🔒'; } /* '\1f512' */ +.icon-lock-open:before { content: '🔓'; } /* '\1f513' */ +.icon-bell:before { content: '🔔'; } /* '\1f514' */ +.icon-bookmark:before { content: '🔖'; } /* '\1f516' */ +.icon-link:before { content: '\1f517'; } /* '\1f517' */ +.icon-back:before { content: '🔙'; } /* '\1f519' */ +.icon-flashlight:before { content: '🔦'; } /* '\1f526' */ +.icon-chart-area:before { content: '🔾'; } /* '\1f53e' */ +.icon-clock:before { content: '🕔'; } /* '\1f554' */ +.icon-rocket:before { content: '🚀'; } /* '\1f680' */ +.icon-block:before { content: '🚫'; } /* '\1f6ab' */ diff --git a/guides/static/shared/css/skeleton.css b/guides/static/shared/css/skeleton.css new file mode 100644 index 00000000000..4b8f5f376db --- /dev/null +++ b/guides/static/shared/css/skeleton.css @@ -0,0 +1,242 @@ +/* +* Skeleton V1.2 +* Copyright 2011, Dave Gamache +* www.getskeleton.com +* Free to use under the MIT license. +* http://www.opensource.org/licenses/mit-license.php +* 6/20/2012 +*/ + + +/* Table of Contents +================================================== + #Base 960 Grid + #Tablet (Portrait) + #Mobile (Portrait) + #Mobile (Landscape) + #Clearing */ + + + +/* #Base 960 Grid +================================================== */ + + .container { position: relative; width: 960px; margin: 0 auto; padding: 0; } + .container .column, + .container .columns { float: left; display: inline; margin-left: 10px; margin-right: 10px; } + .row { margin-bottom: 20px; } + + /* Nested Column Classes */ + .column.alpha, .columns.alpha { margin-left: 0; } + .column.omega, .columns.omega { margin-right: 0; } + + /* Base Grid */ + .container .one.column, + .container .one.columns { width: 40px; } + .container .two.columns { width: 100px; } + .container .three.columns { width: 160px; } + .container .four.columns { width: 220px; } + .container .five.columns { width: 280px; } + .container .six.columns { width: 340px; } + .container .seven.columns { width: 400px; } + .container .eight.columns { width: 460px; } + .container .nine.columns { width: 520px; } + .container .ten.columns { width: 580px; padding-top: 18px;} + .container .eleven.columns { width: 640px; } + .container .twelve.columns { width: 700px; } + .container .thirteen.columns { width: 760px; } + .container .fourteen.columns { width: 820px; } + .container .fifteen.columns { width: 880px; } + .container .sixteen.columns { width: 940px; } + + .container .one-third.column { width: 300px; } + .container .two-thirds.column { width: 620px; } + + /* Offsets */ + .container .offset-by-one { padding-left: 60px; } + .container .offset-by-two { padding-left: 120px; } + .container .offset-by-three { padding-left: 180px; } + .container .offset-by-four { padding-left: 240px; } + .container .offset-by-five { padding-left: 300px; } + .container .offset-by-six { padding-left: 360px; } + .container .offset-by-seven { padding-left: 420px; } + .container .offset-by-eight { padding-left: 480px; } + .container .offset-by-nine { padding-left: 540px; } + .container .offset-by-ten { padding-left: 600px; } + .container .offset-by-eleven { padding-left: 660px; } + .container .offset-by-twelve { padding-left: 720px; } + .container .offset-by-thirteen { padding-left: 780px; } + .container .offset-by-fourteen { padding-left: 840px; } + .container .offset-by-fifteen { padding-left: 900px; } + + + +/* #Tablet (Portrait) +================================================== */ + + /* Note: Design for a width of 768px */ + + @media only screen and (min-width: 768px) and (max-width: 959px) { + .container { width: 768px; } + .container .column, + .container .columns { margin-left: 10px; margin-right: 10px; } + .column.alpha, .columns.alpha { margin-left: 0; margin-right: 10px; } + .column.omega, .columns.omega { margin-right: 0; margin-left: 10px; } + .alpha.omega { margin-left: 0; margin-right: 0; } + + .container .one.column, + .container .one.columns { width: 28px; } + .container .two.columns { width: 76px; } + .container .three.columns { width: 124px; } + .container .four.columns { width: 172px; } + .container .five.columns { width: 220px; } + .container .six.columns { width: 268px; } + .container .seven.columns { width: 316px; } + .container .eight.columns { width: 364px; } + .container .nine.columns { width: 412px; } + .container .ten.columns { width: 460px; } + .container .eleven.columns { width: 508px; } + .container .twelve.columns { width: 556px; } + .container .thirteen.columns { width: 604px; } + .container .fourteen.columns { width: 652px; } + .container .fifteen.columns { width: 700px; } + .container .sixteen.columns { width: 748px; } + + .container .one-third.column { width: 236px; } + .container .two-thirds.column { width: 492px; } + + /* Offsets */ + .container .offset-by-one { padding-left: 48px; } + .container .offset-by-two { padding-left: 96px; } + .container .offset-by-three { padding-left: 144px; } + .container .offset-by-four { padding-left: 192px; } + .container .offset-by-five { padding-left: 240px; } + .container .offset-by-six { padding-left: 288px; } + .container .offset-by-seven { padding-left: 336px; } + .container .offset-by-eight { padding-left: 384px; } + .container .offset-by-nine { padding-left: 432px; } + .container .offset-by-ten { padding-left: 480px; } + .container .offset-by-eleven { padding-left: 528px; } + .container .offset-by-twelve { padding-left: 576px; } + .container .offset-by-thirteen { padding-left: 624px; } + .container .offset-by-fourteen { padding-left: 672px; } + .container .offset-by-fifteen { padding-left: 720px; } + } + + +/* #Mobile (Portrait) +================================================== */ + + /* Note: Design for a width of 320px */ + + @media only screen and (max-width: 767px) { + .container { width: 300px; } + .container .columns, + .container .column { margin: 0; } + + .container .one.column, + .container .one.columns, + .container .two.columns, + .container .three.columns, + .container .four.columns, + .container .five.columns, + .container .six.columns, + .container .seven.columns, + .container .eight.columns, + .container .nine.columns, + .container .ten.columns, + .container .eleven.columns, + .container .twelve.columns, + .container .thirteen.columns, + .container .fourteen.columns, + .container .fifteen.columns, + .container .sixteen.columns, + .container .one-third.column, + .container .two-thirds.column { width: 300px; } + + /* Offsets */ + .container .offset-by-one, + .container .offset-by-two, + .container .offset-by-three, + .container .offset-by-four, + .container .offset-by-five, + .container .offset-by-six, + .container .offset-by-seven, + .container .offset-by-eight, + .container .offset-by-nine, + .container .offset-by-ten, + .container .offset-by-eleven, + .container .offset-by-twelve, + .container .offset-by-thirteen, + .container .offset-by-fourteen, + .container .offset-by-fifteen { padding-left: 0; } + + } + + +/* #Mobile (Landscape) +================================================== */ + + /* Note: Design for a width of 480px */ + + @media only screen and (min-width: 480px) and (max-width: 767px) { + .container { width: 420px; } + .container .columns, + .container .column { margin: 0; } + + .container .one.column, + .container .one.columns, + .container .two.columns, + .container .three.columns, + .container .four.columns, + .container .five.columns, + .container .six.columns, + .container .seven.columns, + .container .eight.columns, + .container .nine.columns, + .container .ten.columns, + .container .eleven.columns, + .container .twelve.columns, + .container .thirteen.columns, + .container .fourteen.columns, + .container .fifteen.columns, + .container .sixteen.columns, + .container .one-third.column, + .container .two-thirds.column { width: 420px; } + } + + +/* #Clearing +================================================== */ + + /* Self Clearing Goodness */ + .container:after { content: "\0020"; display: block; height: 0; clear: both; visibility: hidden; } + + /* Use clearfix class on parent to clear nested columns, + or wrap each row of columns in a
          */ + .clearfix:before, + .clearfix:after, + .row:before, + .row:after { + content: '\0020'; + display: block; + overflow: hidden; + visibility: hidden; + width: 0; + height: 0; } + .row:after, + .clearfix:after { + clear: both; } + .row, + .clearfix { + zoom: 1; } + + /* You can also use a
          to clear columns */ + .clear { + clear: both; + display: block; + overflow: hidden; + visibility: hidden; + width: 0; + height: 0; + } \ No newline at end of file diff --git a/guides/static/shared/fonts/bariol_regular-webfont.eot b/guides/static/shared/fonts/bariol_regular-webfont.eot new file mode 100755 index 00000000000..b8cf00db847 Binary files /dev/null and b/guides/static/shared/fonts/bariol_regular-webfont.eot differ diff --git a/guides/static/shared/fonts/bariol_regular-webfont.svg b/guides/static/shared/fonts/bariol_regular-webfont.svg new file mode 100755 index 00000000000..8450bece33a --- /dev/null +++ b/guides/static/shared/fonts/bariol_regular-webfont.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/guides/static/shared/fonts/bariol_regular-webfont.ttf b/guides/static/shared/fonts/bariol_regular-webfont.ttf new file mode 100755 index 00000000000..ad90f283818 Binary files /dev/null and b/guides/static/shared/fonts/bariol_regular-webfont.ttf differ diff --git a/guides/static/shared/fonts/bariol_regular-webfont.woff b/guides/static/shared/fonts/bariol_regular-webfont.woff new file mode 100755 index 00000000000..1aab58a44bf Binary files /dev/null and b/guides/static/shared/fonts/bariol_regular-webfont.woff differ diff --git a/guides/static/shared/fonts/entypo.eot b/guides/static/shared/fonts/entypo.eot new file mode 100644 index 00000000000..bf9a526f955 Binary files /dev/null and b/guides/static/shared/fonts/entypo.eot differ diff --git a/guides/static/shared/fonts/entypo.svg b/guides/static/shared/fonts/entypo.svg new file mode 100644 index 00000000000..a02d44d55f5 --- /dev/null +++ b/guides/static/shared/fonts/entypo.svg @@ -0,0 +1,295 @@ + + + +Copyright (C) 2012 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/guides/static/shared/fonts/entypo.ttf b/guides/static/shared/fonts/entypo.ttf new file mode 100644 index 00000000000..521d4735fd2 Binary files /dev/null and b/guides/static/shared/fonts/entypo.ttf differ diff --git a/guides/static/shared/fonts/entypo.woff b/guides/static/shared/fonts/entypo.woff new file mode 100644 index 00000000000..417e3f944e9 Binary files /dev/null and b/guides/static/shared/fonts/entypo.woff differ diff --git a/guides/static/shared/fonts/icomoon.eot b/guides/static/shared/fonts/icomoon.eot new file mode 100755 index 00000000000..e9f0782e1ca Binary files /dev/null and b/guides/static/shared/fonts/icomoon.eot differ diff --git a/guides/static/shared/fonts/icomoon.svg b/guides/static/shared/fonts/icomoon.svg new file mode 100755 index 00000000000..fed4d7bc96e --- /dev/null +++ b/guides/static/shared/fonts/icomoon.svg @@ -0,0 +1,15 @@ + + + +Generated by IcoMoon + + + + + + + + + + + \ No newline at end of file diff --git a/guides/static/shared/fonts/icomoon.ttf b/guides/static/shared/fonts/icomoon.ttf new file mode 100755 index 00000000000..7a1c7aaa479 Binary files /dev/null and b/guides/static/shared/fonts/icomoon.ttf differ diff --git a/guides/static/shared/fonts/icomoon.woff b/guides/static/shared/fonts/icomoon.woff new file mode 100755 index 00000000000..7613e598b77 Binary files /dev/null and b/guides/static/shared/fonts/icomoon.woff differ diff --git a/guides/static/shared/images/active-arrow.png b/guides/static/shared/images/active-arrow.png new file mode 100644 index 00000000000..5c1cc36d075 Binary files /dev/null and b/guides/static/shared/images/active-arrow.png differ diff --git a/guides/static/shared/images/api-objects-bg-full.png b/guides/static/shared/images/api-objects-bg-full.png new file mode 100644 index 00000000000..fcc1f003aa3 Binary files /dev/null and b/guides/static/shared/images/api-objects-bg-full.png differ diff --git a/guides/static/shared/images/api-objects-bg.png b/guides/static/shared/images/api-objects-bg.png new file mode 100644 index 00000000000..84898cd241e Binary files /dev/null and b/guides/static/shared/images/api-objects-bg.png differ diff --git a/guides/static/shared/images/background-v2.png b/guides/static/shared/images/background-v2.png new file mode 100644 index 00000000000..33292d8b647 Binary files /dev/null and b/guides/static/shared/images/background-v2.png differ diff --git a/guides/static/shared/images/background-white.png b/guides/static/shared/images/background-white.png new file mode 100644 index 00000000000..c981fb8b8eb Binary files /dev/null and b/guides/static/shared/images/background-white.png differ diff --git a/guides/static/shared/images/bg.png b/guides/static/shared/images/bg.png new file mode 100644 index 00000000000..d1fae0fc078 Binary files /dev/null and b/guides/static/shared/images/bg.png differ diff --git a/guides/static/shared/images/bg_footer_bottom.png b/guides/static/shared/images/bg_footer_bottom.png new file mode 100644 index 00000000000..f63eecfb557 Binary files /dev/null and b/guides/static/shared/images/bg_footer_bottom.png differ diff --git a/guides/static/shared/images/bg_footer_top.png b/guides/static/shared/images/bg_footer_top.png new file mode 100644 index 00000000000..94a481ab687 Binary files /dev/null and b/guides/static/shared/images/bg_footer_top.png differ diff --git a/guides/static/shared/images/crud-sprite.png b/guides/static/shared/images/crud-sprite.png new file mode 100644 index 00000000000..5334e2007d7 Binary files /dev/null and b/guides/static/shared/images/crud-sprite.png differ diff --git a/guides/static/shared/images/dropdown_sprites.jpg b/guides/static/shared/images/dropdown_sprites.jpg new file mode 100644 index 00000000000..3e3cc6f8489 Binary files /dev/null and b/guides/static/shared/images/dropdown_sprites.jpg differ diff --git a/guides/static/shared/images/expand-arrows.png b/guides/static/shared/images/expand-arrows.png new file mode 100644 index 00000000000..190d784b935 Binary files /dev/null and b/guides/static/shared/images/expand-arrows.png differ diff --git a/guides/static/shared/images/nav-rule.png b/guides/static/shared/images/nav-rule.png new file mode 100644 index 00000000000..909c5a85f1f Binary files /dev/null and b/guides/static/shared/images/nav-rule.png differ diff --git a/guides/static/shared/images/next_step_arrow.gif b/guides/static/shared/images/next_step_arrow.gif new file mode 100644 index 00000000000..8f93ee9f49a Binary files /dev/null and b/guides/static/shared/images/next_step_arrow.gif differ diff --git a/guides/static/shared/images/qmark.png b/guides/static/shared/images/qmark.png new file mode 100644 index 00000000000..caa30f5568e Binary files /dev/null and b/guides/static/shared/images/qmark.png differ diff --git a/guides/static/shared/images/subheader_bg.png b/guides/static/shared/images/subheader_bg.png new file mode 100644 index 00000000000..865c60471ea Binary files /dev/null and b/guides/static/shared/images/subheader_bg.png differ diff --git a/guides/static/sources/taxonomies_chart.pages b/guides/static/sources/taxonomies_chart.pages new file mode 100644 index 00000000000..61503f30797 Binary files /dev/null and b/guides/static/sources/taxonomies_chart.pages differ diff --git a/install.rb b/install.rb index 3a8ad36260c..90fae3c52ba 100644 --- a/install.rb +++ b/install.rb @@ -2,7 +2,7 @@ version = ARGV.pop -%w( core api dash promo sample ).each do |framework| +%w( cmd core api backend frontend sample ).each do |framework| puts "Installing #{framework}..." Dir.chdir(framework) do diff --git a/lib/sandbox.sh b/lib/sandbox.sh index fc584858acd..1350d397d23 100755 --- a/lib/sandbox.sh +++ b/lib/sandbox.sh @@ -1,9 +1,28 @@ +#!/bin/sh # Used in the sandbox rake task in Rakefile -#!/bin/bash -rm -rf sandbox -rails new sandbox --skip-bundle -cd sandbox + +rm -rf ./sandbox +bundle exec rails new sandbox --skip-bundle +if [ ! -d "sandbox" ]; then + echo 'sandbox rails application failed' + exit 1 +fi + +cd ./sandbox echo "gem 'spree', :path => '..'" >> Gemfile -echo "gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => 'edge'" >> Gemfile +echo "gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '2-4-stable'" >> Gemfile + +cat <> Gemfile +group :test, :development do + platforms :ruby_19 do + gem 'pry-debugger' + end + platforms :ruby_20, :ruby_21 do + gem 'pry-byebug' + end +end +RUBY + bundle install --gemfile Gemfile -rails g spree:install --auto-accept --user_class=Spree::User +bundle exec rails g spree:install --auto-accept --user_class=Spree::User --enforce_available_locales=true +bundle exec rails g spree:auth:install diff --git a/lib/spree.rb b/lib/spree.rb index c7de4b602af..c4a34c67b8e 100644 --- a/lib/spree.rb +++ b/lib/spree.rb @@ -1,5 +1,15 @@ require 'spree_core' require 'spree_api' -require 'spree_dash' -require 'spree_promo' +require 'spree_backend' +require 'spree_frontend' require 'spree_sample' + +begin + require 'protected_attributes' + puts "*" * 75 + puts "[FATAL] Spree does not work with the protected_attributes gem installed!" + puts "You MUST remove this gem from your Gemfile. It is incompatible with Spree." + puts "*" * 75 + exit +rescue LoadError +end diff --git a/license.md b/license.md new file mode 100644 index 00000000000..506b15e0971 --- /dev/null +++ b/license.md @@ -0,0 +1,13 @@ +Spree License +============= + +Copyright © 2007-2014, Spree Commerce Inc. and other contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of Spree Commerce Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +_This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner of contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage._ diff --git a/missing_translations.rb b/missing_translations.rb new file mode 100644 index 00000000000..f340766c382 --- /dev/null +++ b/missing_translations.rb @@ -0,0 +1,24 @@ +require 'yaml' +require 'pry' + +hash = YAML.load_file("core/config/locales/en.yml") +hash["en"]["spree"].each do |k,v| + if String === v + # Check for translations used in controllers, models, views, mailers, helpers and lib + command = %Q{ack "t\\(:#{k}" */app/**/* */lib/**} + # Check for possible preferences matching translations + # This is because preference_field within backend uses it like Spree.t(:) + symbol_command = %Q{ack ":#{k}" */app/**/* */lib/**} + `#{command}` + command_status = $?.exitstatus + `#{symbol_command}` + symbol_command_status = $?.exitstatus + if command_status == 1 && symbol_command_status == 1 + puts "Couldn't find #{k} translation" + end + else + # TODO: Account for nested keys. + # I didn't do this because there's not that many nested keys in en.yml + # and it's easy to verify with my eyes. + end +end diff --git a/promo/.rspec b/promo/.rspec deleted file mode 100644 index 53607ea52b7..00000000000 --- a/promo/.rspec +++ /dev/null @@ -1 +0,0 @@ ---colour diff --git a/promo/Gemfile b/promo/Gemfile deleted file mode 100644 index af51fbf0c11..00000000000 --- a/promo/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -eval(File.read(File.dirname(__FILE__) + '/../common_spree_dependencies.rb')) - -gem 'spree_core', :path => '../core' - -gemspec diff --git a/promo/Guardfile b/promo/Guardfile deleted file mode 100644 index 4f2814fecbb..00000000000 --- a/promo/Guardfile +++ /dev/null @@ -1,13 +0,0 @@ -guard 'rspec', :version => 2, :spec_paths => %w(spec), - :cli => (File.read('.rspec').split("\n").join(' ') if File.exists?('.rspec')) do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/requests/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch("spec/spec_helper.rb") { "spec" } - watch("config/routes.rb") - watch("app/controllers/application_controller.rb") { "spec/controllers" } - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } -end diff --git a/promo/LICENSE b/promo/LICENSE deleted file mode 100644 index 74f73e35ac3..00000000000 --- a/promo/LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2007-2012, Spree Commerce, Inc. and other contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name Spree nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/promo/README.md b/promo/README.md deleted file mode 100644 index 0144a68ae54..00000000000 --- a/promo/README.md +++ /dev/null @@ -1,16 +0,0 @@ -Promotions -========== - -Promotion functionality for use with Spree - - -Testing -------- - -You need to do a quick one-time creation of a test application and then you can use it to run the tests. - - bundle exec rake test_app - -Then run the tests - - bundle exec rake spec diff --git a/promo/Rakefile b/promo/Rakefile deleted file mode 100644 index ee8bc613892..00000000000 --- a/promo/Rakefile +++ /dev/null @@ -1,28 +0,0 @@ -require 'rake' -require 'rake/testtask' -require 'rake/packagetask' -require 'rubygems/package_task' -require 'rspec/core/rake_task' -require 'spree/core/testing_support/common_rake' - -RSpec::Core::RakeTask.new - -task :default => :spec - -spec = eval(File.read('spree_promo.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end - -desc "Release to gemcutter" -task :release do - version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip - cmd = "cd pkg && gem push spree_promo-#{version}.gem"; puts cmd; system cmd -end - -desc "Generates a dummy app for testing" -task :test_app do - ENV['LIB_NAME'] = 'spree/promo' - Rake::Task['common:test_app'].invoke -end diff --git a/promo/app/assets/javascripts/admin/promotions.js b/promo/app/assets/javascripts/admin/promotions.js deleted file mode 100644 index 176908c6b26..00000000000 --- a/promo/app/assets/javascripts/admin/promotions.js +++ /dev/null @@ -1,98 +0,0 @@ -var initProductActions = function(){ - - // Add classes on promotion items for design - $('a.delete').live('mouseover mouseout', function(event) { - if (event.type == 'mouseover') { - $(this).parent().addClass('action-remove'); - } else { - $(this).parent().removeClass('action-remove'); - } - }); - - $(".variant_autocomplete").variantAutocomplete(); - - $('.calculator-fields').each(function(){ - var $fields_container = $(this); - var $type_select = $fields_container.find('.type-select'); - var $settings = $fields_container.find('.settings'); - var $warning = $fields_container.find('.warning'); - var originalType = $type_select.val(); - - $warning.hide(); - $type_select.change(function(){ - if( $(this).val() == originalType ){ - $warning.hide(); - $settings.show(); - $settings.find('input').removeAttr('disabled'); - } else { - $warning.show(); - $settings.hide(); - $settings.find('input').attr('disabled', 'disabled'); - } - }); - }); - - // - // CreateLineItems Promotion Action - // - ( function(){ - var hideOrShowItemTables = function(){ - $('.promotion_action table').each(function(){ - if($(this).find('td').length == 0){ - $(this).hide(); - } else { - $(this).show(); - } - }); - }; - hideOrShowItemTables(); - - // Remove line item - var setupRemoveLineItems = function(){ - $(".remove_promotion_line_item").click(function(){ - line_items_el = $($('.line_items_string')[0]) - finder = RegExp($(this).data("variant-id") + "x\\d+") - line_items_el.val(line_items_el.val().replace(finder, "")) - $(this).parents('tr').remove(); - hideOrShowItemTables(); - }); - }; - - setupRemoveLineItems(); - // Add line item to list - $(".promotion_action.create_line_items button.add").unbind('click').click(function(e){ - var $container = $(this).parents('.promotion_action'); - var product_name = $container.find("input[name='add_product_name']").val(); - var variant_id = $container.find("input[name='add_variant_id']").val(); - var quantity = $container.find("input[name='add_quantity']").val(); - if(variant_id){ - // Add to the table - var newRow = "" + product_name + "" + quantity + ""; - $container.find('table').append(newRow); - // Add to serialized string in hidden text field - var $hiddenField = $container.find(".line_items_string"); - $hiddenField.val($hiddenField.val() + "," + variant_id + "x" + quantity); - setupRemoveLineItems(); - hideOrShowItemTables(); - } - return false; - }); - - } )(); - -} - -$(document).ready(function() { - initProductActions(); - - // toggle fields for specific events - $('#promotion_event_name').change(function() { - $('#promotion_code_field').toggle($('#promotion_event_name').val() == 'spree.checkout.coupon_code_added'); - $('#promotion_path_field').toggle($('#promotion_event_name').val() == 'spree.content.visited'); - }); - $('#promotion_event_name').change(); - -}); - - - diff --git a/promo/app/assets/javascripts/admin/spree_promo.js b/promo/app/assets/javascripts/admin/spree_promo.js deleted file mode 100644 index 617f70cf9f2..00000000000 --- a/promo/app/assets/javascripts/admin/spree_promo.js +++ /dev/null @@ -1,39 +0,0 @@ -//= require admin/spree_core -//= require_tree . - -function cleanUsers(data) { - var users = $.map(data['users'], function(result) { - return result['user'] - }) - return users; -} - -$(document).ready(function() { - if ($('user_picker').length > 0) { - $('.user_picker').select2({ - minimumInputLength: 1, - multiple: true, - initSelection: function(element, callback) { - $.get(Spree.routes.user_search, { ids: element.val() }, function(data) { - callback(cleanUsers(data)) - }) - }, - ajax: { - url: Spree.routes.user_search, - datatype: 'json', - data: function(term, page) { - return { q: term } - }, - results: function(data, page) { - return { results: cleanUsers(data) } - } - }, - formatResult: function(user) { - return user.email; - }, - formatSelection: function(user) { - return user.email; - } - }); - } -}) diff --git a/promo/app/assets/javascripts/store/spree_promo.js b/promo/app/assets/javascripts/store/spree_promo.js deleted file mode 100644 index d5cb5c754f8..00000000000 --- a/promo/app/assets/javascripts/store/spree_promo.js +++ /dev/null @@ -1 +0,0 @@ -//= require store/spree_core diff --git a/promo/app/assets/stylesheets/admin/spree_promo.css b/promo/app/assets/stylesheets/admin/spree_promo.css deleted file mode 100644 index 21ef02a685b..00000000000 --- a/promo/app/assets/stylesheets/admin/spree_promo.css +++ /dev/null @@ -1,3 +0,0 @@ -/* - *= require admin/spree_core -*/ diff --git a/promo/app/assets/stylesheets/store/spree_promo.css b/promo/app/assets/stylesheets/store/spree_promo.css deleted file mode 100644 index 562fe091048..00000000000 --- a/promo/app/assets/stylesheets/store/spree_promo.css +++ /dev/null @@ -1,17 +0,0 @@ -/* - *= require store/spree_core - *= require_self -*/ - -#update-cart .coupon-code-field { - /* yes, this is ugly... */ - margin-top: -42px !important; -} - -@media only screen and (max-width: 767px) { - #empty-cart, - #update-cart .coupon-code-field { - /* yes, this is ugly... */ - margin-top: 0 !important; - } -} diff --git a/promo/app/controllers/spree/admin/promotion_actions_controller.rb b/promo/app/controllers/spree/admin/promotion_actions_controller.rb deleted file mode 100644 index e313ec9f800..00000000000 --- a/promo/app/controllers/spree/admin/promotion_actions_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -class Spree::Admin::PromotionActionsController < Spree::Admin::BaseController - def create - @calculators = Spree::Promotion::Actions::CreateAdjustment.calculators - @promotion = Spree::Promotion.find(params[:promotion_id]) - @promotion_action = params[:action_type].constantize.new(params[:promotion_action]) - @promotion_action.promotion = @promotion - if @promotion_action.save - flash[:success] = I18n.t(:successfully_created, :resource => I18n.t(:promotion_action)) - end - respond_to do |format| - format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} - format.js { render :layout => false } - end - end - - def destroy - @promotion = Spree::Promotion.find(params[:promotion_id]) - @promotion_action = @promotion.promotion_actions.find(params[:id]) - if @promotion_action.destroy - flash[:success] = I18n.t(:successfully_removed, :resource => I18n.t(:promotion_action)) - end - respond_to do |format| - format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} - format.js { render :layout => false } - end - end -end diff --git a/promo/app/controllers/spree/admin/promotion_rules_controller.rb b/promo/app/controllers/spree/admin/promotion_rules_controller.rb deleted file mode 100644 index f00b8d60e5a..00000000000 --- a/promo/app/controllers/spree/admin/promotion_rules_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Spree::Admin::PromotionRulesController < Spree::Admin::BaseController - helper 'spree/promotion_rules' - - def create - @promotion = Spree::Promotion.find(params[:promotion_id]) - # Remove type key from this hash so that we don't attempt - # to set it when creating a new record, as this is raises - # an error in ActiveRecord 3.2. - promotion_rule_type = params[:promotion_rule].delete(:type) - @promotion_rule = promotion_rule_type.constantize.new(params[:promotion_rule]) - @promotion_rule.promotion = @promotion - if @promotion_rule.save - flash[:success] = I18n.t(:successfully_created, :resource => I18n.t(:promotion_rule)) - end - respond_to do |format| - format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} - format.js { render :layout => false } - end - end - - def destroy - @promotion = Spree::Promotion.find(params[:promotion_id]) - @promotion_rule = @promotion.promotion_rules.find(params[:id]) - if @promotion_rule.destroy - flash[:success] = I18n.t(:successfully_removed, :resource => I18n.t(:promotion_rule)) - end - respond_to do |format| - format.html { redirect_to spree.edit_admin_promotion_path(@promotion)} - format.js { render :layout => false } - end - end -end diff --git a/promo/app/controllers/spree/admin/promotions_controller.rb b/promo/app/controllers/spree/admin/promotions_controller.rb deleted file mode 100644 index a9b48e839aa..00000000000 --- a/promo/app/controllers/spree/admin/promotions_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Spree - module Admin - class PromotionsController < ResourceController - before_filter :load_data - helper 'spree/promotion_rules' - - protected - def build_resource - if params[:promotion] - calculator_type = params[:promotion].delete(:calculator_type) - @promotion = Promotion.new(params[:promotion]) - if calculator_type - @promotion.calculator = calculator_type.constantize.new - end - else - @promotion = Promotion.new - end - @promotion - end - - def location_after_save - spree.edit_admin_promotion_url(@promotion) - end - - def load_data - @calculators = Rails.application.config.spree.calculators.promotion_actions_create_adjustments - end - end - end -end diff --git a/promo/app/controllers/spree/checkout_controller_decorator.rb b/promo/app/controllers/spree/checkout_controller_decorator.rb deleted file mode 100644 index 0979603337b..00000000000 --- a/promo/app/controllers/spree/checkout_controller_decorator.rb +++ /dev/null @@ -1,30 +0,0 @@ -Spree::CheckoutController.class_eval do - - #TODO 90% of this method is duplicated code. DRY - def update - if @order.update_attributes(object_params) - - fire_event('spree.checkout.update') - render :edit and return unless apply_coupon_code - - if @order.next - state_callback(:after) - else - flash[:error] = t(:payment_processing_failed) - redirect_to checkout_state_path(@order.state) - return - end - - if @order.state == 'complete' || @order.completed? - flash.notice = t(:order_processed_successfully) - flash[:commerce_tracking] = 'nothing special' - redirect_to completion_route - else - redirect_to checkout_state_path(@order.state) - end - else - render :edit - end - end - -end diff --git a/promo/app/controllers/spree/content_controller_decorator.rb b/promo/app/controllers/spree/content_controller_decorator.rb deleted file mode 100644 index 55db63fbbfa..00000000000 --- a/promo/app/controllers/spree/content_controller_decorator.rb +++ /dev/null @@ -1,13 +0,0 @@ -Spree::ContentController.class_eval do - after_filter :fire_visited_path, :only => :show - after_filter :fire_visited_action, :except => :show - - def fire_visited_path - fire_event('spree.content.visited', :path => "content/#{params[:path]}") - end - - def fire_visited_action - fire_event('spree.content.visited', :path => "content/#{params[:action]}") - end - -end diff --git a/promo/app/controllers/spree/orders_controller_decorator.rb b/promo/app/controllers/spree/orders_controller_decorator.rb deleted file mode 100644 index a1dd8d13e60..00000000000 --- a/promo/app/controllers/spree/orders_controller_decorator.rb +++ /dev/null @@ -1,24 +0,0 @@ -Spree::OrdersController.class_eval do - - def update - @order = current_order - if @order.update_attributes(params[:order]) - render :edit and return unless apply_coupon_code - - @order.line_items = @order.line_items.select {|li| li.quantity > 0 } - fire_event('spree.order.contents_changed') - respond_with(@order) do |format| - format.html do - if params.has_key?(:checkout) - redirect_to checkout_state_path(@order.checkout_steps.first) - else - redirect_to cart_path - end - end - end - else - respond_with(@order) - end - end - -end diff --git a/promo/app/controllers/spree/store_controller_decorator.rb b/promo/app/controllers/spree/store_controller_decorator.rb deleted file mode 100644 index aabe1c233e3..00000000000 --- a/promo/app/controllers/spree/store_controller_decorator.rb +++ /dev/null @@ -1,55 +0,0 @@ -Spree::StoreController.class_eval do - - protected - def apply_coupon_code - if @order.coupon_code.present? - # check if coupon code is already applied - if @order.adjustments.promotion.eligible.detect { |p| p.originator.promotion.code == @order.coupon_code }.present? - flash[:notice] = t(:coupon_code_already_applied) - true - else - event_name = "spree.checkout.coupon_code_added" - - # TODO should restrict to payload's event name? - promotion = Spree::Promotion.find_by_code(@order.coupon_code) - - if promotion.present? - if promotion.expired? - flash[:error] = t(:coupon_code_expired) - return false - end - - if promotion.usage_limit_exceeded? - flash[:error] = t(:coupon_code_max_usage) - return false - end - - previous_promo = @order.adjustments.promotion.eligible.first - fire_event(event_name, :coupon_code => @order.coupon_code) - promo = @order.adjustments.promotion.detect { |p| p.originator.promotion.code == @order.coupon_code } - - if promo.present? and promo.eligible - flash[:success] = t(:coupon_code_applied) - true - elsif previous_promo.present? and promo.present? - flash[:error] = t(:coupon_code_better_exists) - false - elsif promo.present? - flash[:error] = t(:coupon_code_not_eligible) - false - else - # if the promotion was created after the order - flash[:error] = t(:coupon_code_not_found) - false - end - else - flash[:error] = t(:coupon_code_not_found) - false - end - end - else - true - end - end - -end diff --git a/promo/app/helpers/spree/promotion_rules_helper.rb b/promo/app/helpers/spree/promotion_rules_helper.rb deleted file mode 100644 index 20c1722cbf4..00000000000 --- a/promo/app/helpers/spree/promotion_rules_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Spree - module PromotionRulesHelper - - def options_for_promotion_rule_types(promotion) - existing = promotion.rules.map { |rule| rule.class.name } - rule_names = Rails.application.config.spree.promotions.rules.map(&:name).reject{ |r| existing.include? r } - options = rule_names.map { |name| [ t("promotion_rule_types.#{name.demodulize.underscore}.name"), name] } - options_for_select(options) - end - - end -end - diff --git a/promo/app/models/spree/adjustment_decorator.rb b/promo/app/models/spree/adjustment_decorator.rb deleted file mode 100644 index 36fd9dde532..00000000000 --- a/promo/app/models/spree/adjustment_decorator.rb +++ /dev/null @@ -1,7 +0,0 @@ -Spree::Adjustment.class_eval do - class << self - def promotion - where(:originator_type => 'Spree::PromotionAction') - end - end -end diff --git a/promo/app/models/spree/calculator/free_shipping.rb b/promo/app/models/spree/calculator/free_shipping.rb deleted file mode 100644 index abc9c8de460..00000000000 --- a/promo/app/models/spree/calculator/free_shipping.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Spree - class Calculator::FreeShipping < Calculator - def self.description - I18n.t(:free_shipping) - end - - def compute(object) - if object.is_a?(Array) - return if object.empty? - order = object.first.order - else - order = object - end - - order.ship_total - end - end -end diff --git a/promo/app/models/spree/calculator/percent_per_item.rb b/promo/app/models/spree/calculator/percent_per_item.rb deleted file mode 100644 index d49bfa2ae22..00000000000 --- a/promo/app/models/spree/calculator/percent_per_item.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Spree - - # A calculator for promotions that calculates a percent-off discount - # for all matching products in an order. This should not be used as a - # shipping calculator since it would be the same thing as a flat percent - # off the entire order. - - class Calculator::PercentPerItem < Calculator - preference :percent, :decimal, :default => 0 - - attr_accessible :preferred_percent - - def self.description - I18n.t(:percent_per_item) - end - - def compute(object=nil) - return 0 if object.nil? - object.line_items.reduce(0) do |sum, line_item| - sum += value_for_line_item(line_item) - end - end - - private - - # Returns all products that match the promotion's rule. - def matching_products - @matching_products ||= if compute_on_promotion? - self.calculable.promotion.rules.map(&:products).flatten - end - end - - # Calculates the discount value of each line item. Returns zero - # unless the product is included in the promotion rules. - def value_for_line_item(line_item) - if compute_on_promotion? - return 0 unless matching_products.include?(line_item.product) - end - line_item.price * line_item.quantity * preferred_percent - end - - # Determines wether or not the calculable object is a promotion - def compute_on_promotion? - @compute_on_promotion ||= self.calculable.respond_to?(:promotion) - end - - end -end diff --git a/promo/app/models/spree/order_decorator.rb b/promo/app/models/spree/order_decorator.rb deleted file mode 100644 index a14e45f24ce..00000000000 --- a/promo/app/models/spree/order_decorator.rb +++ /dev/null @@ -1,19 +0,0 @@ -Spree::Order.class_eval do - attr_accessible :coupon_code - attr_reader :coupon_code - - def coupon_code=(code) - @coupon_code = code.strip.downcase rescue nil - end - - # Tells us if there if the specified promotion is already associated with the order - # regardless of whether or not its currently eligible. Useful because generally - # you would only want a promotion to apply to order no more than once. - def promotion_credit_exists?(promotion) - !! adjustments.promotion.reload.detect { |credit| credit.originator.promotion.id == promotion.id } - end - - def promo_total - adjustments.eligible.promotion.map(&:amount).sum - end -end diff --git a/promo/app/models/spree/order_updater_decorator.rb b/promo/app/models/spree/order_updater_decorator.rb deleted file mode 100644 index 53799503e6d..00000000000 --- a/promo/app/models/spree/order_updater_decorator.rb +++ /dev/null @@ -1,14 +0,0 @@ -Spree::OrderUpdater.class_eval do - unless self.method_defined?('update_adjustments_with_promotion_limiting') - def update_adjustments_with_promotion_limiting - update_adjustments_without_promotion_limiting - return if adjustments.promotion.eligible.none? - most_valuable_adjustment = adjustments.promotion.eligible.max{|a,b| a.amount.abs <=> b.amount.abs} - current_adjustments = (adjustments.promotion.eligible - [most_valuable_adjustment]) - current_adjustments.each do |adjustment| - adjustment.update_attribute_without_callbacks(:eligible, false) - end - end - alias_method_chain :update_adjustments, :promotion_limiting - end -end diff --git a/promo/app/models/spree/payment_decorator.rb b/promo/app/models/spree/payment_decorator.rb deleted file mode 100644 index 4abcae150fa..00000000000 --- a/promo/app/models/spree/payment_decorator.rb +++ /dev/null @@ -1,5 +0,0 @@ -Spree::Payment.class_eval do - def promo_total - order.promo_total * 100 - end -end diff --git a/promo/app/models/spree/product_decorator.rb b/promo/app/models/spree/product_decorator.rb deleted file mode 100644 index ccf3345eff6..00000000000 --- a/promo/app/models/spree/product_decorator.rb +++ /dev/null @@ -1,8 +0,0 @@ -Spree::Product.class_eval do - has_and_belongs_to_many :promotion_rules, :join_table => :spree_products_promotion_rules - - def possible_promotions - promotion_ids = promotion_rules.map(&:activator_id).uniq - Spree::Promotion.advertised.where(:id => promotion_ids).reject(&:expired?) - end -end diff --git a/promo/app/models/spree/promotion.rb b/promo/app/models/spree/promotion.rb deleted file mode 100644 index 0e8acd94544..00000000000 --- a/promo/app/models/spree/promotion.rb +++ /dev/null @@ -1,105 +0,0 @@ -module Spree - class Promotion < Spree::Activator - MATCH_POLICIES = %w(all any) - UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"] - - Activator.event_names << 'spree.checkout.coupon_code_added' - Activator.event_names << 'spree.content.visited' - - has_many :promotion_rules, :foreign_key => :activator_id, :autosave => true, :dependent => :destroy - alias_method :rules, :promotion_rules - accepts_nested_attributes_for :promotion_rules - - has_many :promotion_actions, :foreign_key => :activator_id, :autosave => true, :dependent => :destroy - alias_method :actions, :promotion_actions - accepts_nested_attributes_for :promotion_actions - - validates_associated :rules - - attr_accessible :name, :event_name, :code, :match_policy, - :path, :advertise, :description, :usage_limit, - :starts_at, :expires_at, :promotion_rules_attributes, - :promotion_actions_attributes - - # TODO: This shouldn't be necessary with :autosave option but nested attribute updating of actions is broken without it - after_save :save_rules_and_actions - def save_rules_and_actions - (rules + actions).each &:save - end - - validates :name, :presence => true - validates :code, :presence => true, :if => lambda{|r| r.event_name == 'spree.checkout.coupon_code_added' } - validates :path, :presence => true, :if => lambda{|r| r.event_name == 'spree.content.visited' } - validates :usage_limit, :numericality => { :greater_than => 0, :allow_nil => true } - - def self.advertised - where(:advertise => true) - end - - def activate(payload) - return unless order_activatable? payload[:order] - - if code.present? - event_code = payload[:coupon_code] - return unless event_code == self.code - end - - if path.present? - return unless path == payload[:path] - end - - actions.each do |action| - action.perform(payload) - end - end - - # called anytime order.update! happens - def eligible?(order) - return false if expired? || usage_limit_exceeded?(order) - rules_are_eligible?(order, {}) - end - - def rules_are_eligible?(order, options = {}) - return true if rules.none? - eligible = lambda { |r| r.eligible?(order, options) } - if match_policy == 'all' - rules.all?(&eligible) - else - rules.any?(&eligible) - end - end - - def order_activatable?(order) - order && - created_at.to_i < order.created_at.to_i && - !UNACTIVATABLE_ORDER_STATES.include?(order.state) - end - - # Products assigned to all product rules - def products - @products ||= rules.of_type('Spree::Promotion::Rules::Product').map(&:products).flatten.uniq - end - - def usage_limit_exceeded?(order = nil) - usage_limit.present? && usage_limit > 0 && adjusted_credits_count(order) >= usage_limit - end - - def adjusted_credits_count(order) - return credits_count if order.nil? - credits_count - (order.promotion_credit_exists?(self) ? 1 : 0) - end - - def credits - Adjustment.promotion.where(:originator_id => actions.map(&:id)) - end - - def credits_count - credits.count - end - - def code=(coupon_code) - write_attribute(:code, (coupon_code.downcase.strip rescue nil)) - end - - end -end diff --git a/promo/app/models/spree/promotion/actions/create_adjustment.rb b/promo/app/models/spree/promotion/actions/create_adjustment.rb deleted file mode 100644 index 7135c889f83..00000000000 --- a/promo/app/models/spree/promotion/actions/create_adjustment.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Spree - class Promotion - module Actions - class CreateAdjustment < PromotionAction - calculated_adjustments - - delegate :eligible?, :to => :promotion - - before_validation :ensure_action_has_calculator - - def perform(options = {}) - return unless order = options[:order] - # Nothing to do if the promotion is already associated with the order - return if order.promotion_credit_exists?(promotion) - - order.adjustments.promotion.reload.clear - order.update! - create_adjustment("#{I18n.t(:promotion)} (#{promotion.name})", order, order) - order.update! - end - - # override of CalculatedAdjustments#create_adjustment so promotional - # adjustments are added all the time. They will get their eligability - # set to false if the amount is 0 - def create_adjustment(label, target, calculable, mandatory=false) - amount = compute_amount(calculable) - params = { :amount => amount, - :source => calculable, - :originator => self, - :label => label, - :mandatory => mandatory } - target.adjustments.create(params, :without_protection => true) - end - - # Ensure a negative amount which does not exceed the sum of the order's item_total and ship_total - def compute_amount(calculable) - [(calculable.item_total + calculable.ship_total), super.to_f.abs].min * -1 - end - - private - def ensure_action_has_calculator - return if self.calculator - self.calculator = Calculator::FlatPercentItemTotal.new - end - end - end - end -end diff --git a/promo/app/models/spree/promotion/actions/create_line_items.rb b/promo/app/models/spree/promotion/actions/create_line_items.rb deleted file mode 100644 index 3cc107d1f08..00000000000 --- a/promo/app/models/spree/promotion/actions/create_line_items.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Spree - class Promotion - module Actions - class CreateLineItems < PromotionAction - has_many :promotion_action_line_items, :foreign_key => :promotion_action_id - accepts_nested_attributes_for :promotion_action_line_items - attr_accessible :promotion_action_line_items_attributes - - - def perform(options = {}) - return unless order = options[:order] - promotion_action_line_items.each do |item| - current_quantity = order.quantity_of(item.variant) - if current_quantity < item.quantity - order.add_variant(item.variant, item.quantity - current_quantity) - order.update! - end - end - end - end - end - end -end diff --git a/promo/app/models/spree/promotion/rules/first_order.rb b/promo/app/models/spree/promotion/rules/first_order.rb deleted file mode 100644 index 910d2952798..00000000000 --- a/promo/app/models/spree/promotion/rules/first_order.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Spree - class Promotion - module Rules - class FirstOrder < PromotionRule - def eligible?(order, options = {}) - user = order.try(:user) || options[:user] - !!(user && user.orders.complete.count == 0) - end - end - end - end -end diff --git a/promo/app/models/spree/promotion/rules/item_total.rb b/promo/app/models/spree/promotion/rules/item_total.rb deleted file mode 100644 index bd47243c53c..00000000000 --- a/promo/app/models/spree/promotion/rules/item_total.rb +++ /dev/null @@ -1,21 +0,0 @@ -# A rule to apply to an order greater than (or greater than or equal to) -# a specific amount -module Spree - class Promotion - module Rules - class ItemTotal < PromotionRule - preference :amount, :decimal, :default => 100.00 - preference :operator, :string, :default => '>' - - attr_accessible :preferred_amount, :preferred_operator - - OPERATORS = ['gt', 'gte'] - - def eligible?(order, options = {}) - item_total = order.line_items.map(&:amount).sum - item_total.send(preferred_operator == 'gte' ? :>= : :>, BigDecimal.new(preferred_amount.to_s)) - end - end - end - end -end diff --git a/promo/app/models/spree/promotion/rules/product.rb b/promo/app/models/spree/promotion/rules/product.rb deleted file mode 100644 index f4f8a485974..00000000000 --- a/promo/app/models/spree/promotion/rules/product.rb +++ /dev/null @@ -1,46 +0,0 @@ -# A rule to limit a promotion based on products in the order. -# Can require all or any of the products to be present. -# Valid products either come from assigned product group or are assingned directly to the rule. -module Spree - class Promotion - module Rules - class Product < PromotionRule - has_and_belongs_to_many :products, :class_name => '::Spree::Product', :join_table => 'spree_products_promotion_rules', :foreign_key => 'promotion_rule_id' - validate :only_one_promotion_per_product - - MATCH_POLICIES = %w(any all) - preference :match_policy, :string, :default => MATCH_POLICIES.first - - # scope/association that is used to test eligibility - def eligible_products - products - end - - def eligible?(order, options = {}) - return true if eligible_products.empty? - if preferred_match_policy == 'all' - eligible_products.all? {|p| order.products.include?(p) } - else - order.products.any? {|p| eligible_products.include?(p) } - end - end - - def product_ids_string - product_ids.join(',') - end - - def product_ids_string=(s) - self.product_ids = s.to_s.split(',').map(&:strip) - end - - private - - def only_one_promotion_per_product - if Spree::Promotion::Rules::Product.all.map(&:products).flatten.uniq! - errors[:base] << "You can't create two promotions for the same product" - end - end - end - end - end -end diff --git a/promo/app/models/spree/promotion/rules/user.rb b/promo/app/models/spree/promotion/rules/user.rb deleted file mode 100644 index 482ebabe0b0..00000000000 --- a/promo/app/models/spree/promotion/rules/user.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Spree - class Promotion - module Rules - class User < PromotionRule - attr_accessible :user_ids_string - - belongs_to :user, :class_name => Spree.user_class.to_s - has_and_belongs_to_many :users, :class_name => Spree.user_class.to_s, :join_table => 'spree_promotion_rules_users', :foreign_key => 'promotion_rule_id' - - def eligible?(order, options = {}) - users.none? or users.include?(order.user) - end - - def user_ids_string - user_ids.join(',') - end - - def user_ids_string=(s) - self.user_ids = s.to_s.split(',').map(&:strip) - end - end - end - end -end diff --git a/promo/app/models/spree/promotion/rules/user_logged_in.rb b/promo/app/models/spree/promotion/rules/user_logged_in.rb deleted file mode 100644 index 9c54ff29b61..00000000000 --- a/promo/app/models/spree/promotion/rules/user_logged_in.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Spree - class Promotion - module Rules - class UserLoggedIn < PromotionRule - - def eligible?(order, options = {}) - # this is tricky. We couldn't use any of the devise methods since we aren't in the controller. - # we need to rely on the controller already having done this for us. - - # The thinking is that the controller should have some sense of what state - # we should be in before firing events, - # so the controller will have to set this field. - - return options && options[:user_signed_in] - end - - end - end - end -end diff --git a/promo/app/models/spree/promotion_action.rb b/promo/app/models/spree/promotion_action.rb deleted file mode 100644 index 7487da0b196..00000000000 --- a/promo/app/models/spree/promotion_action.rb +++ /dev/null @@ -1,19 +0,0 @@ -# Base class for all types of promotion action. -# PromotionActions perform the necessary tasks when a promotion is activated by an event and determined to be eligible. -module Spree - class PromotionAction < ActiveRecord::Base - belongs_to :promotion, :foreign_key => 'activator_id', :class_name => "Spree::Promotion" - - scope :of_type, lambda {|t| {:conditions => {:type => t}}} - - attr_accessible :line_items_string - - # This method should be overriden in subclass - # Updates the state of the order or performs some other action depending on the subclass - # options will contain the payload from the event that activated the promotion. This will include - # the key :user which allows user based actions to be performed in addition to actions on the order - def perform(options = {}) - raise 'perform should be implemented in a sub-class of PromotionAction' - end - end -end diff --git a/promo/app/models/spree/promotion_action_line_item.rb b/promo/app/models/spree/promotion_action_line_item.rb deleted file mode 100644 index 689a7266553..00000000000 --- a/promo/app/models/spree/promotion_action_line_item.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Spree - class PromotionActionLineItem < ActiveRecord::Base - belongs_to :promotion_action, :class_name => 'Spree::Promotion::Actions::CreateLineItems' - belongs_to :variant, :class_name => "Spree::Variant" - - attr_accessible :quantity, :variant_id - end -end diff --git a/promo/app/models/spree/promotion_rule.rb b/promo/app/models/spree/promotion_rule.rb deleted file mode 100644 index 7cfad073319..00000000000 --- a/promo/app/models/spree/promotion_rule.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Base class for all promotion rules -module Spree - class PromotionRule < ActiveRecord::Base - belongs_to :promotion, :foreign_key => 'activator_id', :class_name => "Spree::Promotion" - - scope :of_type, lambda {|t| {:conditions => {:type => t}}} - - validate :promotion, :presence => true - validate :unique_per_activator, :on => :create - - attr_accessible :preferred_operator, :preferred_amount, :product, :product_ids_string, :preferred_match_policy - - def eligible?(order, options = {}) - raise 'eligible? should be implemented in a sub-class of Promotion::PromotionRule' - end - - private - def unique_per_activator - if Spree::PromotionRule.exists?(:activator_id => activator_id, :type => self.class.name) - errors[:base] << "Promotion already contains this rule type" - end - end - - end -end diff --git a/promo/app/overrides/promo_admin_tabs.rb b/promo/app/overrides/promo_admin_tabs.rb deleted file mode 100644 index f846365ed7b..00000000000 --- a/promo/app/overrides/promo_admin_tabs.rb +++ /dev/null @@ -1,6 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/layouts/admin", - :name => "promo_admin_tabs", - :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]", - :text => "<%= tab(:promotions, :url => spree.admin_promotions_path, :icon => 'icon-bullhorn') %>", - :disabled => false, - :original => '3e847740dc3e7f924aba1ccb4cb00c7b841649e3') diff --git a/promo/app/overrides/promo_cart_coupon_code_field.rb b/promo/app/overrides/promo_cart_coupon_code_field.rb deleted file mode 100644 index 0bb04d03449..00000000000 --- a/promo/app/overrides/promo_cart_coupon_code_field.rb +++ /dev/null @@ -1,6 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/orders/edit", - :name => "promo_cart_coupon_code_field", - :insert_after => "[data-hook='cart_buttons']", - :partial => "spree/orders/coupon_code_field", - :disabled => false, - :original => "c11d9a1996fb86e992aba19035074cf5f688dea2") diff --git a/promo/app/overrides/promo_coupon_code_field.rb b/promo/app/overrides/promo_coupon_code_field.rb deleted file mode 100644 index f7500a60f53..00000000000 --- a/promo/app/overrides/promo_coupon_code_field.rb +++ /dev/null @@ -1,6 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/checkout/_payment", - :name => "promo_coupon_code_field", - :replace => "[data-hook='coupon_code_field'], #coupon_code_field[data-hook]", - :partial => "spree/checkout/coupon_code_field", - :disabled => false, - :original => '9c9f7058eb6fd9236a241621ab53b43e1caa1a0b' ) diff --git a/promo/app/overrides/promo_product_properties.rb b/promo/app/overrides/promo_product_properties.rb deleted file mode 100644 index f507f241bb6..00000000000 --- a/promo/app/overrides/promo_product_properties.rb +++ /dev/null @@ -1,6 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/products/show", - :name => "promo_product_properties", - :insert_after => "[data-hook='product_properties'], #product_properties[data-hook]", - :partial => "spree/products/promotions", - :disabled => false, - :original => '21a1d0ddb6ae24042f130d64f0ad4b90e69cd088') diff --git a/promo/app/views/spree/admin/promotion_actions/create.js.erb b/promo/app/views/spree/admin/promotion_actions/create.js.erb deleted file mode 100644 index be3418a86cb..00000000000 --- a/promo/app/views/spree/admin/promotion_actions/create.js.erb +++ /dev/null @@ -1,12 +0,0 @@ -$('#actions').append('<%= escape_javascript( render(:partial => 'spree/admin/promotions/promotion_action', :object => @promotion_action) ) %>'); -$('#actions .no-objects-found').hide(); -initProductActions(); - -<% unless Rails.env.test? %> - $('.type-select.select2').select2(); -<% end %> - -$('#<%= dom_id @promotion_action %>').hide(); -$('#<%= dom_id @promotion_action %>').fadeIn(); -$('#<%= dom_id @promotion_action %> .tokeninput.products').productPicker(); -$('.product_autocomplete').product_autocomplete(); \ No newline at end of file diff --git a/promo/app/views/spree/admin/promotion_rules/create.js.erb b/promo/app/views/spree/admin/promotion_rules/create.js.erb deleted file mode 100644 index 5f3dc601e17..00000000000 --- a/promo/app/views/spree/admin/promotion_rules/create.js.erb +++ /dev/null @@ -1,13 +0,0 @@ -$('#rules').append('<%= escape_javascript( render(:partial => 'spree/admin/promotions/promotion_rule', :object => @promotion_rule) ) %>'); -$('#rules .no-objects-found').hide(); - -$('#<%= dom_id @promotion_rule %>').hide(); -$('#<%= dom_id @promotion_rule %>').fadeIn(); -$('#<%= dom_id @promotion_rule %> .tokeninput.products').productPicker(); -$('#<%= dom_id @promotion_rule %> .tokeninput.users').userPicker(); - -$('#promotion_rule_type').html('<%= escape_javascript options_for_promotion_rule_types(@promotion) %>'); - -<% unless Rails.env.test? %> - $('.select_item_total.select2, .select_product.select2').select2(); -<% end %> \ No newline at end of file diff --git a/promo/app/views/spree/admin/promotions/_actions.html.erb b/promo/app/views/spree/admin/promotions/_actions.html.erb deleted file mode 100644 index 069e45e7896..00000000000 --- a/promo/app/views/spree/admin/promotions/_actions.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -
          - - <%= form_tag spree.admin_promotion_promotion_actions_path(@promotion), :remote => true, :id => 'new_promotion_action_form' do %> - <% options = options_for_select( Rails.application.config.spree.promotions.actions.map(&:name).map {|name| [ t("promotion_action_types.#{name.demodulize.underscore}.name"), name] } ) %> -
          - <%= t(:promotion_actions) %> -
          - <%= label_tag :action_type, t(:add_action_of_type)%> - <%= select_tag 'action_type', options, :class => 'select2 fullwidth' %> -
          -
          - <%= button t(:add), 'icon-plus', :class => 'fullwidth' %> -
          -
          - <% end %> - - <%= form_for @promotion, :url => spree.admin_promotion_path(@promotion), :method => :put do |f| %> -
          - <% if @promotion.actions.any? %> - <%= render :partial => 'promotion_action', :collection => @promotion.actions %> - <% else %> -
          - <%= t(:no_actions_added) %> -
          - <% end %> -
          -
          - <%= button t(:update), 'icon-refresh' %> -
          - <% end %> - -
          diff --git a/promo/app/views/spree/admin/promotions/_form.html.erb b/promo/app/views/spree/admin/promotions/_form.html.erb deleted file mode 100644 index de5cefab70c..00000000000 --- a/promo/app/views/spree/admin/promotions/_form.html.erb +++ /dev/null @@ -1,56 +0,0 @@ -<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @promotion } %> -
          -
          -
          - <%= f.field_container :name do %> - <%= f.label :name %> - <%= f.text_field :name, :class => 'fullwidth' %> - <% end %> - - <%= f.field_container :event_name do %> - <%= f.label :event_name %> - <%= f.select :event_name, Spree::Activator.event_names.map{|name| [t("events.#{name}"), name] }, {}, {:class => 'select2 fullwidth'} %> - <% end %> - - <%= f.field_container :code do %> - <%= f.label :code %> - <%= f.text_field :code, :class => 'fullwidth' %> - <% end %> - - <%= f.field_container :path do %> - <%= f.label :path %> - <%= f.text_field :path, :class => 'fullwidth' %> - <% end %> - - <%= f.field_container :advertise do %> - <%= f.check_box :advertise %> - <%= f.label :advertise %> - <% end %> -
          - -
          - <%= f.field_container :description do %> - <%= f.label :description %>
          - <%= f.text_area :description, :rows => 7, :class => 'fullwidth' %> - <% end %> -
          -
          - -
          - <%= f.field_container :usage_limit do %> - <%= f.label :usage_limit %>
          - <%= f.number_field :usage_limit, :min => 0, :class => 'fullwidth' %>
          - <%= t(:current_promotion_usage, :count => @promotion.credits_count) %> - <% end %> - -
          - <%= f.label :starts_at %> - <%= f.text_field :starts_at, :class => 'datepicker datepicker-from fullwidth' %> -
          - -
          - <%= f.label :expires_at %> - <%= f.text_field :expires_at, :class => 'datepicker datepicker-top fullwidth' %> -
          -
          -
          diff --git a/promo/app/views/spree/admin/promotions/_promotion_action.html.erb b/promo/app/views/spree/admin/promotions/_promotion_action.html.erb deleted file mode 100644 index b38cec40912..00000000000 --- a/promo/app/views/spree/admin/promotions/_promotion_action.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -
          - <% type_name = promotion_action.class.name.demodulize.underscore %> -
          <%= t("promotion_action_types.#{type_name}.description") %>
          - <%= link_to_with_icon 'icon-trash', '', spree.admin_promotion_promotion_action_path(@promotion, promotion_action), :remote => true, :method => :delete, :class => 'delete' %> - - <% param_prefix = "promotion[promotion_actions_attributes][#{promotion_action.id}]" %> - <%= hidden_field_tag "#{param_prefix}[id]", promotion_action.id %> - - <%= render :partial => "spree/admin/promotions/actions/#{type_name}", - :locals => { :promotion_action => promotion_action, :param_prefix => param_prefix } %> -
          \ No newline at end of file diff --git a/promo/app/views/spree/admin/promotions/_promotion_rule.html.erb b/promo/app/views/spree/admin/promotions/_promotion_rule.html.erb deleted file mode 100644 index ee6ecaafbc4..00000000000 --- a/promo/app/views/spree/admin/promotions/_promotion_rule.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -
          - <% type_name = promotion_rule.class.name.demodulize.underscore %> -
          '><%= t("promotion_rule_types.#{type_name}.description") %>
          - <%= link_to_with_icon 'icon-trash', '', spree.admin_promotion_promotion_rule_path(@promotion, promotion_rule), :remote => true, :method => :delete, :class => 'delete' %> - - <% param_prefix = "promotion[promotion_rules_attributes][#{promotion_rule.id}]" %> - <%= hidden_field_tag "#{param_prefix}[id]", promotion_rule.id %> - <%= render :partial => "spree/admin/promotions/rules/#{type_name}", :locals => { :promotion_rule => promotion_rule, :param_prefix => param_prefix } %> -
          diff --git a/promo/app/views/spree/admin/promotions/actions/_create_adjustment.html.erb b/promo/app/views/spree/admin/promotions/actions/_create_adjustment.html.erb deleted file mode 100644 index 3094bb1ef4f..00000000000 --- a/promo/app/views/spree/admin/promotions/actions/_create_adjustment.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -
          - -
          - <% field_name = "#{param_prefix}[calculator_type]" %> - <%= label_tag field_name, t(:calculator) %> - <%= select_tag field_name, - options_from_collection_for_select(@calculators, :to_s, :description, promotion_action.calculator.type), - :class => 'type-select select2 fullwidth' %> - <% if promotion_action.calculator.respond_to?(:preferences) %> - <%= t(:calculator_settings_warning) %> - <% end %> -
          - - <% unless promotion_action.new_record? %> -
          - <% promotion_action.calculator.preferences.keys.map do |key| %> - <% field_name = "#{param_prefix}[calculator_attributes][preferred_#{key}]" %> - <%= label_tag field_name, t(key.to_s) %> - <%= preference_field_tag(field_name, - promotion_action.calculator.get_preference(key), - :type => promotion_action.calculator.preference_type(key)) %> - <% end %> - <%= hidden_field_tag "#{param_prefix}[calculator_attributes][id]", promotion_action.calculator.id %> -
          - <% end %> -
          diff --git a/promo/app/views/spree/admin/promotions/actions/_create_line_items.html.erb b/promo/app/views/spree/admin/promotions/actions/_create_line_items.html.erb deleted file mode 100644 index a403152dbfb..00000000000 --- a/promo/app/views/spree/admin/promotions/actions/_create_line_items.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<% promotion_action.promotion_action_line_items.each do |item| %> - <%= item.quantity %> x <%= item.variant.product.name %> - <%= item.variant.options_text %> -<% end %> - -<% if promotion_action.promotion_action_line_items.empty? %> - <% line_items = promotion_action.promotion_action_line_items %> - <% line_items.build %> - - <% line_items.each_with_index do |line_item, index| %> -
          -
          - <% line_item_prefix = "#{param_prefix}[promotion_action_line_items_attributes][#{index}]" %> - <%= hidden_field_tag "#{line_item_prefix}[variant_id]", line_item.variant_id, :class => "variant_autocomplete fullwidth" %> -
          -
          -
          - <%= number_field_tag "#{line_item_prefix}[quantity]", line_item.quantity, :min => 1, :class => 'fullwidth' %> -
          -
          - <% end %> -<% end %> diff --git a/promo/app/views/spree/admin/promotions/edit.html.erb b/promo/app/views/spree/admin/promotions/edit.html.erb deleted file mode 100644 index 21b27ac422b..00000000000 --- a/promo/app/views/spree/admin/promotions/edit.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<% content_for :page_title do %> - <%= t(:editing_promotion) %> -<% end %> - -<% content_for :page_actions do %> -
        • - <%= button_link_to t(:back_to_promotions_list), admin_promotions_path, :icon => 'icon-arrow-left' %> -
        • -<% end %> - -<%= form_for @promotion, :url => object_url, :method => :put do |f| %> -
          - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/edit_resource_links' %> -
          -<% end %> - -
          -
          - <%= render :partial => 'rules' %> -
          - -
          - <%= render :partial => 'actions' %> -
          -
          - -<%= render :partial => "spree/admin/variants/autocomplete", :formats => [:js] %> diff --git a/promo/app/views/spree/admin/promotions/index.html.erb b/promo/app/views/spree/admin/promotions/index.html.erb deleted file mode 100644 index 59822a49412..00000000000 --- a/promo/app/views/spree/admin/promotions/index.html.erb +++ /dev/null @@ -1,52 +0,0 @@ -<% content_for :page_title do %> - <%= t(:promotions) %> -<% end %> - -<% content_for :page_actions do %> -
        • - <%= button_link_to t(:new_promotion), spree.new_admin_promotion_path, :icon => 'icon-plus' %> -
        • -<% end %> - -<% unless @promotions.any? %> -
          - <%= t(:no_promotions_found) %>. - <%= link_to t(:add_one), spree.new_admin_promotion_path %>! -
          -<% else %> - - - - - - - - - - - - - - - - - - - - - <% @promotions.each do |promotion| %> - - - - - - - - - <% end %> - -
          <%= t(:name) %><%= t(:code) %><%= t(:description) %><%= t(:usage_limit) %><%= t(:expiration) %>
          <%= promotion.name %><%= promotion.code %><%= promotion.description %><%= promotion.usage_limit %><%= promotion.expires_at.to_date.to_s(:short_date) if promotion.expires_at %> - <%= link_to_edit promotion, :no_text => true %> - <%= link_to_delete promotion, :no_text => true %> -
          -<% end %> \ No newline at end of file diff --git a/promo/app/views/spree/admin/promotions/new.html.erb b/promo/app/views/spree/admin/promotions/new.html.erb deleted file mode 100644 index d1b3fdedbf1..00000000000 --- a/promo/app/views/spree/admin/promotions/new.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<% content_for :page_title do %> - <%= t(:new_promotion) %> -<% end %> - -<% content_for :page_actions do %> -
        • - <%= button_link_to t(:back_to_promotions_list), spree.admin_promotions_path, :icon => 'icon-arrow-left' %> -
        • -<% end %> - -<%= form_for :promotion, :url => collection_url do |f| %> -
          - <%= render :partial => 'form', :locals => { :f => f } %> - <%= render :partial => 'spree/admin/shared/new_resource_links' %> -
          -<% end %> diff --git a/promo/app/views/spree/admin/promotions/rules/_item_total.html.erb b/promo/app/views/spree/admin/promotions/rules/_item_total.html.erb deleted file mode 100644 index 56d6c53b1fb..00000000000 --- a/promo/app/views/spree/admin/promotions/rules/_item_total.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -
          - <%= select_tag "#{param_prefix}[preferred_operator]", options_for_select(Spree::Promotion::Rules::ItemTotal::OPERATORS.map{|o| [t("item_total_rule.operators.#{o}"),o]}, promotion_rule.preferred_operator), {:class => 'select2 select_item_total fullwidth'} %> -
          -
          - <%= text_field_tag "#{param_prefix}[preferred_amount]", promotion_rule.preferred_amount, :class => 'fullwidth' %> -
          diff --git a/promo/app/views/spree/admin/promotions/rules/_landing_page.html.erb b/promo/app/views/spree/admin/promotions/rules/_landing_page.html.erb deleted file mode 100644 index 003d8a53e11..00000000000 --- a/promo/app/views/spree/admin/promotions/rules/_landing_page.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
          - - <%= text_field_tag "#{param_prefix}[preferred_path]", promotion_rule.preferred_path, :class => 'fullwidth' %> - <%= t('landing_page_rule.must_have_visited_path') %> -
          \ No newline at end of file diff --git a/promo/app/views/spree/admin/promotions/rules/_product.html.erb b/promo/app/views/spree/admin/promotions/rules/_product.html.erb deleted file mode 100644 index 2a4758f8a03..00000000000 --- a/promo/app/views/spree/admin/promotions/rules/_product.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -
          - <%= label_tag t('product_rule.choose_products') %> - <%= product_picker_field "#{param_prefix}[product_ids_string]", promotion_rule.product_ids_string %> -
          -
          - -
          diff --git a/promo/app/views/spree/checkout/_coupon_code_field.html.erb b/promo/app/views/spree/checkout/_coupon_code_field.html.erb deleted file mode 100644 index 5b0f3aec58f..00000000000 --- a/promo/app/views/spree/checkout/_coupon_code_field.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<% if Spree::Promotion.count > 0 %> -

          - <%= form.label :coupon_code %>
          - <%= form.text_field :coupon_code %> -

          -<% end %> diff --git a/promo/app/views/spree/orders/_coupon_code_field.html.erb b/promo/app/views/spree/orders/_coupon_code_field.html.erb deleted file mode 100644 index 7b149a9da51..00000000000 --- a/promo/app/views/spree/orders/_coupon_code_field.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<% if Spree::Promotion.count > 0 %> -
          - <%= order_form.label :coupon_code %>
          - <%= order_form.text_field :coupon_code, :size => 10 %> - <%= order_form.submit I18n.t(:apply) %> -
          -<% end %> diff --git a/promo/app/views/spree/products/_promotions.html.erb b/promo/app/views/spree/products/_promotions.html.erb deleted file mode 100644 index c5cf85c9eab..00000000000 --- a/promo/app/views/spree/products/_promotions.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -<% promotions = @product.possible_promotions %> -<% if promotions.any? %> -
          -

          <%= t(:promotions) %>

          - - <% promotions.each do |promotion| %> -
          -

          <%= promotion.name %>

          -

          <%= promotion.description %>

          - <% if promotion.products.any? %> -
            - <% promotion.products.each do |product| %> -
          • <%= link_to product.name, product_path(product) %>
          • - <% end %> -
          - <% end %> -
          - <% end %> - -
          -<% end %> - - diff --git a/promo/config/locales/en.yml b/promo/config/locales/en.yml deleted file mode 100644 index 45de94b3541..00000000000 --- a/promo/config/locales/en.yml +++ /dev/null @@ -1,98 +0,0 @@ ---- -en: - activerecord: - attributes: - spree/promotion: - advertise: Advertise - code: Code - description: Description - event_name: Event Name - expires_at: Expires At - name: Name - path: Path - starts_at: Starts At - usage_limit: Usage Limit - add_action_of_type: Add action of type - add_rule_of_type: Add rule of type - back_to_promotions_list: "Back To Promotions List" - coupon: Coupon - coupon_code: Coupon code - coupon_code_applied: The coupon code was successfully applied to your order. - coupon_code_expired: The coupon code is expired - coupon_code_already_applied: The coupon code has already been applied to this order - coupon_code_better_exists: The previously applied coupon code results in a better deal - coupon_code_not_found: The coupon code you entered doesn't exist. Please try again. - coupon_code_max_usage: Coupon code usage limit exceeded - coupon_code_not_eligible: This coupon code is not eligible for this order - editing_promotion: Editing Promotion - current_promotion_usage: 'Current Usage: %{count}' - events: - spree: - checkout: - coupon_code_added: Coupon code added - content: - visited: Visit static content page - expiry: Expiry - free_shipping: Free Shipping - item_total_rule: - operators: - gt: greater than - gte: greater than or equal to - landing_page_rule: - path: Path - new_promotion: New Promotion - no_rules_added: No rules added - percent_per_item: Percent Per Item - product_rule: - choose_products: Choose products - label: "Order must contain %{select} of these products" - match_any: at least one - match_all: all - product_source: - group: From product group - manual: Manually choose - promotion: Promotion - promotion_action: Promotion Action - promotion_actions: Actions - promotion_action_types: - create_adjustment: - name: Create adjustment - description: Creates a promotion credit adjustment on the order - create_line_items: - name: Create line items - description: Populates the cart with the specified quantity of variant - give_store_credit: - name: Give store credit - description: Gives the user store credit of the amount specified - promotion_form: - match_policies: - all: Match all of these rules - any: Match any of these rules - promotions: Promotions - promotions_description: Manage offers and coupons with promotions - promotion_rule: Promotion Rule - promotion_rule_types: - first_order: - name: First order - description: "Must be the customer's first order" - item_total: - name: Item total - description: Order total meets these criteria - landing_page: - name: Landing Page - description: Customer must have visited the specified page - product: - name: Product(s) - description: Order includes specified product(s) - user: - name: User - description: Available only to the specified users - user_logged_in: - name: User Logged In - description: Available only to logged in users - rules: Rules - spree/order: - coupon_code: Coupon Code - user_rule: - choose_users: Choose users - diff --git a/promo/config/routes.rb b/promo/config/routes.rb deleted file mode 100644 index 9f2d4b08414..00000000000 --- a/promo/config/routes.rb +++ /dev/null @@ -1,8 +0,0 @@ -Spree::Core::Engine.routes.prepend do - namespace :admin do - resources :promotions do - resources :promotion_rules - resources :promotion_actions - end - end -end diff --git a/promo/lib/spree/promo.rb b/promo/lib/spree/promo.rb deleted file mode 100644 index b003268eedc..00000000000 --- a/promo/lib/spree/promo.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'spree/core' - -module Spree - module Promo - - end -end - -require 'spree/promo/engine' diff --git a/promo/lib/spree/promo/engine.rb b/promo/lib/spree/promo/engine.rb deleted file mode 100644 index ddd2b97d600..00000000000 --- a/promo/lib/spree/promo/engine.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Spree - module Promo - class Engine < Rails::Engine - isolate_namespace Spree - engine_name 'spree_promo' - - def self.activate - Dir.glob(File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')) do |c| - Rails.configuration.cache_classes ? require(c) : load(c) - end - - Spree::StoreController.class_eval do - # Include list of visited paths in notification payload hash - def default_notification_payload - { :user => try_spree_current_user, :order => current_order, :visited_paths => session[:visited_paths] } - end - end - end - - config.autoload_paths += %W(#{config.root}/lib) - config.to_prepare &method(:activate).to_proc - - initializer 'spree.promo.environment', :after => 'spree.environment' do |app| - app.config.spree.add_class('promotions') - app.config.spree.promotions = Spree::Promo::Environment.new - end - - initializer 'spree.promo.register.promotion.calculators' do |app| - app.config.spree.calculators.add_class('promotion_actions_create_adjustments') - app.config.spree.calculators.promotion_actions_create_adjustments = [ - Spree::Calculator::FlatPercentItemTotal, - Spree::Calculator::FlatRate, - Spree::Calculator::FlexiRate, - Spree::Calculator::PerItem, - Spree::Calculator::PercentPerItem, - Spree::Calculator::FreeShipping - ] - end - - initializer 'spree.promo.register.promotions.rules' do |app| - app.config.spree.promotions.rules = [ - Spree::Promotion::Rules::ItemTotal, - Spree::Promotion::Rules::Product, - Spree::Promotion::Rules::User, - Spree::Promotion::Rules::FirstOrder, - Spree::Promotion::Rules::UserLoggedIn] - end - - initializer 'spree.promo.register.promotions.actions' do |app| - app.config.spree.promotions.actions = [Spree::Promotion::Actions::CreateAdjustment, - Spree::Promotion::Actions::CreateLineItems] - end - - end - end -end diff --git a/promo/lib/spree_promo.rb b/promo/lib/spree_promo.rb deleted file mode 100644 index 808428c3f9b..00000000000 --- a/promo/lib/spree_promo.rb +++ /dev/null @@ -1 +0,0 @@ -require 'spree/promo' diff --git a/promo/script/rails b/promo/script/rails deleted file mode 100755 index 0b5d05e64a1..00000000000 --- a/promo/script/rails +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -ENGINE_PATH = File.expand_path('../..', __FILE__) -load File.expand_path('../../spec/dummy/script/rails', __FILE__) diff --git a/promo/spec/controllers/spree/content_controller_spec.rb b/promo/spec/controllers/spree/content_controller_spec.rb deleted file mode 100644 index f6c446b1404..00000000000 --- a/promo/spec/controllers/spree/content_controller_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Spree::ContentController do - before :each do - controller.stub :spree_current_user => create(:user) - end - - it "fires event for #show" do - # we are using cvv because the file exists in core/views/content - controller.should_receive(:fire_event). - with('spree.content.visited', hash_including(:path => "content/cvv")) - spree_get :show, :path => "cvv" - end - - it "fires event for content actions like #cvv" do - controller.should_receive(:fire_event). - with('spree.content.visited', hash_including(:path => "content/cvv")) - spree_get :cvv - end - -end diff --git a/promo/spec/controllers/spree/orders_controller_spec.rb b/promo/spec/controllers/spree/orders_controller_spec.rb deleted file mode 100644 index ca0e0ba1065..00000000000 --- a/promo/spec/controllers/spree/orders_controller_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe Spree::OrdersController do - - let(:user) { create(:user) } - let(:order) { user.spree_orders.create } - let(:promotion) do - Spree::Promotion.create({ - :name => "TestPromo", - :code => "TEST1", - :expires_at => 1.day.from_now, - :created_at => 1.day.ago, - :event_name => "spree.checkout.coupon_code_added", - :match_policy => "any" - }, :without_protection => true) - end - - let(:coupon_code) { promotion.code } - let(:invalid_coupon_code) { "12345" } - - before :each do - controller.stub :current_user => user - controller.stub :current_order => order - end - - describe "#update" do - it "renders orders#edit when coupon code is invalid" do - controller.should_not_receive(:fire_event). - with('spree.checkout.coupon_code_added', hash_including(:coupon_code => invalid_coupon_code)) - spree_put :update, :order => { :coupon_code => invalid_coupon_code } - flash[:error].should == I18n.t(:coupon_code_not_found) - response.should render_template :edit - end - - end - -end diff --git a/promo/spec/factories.rb b/promo/spec/factories.rb deleted file mode 100644 index 7b4b3ca1674..00000000000 --- a/promo/spec/factories.rb +++ /dev/null @@ -1,5 +0,0 @@ -FactoryGirl.define do - factory :promotion, :class => Spree::Promotion, :parent => :activator do - name 'Promo' - end -end diff --git a/promo/spec/helpers/spree/promotion_rules_helper_spec.rb b/promo/spec/helpers/spree/promotion_rules_helper_spec.rb deleted file mode 100644 index 5bfe48adf1f..00000000000 --- a/promo/spec/helpers/spree/promotion_rules_helper_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -module Spree - describe PromotionRulesHelper do - it "does not include existing rules in options" do - promotion = Spree::Promotion.new - promotion.promotion_rules << Spree::Promotion::Rules::ItemTotal.new - - options = helper.options_for_promotion_rule_types(promotion) - options.should_not =~ /ItemTotal/ - end - end -end diff --git a/promo/spec/models/calculator/percent_per_item_spec.rb b/promo/spec/models/calculator/percent_per_item_spec.rb deleted file mode 100644 index 75b23e8da91..00000000000 --- a/promo/spec/models/calculator/percent_per_item_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe Spree::Calculator::PercentPerItem do - # Like an order object, but not quite... - let!(:product1) { double("Product") } - let!(:product2) { double("Product") } - let!(:line_items) { [double("LineItem", :quantity => 5, :product => product1, :price => 10), double("LineItem", :quantity => 1, :product => product2, :price => 10)] } - let!(:object) { double("Order", :line_items => line_items) } - - let!(:promotion_calculable) { double("Calculable", :promotion => promotion) } - - let!(:promotion) { double("Promotion", :rules => [double("Rule", :products => [product1])]) } - - let!(:calculator) { Spree::Calculator::PercentPerItem.new(:preferred_percent => 0.25) } - - it "has a translation for description" do - calculator.description.should_not include("translation missing") - calculator.description.should == I18n.t(:percent_per_item) - end - - it "correctly calculates per item promotion" do - calculator.stub(:calculable => promotion_calculable) - calculator.compute(object).to_f.should == 12.5 # 5 x 10 x 0.25 since only product1 is included in the promotion rule - end - - it "returns 0 when no object passed" do - calculator.stub(:calculable => promotion_calculable) - calculator.compute.should == 0 - end - - it "computes on promotion when promotion is present" do - calculator.send(:compute_on_promotion?).should_not be_true - calculator.stub(:calculable => promotion_calculable) - calculator.send(:compute_on_promotion?).should be_true - end - -end diff --git a/promo/spec/models/order_spec.rb b/promo/spec/models/order_spec.rb deleted file mode 100644 index 8c313e32750..00000000000 --- a/promo/spec/models/order_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe Spree::Order do - - let(:order) { create(:order) } - let(:updater) { Spree::OrderUpdater.new(order) } - - context "#update_adjustments" do - let(:originator) do - originator = Spree::Promotion::Actions::CreateAdjustment.create - calculator = Spree::Calculator::PerItem.create({:calculable => originator}, :without_protection => true) - originator.calculator = calculator - originator.save - originator - end - - def create_adjustment(label, amount) - create(:adjustment, :adjustable => order, - :originator => originator, - :amount => amount, - :locked => true, - :label => label) - end - - it "should make all but the most valuable promotion adjustment ineligible, leaving non promotion adjustments alone" do - create_adjustment("Promotion A", -100) - create_adjustment("Promotion B", -200) - create_adjustment("Promotion C", -300) - create(:adjustment, :adjustable => order, - :originator => nil, - :amount => -500, - :locked => true, - :label => "Some other credit") - order.adjustments.each {|a| a.update_attribute_without_callbacks(:eligible, true)} - - updater.update_adjustments - - order.adjustments.eligible.promotion.count.should == 1 - order.adjustments.eligible.promotion.first.label.should == 'Promotion C' - end - - it "should only leave one adjustment even if 2 have the same amount" do - create_adjustment("Promotion A", -100) - create_adjustment("Promotion B", -200) - create_adjustment("Promotion C", -200) - - updater.update_adjustments - - order.adjustments.eligible.promotion.count.should == 1 - order.adjustments.eligible.promotion.first.amount.to_i.should == -200 - end - - it "should only include eligible adjustments in promo_total" do - create_adjustment("Promotion A", -100) - create(:adjustment, :adjustable => order, - :originator => nil, - :amount => -1000, - :locked => true, - :eligible => false, - :label => 'Bad promo') - - order.promo_total.to_f.should == -100.to_f - end - end - -end - diff --git a/promo/spec/models/payment_spec.rb b/promo/spec/models/payment_spec.rb deleted file mode 100644 index 69bb7869777..00000000000 --- a/promo/spec/models/payment_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -module Spree - describe Payment do - - # Regression test for feature introduced in #1956 - # Previous implementation caused it to stack level too deep - it "does not stack level too deep when asked for gateway options" do - order = stub_model(Order, :promo_total => 1) - payment = stub_model(Payment, :order => order) - - payment.gateway_options[:discount].should == 100 - end - end -end diff --git a/promo/spec/models/promotion/actions/create_adjustment_spec.rb b/promo/spec/models/promotion/actions/create_adjustment_spec.rb deleted file mode 100644 index 1666f4d0932..00000000000 --- a/promo/spec/models/promotion/actions/create_adjustment_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'spec_helper' - -describe Spree::Promotion::Actions::CreateAdjustment do - let(:order) { create(:order) } - let(:promotion) { create(:promotion) } - let(:action) { Spree::Promotion::Actions::CreateAdjustment.new } - - # From promotion spec: - context "#perform" do - - before do - action.calculator = Spree::Calculator::FreeShipping.new - promotion.promotion_actions = [action] - action.stub(:promotion => promotion) - end - - - it "should create a discount with correct negative amount when order is eligible" do - order.stub(:ship_total => 2500, :item_total => 5000, :reload => nil) - promotion.stub(:eligible? => true) - - action.perform(:order => order) - promotion.credits_count.should == 1 - order.adjustments.count.should == 1 - order.adjustments.first.amount.to_i.should == -2500 - end - - it "should not create a discount when order already has one from this promotion" do - order.stub(:ship_total => 5, :item_total => 50, :reload => nil) - promotion.stub(:eligible? => true) - action.calculator.stub(:compute => 2500) - - action.perform(:order => order) - action.perform(:order => order) - promotion.credits_count.should == 1 - end - - end - - context "#compute_amount" do - before do - action.calculator = Spree::Calculator::FreeShipping.new - end - - it "should always return a negative amount" do - order.stub(:item_total => 1000) - action.calculator.stub(:compute => -200) - action.compute_amount(order).to_i.should == -200 - action.calculator.stub(:compute => 300) - action.compute_amount(order).to_i.should == -300 - end - it "should not return an amount that exceeds order's item_total + ship_total" do - order.stub(:item_total => 1000, :ship_total => 100) - action.calculator.stub(:compute => 1000) - action.compute_amount(order).to_i.should == -1000 - action.calculator.stub(:compute => 1100) - action.compute_amount(order).to_i.should == -1100 - action.calculator.stub(:compute => 1200) - action.compute_amount(order).to_i.should == -1100 - end - end - -end - diff --git a/promo/spec/models/promotion/actions/create_line_items_spec.rb b/promo/spec/models/promotion/actions/create_line_items_spec.rb deleted file mode 100644 index cb3af65aa31..00000000000 --- a/promo/spec/models/promotion/actions/create_line_items_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' - -describe Spree::Promotion::Actions::CreateLineItems do - let(:order) { create(:order) } - let(:promotion) { Spree::Promotion.new } - let(:action) { Spree::Promotion::Actions::CreateLineItems.create } - - context "#perform" do - before do - @v1 = create(:variant) - @v2 = create(:variant) - action.promotion_action_line_items.create!({ - :variant => @v1, - :quantity => 1}, :without_protection => true - ) - action.promotion_action_line_items.create!({ - :variant => @v2, - :quantity => 2}, :without_protection => true - ) - end - - it "adds line items to order with correct variant and quantity" do - action.perform(:order => order) - order.line_items.count.should == 2 - line_item = order.line_items.find_by_variant_id(@v1.id) - line_item.should_not be_nil - line_item.quantity.should == 1 - end - - it "only adds the delta of quantity to an order" do - order.add_variant(@v2, 1) - action.perform(:order => order) - line_item = order.line_items.find_by_variant_id(@v2.id) - line_item.should_not be_nil - line_item.quantity.should == 2 - end - - it "doesn't add if the quantity is greater" do - order.add_variant(@v2, 3) - action.perform(:order => order) - line_item = order.line_items.find_by_variant_id(@v2.id) - line_item.should_not be_nil - line_item.quantity.should == 3 - end - end -end - diff --git a/promo/spec/models/promotion/rules/first_order_spec.rb b/promo/spec/models/promotion/rules/first_order_spec.rb deleted file mode 100644 index cd75c8979bb..00000000000 --- a/promo/spec/models/promotion/rules/first_order_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Spree::Promotion::Rules::FirstOrder do - let(:rule) { Spree::Promotion::Rules::FirstOrder.new } - let(:order) { mock_model(Spree::Order, :user => nil) } - - it "should not be eligible without a user" do - rule.should_not be_eligible(order) - end - - context "should be eligible if user does not have any other completed orders yet" do - let(:user) { mock_model(Spree::LegacyUser) } - - before do - user.stub_chain(:orders, :complete, :count => 0) - end - - it "for an order without a user, but with user in payload data" do - rule.should be_eligible(order, :user => user) - end - - it "for an order with a user, no user in payload data" do - order.stub :user => user - rule.should be_eligible(order) - end - end - - it "should be not eligible if user have at least one complete order" do - user = mock_model(Spree::LegacyUser) - user.stub_chain(:orders, :complete, :count => 1) - order.stub(:user => user) - - rule.should_not be_eligible(order) - end -end diff --git a/promo/spec/models/promotion/rules/item_total_spec.rb b/promo/spec/models/promotion/rules/item_total_spec.rb deleted file mode 100644 index 09c1db60eb5..00000000000 --- a/promo/spec/models/promotion/rules/item_total_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'spec_helper' - -describe Spree::Promotion::Rules::ItemTotal do - let(:rule) { Spree::Promotion::Rules::ItemTotal.new } - let(:order) { mock(:order) } - - before { rule.preferred_amount = 50 } - - context "preferred operator set to gt" do - before { rule.preferred_operator = 'gt' } - - it "should be eligible when item total is greater than preferred amount" do - order.stub :line_items => [mock(:line_item, :amount => 30), mock(:line_item, :amount => 21)] - rule.should be_eligible(order) - end - - it "should not be eligible when item total is equal to preferred amount" do - order.stub :line_items => [mock(:line_item, :amount => 30), mock(:line_item, :amount => 20)] - rule.should_not be_eligible(order) - end - - it "should not be eligible when item total is lower than to preferred amount" do - order.stub :line_items => [mock(:line_item, :amount => 30), mock(:line_item, :amount => 19)] - rule.should_not be_eligible(order) - end - end - - context "preferred operator set to gte" do - before { rule.preferred_operator = 'gte' } - - it "should be eligible when item total is greater than preferred amount" do - order.stub :line_items => [mock(:line_item, :amount => 30), mock(:line_item, :amount => 21)] - rule.should be_eligible(order) - end - - it "should be eligible when item total is equal to preferred amount" do - order.stub :line_items => [mock(:line_item, :amount => 30), mock(:line_item, :amount => 20)] - rule.should be_eligible(order) - end - - it "should not be eligible when item total is lower than to preferred amount" do - order.stub :line_items => [mock(:line_item, :amount => 30), mock(:line_item, :amount => 19)] - rule.should_not be_eligible(order) - end - end -end diff --git a/promo/spec/models/promotion/rules/product_spec.rb b/promo/spec/models/promotion/rules/product_spec.rb deleted file mode 100644 index b1c9256d63f..00000000000 --- a/promo/spec/models/promotion/rules/product_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe Spree::Promotion::Rules::Product do - let(:rule) { Spree::Promotion::Rules::Product.new } - - context "#eligible?(order)" do - let(:order) { Spree::Order.new } - - it "should be eligible if there are no products" do - rule.stub(:eligible_products => []) - rule.should be_eligible(order) - end - - before do - 3.times { |i| instance_variable_set("@product#{i}", mock_model(Spree::Product)) } - end - - context "with 'any' match policy" do - before { rule.preferred_match_policy = 'any' } - - it "should be eligible if any of the products is in eligible products" do - order.stub(:products => [@product1, @product2]) - rule.stub(:eligible_products => [@product2, @product3]) - rule.should be_eligible(order) - end - - it "should not be eligible if none of the products is in eligible products" do - order.stub(:products => [@product1]) - rule.stub(:eligible_products => [@product2, @product3]) - rule.should_not be_eligible(order) - end - end - - context "with 'all' match policy" do - before { rule.preferred_match_policy = 'all' } - - it "should be eligible if all of the eligible products are ordered" do - order.stub(:products => [@product3, @product2, @product1]) - rule.stub(:eligible_products => [@product2, @product3]) - rule.should be_eligible(order) - end - - it "should not be eligible if any of the eligible products is not ordered" do - order.stub(:products => [@product1, @product2]) - rule.stub(:eligible_products => [@product1, @product2, @product3]) - rule.should_not be_eligible(order) - end - end - end -end diff --git a/promo/spec/models/promotion/rules/user_spec.rb b/promo/spec/models/promotion/rules/user_spec.rb deleted file mode 100644 index 4f90a87a508..00000000000 --- a/promo/spec/models/promotion/rules/user_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Spree::Promotion::Rules::User do - let(:rule) { Spree::Promotion::Rules::User.new } - - context "#eligible?(order)" do - let(:order) { Spree::Order.new } - - it "should be eligible if users are not provided" do - users = mock("users", :none? => true) - rule.stub(:users => users) - - rule.should be_eligible(order) - end - - it "should be eligible if users include user placing the order" do - user = mock_model(Spree::LegacyUser) - users = [user, mock_model(Spree::LegacyUser)] - users.stub(:none? => false) - rule.stub(:users => users) - order.stub(:user => user) - - rule.should be_eligible(order) - end - - it "should not be eligible if user placing the order is not listed" do - order.stub(:user => mock_model(Spree::LegacyUser)) - users = [mock_model(Spree::LegacyUser), mock_model(Spree::LegacyUser)] - users.stub(:none? => false) - rule.stub(:users => users) - - rule.should_not be_eligible(order) - end - end -end diff --git a/promo/spec/models/promotion_action_spec.rb b/promo/spec/models/promotion_action_spec.rb deleted file mode 100644 index 609e70b35c5..00000000000 --- a/promo/spec/models/promotion_action_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' - -describe Spree::PromotionAction do - - it "should force developer to implement 'perform' method" do - lambda { MyAction.new.perform }.should raise_error - end - -end - diff --git a/promo/spec/models/promotion_rule_spec.rb b/promo/spec/models/promotion_rule_spec.rb deleted file mode 100644 index 4b7762df1a0..00000000000 --- a/promo/spec/models/promotion_rule_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -module Spree - describe PromotionRule do - - class BadTestRule < PromotionRule; end - - class TestRule < PromotionRule - def eligible? - true - end - end - - it "should force developer to implement eligible? method" do - lambda { BadTestRule.new.eligible? }.should raise_error - end - - it "validates unique rules for a promotion" do - p1 = TestRule.new - p1.activator_id = 1 - p1.save - - p2 = TestRule.new - p2.activator_id = 1 - p2.should_not be_valid - end - - end -end diff --git a/promo/spec/models/promotion_spec.rb b/promo/spec/models/promotion_spec.rb deleted file mode 100644 index cb8bedf30dd..00000000000 --- a/promo/spec/models/promotion_spec.rb +++ /dev/null @@ -1,274 +0,0 @@ -require 'spec_helper' - -describe Spree::Promotion do - let(:promotion) { Spree::Promotion.new } - - describe "validations" do - before :each do - @valid_promotion = Spree::Promotion.new :name => "A promotion", - :event_name => 'spree.checkout.coupon_code_added', - :code => 'XXX' - end - - it "valid_promotion is valid" do - @valid_promotion.should be_valid - end - - it "validates the coupon code when event is spree.checkout.coupon_code_added" do - @valid_promotion.code = nil - @valid_promotion.should_not be_valid - end - - it "validates the path when event is spree.content.visited" do - @valid_promotion.event_name = 'spree.content.visited' - @valid_promotion.should_not be_valid - - @valid_promotion.path = 'content/cvv' - @valid_promotion.should be_valid - end - - it "validates usage limit" do - @valid_promotion.usage_limit = -1 - @valid_promotion.should_not be_valid - - @valid_promotion.usage_limit = 100 - @valid_promotion.should be_valid - end - - it "validates name" do - @valid_promotion.name = nil - @valid_promotion.should_not be_valid - end - - end - - describe ".advertised" do - let(:promotion) { create(:promotion) } - let(:advertised_promotion) { create(:promotion, :advertise => true) } - - it "only shows advertised promotions" do - advertised = Spree::Promotion.advertised - advertised.should include(advertised_promotion) - advertised.should_not include(promotion) - end - end - - describe "#delete" do - it "deletes actions" do - p = Spree::Promotion.create(:name => "delete me") - p.actions << Spree::Promotion::Actions::CreateAdjustment.new - p.destroy - - Spree::PromotionAction.count.should == 0 - end - - it "deletes rules" do - p = Spree::Promotion.create(:name => "delete me") - p.rules << Spree::Promotion::Rules::FirstOrder.new - p.destroy - - Spree::PromotionRule.count.should == 0 - end - - end - - describe "#activate" do - before do - @action1 = mock_model(Spree::PromotionAction, :perform => true) - @action2 = mock_model(Spree::PromotionAction, :perform => true) - promotion.promotion_actions = [@action1, @action2] - promotion.created_at = 2.days.ago - - @user = stub_model(Spree::LegacyUser, :email => "spree@example.com") - @order = stub_model(Spree::Order, :user => @user, :created_at => DateTime.now) - @payload = { :order => @order, :user => @user } - end - - it "should check code if present" do - promotion.code = 'xxx' - @payload[:coupon_code] = 'xxx' - @action1.should_receive(:perform).with(@payload) - @action2.should_receive(:perform).with(@payload) - promotion.activate(@payload) - end - - it "should check path if present" do - promotion.path = 'content/cvv' - @payload[:path] = 'content/cvv' - @action1.should_receive(:perform).with(@payload) - @action2.should_receive(:perform).with(@payload) - promotion.activate(@payload) - end - - it "does not perform actions against an order in a finalized state" do - @action1.should_not_receive(:perform).with(@payload) - - @order.state = 'complete' - promotion.activate(@payload) - - @order.state = 'awaiting_return' - promotion.activate(@payload) - - @order.state = 'returned' - promotion.activate(@payload) - end - - it "does not activate if newer then order" do - @action1.should_not_receive(:perform).with(@payload) - promotion.created_at = DateTime.now + 2 - promotion.activate(@payload) - end - end - - context "#usage_limit_exceeded" do - it "should not have its usage limit exceeded" do - promotion.should_not be_usage_limit_exceeded - end - - it "should have its usage limit exceeded" do - promotion.usage_limit = 2 - promotion.stub(:credits_count => 2) - promotion.usage_limit_exceeded?.should == true - - promotion.stub(:credits_count => 3) - promotion.usage_limit_exceeded?.should == true - end - end - - context "#expired" do - it "should not be exipired" do - promotion.should_not be_expired - end - - it "should be expired if it hasn't started yet" do - promotion.starts_at = Time.now + 1.day - promotion.should be_expired - end - - it "should be expired if it has already ended" do - promotion.expires_at = Time.now - 1.day - promotion.should be_expired - end - - it "should not be expired if it has started already" do - promotion.starts_at = Time.now - 1.day - promotion.should_not be_expired - end - - it "should not be expired if it has not ended yet" do - promotion.expires_at = Time.now + 1.day - promotion.should_not be_expired - end - - it "should not be expired if current time is within starts_at and expires_at range" do - promotion.expires_at = Time.now - 1.day - promotion.expires_at = Time.now + 1.day - promotion.should_not be_expired - end - - it "should not be expired if usage limit is not exceeded" do - promotion.usage_limit = 2 - promotion.stub(:credits_count => 1) - promotion.should_not be_expired - end - end - - context "#products" do - context "when it has product rules with products associated" do - let(:promotion) { create(:promotion) } - - before do - promotion_rule = Spree::Promotion::Rules::Product.new - promotion_rule.promotion = promotion - promotion_rule.products << create(:product) - promotion_rule.save - end - - it "should have products" do - promotion.products.size.should == 1 - end - end - end - - context "#eligible?" do - before do - @order = create(:order) - promotion.event_name = 'spree.checkout.coupon_code_added' - promotion.name = "Foo" - promotion.code = "XXX" - calculator = Spree::Calculator::FlatRate.new - action_params = { :promotion => promotion, :calculator => calculator } - @action = Spree::Promotion::Actions::CreateAdjustment.create(action_params, :without_protection => true) - end - - context "when it is expired" do - before { promotion.stub(:expired? => true) } - - specify { promotion.should_not be_eligible(@order) } - end - - context "when it is not expired" do - before { promotion.expires_at = Time.now + 1.day } - - specify { promotion.should be_eligible(@order) } - end - - context "when a coupon code has already resulted in an adjustment on the order" do - before do - promotion.save! - - @order.adjustments.create({:amount => 1, - :source => @order, - :originator => @action, - :label => "Foo"}, :without_protection => true) - end - - it "should be eligible" do - promotion.should be_eligible(@order) - end - end - - end - - context "rules" do - before { @order = Spree::Order.new } - - it "should have eligible rules if there are no rules" do - promotion.rules_are_eligible?(@order).should be_true - end - - context "with 'all' match policy" do - before { promotion.match_policy = 'all' } - - it "should have eligible rules if all rules are eligible" do - promotion.promotion_rules = [mock_model(Spree::PromotionRule, :eligible? => true), - mock_model(Spree::PromotionRule, :eligible? => true)] - promotion.rules_are_eligible?(@order).should be_true - end - - it "should not have eligible rules if any of the rules is not eligible" do - promotion.promotion_rules = [mock_model(Spree::PromotionRule, :eligible? => true), - mock_model(Spree::PromotionRule, :eligible? => false)] - promotion.rules_are_eligible?(@order).should be_false - end - end - - context "with 'any' match policy" do - before(:each) do - @promotion = Spree::Promotion.new(:name => "Promo", :match_policy => 'any') - @promotion.save - end - - it "should have eligible rules if any of the rules is eligible" do - true_rule = Spree::PromotionRule.create({:promotion => @promotion}, :without_protection => true) - true_rule.stub(:eligible?).and_return(true) - false_rule = Spree::PromotionRule.create({:promotion => @promotion}, :without_protection => true) - false_rule.stub(:eligible?).and_return(false) - @promotion.rules << true_rule - @promotion.rules_are_eligible?(@order).should be_true - end - end - - end - -end diff --git a/promo/spec/requests/checkout_spec.rb b/promo/spec/requests/checkout_spec.rb deleted file mode 100644 index fb89cd03695..00000000000 --- a/promo/spec/requests/checkout_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -require 'spec_helper' - -describe "Checkout" do - stub_authorization! - - context "visitor makes checkout as guest without registration", :js => true do - before do - @product = create(:product, :name => "RoR Mug") - create(:zone) - create(:shipping_method) - create(:payment_method) - - @promotion = create_per_order_coupon_promotion 1, 2, 'onetwo' - - visit spree.root_path - click_link "RoR Mug" - click_button "add-to-cart-button" - end - - # let!(:promotion) { create(:promotion, :code => "onetwo") } - let(:promotion) { @promotion } - - # OrdersController - context "on the payment page" do - before do - click_button "Checkout" - fill_in "order_email", :with => "spree@example.com" - click_button "Continue" - - fill_in "First Name", :with => "John" - fill_in "Last Name", :with => "Smith" - fill_in "Street Address", :with => "1 John Street" - fill_in "City", :with => "City of John" - fill_in "Zip", :with => "01337" - select "United States", :from => "Country" - select "Alaska", :from => "order[bill_address_attributes][state_id]" - fill_in "Phone", :with => "555-555-5555" - check "Use Billing Address" - - # To shipping method screen - click_button "Save and Continue" - # To payment screen - click_button "Save and Continue" - end - - it "informs about an invalid coupon code" do - fill_in "Coupon code", :with => "coupon_codes_rule_man" - click_button "Save and Continue" - page.should have_content(I18n.t(:coupon_code_not_found)) - end - - it "applies a promotion to an order" do - fill_in "Coupon code", :with => "onetwo" - click_button "Save and Continue" - page.should have_content(I18n.t(:coupon_code_applied)) - end - end - - # CheckoutController - context "on the cart page" do - it "can enter a coupon code and receives success notification" do - fill_in "Coupon code", :with => "onetwo" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_applied)) - end - - it "can enter a promotion code with both upper and lower case letters" do - fill_in "Coupon code", :with => "ONETwO" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_applied)) - end - - it "cannot enter a promotion code that was created after the order" do - promotion.update_column(:created_at, 1.day.from_now) - fill_in "Coupon code", :with => "onetwo" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_not_found)) - end - - it "informs the user about a coupon code which has exceeded its usage" do - promotion.update_column(:usage_limit, 5) - promotion.class.any_instance.stub(:credits_count => 10) - - fill_in "Coupon code", :with => "onetwo" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_max_usage)) - end - - it "informs the user if the previous promotion is better" do - big_promotion = create_per_order_coupon_promotion 1, 5, 'onefive' - big_promotion.update_column(:created_at, 1.day.ago) - - visit spree.cart_path - - fill_in "Coupon code", :with => "onefive" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_applied)) - - fill_in "Coupon code", :with => "onetwo" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_better_exists)) - end - - it "informs the user if the coupon code is not eligible" do - promotion.rules.first.preferred_amount = 100 - - fill_in "Coupon code", :with => "onetwo" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_not_eligible)) - end - - it "informs the user if the coupon is expired" do - promotion.expires_at = Date.today.beginning_of_week - promotion.starts_at = Date.today.beginning_of_week.advance(:day => 3) - promotion.save! - - fill_in "Coupon code", :with => "onetwo" - click_button "Apply" - page.should have_content(I18n.t(:coupon_code_expired)) - end - end - end -end diff --git a/promo/spec/requests/promotion_adjustments_spec.rb b/promo/spec/requests/promotion_adjustments_spec.rb deleted file mode 100644 index db2f7bd7da2..00000000000 --- a/promo/spec/requests/promotion_adjustments_spec.rb +++ /dev/null @@ -1,449 +0,0 @@ -require 'spec_helper' - -describe "Promotion Adjustments" do - stub_authorization! - - context "coupon promotions", :js => true do - before(:each) do - # creates a default shipping method which is required for checkout - create(:bogus_payment_method, :environment => 'test') - # creates a check payment method so we don't need to worry about cc details - create(:payment_method) - - sm = create(:shipping_method, :zone => Spree::Zone.find_by_name('North America')) - sm.calculator.set_preference(:amount, 10) - - user = create(:admin_user) - create(:product, :name => "RoR Mug", :price => "40") - create(:product, :name => "RoR Bag", :price => "20") - - visit spree.admin_path - click_link "Promotions" - click_link "New Promotion" - end - - let!(:address) { create(:address, :state => Spree::State.first) } - - it "should properly populate Spree::Product#possible_promotions" do - promotion = create_per_product_promotion 'RoR Mug', 5.0 - promotion.update_column :advertise, true - - mug = Spree::Product.find_by_name 'RoR Mug' - bag = Spree::Product.find_by_name 'RoR Bag' - - mug.possible_promotions.size.should == 1 - bag.possible_promotions.size.should == 0 - - # expire the promotion - promotion.expires_at = Date.today.beginning_of_week - promotion.starts_at = Date.today.beginning_of_week.advance(:day => 3) - promotion.save! - - mug.possible_promotions.size.should == 0 - end - - it "should allow an admin to create a flat rate discount coupon promo" do - create_per_order_coupon_promotion 30, 5, "ORDER_38" - - visit spree.root_path - click_link "RoR Mug" - click_button "Add To Cart" - click_button "Checkout" - - fill_in "Customer E-Mail", :with => "spree@example.com" - str_addr = "bill_address" - select "United States", :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - click_button "Save and Continue" - - choose('Credit Card') - fill_in "card_number", :with => "4111111111111111" - fill_in "card_code", :with => "123" - - fill_in "order_coupon_code", :with => "ORDER_38" - click_button "Save and Continue" - - Spree::Order.last.adjustments.promotion.map(&:amount).sum.should == -5.0 - end - - it "should allow an admin to create a single user coupon promo with flat rate discount" do - fill_in "Name", :with => "Order's total > $30" - fill_in "Usage Limit", :with => "1" - select "Coupon code added", :from => "Event" - fill_in "Code", :with => "SINGLE_USE" - click_button "Create" - page.should have_content("Editing Promotion") - - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Rate (per order)", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('#action_fields') { fill_in "Amount", :with => "5" } - within('#actions_container') { click_button "Update" } - - visit spree.root_path - click_link "RoR Mug" - click_button "Add To Cart" - click_button "Checkout" - - fill_in "Customer E-Mail", :with => "spree@example.com" - str_addr = "bill_address" - select "United States", :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - click_button "Save and Continue" - fill_in "order_coupon_code", :with => "SINGLE_USE" - - choose('Credit Card') - fill_in "card_number", :with => "4111111111111111" - fill_in "card_code", :with => "123" - click_button "Save and Continue" - - Spree::Order.first.total.to_f.should == 45.00 - - click_button "Place Order" - - visit spree.root_path - click_link "RoR Mug" - click_button "Add To Cart" - click_button "Checkout" - - fill_in "Customer E-Mail", :with => "spree@example.com" - str_addr = "bill_address" - select "United States", :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - click_button "Save and Continue" - - choose('Credit Card') - fill_in "card_number", :with => "4111111111111111" - fill_in "card_code", :with => "123" - fill_in "order_coupon_code", :with => "SINGLE_USE" - click_button "Save and Continue" - - Spree::Order.last.total.to_f.should == 50.00 - end - - it "should allow an admin to create an automatic promo with flat percent discount" do - fill_in "Name", :with => "Order's total > $30" - select "Order contents changed", :from => "Event" - click_button "Create" - page.should have_content("Editing Promotion") - - select "Item total", :from => "Add rule of type" - within('#rule_fields') { click_button "Add" } - - eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount", :with => 30 - within('#rule_fields') { click_button "Update" } - - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Percent", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('.calculator-fields') { fill_in "Flat Percent", :with => "10" } - within('#actions_container') { click_button "Update" } - - visit spree.root_path - click_link "RoR Mug" - click_button "Add To Cart" - Spree::Order.last.total.to_f.should == 36.00 - visit spree.root_path - click_link "RoR Bag" - click_button "Add To Cart" - Spree::Order.last.total.to_f.should == 54.00 - end - - it "should allow an admin to create an automatic promotion with free shipping (no code)" do - fill_in "Name", :with => "Free Shipping" - click_button "Create" - page.should have_content("Editing Promotion") - - select "Item total", :from => "Add rule of type" - within('#rule_fields') { click_button "Add" } - eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount", :with => "30" - within('#rule_fields') { click_button "Update" } - - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Free Shipping", :from => "Calculator" - within('#actions_container') { click_button "Update" } - - visit spree.root_path - click_link "RoR Bag" - click_button "Add To Cart" - click_button "Checkout" - - fill_in "Customer E-Mail", :with => "spree@example.com" - str_addr = "bill_address" - select "United States", :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - click_button "Save and Continue" - - choose('Credit Card') - fill_in "card_number", :with => "4111111111111111" - fill_in "card_code", :with => "123" - click_button "Save and Continue" - Spree::Order.last.total.to_f.should == 30.00 # bag(20) + shipping(10) - page.should_not have_content("Free Shipping") - - visit spree.root_path - click_link "RoR Mug" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - select "United States", :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - click_button "Save and Continue" - choose('Credit Card') - fill_in "card_number", :with => "4111111111111111" - fill_in "card_code", :with => "123" - - click_button "Save and Continue" - Spree::Order.last.total.to_f.should == 60.00 # bag(20) + mug(40) + free shipping(0) - page.should have_content("Free Shipping") - end - - it "should allow an admin to create an automatic promo requiring a landing page to be visited" do - fill_in "Name", :with => "Deal" - select "Visit static content page", :from => "Event" - fill_in "Path", :with => "content/cvv" - click_button "Create" - page.should have_content("Editing Promotion") - - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Rate (per order)", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('.calculator-fields') { fill_in "Amount", :with => "4" } - within('#actions_container') { click_button "Update" } - - visit spree.root_path - click_link "RoR Mug" - click_button "Add To Cart" - Spree::Order.last.total.to_f.should == 40.00 - - visit "/content/cvv" - visit spree.root_path - click_link "RoR Mug" - click_button "Add To Cart" - Spree::Order.last.total.to_f.should == 76.00 - end - - it "should not allow an admin to create two automatic promo for the same specific product" do - create_per_product_promotion("RoR Mug", 5.0) - create_per_product_promotion("RoR Mug", 10.0) - - Spree::Promotion.last.should_not be_valid - end - - # Regression test for #1416 - it "should allow an admin to create an automatic promo requiring a specific product to be bought" do - create_per_product_promotion("RoR Mug", 5.0) - create_per_product_promotion("RoR Bag", 10.0) - - add_to_cart "RoR Mug" - add_to_cart "RoR Bag" - - # first promotion should be effective on current order - first_promotion = Spree::Promotion.first - first_promotion.actions.first.calculator.compute(Spree::Order.last).should == 5.0 - - # second promotion should be effective on current order - second_promotion = Spree::Promotion.last - second_promotion.actions.first.calculator.compute(Spree::Order.last).should == 10.0 - - do_checkout - - # Mug discount ($5) is not taken into account due to #1526 - # Only "best" discount is taken into account - Spree::Order.last.total.to_f.should == 60.0 # mug(40) + bag(20) - bag_discount(10) + shipping(10) - end - - it "should allow an admin to create a promotion that adds a 'free' item to the cart" do - fill_in "Name", :with => "Bundle" - select "Coupon code added", :from => "Event" - fill_in "Code", :with => "5ZHED2DH" - click_button "Create" - page.should have_content("Editing Promotion") - - select "Create line items", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - # Forced narcolepsy, thanks to JavaScript - sleep(1) - page.execute_script "$('.create_line_items .select2-choice').mousedown();" - sleep(1) - page.execute_script "$('.select2-focused').val('RoR Mug').trigger('keyup-change');" - sleep(1) - page.execute_script "$('.select2-highlighted').mouseup();" - - within('#actions_container') { click_button "Update" } - - select "Create adjustment", :from => "Add action of type" - within('#new_promotion_action_form') { click_button "Add" } - select "Flat Rate (per order)", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('.calculator-fields') { fill_in "Amount", :with => "40.00" } - within('#actions_container') { click_button "Update" } - - visit spree.root_path - click_link "RoR Bag" - click_button "Add To Cart" - click_button "Checkout" - - str_addr = "bill_address" - fill_in "order_email", :with => "buyer@spreecommerce.com" - select "United States", :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - click_button "Save and Continue" - - choose('Credit Card') - fill_in "card_number", :with => "4111111111111111" - fill_in "card_code", :with => "123" - - fill_in "order_coupon_code", :with => "5ZHED2DH" - click_button "Save and Continue" - - last_order = Spree::Order.last - last_order.line_items.count.should == 2 - last_order.line_items.map(&:price).should =~ [20.00, 40.00] - last_order.item_total.to_f.should == 60.00 - last_order.adjustments.promotion.map(&:amount).sum.to_f.should == -40.00 - last_order.total.to_f.should == 30.00 - end - - it "ceasing to be eligible for a promotion with item total rule then becoming eligible again" do - fill_in "Name", :with => "Spend over $50 and save $5" - select "Order contents changed", :from => "Event" - click_button "Create" - page.should have_content("Editing Promotion") - - select "Item total", :from => "Add rule of type" - within('#rule_fields') { click_button "Add" } - eventually_fill_in "promotion_promotion_rules_attributes_1_preferred_amount", :with => "50" - within('#rule_fields') { click_button "Update" } - - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Rate (per order)", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('.calculator-fields') { fill_in "Amount", :with => "5" } - within('#actions_container') { click_button "Update" } - - visit spree.root_path - click_link "RoR Bag" - click_button "Add To Cart" - Spree::Order.last.total.to_f.should == 20.00 - - fill_in "order[line_items_attributes][0][quantity]", :with => "2" - click_button "Update" - Spree::Order.last.total.to_f.should == 40.00 - Spree::Order.last.adjustments.eligible.promotion.count.should == 0 - - fill_in "order[line_items_attributes][0][quantity]", :with => "3" - click_button "Update" - Spree::Order.last.total.to_f.should == 55.00 - Spree::Order.last.adjustments.eligible.promotion.count.should == 1 - - fill_in "order[line_items_attributes][0][quantity]", :with => "2" - click_button "Update" - Spree::Order.last.total.to_f.should == 40.00 - Spree::Order.last.adjustments.eligible.promotion.count.should == 0 - - fill_in "order[line_items_attributes][0][quantity]", :with => "3" - click_button "Update" - Spree::Order.last.total.to_f.should == 55.00 - end - - it "only counting the most valuable promotion adjustment in an order" do - fill_in "Name", :with => "$5 off" - select "Order contents changed", :from => "Event" - click_button "Create" - page.should have_content("Editing Promotion") - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Rate (per order)", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('.calculator-fields') { fill_in "Amount", :with => "5" } - within('#actions_container') { click_button "Update" } - - visit spree.admin_promotions_path - click_link "New Promotion" - fill_in "Name", :with => "10% off" - select "Order contents changed", :from => "Event" - click_button "Create" - page.should have_content("Editing Promotion") - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Percent", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('.calculator-fields') { fill_in "Flat Percent", :with => "10" } - within('#actions_container') { click_button "Update" } - - visit spree.root_path - click_link "RoR Bag" - click_button "Add To Cart" - Spree::Order.last.total.to_f.should == 15.00 - - fill_in "order[line_items_attributes][0][quantity]", :with => "2" - click_button "Update" - Spree::Order.last.total.to_f.should == 35.00 - - fill_in "order[line_items_attributes][0][quantity]", :with => "3" - click_button "Update" - Spree::Order.last.total.to_f.should == 54.00 - end - - def add_to_cart product_name - visit spree.root_path - click_link product_name - click_button "Add To Cart" - end - - def do_checkout - click_button "Checkout" - str_addr = "bill_address" - fill_in "order_email", :with => "buyer@spreecommerce.com" - select "United States", :from => "order_#{str_addr}_attributes_country_id" - ['firstname', 'lastname', 'address1', 'city', 'zipcode', 'phone'].each do |field| - fill_in "order_#{str_addr}_attributes_#{field}", :with => "#{address.send(field)}" - end - select "#{address.state.name}", :from => "order_#{str_addr}_attributes_state_id" - check "order_use_billing" - click_button "Save and Continue" - click_button "Save and Continue" - choose('Credit Card') - fill_in "card_number", :with => "4111111111111111" - fill_in "card_code", :with => "123" - click_button "Save and Continue" - end - end -end diff --git a/promo/spec/spec_helper.rb b/promo/spec/spec_helper.rb deleted file mode 100644 index 7c5e7d2d821..00000000000 --- a/promo/spec/spec_helper.rb +++ /dev/null @@ -1,45 +0,0 @@ -# This file is copied to ~/spec when you run 'ruby script/generate rspec' -# from the project root directory. -ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../dummy/config/environment", __FILE__) -require 'rspec/rails' -require 'database_cleaner' -require 'spree/core/url_helpers' -require 'ffaker' - -# Requires supporting files with custom matchers and macros, etc, -# in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} - -require 'spree/core/testing_support/factories' -require 'spree/core/testing_support/authorization_helpers' - -require 'factories' -require 'active_record/fixtures' -fixtures_dir = File.expand_path('../../../core/db/default', __FILE__) -ActiveRecord::Fixtures.create_fixtures(fixtures_dir, ['spree/countries', 'spree/zones', 'spree/zone_members', 'spree/states', 'spree/roles']) - -RSpec.configure do |config| - config.mock_with :rspec - - config.fixture_path = "#{::Rails.root}/spec/fixtures" - - config.use_transactional_fixtures = false - - config.before(:suite) do - DatabaseCleaner.strategy = :truncation, { :except => ['spree_countries', 'spree_zones', 'spree_zone_members', 'spree_states', 'spree_roles'] } - end - - config.before(:each) do - DatabaseCleaner.start - end - - config.after(:each) do - DatabaseCleaner.clean - end - - config.include FactoryGirl::Syntax::Methods - config.include Spree::Core::UrlHelpers - config.include Spree::Core::TestingSupport::ControllerRequests, :type => :controller - config.include Rack::Test::Methods, :type => :requests -end diff --git a/promo/spec/support/capybara_ext.rb b/promo/spec/support/capybara_ext.rb deleted file mode 100644 index bf5e024a5ce..00000000000 --- a/promo/spec/support/capybara_ext.rb +++ /dev/null @@ -1,28 +0,0 @@ -module CapybaraExt - def page! - save_and_open_page - end - - def click_icon(type) - find(".icon-#{type}").click - end - - def within_row(num, &block) - within("table.index tbody tr:nth-child(#{num})", &block) - end - - def column_text(num) - find("td:nth-child(#{num})").text - end - - def eventually_fill_in(field, options={}) - Capybara.wait_until do - find_field field - end - fill_in field, options - end -end - -RSpec.configure do |c| - c.include CapybaraExt -end diff --git a/promo/spec/support/promotion_creation.rb b/promo/spec/support/promotion_creation.rb deleted file mode 100644 index cc76262f1c9..00000000000 --- a/promo/spec/support/promotion_creation.rb +++ /dev/null @@ -1,70 +0,0 @@ -module PromotionCreation - def create_per_product_promotion product_name, discount_amount, event = "Add to cart" - promotion_name = "Bundle d#{discount_amount}" - - visit spree.admin_path - click_link "Promotions" - click_link "New Promotion" - - fill_in "Name", :with => promotion_name - select event, :from => "Event" - click_button "Create" - page.should have_content("Editing Promotion") - - # add product_name to last promotion - promotion = Spree::Promotion.last - promotion.rules << Spree::Promotion::Rules::Product.new() - product = Spree::Product.find_by_name(product_name) - rule = promotion.rules.last - rule.products << product - if rule.save - puts "Created promotion: new price for #{product_name} is #{product.price - discount_amount} (was #{product.price})" - else - puts "Failed to create promotion: price for #{product_name} is still #{product.price}" - end - - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Rate (per item)", :from => "Calculator" - within('#actions_container') { click_button "Update" } - within('.calculator-fields') { fill_in "Amount", :with => discount_amount.to_s } - within('#actions_container') { click_button "Update" } - - Spree::Promotion.find_by_name promotion_name - end - - def create_per_order_coupon_promotion order_min, order_discount, coupon_code - visit spree.admin_path - click_link "Promotions" - click_link "New Promotion" - - promotion_name = "Order's total > $#{order_min}, Discount #{order_discount}" - fill_in "Name", :with => promotion_name - fill_in "Usage Limit", :with => "100" - select "Coupon code added", :from => "Event" - fill_in "Code", :with => coupon_code - click_button "Create" - page.should have_content("Editing Promotion") - - select "Item total", :from => "Add rule of type" - within('#rule_fields') { click_button "Add" } - - eventually_fill_in "promotion_promotion_rules_attributes_#{Spree::Promotion.count}_preferred_amount", :with => order_min - within('#rule_fields') { click_button "Update" } - - select "Create adjustment", :from => "Add action of type" - within('#action_fields') { click_button "Add" } - select "Flat Rate (per order)", :from => "Calculator" - within('#actions_container') { click_button "Update" } - - within('.calculator-fields') { fill_in "Amount", :with => order_discount } - within('#actions_container') { click_button "Update" } - - Spree::Promotion.find_by_name promotion_name - end -end - - -RSpec.configure do |c| - c.include PromotionCreation -end diff --git a/promo/spree_promo.gemspec b/promo/spree_promo.gemspec deleted file mode 100644 index 65ecad64bed..00000000000 --- a/promo/spree_promo.gemspec +++ /dev/null @@ -1,21 +0,0 @@ -# encoding: UTF-8 -version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip - -Gem::Specification.new do |s| - s.platform = Gem::Platform::RUBY - s.name = 'spree_promo' - s.version = version - s.summary = 'Promotion functionality for use with Spree.' - s.description = 'Required dependency for Spree' - - s.required_ruby_version = '>= 1.8.7' - s.author = 'David North' - s.email = 'david@spreecommerce.com' - s.homepage = 'http://spreecommerce.com' - - s.files = Dir['README', 'LICENSE', 'app/**/*', 'config/**/*', 'lib/**/*', 'db/**/*'] - s.require_path = 'lib' - s.requirements << 'none' - - s.add_dependency 'spree_core', version -end diff --git a/dash/Gemfile b/sample/Gemfile similarity index 100% rename from dash/Gemfile rename to sample/Gemfile diff --git a/sample/LICENSE b/sample/LICENSE index 74f73e35ac3..bef97d82cc6 100644 --- a/sample/LICENSE +++ b/sample/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2012, Spree Commerce, Inc. and other contributors +Copyright (c) 2007-2014, Spree Commerce, Inc. and other contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/sample/Rakefile b/sample/Rakefile index 4d41be3b0c9..e3ae1803560 100644 --- a/sample/Rakefile +++ b/sample/Rakefile @@ -2,6 +2,8 @@ require 'rake' require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' +require 'rspec/core/rake_task' +require 'spree/testing_support/common_rake' spec = eval(File.read('spree_sample.gemspec')) @@ -13,4 +15,14 @@ desc "Release to gemcutter" task :release do version = File.read(File.expand_path("../../SPREE_VERSION", __FILE__)).strip cmd = "cd pkg && gem push spree_sample-#{version}.gem"; puts cmd; system cmd -end \ No newline at end of file +end + +desc "Generates a dummy app for testing" +task :test_app do + ENV['LIB_NAME'] = 'spree/sample' + Rake::Task['common:test_app'].invoke + Rake::Task['common:seed'].invoke +end + +RSpec::Core::RakeTask.new +task :default => :spec diff --git a/sample/db/sample/spree/addresses.yml b/sample/db/sample/spree/addresses.yml deleted file mode 100644 index 03a06d78ca8..00000000000 --- a/sample/db/sample/spree/addresses.yml +++ /dev/null @@ -1,27 +0,0 @@ -<% -I18n.reload! -1.upto(100) do |i| -%> -ship_address_<%= i %>: - firstname: <%= Faker::Name.first_name %> - lastname: <%= Faker::Name.last_name %> - address1: <%= Faker::Address.street_address %> - address2: <%= Faker::Address.secondary_address %> - city: <%= Faker::Address.city %> - state_id: 889445952 - zipcode: 16804 - country_id: 214 - phone: <%= Faker::PhoneNumber.phone_number %> -<% end %> -<% 1.upto(100) do |i| %> -bill_address_<%= i %>: - firstname: <%= Faker::Name.first_name %> - lastname: <%= Faker::Name.last_name %> - address1: <%= Faker::Address.street_address %> - address2: <%= Faker::Address.secondary_address %> - city: <%= Faker::Address.city %> - state_id: 889445952 - zipcode: 16804 - country_id: 214 - phone: <%= Faker::PhoneNumber.phone_number %> -<% end %> diff --git a/sample/db/sample/spree/adjustments.yml b/sample/db/sample/spree/adjustments.yml deleted file mode 100644 index 050b89d5ddd..00000000000 --- a/sample/db/sample/spree/adjustments.yml +++ /dev/null @@ -1,26 +0,0 @@ -<% 1.upto(100) do |i| %> -tax_<%= i %>: - adjustable: order_<%= i %> - adjustable_type: Spree::Order - amount: 0 - source: order_<%= i %> - source_type: Spree::Order - originator: tax_rate_north_america - originator_type: Spree::TaxRate - label: Tax - locked: false - mandatory: true -<% end %> -<% 1.upto(100) do |i| %> -ship_<%= i %>: - adjustable_id: order_<%= i %> - adjustable_type: Spree::Order - amount: 5 - source: shipment_<%= i %> - source_type: Spree::Shipment - originator: ups_ground_usd - originator_type: Spree::ShippingMethod - label: Shipping - locked: true - mandatory: true -<% end %> diff --git a/sample/db/sample/spree/assets.yml b/sample/db/sample/spree/assets.yml deleted file mode 100644 index 9cb29da60b6..00000000000 --- a/sample/db/sample/spree/assets.yml +++ /dev/null @@ -1,440 +0,0 @@ -img_tote: - id: 1 - viewable: ror_tote_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_tote.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_tote_back: - id: 2 - viewable: ror_tote_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_tote_back.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 2 -img_bag: - id: 3 - viewable: ror_bag_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_bag.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_baseball: - id: 4 - viewable: ror_baseball_jersey_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_baseball.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_baseball_back: - id: 5 - viewable: ror_baseball_jersey_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_baseball_back.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 2 -img_jr_spaghetti: - id: 6 - viewable: ror_jr_spaghetti_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_jr_spaghetti.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_mug: - id: 7 - viewable: ror_mug_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_mug.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_mug_back: - id: 8 - viewable: ror_mug_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_mug_back.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 2 -img_ringer: - id: 9 - viewable: ror_ringer_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_ringer.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_ringer_back: - id: 10 - viewable: ror_ringer_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_ringer_back.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 2 -img_stein: - id: 11 - viewable: ror_stein_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_stein.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_stein_back: - id: 12 - viewable: ror_stein_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: ror_stein_back.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 2 -img_apache_baseball: - id: 1004 - viewable: apache_baseball_jersey_v - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: apache_baseball.png - attachment_width: 504 - attachment_height: 484 - type: Spree::Image - position: 1 -img_ruby_baseball: - id: 1008 - viewable: ruby_baseball_jersey_v - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ruby_baseball.png - attachment_width: 495 - attachment_height: 477 - type: Spree::Image - position: 1 -img_baseball_small_green: - id: 1009 - viewable: small-green-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_green.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_small_green_back: - id: 1010 - viewable: small-green-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_green.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_med_green: - id: 1011 - viewable: med-green-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_green.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_med_green_back: - id: 1012 - viewable: med-green-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_green.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_large_green: - id: 1013 - viewable: large-green-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_green.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_large_green_back: - id: 1014 - viewable: large-green-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_green.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_small_blue: - id: 1015 - viewable: small-blue-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_blue.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_small_blue_back: - id: 1016 - viewable: small-blue-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_blue.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_med_blue: - id: 1017 - viewable: med-blue-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_blue.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_med_blue_back: - id: 1018 - viewable: med-blue-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_blue.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_large_blue: - id: 1019 - viewable: large-blue-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_blue.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_large_blue_back: - id: 1020 - viewable: large-blue-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_blue.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_large_red: - id: 1021 - viewable: large-red-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_red.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_large_red_back: - id: 1022 - viewable: large-red-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_red.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_med_red: - id: 1023 - viewable: med-red-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_red.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_med_red_back: - id: 1024 - viewable: med-red-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_red.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_baseball_small_red: - id: 1025 - viewable: small-red-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_red.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 1 -img_baseball_small_red_back: - id: 1026 - viewable: small-red-baseball - viewable_type: Spree::Variant - attachment_content_type: image/png - attachment_file_name: ror_baseball_jersey_back_red.png - attachment_width: 240 - attachment_height: 240 - type: Spree::Image - position: 2 -img_spree_bag: - id: 1027 - viewable: spree_bag_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_bag.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 1 -img_spree_tote: - id: 1028 - viewable: spree_tote_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_tote_front.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 1 -img_tote_back: - id: 1029 - viewable: spree_tote_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_tote_back.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 2 -img_spree_ringer: - id: 1030 - viewable: spree_ringer_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_ringer_t.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 1 -img_spree_ringer_back: - id: 1031 - viewable: spree_ringer_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_ringer_t_back.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 2 -img_spree_jr_spaghetti: - id: 1032 - viewable: spree_jr_spaghetti_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_spaghetti.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 1 -img_spree_stein: - id: 1033 - viewable: spree_stein_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_stein.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 1 -img_spree_stein_back: - id: 1034 - viewable: spree_stein_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_stein_back.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 2 -img_spree_mug: - id: 1035 - viewable: spree_mug_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_mug.jpeg - attachment_width: 360 - attachment_height: 360 - type: Spree::Image - position: 1 -img_spree_mug_back: - id: 1036 - viewable: spree_mug_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_mug_back.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 2 -img_spree_baseball: - id: 1037 - viewable: spree_baseball_jersey_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_jersey.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 1 -img_spree_baseball_back: - id: 1038 - viewable: spree_baseball_jersey_v - viewable_type: Spree::Variant - attachment_content_type: image/jpg - attachment_file_name: spree_jersey_back.jpeg - attachment_width: 480 - attachment_height: 480 - type: Spree::Image - position: 2 diff --git a/sample/db/sample/spree/calculators.yml b/sample/db/sample/spree/calculators.yml deleted file mode 100644 index 7b2eac007a6..00000000000 --- a/sample/db/sample/spree/calculators.yml +++ /dev/null @@ -1,26 +0,0 @@ -ups_ground_usd: - calculable: ups_ground_usd - calculable_type: Spree::ShippingMethod - type: Spree::Calculator::FlatRate -ups_two_day_usd: - calculable: ups_two_day_usd - calculable_type: Spree::ShippingMethod - type: Spree::Calculator::FlatRate -ups_one_day_usd: - calculable: ups_one_day_usd - calculable_type: Spree::ShippingMethod - type: Spree::Calculator::FlatRate -ups_ground_eur: - calculable: ups_ground_eur - calculable_type: Spree::ShippingMethod - type: Spree::Calculator::FlatRate -flat_rate_coupon_calculator: - calculable: spree_coupon - calculable_type: Spree::Promotion - type: Spree::Calculator::FlatRate -tax_rate_calculator: - calculable: tax_rate_north_america - calculable_type: Spree::TaxRate - type: Spree::Calculator::DefaultTax - - diff --git a/sample/db/sample/spree/inventory_units.rb b/sample/db/sample/spree/inventory_units.rb deleted file mode 100644 index c2efa2c17d8..00000000000 --- a/sample/db/sample/spree/inventory_units.rb +++ /dev/null @@ -1,4 +0,0 @@ -# create the inventory units associated with the line item (we need to do this after the fixture b/c quantity is random) -Spree::LineItem.all.each do |li| - li.quantity.times { li.order.inventory_units.create({:variant => li.variant, :state => 'sold', :shipment => li.order.shipment}, :without_protection => true) } -end diff --git a/sample/db/sample/spree/line_items.yml b/sample/db/sample/spree/line_items.yml deleted file mode 100644 index d75dc7f4109..00000000000 --- a/sample/db/sample/spree/line_items.yml +++ /dev/null @@ -1,40 +0,0 @@ -<% for i in 1..100 do%> -li_<%= i %>: - order: order_<%= i %> - variant: ror_tote_v - quantity: <%= rand(4) + 1 %> - price: 15.99 -<% end %> - -<% for i in 1..10 do%> -li_<%= i + 1000 %>: - order: order_<%= i %> - variant: ror_bag_v - quantity: <%= rand(2) + 1 %> - price: 22.99 -<% end %> - -<% for i in 10..20 do%> -li_<%= i + 2000 %>: - order: order_<%= i %> - variant: large-blue-baseball - quantity: <%= rand(3) + 1 %> - price: 19.99 -<% end %> - -<% for i in 10..30 do%> -li_<%= i + 3000 %>: - order: order_<%= i %> - variant: ror_stein_v - quantity: <%= rand(3) + 1 %> - price: 16.99 -<% end %> - -<% for i in 30..50 do%> -li_<%= i + 4000 %>: - order: order_<%= i %> - variant: ror_mug_v - quantity: <%= rand(10) + 1 %> - price: 13.99 -<% end %> - diff --git a/sample/db/sample/spree/option_types.yml b/sample/db/sample/spree/option_types.yml deleted file mode 100644 index 39f6b7eee06..00000000000 --- a/sample/db/sample/spree/option_types.yml +++ /dev/null @@ -1,8 +0,0 @@ -size: - name: tshirt-size - presentation: Size - position: 1 -color: - name: tshirt-color - presentation: Color - position: 2 diff --git a/sample/db/sample/spree/option_values.yml b/sample/db/sample/spree/option_values.yml deleted file mode 100644 index c7108eeb929..00000000000 --- a/sample/db/sample/spree/option_values.yml +++ /dev/null @@ -1,35 +0,0 @@ -s: - name: Small - presentation: S - position: 1 - option_type: size -m: - name: Medium - presentation: M - position: 2 - option_type: size -l: - name: Large - presentation: L - position: 3 - option_type: size -xl: - name: Extra Large - presentation: XL - position: 4 - option_type: size -red: - name: Red - presentation: Red - position: 1 - option_type: color -green: - name: Green - presentation: Green - position: 2 - option_type: color -blue: - name: Blue - presentation: Blue - position: 3 - option_type: color \ No newline at end of file diff --git a/sample/db/sample/spree/orders.yml b/sample/db/sample/spree/orders.yml deleted file mode 100644 index 1312afb588a..00000000000 --- a/sample/db/sample/spree/orders.yml +++ /dev/null @@ -1,20 +0,0 @@ -<% -order_date = Time.now -1.upto(100) do |i| - order_date -= rand(12).hours - item_total = "#{1 + rand(400)}.#{rand(100)}".to_f - charges_total = "#{1 + rand(30)}.#{rand(100)}".to_f -%> -order_<%= i %>: - number: <%= "R#{Array.new(9){rand(9)}.join}" %> - user: user_<%= i %> - state: complete - email: <%= Faker::Internet.email %> - item_total: <%= item_total %> - created_at: <%= order_date.to_s(:db) %> - completed_at: <%= order_date.to_s(:db) %> - total: <%= item_total + charges_total %> - adjustment_total: <%= charges_total %> - ship_address: ship_address_<%= i %> - bill_address: bill_address_<%= i %> -<% end %> diff --git a/sample/db/sample/spree/payment_methods.yml b/sample/db/sample/spree/payment_methods.yml deleted file mode 100644 index 207e4a7f471..00000000000 --- a/sample/db/sample/spree/payment_methods.yml +++ /dev/null @@ -1,29 +0,0 @@ -bogus_dev: - name: Credit Card - description: Bogus payment gateway for development. - environment: development - active: true - type: Spree::Gateway::Bogus -check_method: - name: Check - description: Pay by check. - active: true - type: Spree::PaymentMethod::Check -bogus_test: - name: Credit Card - description: Bogus payment gateway for test. - environment: test - active: true - type: Spree::Gateway::Bogus -bogus_prod: - name: Credit Card - description: Bogus payment gateway for production. - environment: production - active: true - type: Spree::Gateway::Bogus -bogus_staging: - name: Credit Card - description: Bogus payment gateway for staging. - environment: staging - active: true - type: Spree::Gateway::Bogus diff --git a/sample/db/sample/spree/payments.rb b/sample/db/sample/spree/payments.rb deleted file mode 100644 index f0f27c2a0d9..00000000000 --- a/sample/db/sample/spree/payments.rb +++ /dev/null @@ -1,29 +0,0 @@ -# create payments based on the totals since they can't be known in YAML (quantities are random) -method = Spree::PaymentMethod.where(:name => 'Credit Card', :active => true).first - -# Hack the current method so we're able to return a gateway without a RAILS_ENV -Spree::Gateway.class_eval do - def self.current - Spree::Gateway::Bogus.new - end -end - -# This table was previously called spree_creditcards, and older migrations -# reference it as such. Make it explicit here that this table has been renamed. -Spree::CreditCard.table_name = 'spree_credit_cards' - -creditcard = Spree::CreditCard.create({ :cc_type => 'visa', :month => 12, :year => 2014, :last_digits => '1111', - :first_name => 'Sean', :last_name => 'Schofield', - :gateway_customer_profile_id => 'BGS-1234' }, :without_protection => true) - -Spree::Order.all.each_with_index do |order, index| - printf "\rProcessing order #{index}" - STDOUT.flush - order.update! - payment = order.payments.create({ :amount => order.total, :source => creditcard.clone, :payment_method => method }, :without_protection => true) - payment.update_attributes_without_callbacks({ - :state => 'pending', - :response_code => '12345' - }) -end -puts diff --git a/sample/db/sample/spree/preferences.rb b/sample/db/sample/spree/preferences.rb deleted file mode 100644 index 61ff8947e19..00000000000 --- a/sample/db/sample/spree/preferences.rb +++ /dev/null @@ -1,21 +0,0 @@ -shipping_method = Spree::ShippingMethod.find_by_name("UPS Ground (USD)") -shipping_method.calculator.preferred_amount = 5 -shipping_method.calculator.preferred_currency = 'USD' - -shipping_method = Spree::ShippingMethod.find_by_name("UPS Ground (EUR)") -shipping_method.calculator.preferred_amount = 5 -shipping_method.calculator.preferred_currency = 'EUR' - -shipping_method = Spree::ShippingMethod.find_by_name("UPS One Day (USD)") -shipping_method.calculator.preferred_amount = 15 -shipping_method.calculator.preferred_currency = 'USD' - -shipping_method = Spree::ShippingMethod.find_by_name("UPS Two Day (USD)") -shipping_method.calculator.preferred_amount = 10 -shipping_method.calculator.preferred_currency = 'USD' - -# flat_rate_five_dollars: -# name: amount -# owner: flat_rate_coupon_calculator -# owner_type: Spree::Calculator -# value: 5 diff --git a/sample/db/sample/spree/prices.yml b/sample/db/sample/spree/prices.yml deleted file mode 100644 index ab9820597e8..00000000000 --- a/sample/db/sample/spree/prices.yml +++ /dev/null @@ -1,224 +0,0 @@ -small-red-baseball-price-usd: - variant: small-red-baseball - amount: 19.99 - currency: USD -small-blue-baseball-price-eur: - variant: small-blue-baseball - amount: 16.00 - currency: EUR -small-green-baseball-price-usd: - variant: small-green-baseball - amount: 19.99 - currency: USD -small-green-baseball-price-eur: - variant: small-green-baseball - amount: 16.00 - currency: EUR -med-red-baseball-price-usd: - variant: med-red-baseball - amount: 19.99 - currency: USD -med-red-baseball-price-eur: - variant: med-red-baseball - amount: 16.00 - currency: EUR -med-blue-baseball-price-usd: - variant: med-blue-baseball - amount: 19.99 - currency: USD -med-blue-baseball-price-eur: - variant: med-blue-baseball - amount: 16.00 - currency: EUR -med-green-baseball-price-usd: - variant: med-green-baseball - amount: 19.99 - currency: USD -med-green-baseball-price-eur: - variant: med-green-baseball - amount: 16.00 - currency: EUR -large-red-baseball-price-usd: - variant: large-red-baseball - amount: 19.99 - currency: USD -large-red-baseball-price-eur: - variant: large-red-baseball - amount: 16.00 - currency: EUR -large-blue-baseball-price-usd: - variant: large-blue-baseball - amount: 19.99 - currency: USD -large-blue-baseball-price-eur: - variant: large-blue-baseball - amount: 16.00 - currency: EUR -large-green-baseball-price-usd: - variant: large-green-baseball - amount: 19.99 - currency: USD -large-green-baseball-price-eur: - variant: large-green-baseball - amount: 16.00 - currency: EUR -xlarge-green-baseball-price-usd: - variant: xlarge-green-baseball - amount: 21.99 - currency: USD -xlarge-green-baseball-price-eur: - variant: xlarge-green-baseball - amount: 18.50 - currency: EUR -ror_baseball_jersey_v-price-usd: - variant: ror_baseball_jersey_v - amount: 19.99 - currency: USD -ror_baseball_jersey_v-price-eur: - variant: ror_baseball_jersey_v - amount: 16.00 - currency: EUR -ror_tote_v-price-usd: - variant: ror_tote_v - amount: 15.99 - currency: USD -ror_tote_v-price-eur: - variant: ror_tote_v - amount: 14.00 - currency: EUR -ror_bag_v-price-usd: - variant: ror_bag_v - amount: 22.99 - currency: USD -ror_bag_v-price-eur: - variant: ror_bag_v - amount: 19.00 - currency: EUR -ror_jr_spaghetti_v-price-usd: - variant: ror_jr_spaghetti_v - amount: 19.99 - currency: USD -ror_jr_spaghetti_v-price-eur: - variant: ror_jr_spaghetti_v - amount: 16.00 - currency: EUR -ror_mug_v-price-usd: - variant: ror_mug_v - amount: 13.99 - currency: USD -ror_mug_v-price-eur: - variant: ror_mug_v - amount: 12.00 - currency: EUR -ror_ringer_v-price-usd: - variant: ror_ringer_v - amount: 19.99 - currency: USD -ror_ringer_v-price-eur: - variant: ror_ringer_v - amount: 16.50 - currency: EUR -ror_stein_v-price-usd: - variant: ror_stein_v - amount: 16.99 - currency: USD -ror_stein_v-price-eur: - variant: ror_stein_v - amount: 14.00 - currency: EUR -apache_baseball_jersey_v-price-usd: - variant: apache_baseball_jersey_v - amount: 19.99 - currency: USD -apache_baseball_jersey_v-price-eur: - variant: apache_baseball_jersey_v - amount: 16.00 - currency: EUR -ruby_baseball_jersey_v-price-usd: - variant: ruby_baseball_jersey_v - amount: 19.99 - currency: USD -ruby_baseball_jersey_v-price-eur: - variant: ruby_baseball_jersey_v - amount: 16.00 - currency: EUR -spree_baseball_jersey_v-price-usd: - variant: spree_baseball_jersey_v - amount: 19.99 - currency: USD -spree_baseball_jersey_v-price-eur: - variant: spree_baseball_jersey_v - amount: 16.00 - currency: EUR -spree_stein_v-price-usd: - variant: spree_stein_v - amount: 16.99 - currency: USD -spree_stein_v-price-eur: - variant: spree_stein_v - amount: 14.00 - currency: EUR -spree_jr_spaghetti_v-price-usd: - variant: spree_jr_spaghetti_v - amount: 19.99 - currency: USD -spree_jr_spaghetti_v-price-eur: - variant: spree_jr_spaghetti_v - amount: 16.00 - currency: EUR -spree_mug_v-price-usd: - variant: spree_mug_v - amount: 13.99 - currency: USD -spree_mug_v-price-eur: - variant: spree_mug_v - amount: 12.00 - currency: EUR -spree_ringer_v-price-usd: - variant: spree_ringer_v - amount: 17.99 - currency: USD -spree_ringer_v-price-eur: - variant: spree_ringer_v - amount: 15.00 - currency: EUR -spree_tote_v-price-usd: - variant: spree_tote_v - amount: 15.99 - currency: USD -spree_tote_v-price-eur: - variant: spree_tote_v - amount: 14.00 - currency: EUR -spree_bag_v-price-usd: - variant: spree_bag_v - amount: 22.99 - currency: USD -spree_bag_v-price-eur: - variant: spree_bag_v - amount: 19.00 - currency: EUR -small-spree-baseball-price-usd: - variant: small-spree-baseball - amount: 19.99 - currency: USD -small-spree-baseball-price-eur: - variant: small-spree-baseball - amount: 16.00 - currency: EUR -med-spree-baseball-price-usd: - variant: med-spree-baseball - amount: 19.99 - currency: USD -med-spree-baseball-price-eur: - variant: med-spree-baseball - amount: 16.00 - currency: EUR -large-spree-baseball-price-usd: - variant: large-spree-baseball - amount: 19.99 - currency: USD -large-spree-baseball-price-eur: - variant: large-spree-baseball - amount: 16.00 - currency: EUR diff --git a/sample/db/sample/spree/product_option_types.yml b/sample/db/sample/spree/product_option_types.yml deleted file mode 100644 index 5389abc0ab4..00000000000 --- a/sample/db/sample/spree/product_option_types.yml +++ /dev/null @@ -1,16 +0,0 @@ -ror_baseball_size: - product: ror_baseball_jersey - option_type: size - position: 1 -ror_baseball_color: - product: ror_baseball_jersey - option_type: color - position: 2 -spree_baseball_size: - product: spree_baseball_jersey - option_type: size - position: 3 -spree_baseball_color: - product: spree_baseball_jersey - option_type: color - position: 4 diff --git a/sample/db/sample/spree/product_properties.yml b/sample/db/sample/spree/product_properties.yml deleted file mode 100644 index c2c8606c83f..00000000000 --- a/sample/db/sample/spree/product_properties.yml +++ /dev/null @@ -1,264 +0,0 @@ -shirt_man_a: - product: ror_baseball_jersey - property: prop_manufacturer - value: Wilson -shirt_man_b: - product: ror_jr_spaghetti - property: prop_manufacturer - value: Jerseys -shirt_man_c: - product: ror_ringer - property: prop_manufacturer - value: Jerseys -shirt_brand_a: - product: ror_baseball_jersey - property: prop_brand - value: Wannabe Sports -shirt_brand_b: - product: ror_jr_spaghetti - property: prop_brand - value: Resilance -shirt_brand_c: - product: ror_ringer - property: prop_brand - value: Conditioned -shirt_model_a: - product: ror_baseball_jersey - property: prop_model - value: JK1002 -shirt_model_b: - product: ror_jr_spaghetti - property: prop_model - value: TL174 -shirt_model_c: - product: ror_ringer - property: prop_model - value: TL9002 -shirt_type_a: - product: ror_baseball_jersey - property: prop_shirt_type - value: Baseball Jersey -shirt_type_b: - product: ror_jr_spaghetti - property: prop_shirt_type - value: Jr Spaghetti T -shirt_type_c: - product: ror_ringer - property: prop_shirt_type - value: Ringer T -shirt_sleeve_a: - product: ror_baseball_jersey - property: prop_shirt_sleeve_type - value: long -shirt_sleeve_b: - product: ror_jr_spaghetti - property: prop_shirt_sleeve_type - value: none -shirt_sleeve_c: - product: ror_ringer - property: prop_shirt_sleeve_type - value: short -shirt_fab_a: - product: ror_baseball_jersey - property: prop_shirt_fabric - value: 100% Cotton -shirt_fab_b: - product: ror_jr_spaghetti - property: prop_shirt_fabric - value: 90% Cotton, 10% Nylon -shirt_fab_c: - product: ror_ringer - property: prop_shirt_fabric - value: 100% Vellum -shirt_fit_a: - product: ror_baseball_jersey - property: prop_shirt_fit - value: loose -shirt_fit_b: - product: ror_jr_spaghetti - property: prop_shirt_fit - value: form -shirt_fit_c: - product: ror_ringer - property: prop_shirt_fit - value: loose -shirt_gender_a: - product: ror_baseball_jersey - property: prop_gender - value: Men's -shirt_gender_b: - product: ror_jr_spaghetti - property: prop_gender - value: Women's -shirt_gender_c: - product: ror_ringer - property: prop_gender - value: Men's -bag_type_a: - product: ror_tote - property: prop_bag_type - value: Tote -bag_type_b: - product: ror_bag - property: prop_bag_type - value: Messenger -bag_size_a: - product: ror_tote - property: prop_bag_size - value: 15" x 18" x 6" -bag_size_b: - product: ror_bag - property: prop_bag_size - value: 14 1/2" x 12" x 5" -bag_mats_a: - product: ror_tote - property: prop_bag_material - value: Canvas -bag_mats_b: - product: ror_bag - property: prop_bag_material - value: 600 Denier Polyester -mug_type_a: - product: ror_mug - property: prop_mug_type - value: Mug -mug_type_b: - product: ror_stein - property: prop_mug_type - value: Stein -mug_size_a: - product: ror_mug - property: prop_mug_size - value: 4.5" tall, 3.25" dia. -mug_size_b: - product: ror_stein - property: prop_mug_size - value: 6.75" tall, 3.75" dia. base, 3" dia. rim -spree_mug_size_a: - product: spree_mug - property: prop_mug_size - value: 4.5" tall, 3.25" dia. -spree_mug_size_b: - product: spree_stein - property: prop_mug_size - value: 6.75" tall, 3.75" dia. base, 3" dia. rim -s_mug_type_a: - product: spree_mug - property: prop_mug_type - value: Mug -s_mug_type_b: - product: spree_stein - property: prop_mug_type - value: Stein -s_bag_type_a: - product: spree_tote - property: prop_bag_type - value: Tote -s_bag_type_b: - product: spree_bag - property: prop_bag_type - value: Messenger -s_bag_size_a: - product: spree_tote - property: prop_bag_size - value: 15" x 18" x 6" -s_bag_size_b: - product: spree_bag - property: prop_bag_size - value: 14 1/2" x 12" x 5" -s_shirt_man_a: - product: spree_baseball_jersey - property: prop_manufacturer - value: Wilson -s_shirt_man_b: - product: spree_jr_spaghetti - property: prop_manufacturer - value: Jerseys -s_shirt_man_c: - product: spree_ringer - property: prop_manufacturer - value: Jerseys -s_shirt_brand_a: - product: spree_baseball_jersey - property: prop_brand - value: Wannabe Sports -s_shirt_brand_b: - product: spree_jr_spaghetti - property: prop_brand - value: Resilance -s_shirt_brand_c: - product: spree_ringer - property: prop_brand - value: Conditioned -s_shirt_model_a: - product: spree_baseball_jersey - property: prop_model - value: JK1002 -s_shirt_model_b: - product: spree_jr_spaghetti - property: prop_model - value: TL174 -s_shirt_model_c: - product: spree_ringer - property: prop_model - value: TL9002 -s_shirt_type_a: - product: spree_baseball_jersey - property: prop_shirt_type - value: Baseball Jersey -s_shirt_type_b: - product: spree_jr_spaghetti - property: prop_shirt_type - value: Jr Spaghetti T -s_shirt_type_c: - product: spree_ringer - property: prop_shirt_type - value: Ringer T -s_shirt_sleeve_a: - product: spree_baseball_jersey - property: prop_shirt_sleeve_type - value: long -s_shirt_sleeve_b: - product: spree_jr_spaghetti - property: prop_shirt_sleeve_type - value: none -s_shirt_sleeve_c: - product: spree_ringer - property: prop_shirt_sleeve_type - value: short -s_shirt_fab_a: - product: spree_baseball_jersey - property: prop_shirt_fabric - value: 100% Cotton -s_shirt_fab_b: - product: spree_jr_spaghetti - property: prop_shirt_fabric - value: 90% Cotton, 10% Nylon -s_shirt_fab_c: - product: spree_ringer - property: prop_shirt_fabric - value: 100% Vellum -s_shirt_fit_a: - product: spree_baseball_jersey - property: prop_shirt_fit - value: loose -s_shirt_fit_b: - product: spree_jr_spaghetti - property: prop_shirt_fit - value: form -s_shirt_fit_c: - product: spree_ringer - property: prop_shirt_fit - value: loose -s_shirt_gender_a: - product: spree_baseball_jersey - property: prop_gender - value: Men's -s_shirt_gender_b: - product: spree_jr_spaghetti - property: prop_gender - value: Women's -s_shirt_gender_c: - product: spree_ringer - property: prop_gender - value: Men's \ No newline at end of file diff --git a/sample/db/sample/spree/products.rb b/sample/db/sample/spree/products.rb deleted file mode 100644 index 6bb49e9d67b..00000000000 --- a/sample/db/sample/spree/products.rb +++ /dev/null @@ -1,15 +0,0 @@ -# make sure the product images directory exists -FileUtils.mkdir_p "#{Rails.root}/public/spree/products/" - -Spree::Asset.all.each do |asset| - filename = asset.attachment_file_name - puts "-- Processing image: #{filename}\r" - path = File.join(File.dirname(__FILE__), "products/#{filename}") - - if FileTest.exists? path - asset.attachment = File.open(path) - asset.save - else - puts "--- Could not find image at: #{path}" - end -end diff --git a/sample/db/sample/spree/products.yml b/sample/db/sample/spree/products.yml deleted file mode 100644 index 2c2c0bcc39b..00000000000 --- a/sample/db/sample/spree/products.yml +++ /dev/null @@ -1,108 +0,0 @@ -ror_tote: - name: Ruby on Rails Tote - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-on-rails-tote - count_on_hand: 10 - tax_category: tax_cat_clothing -ror_bag: - name: Ruby on Rails Bag - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-on-rails-bag - count_on_hand: 10 - tax_category: tax_cat_clothing -ror_baseball_jersey: - name: Ruby on Rails Baseball Jersey - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-on-rails-baseball-jersey - tax_category: tax_cat_clothing - count_on_hand: 88 -ror_jr_spaghetti: - name: Ruby on Rails Jr. Spaghetti - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-on-rails-jr-spaghetti - count_on_hand: 10 - tax_category: tax_cat_clothing -ror_mug: - name: Ruby on Rails Mug - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-on-rails-mug - count_on_hand: 10 -ror_ringer: - name: Ruby on Rails Ringer T-Shirt - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-on-rails-ringer-t-shirt - count_on_hand: 10 - tax_category: tax_cat_clothing -ror_stein: - name: Ruby on Rails Stein - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-on-rails-stein - count_on_hand: 10 -ruby_baseball_jersey: - name: Ruby Baseball Jersey - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: ruby-baseball-jersey - count_on_hand: 10 - tax_category: tax_cat_clothing -apache_baseball_jersey: - name: Apache Baseball Jersey - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: apache-baseball-jersey - count_on_hand: 10 - tax_category: tax_cat_clothing -spree_baseball_jersey: - name: Spree Baseball Jersey - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: spree-baseball-jersey - count_on_hand: 30 - tax_category: tax_cat_clothing -spree_stein: - name: Spree Stein - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: spree-stein - count_on_hand: 10 -spree_jr_spaghetti: - name: Spree Jr. Spaghetti - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: spree-jr-spaghetti - count_on_hand: 10 - tax_category: tax_cat_clothing -spree_mug: - name: Spree Mug - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: spree-mug - count_on_hand: 10 -spree_ringer: - name: Spree Ringer T-Shirt - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: spree-ringer-t-shirt - count_on_hand: 10 - tax_category: tax_cat_clothing -spree_tote: - name: Spree Tote - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: spree-tote - count_on_hand: 10 - tax_category: tax_cat_clothing -spree_bag: - name: Spree Bag - description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh. - available_on: <%= Time.zone.now.to_s(:db) %> - permalink: spree-bag - count_on_hand: 10 - tax_category: tax_cat_clothing diff --git a/sample/db/sample/spree/products/apache_baseball.png b/sample/db/sample/spree/products/apache_baseball.png deleted file mode 100644 index cc677bc0fd1..00000000000 Binary files a/sample/db/sample/spree/products/apache_baseball.png and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_bag.jpeg b/sample/db/sample/spree/products/ror_bag.jpeg deleted file mode 100644 index e062536f989..00000000000 Binary files a/sample/db/sample/spree/products/ror_bag.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball.jpeg b/sample/db/sample/spree/products/ror_baseball.jpeg deleted file mode 100644 index 3aaf65aeafa..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball_back.jpeg b/sample/db/sample/spree/products/ror_baseball_back.jpeg deleted file mode 100644 index 08b9db6efe6..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball_jersey_back_blue.png b/sample/db/sample/spree/products/ror_baseball_jersey_back_blue.png deleted file mode 100644 index de551c624e2..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball_jersey_back_blue.png and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball_jersey_back_green.png b/sample/db/sample/spree/products/ror_baseball_jersey_back_green.png deleted file mode 100644 index 0f2548937bc..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball_jersey_back_green.png and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball_jersey_back_red.png b/sample/db/sample/spree/products/ror_baseball_jersey_back_red.png deleted file mode 100644 index 864fe0923e4..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball_jersey_back_red.png and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball_jersey_blue.png b/sample/db/sample/spree/products/ror_baseball_jersey_blue.png deleted file mode 100644 index 55f98b14165..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball_jersey_blue.png and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball_jersey_green.png b/sample/db/sample/spree/products/ror_baseball_jersey_green.png deleted file mode 100644 index a14b7fcb431..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball_jersey_green.png and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_baseball_jersey_red.png b/sample/db/sample/spree/products/ror_baseball_jersey_red.png deleted file mode 100644 index 1b534f9e058..00000000000 Binary files a/sample/db/sample/spree/products/ror_baseball_jersey_red.png and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_jr_spaghetti.jpeg b/sample/db/sample/spree/products/ror_jr_spaghetti.jpeg deleted file mode 100644 index b24d4313b7a..00000000000 Binary files a/sample/db/sample/spree/products/ror_jr_spaghetti.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_mug.jpeg b/sample/db/sample/spree/products/ror_mug.jpeg deleted file mode 100644 index 6d5b406b20e..00000000000 Binary files a/sample/db/sample/spree/products/ror_mug.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_mug_back.jpeg b/sample/db/sample/spree/products/ror_mug_back.jpeg deleted file mode 100644 index 687e949c7f1..00000000000 Binary files a/sample/db/sample/spree/products/ror_mug_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_ringer.jpeg b/sample/db/sample/spree/products/ror_ringer.jpeg deleted file mode 100644 index 935ad95f50c..00000000000 Binary files a/sample/db/sample/spree/products/ror_ringer.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_ringer_back.jpeg b/sample/db/sample/spree/products/ror_ringer_back.jpeg deleted file mode 100644 index c8f5f30c636..00000000000 Binary files a/sample/db/sample/spree/products/ror_ringer_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_stein.jpeg b/sample/db/sample/spree/products/ror_stein.jpeg deleted file mode 100644 index 2d65f6e5880..00000000000 Binary files a/sample/db/sample/spree/products/ror_stein.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_stein_back.jpeg b/sample/db/sample/spree/products/ror_stein_back.jpeg deleted file mode 100644 index cdc98718a72..00000000000 Binary files a/sample/db/sample/spree/products/ror_stein_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_tote.jpeg b/sample/db/sample/spree/products/ror_tote.jpeg deleted file mode 100644 index 08bff014de9..00000000000 Binary files a/sample/db/sample/spree/products/ror_tote.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ror_tote_back.jpeg b/sample/db/sample/spree/products/ror_tote_back.jpeg deleted file mode 100644 index bb0e7681bf5..00000000000 Binary files a/sample/db/sample/spree/products/ror_tote_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/ruby_baseball.png b/sample/db/sample/spree/products/ruby_baseball.png deleted file mode 100644 index 8eef1bede0a..00000000000 Binary files a/sample/db/sample/spree/products/ruby_baseball.png and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_bag.jpeg b/sample/db/sample/spree/products/spree_bag.jpeg deleted file mode 100644 index f57951e24ac..00000000000 Binary files a/sample/db/sample/spree/products/spree_bag.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_jersey.jpeg b/sample/db/sample/spree/products/spree_jersey.jpeg deleted file mode 100644 index cd569b35616..00000000000 Binary files a/sample/db/sample/spree/products/spree_jersey.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_jersey_back.jpeg b/sample/db/sample/spree/products/spree_jersey_back.jpeg deleted file mode 100644 index 5bd7433c6b4..00000000000 Binary files a/sample/db/sample/spree/products/spree_jersey_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_mug.jpeg b/sample/db/sample/spree/products/spree_mug.jpeg deleted file mode 100644 index 9e9b6496977..00000000000 Binary files a/sample/db/sample/spree/products/spree_mug.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_mug_back.jpeg b/sample/db/sample/spree/products/spree_mug_back.jpeg deleted file mode 100644 index 9f7d5ce9d61..00000000000 Binary files a/sample/db/sample/spree/products/spree_mug_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_ringer_t.jpeg b/sample/db/sample/spree/products/spree_ringer_t.jpeg deleted file mode 100644 index f8594896cc5..00000000000 Binary files a/sample/db/sample/spree/products/spree_ringer_t.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_ringer_t_back.jpeg b/sample/db/sample/spree/products/spree_ringer_t_back.jpeg deleted file mode 100644 index e2dc7c58d32..00000000000 Binary files a/sample/db/sample/spree/products/spree_ringer_t_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_spaghetti.jpeg b/sample/db/sample/spree/products/spree_spaghetti.jpeg deleted file mode 100644 index 81e1221aad6..00000000000 Binary files a/sample/db/sample/spree/products/spree_spaghetti.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_stein.jpeg b/sample/db/sample/spree/products/spree_stein.jpeg deleted file mode 100644 index deba9854ef6..00000000000 Binary files a/sample/db/sample/spree/products/spree_stein.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_stein_back.jpeg b/sample/db/sample/spree/products/spree_stein_back.jpeg deleted file mode 100644 index ecadca94202..00000000000 Binary files a/sample/db/sample/spree/products/spree_stein_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_tote_back.jpeg b/sample/db/sample/spree/products/spree_tote_back.jpeg deleted file mode 100644 index 18119c3381e..00000000000 Binary files a/sample/db/sample/spree/products/spree_tote_back.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/products/spree_tote_front.jpeg b/sample/db/sample/spree/products/spree_tote_front.jpeg deleted file mode 100644 index 4e7d7fb4278..00000000000 Binary files a/sample/db/sample/spree/products/spree_tote_front.jpeg and /dev/null differ diff --git a/sample/db/sample/spree/properties.yml b/sample/db/sample/spree/properties.yml deleted file mode 100644 index 1483a891703..00000000000 --- a/sample/db/sample/spree/properties.yml +++ /dev/null @@ -1,39 +0,0 @@ -prop_manufacturer: - name: manufacturer - presentation: Manufacturer -prop_brand: - name: brand - presentation: Brand -prop_model: - name: model - presentation: Model -prop_shirt_type: - name: shirt_type - presentation: Type -prop_shirt_sleeve_type: - name: shirt_sleeve_length - presentation: Sleeve -prop_shirt_fabric: - name: shirt_fabric - presentation: Fabric -prop_shirt_fit: - name: shirt_fit - presentation: fit -prop_gender: - name: gender - presentation: Gender -prop_mug_type: - name: mug_type - presentation: Type -prop_mug_size: - name: mug_size - presentation: Size -prop_bag_type: - name: bag_type - presentation: Type -prop_bag_material: - name: bag_material - presentation: Material -prop_bag_size: - name: bag_size - presentation: Size \ No newline at end of file diff --git a/sample/db/sample/spree/prototypes.yml b/sample/db/sample/spree/prototypes.yml deleted file mode 100644 index ab2c9a19cb4..00000000000 --- a/sample/db/sample/spree/prototypes.yml +++ /dev/null @@ -1,9 +0,0 @@ -proto_shirts: - name: Shirt - properties: prop_manufacturer, prop_brand, prop_model, prop_shirt_type, prop_shirt_sleeve_type, prop_shirt_fabric, prop_shirt_fit, prop_gender -proto_bags: - name: Bag - properties: prop_bag_type, prob_bag_size, prop_bag_material -proto_mugs: - name: Mug - properties: prop_mug_size, prop_mug_type \ No newline at end of file diff --git a/sample/db/sample/spree/shipments.yml b/sample/db/sample/spree/shipments.yml deleted file mode 100644 index 8599f7298d6..00000000000 --- a/sample/db/sample/spree/shipments.yml +++ /dev/null @@ -1,8 +0,0 @@ -<% 1.upto(100) do |i| %> -shipment_<%= i %>: - number: H<%= Array.new(11){rand(11)}.join %> - order: order_<%= i %> - shipping_method: ups_ground_usd - address: ship_address_<%= i %> - state: pending -<% end %> diff --git a/sample/db/sample/spree/shipping_categories.yml b/sample/db/sample/spree/shipping_categories.yml deleted file mode 100644 index a39aa451bc8..00000000000 --- a/sample/db/sample/spree/shipping_categories.yml +++ /dev/null @@ -1,2 +0,0 @@ -default_shipping: - name: Default Shipping \ No newline at end of file diff --git a/sample/db/sample/spree/shipping_methods.yml b/sample/db/sample/spree/shipping_methods.yml deleted file mode 100644 index 547ddfb9095..00000000000 --- a/sample/db/sample/spree/shipping_methods.yml +++ /dev/null @@ -1,12 +0,0 @@ -ups_ground_usd: - name: UPS Ground (USD) - zone_id: 2 -ups_two_day_usd: - name: UPS Two Day (USD) - zone_id: 2 -ups_one_day_usd: - name: UPS One Day (USD) - zone_id: 2 -ups_ground_eur: - name: UPS Ground (EUR) - zone_id: 2 diff --git a/sample/db/sample/spree/tax_categories.yml b/sample/db/sample/spree/tax_categories.yml deleted file mode 100644 index 7d659172d9f..00000000000 --- a/sample/db/sample/spree/tax_categories.yml +++ /dev/null @@ -1,6 +0,0 @@ -tax_cat_clothing: - name: Clothing - description: -tax_cat_food: - name: Food - description: diff --git a/sample/db/sample/spree/tax_rates.yml b/sample/db/sample/spree/tax_rates.yml deleted file mode 100644 index 509444375fe..00000000000 --- a/sample/db/sample/spree/tax_rates.yml +++ /dev/null @@ -1,4 +0,0 @@ -tax_rate_north_america: - zone_id: 2 - amount: 0.05 - tax_category: tax_cat_clothing diff --git a/sample/db/sample/spree/taxonomies.yml b/sample/db/sample/spree/taxonomies.yml deleted file mode 100644 index 02db5f3d55c..00000000000 --- a/sample/db/sample/spree/taxonomies.yml +++ /dev/null @@ -1,4 +0,0 @@ -master_categories: - name: Categories -brand: - name: Brand diff --git a/sample/db/sample/spree/taxons.rb b/sample/db/sample/spree/taxons.rb deleted file mode 100644 index e8d37d425d0..00000000000 --- a/sample/db/sample/spree/taxons.rb +++ /dev/null @@ -1,3 +0,0 @@ -# Fixtures were created for acts_as_adjency_list, but now we have nested set, so we need to rebuild it after import -Spree::Taxon.rebuild! -Spree::Taxon.all.each{ |t| t.send(:set_permalink); t.save } diff --git a/sample/db/sample/spree/taxons.yml b/sample/db/sample/spree/taxons.yml deleted file mode 100644 index a8f52d90155..00000000000 --- a/sample/db/sample/spree/taxons.yml +++ /dev/null @@ -1,100 +0,0 @@ -mc_root: - id: 1000 - name: Categories - taxonomy: master_categories - permalink: categories - position: 0 - lft: 1 - rgt: 12 -mc_l1_n1: - id: 1002 - name: Bags - taxonomy: master_categories - parent_id: 1000 - position: 1 - products: ror_tote, ror_bag, spree_tote, spree_bag - permalink: categories/bags - lft: 8 - rgt: 9 -mc_l1_n2: - id: 1003 - name: Mugs - taxonomy: master_categories - parent_id: 1000 - position: 2 - products: ror_mug, ror_stein, spree_stein, spree_mug - permalink: categories/mugs - lft: 10 - rgt: 11 -clothing: - id: 1001 - name: Clothing - taxonomy: master_categories - parent_id: 1000 - #products: ror_baseball_jersey, ror_jr_spaghetti, ror_ringer - permalink: categories/clothing - lft: 2 - rgt: 7 -shirts: - id: 2000 - name: Shirts - taxonomy: master_categories - parent_id: 1001 - position: 0 - products: ror_jr_spaghetti, spree_jr_spaghetti - permalink: categories/clothing/shirts - lft: 3 - rgt: 6 -t_shirts: - id: 3000 - name: T-Shirts - taxonomy: master_categories - parent_id: 2000 - products: ror_baseball_jersey, ror_ringer, apache_baseball_jersey, ruby_baseball_jersey, spree_baseball_jersey, spree_ringer - position: 0 - permalink: categories/clothing/shirts/t-shirts - lft: 4 - rgt: 5 -brand_root: - id: 20000 - name: Brands - taxonomy: brand - permalink: brands - lft: 13 - rgt: 22 -ruby: - name: Ruby - taxonomy: brand - parent_id: 20000 - position: 0 - products: ruby_baseball_jersey - permalink: brands/ruby - lft: 16 - rgt: 17 -rails: - name: Ruby on Rails - taxonomy: brand - parent_id: 20000 - position: 1 - products: ror_baseball_jersey, ror_jr_spaghetti, ror_ringer, ror_stein, ror_bag, ror_tote, ror_mug - permalink: brands/ruby-on-rails - lft: 14 - rgt: 15 -apache: - name: Apache - taxonomy: brand - parent_id: 20000 - position: 2 - products: apache_baseball_jersey - permalink: brands/apache - lft: 18 - rgt: 19 -spree: - name: Spree - taxonomy: brand - parent_id: 20000 - position: 3 - products: spree_baseball_jersey, spree_stein, spree_jr_spaghetti, spree_mug, spree_ringer, spree_tote, spree_bag - permalink: brands/spree - lft: 20 - rgt: 21 \ No newline at end of file diff --git a/sample/db/sample/spree/variants.yml b/sample/db/sample/spree/variants.yml deleted file mode 100644 index 668e68dd2f9..00000000000 --- a/sample/db/sample/spree/variants.yml +++ /dev/null @@ -1,174 +0,0 @@ -small-red-baseball: - product: ror_baseball_jersey - option_values: s, red - sku: ROR-00001 - cost_price: 17.00 - count_on_hand: 10 -small-blue-baseball: - product: ror_baseball_jersey - option_values: s, blue - sku: ROR-00002 - cost_price: 17.00 - count_on_hand: 10 -small-green-baseball: - product: ror_baseball_jersey - option_values: s, green - sku: ROR-00003 - cost_price: 17.00 - count_on_hand: 10 -med-red-baseball: - product: ror_baseball_jersey - option_values: m, red - sku: ROR-00004 - cost_price: 17.00 - count_on_hand: 3 -med-blue-baseball: - product: ror_baseball_jersey - option_values: m, blue - sku: ROR-00005 - cost_price: 17.00 - count_on_hand: 10 -med-green-baseball: - product: ror_baseball_jersey - option_values: m, green - sku: ROR-00006 - cost_price: 17.00 - count_on_hand: 14 -large-red-baseball: - product: ror_baseball_jersey - option_values: l, red - sku: ROR-00007 - cost_price: 17.00 - count_on_hand: 1 -large-blue-baseball: - product: ror_baseball_jersey - option_values: l, blue - sku: ROR-00008 - cost_price: 17.00 - count_on_hand: 10 -large-green-baseball: - product: ror_baseball_jersey - option_values: l, green - sku: ROR-00009 - cost_price: 17.00 - count_on_hand: 10 -xlarge-green-baseball: - product: ror_baseball_jersey - option_values: xl, red - sku: ROR-00010 - cost_price: 20.00 - count_on_hand: 10 -ror_baseball_jersey_v: - product: ror_baseball_jersey - sku: ROR-001 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -ror_tote_v: - product: ror_tote - sku: ROR-00011 - cost_price: 13.00 - is_master: true - count_on_hand: 10 -ror_bag_v: - product: ror_bag - sku: ROR-00012 - cost_price: 21.00 - is_master: true - count_on_hand: 10 -ror_jr_spaghetti_v: - product: ror_jr_spaghetti - sku: ROR-00013 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -ror_mug_v: - product: ror_mug - sku: ROR-00014 - cost_price: 11.00 - is_master: true - count_on_hand: 10 -ror_ringer_v: - product: ror_ringer - sku: ROR-00015 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -ror_stein_v: - product: ror_stein - sku: ROR-00016 - cost_price: 15.00 - is_master: true - count_on_hand: 10 -apache_baseball_jersey_v: - product: apache_baseball_jersey - sku: APC-00001 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -ruby_baseball_jersey_v: - product: ruby_baseball_jersey - sku: RUB-00001 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -spree_baseball_jersey_v: - product: spree_baseball_jersey - sku: SPR-00001 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -spree_stein_v: - product: spree_stein - sku: SPR-00016 - cost_price: 15.00 - is_master: true - count_on_hand: 10 -spree_jr_spaghetti_v: - product: spree_jr_spaghetti - sku: SPR-00013 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -spree_mug_v: - product: spree_mug - sku: SPR-00014 - cost_price: 11.00 - is_master: true - count_on_hand: 10 -spree_ringer_v: - product: spree_ringer - sku: SPR-00015 - cost_price: 17.00 - is_master: true - count_on_hand: 10 -spree_tote_v: - product: spree_tote - sku: SPR-00011 - cost_price: 13.00 - is_master: true - count_on_hand: 10 -spree_bag_v: - product: spree_bag - sku: SPR-00012 - cost_price: 21.00 - is_master: true - count_on_hand: 10 -small-spree-baseball: - product: spree_baseball_jersey - option_values: s - sku: SPR-00002 - cost_price: 17.00 - count_on_hand: 10 -med-spree-baseball: - product: spree_baseball_jersey - option_values: m - sku: SPR-00005 - cost_price: 17.00 - count_on_hand: 10 -large-spree-baseball: - product: spree_baseball_jersey - option_values: l - sku: SPR-00008 - cost_price: 17.00 - count_on_hand: 10 diff --git a/sample/db/samples.rb b/sample/db/samples.rb new file mode 100644 index 00000000000..a6169ce7c86 --- /dev/null +++ b/sample/db/samples.rb @@ -0,0 +1,2 @@ +Spree::Sample.load_sample("payment_methods") +Spree::Sample.load_sample("shipping_categories") diff --git a/sample/db/samples/addresses.rb b/sample/db/samples/addresses.rb new file mode 100644 index 00000000000..c7f21bc3179 --- /dev/null +++ b/sample/db/samples/addresses.rb @@ -0,0 +1,26 @@ +united_states = Spree::Country.find_by_name!("United States") +new_york = Spree::State.find_by_name!("New York") + +# Billing address +Spree::Address.create!( + :firstname => Faker::Name.first_name, + :lastname => Faker::Name.last_name, + :address1 => Faker::Address.street_address, + :address2 => Faker::Address.secondary_address, + :city => Faker::Address.city, + :state => new_york, + :zipcode => 16804, + :country => united_states, + :phone => Faker::PhoneNumber.phone_number) + +#Shipping address +Spree::Address.create!( + :firstname => Faker::Name.first_name, + :lastname => Faker::Name.last_name, + :address1 => Faker::Address.street_address, + :address2 => Faker::Address.secondary_address, + :city => Faker::Address.city, + :state => new_york, + :zipcode => 16804, + :country => united_states, + :phone => Faker::PhoneNumber.phone_number) diff --git a/sample/db/samples/adjustments.rb b/sample/db/samples/adjustments.rb new file mode 100644 index 00000000000..2f11957aa41 --- /dev/null +++ b/sample/db/samples/adjustments.rb @@ -0,0 +1,20 @@ +Spree::Sample.load_sample("orders") + +first_order = Spree::Order.find_by_number!("R123456789") +last_order = Spree::Order.find_by_number!("R987654321") + +first_order.adjustments.create!( + :amount => 0, + :source => Spree::TaxRate.find_by_name!("North America"), + :order => first_order, + :label => "Tax", + :state => "open", + :mandatory => true) + +last_order.adjustments.create!( + :amount => 0, + :source => Spree::TaxRate.find_by_name!("North America"), + :order => last_order, + :label => "Tax", + :state => "open", + :mandatory => true) diff --git a/sample/db/samples/assets.rb b/sample/db/samples/assets.rb new file mode 100644 index 00000000000..956012e8701 --- /dev/null +++ b/sample/db/samples/assets.rb @@ -0,0 +1,159 @@ +Spree::Sample.load_sample("products") +Spree::Sample.load_sample("variants") + +products = {} +products[:ror_baseball_jersey] = Spree::Product.find_by_name!("Ruby on Rails Baseball Jersey") +products[:ror_tote] = Spree::Product.find_by_name!("Ruby on Rails Tote") +products[:ror_bag] = Spree::Product.find_by_name!("Ruby on Rails Bag") +products[:ror_jr_spaghetti] = Spree::Product.find_by_name!("Ruby on Rails Jr. Spaghetti") +products[:ror_mug] = Spree::Product.find_by_name!("Ruby on Rails Mug") +products[:ror_ringer] = Spree::Product.find_by_name!("Ruby on Rails Ringer T-Shirt") +products[:ror_stein] = Spree::Product.find_by_name!("Ruby on Rails Stein") +products[:spree_baseball_jersey] = Spree::Product.find_by_name!("Spree Baseball Jersey") +products[:spree_stein] = Spree::Product.find_by_name!("Spree Stein") +products[:spree_jr_spaghetti] = Spree::Product.find_by_name!("Spree Jr. Spaghetti") +products[:spree_mug] = Spree::Product.find_by_name!("Spree Mug") +products[:spree_ringer] = Spree::Product.find_by_name!("Spree Ringer T-Shirt") +products[:spree_tote] = Spree::Product.find_by_name!("Spree Tote") +products[:spree_bag] = Spree::Product.find_by_name!("Spree Bag") +products[:ruby_baseball_jersey] = Spree::Product.find_by_name!("Ruby Baseball Jersey") +products[:apache_baseball_jersey] = Spree::Product.find_by_name!("Apache Baseball Jersey") + + +def image(name, type="jpeg") + images_path = Pathname.new(File.dirname(__FILE__)) + "images" + path = images_path + "#{name}.#{type}" + return false if !File.exist?(path) + File.open(path) +end + +images = { + products[:ror_tote].master => [ + { + :attachment => image("ror_tote") + }, + { + :attachment => image("ror_tote_back") + } + ], + products[:ror_bag].master => [ + { + :attachment => image("ror_bag") + } + ], + products[:ror_baseball_jersey].master => [ + { + :attachment => image("ror_baseball") + }, + { + :attachment => image("ror_baseball_back") + } + ], + products[:ror_jr_spaghetti].master => [ + { + :attachment => image("ror_jr_spaghetti") + } + ], + products[:ror_mug].master => [ + { + :attachment => image("ror_mug") + }, + { + :attachment => image("ror_mug_back") + } + ], + products[:ror_ringer].master => [ + { + :attachment => image("ror_ringer") + }, + { + :attachment => image("ror_ringer_back") + } + ], + products[:ror_stein].master => [ + { + :attachment => image("ror_stein") + }, + { + :attachment => image("ror_stein_back") + } + ], + products[:apache_baseball_jersey].master => [ + { + :attachment => image("apache_baseball", "png") + }, + ], + products[:ruby_baseball_jersey].master => [ + { + :attachment => image("ruby_baseball", "png") + }, + ], + products[:spree_bag].master => [ + { + :attachment => image("spree_bag") + }, + ], + products[:spree_tote].master => [ + { + :attachment => image("spree_tote_front") + }, + { + :attachment => image("spree_tote_back") + } + ], + products[:spree_ringer].master => [ + { + :attachment => image("spree_ringer_t") + }, + { + :attachment => image("spree_ringer_t_back") + } + ], + products[:spree_jr_spaghetti].master => [ + { + :attachment => image("spree_spaghetti") + } + ], + products[:spree_baseball_jersey].master => [ + { + :attachment => image("spree_jersey") + }, + { + :attachment => image("spree_jersey_back") + } + ], + products[:spree_stein].master => [ + { + :attachment => image("spree_stein") + }, + { + :attachment => image("spree_stein_back") + } + ], + products[:spree_mug].master => [ + { + :attachment => image("spree_mug") + }, + { + :attachment => image("spree_mug_back") + } + ], +} + +products[:ror_baseball_jersey].variants.each do |variant| + color = variant.option_value("tshirt-color").downcase + main_image = image("ror_baseball_jersey_#{color}", "png") + variant.images.create!(:attachment => main_image) + back_image = image("ror_baseball_jersey_back_#{color}", "png") + if back_image + variant.images.create!(:attachment => back_image) + end +end + +images.each do |variant, attachments| + puts "Loading images for #{variant.product.name}" + attachments.each do |attachment| + variant.images.create!(attachment) + end +end + diff --git a/sample/db/samples/images/apache_baseball.png b/sample/db/samples/images/apache_baseball.png new file mode 100644 index 00000000000..40d68d1f240 Binary files /dev/null and b/sample/db/samples/images/apache_baseball.png differ diff --git a/sample/db/samples/images/ror_bag.jpeg b/sample/db/samples/images/ror_bag.jpeg new file mode 100644 index 00000000000..5c88aa7fb6f Binary files /dev/null and b/sample/db/samples/images/ror_bag.jpeg differ diff --git a/sample/db/samples/images/ror_baseball.jpeg b/sample/db/samples/images/ror_baseball.jpeg new file mode 100644 index 00000000000..d76f979e9d3 Binary files /dev/null and b/sample/db/samples/images/ror_baseball.jpeg differ diff --git a/sample/db/samples/images/ror_baseball_back.jpeg b/sample/db/samples/images/ror_baseball_back.jpeg new file mode 100644 index 00000000000..a05a14d2a5a Binary files /dev/null and b/sample/db/samples/images/ror_baseball_back.jpeg differ diff --git a/sample/db/samples/images/ror_baseball_jersey_back_blue.png b/sample/db/samples/images/ror_baseball_jersey_back_blue.png new file mode 100644 index 00000000000..2f023f239f4 Binary files /dev/null and b/sample/db/samples/images/ror_baseball_jersey_back_blue.png differ diff --git a/sample/db/samples/images/ror_baseball_jersey_back_green.png b/sample/db/samples/images/ror_baseball_jersey_back_green.png new file mode 100644 index 00000000000..c769e35129b Binary files /dev/null and b/sample/db/samples/images/ror_baseball_jersey_back_green.png differ diff --git a/sample/db/samples/images/ror_baseball_jersey_back_red.png b/sample/db/samples/images/ror_baseball_jersey_back_red.png new file mode 100644 index 00000000000..2be2275f901 Binary files /dev/null and b/sample/db/samples/images/ror_baseball_jersey_back_red.png differ diff --git a/sample/db/samples/images/ror_baseball_jersey_blue.png b/sample/db/samples/images/ror_baseball_jersey_blue.png new file mode 100644 index 00000000000..ad15f9f0bde Binary files /dev/null and b/sample/db/samples/images/ror_baseball_jersey_blue.png differ diff --git a/sample/db/samples/images/ror_baseball_jersey_green.png b/sample/db/samples/images/ror_baseball_jersey_green.png new file mode 100644 index 00000000000..4bd8c0aea65 Binary files /dev/null and b/sample/db/samples/images/ror_baseball_jersey_green.png differ diff --git a/sample/db/samples/images/ror_baseball_jersey_red.png b/sample/db/samples/images/ror_baseball_jersey_red.png new file mode 100644 index 00000000000..4deb99a1d58 Binary files /dev/null and b/sample/db/samples/images/ror_baseball_jersey_red.png differ diff --git a/sample/db/samples/images/ror_jr_spaghetti.jpeg b/sample/db/samples/images/ror_jr_spaghetti.jpeg new file mode 100644 index 00000000000..51712dd4ca7 Binary files /dev/null and b/sample/db/samples/images/ror_jr_spaghetti.jpeg differ diff --git a/sample/db/samples/images/ror_mug.jpeg b/sample/db/samples/images/ror_mug.jpeg new file mode 100644 index 00000000000..1569a3767ba Binary files /dev/null and b/sample/db/samples/images/ror_mug.jpeg differ diff --git a/sample/db/samples/images/ror_mug_back.jpeg b/sample/db/samples/images/ror_mug_back.jpeg new file mode 100644 index 00000000000..688861c17e2 Binary files /dev/null and b/sample/db/samples/images/ror_mug_back.jpeg differ diff --git a/sample/db/samples/images/ror_ringer.jpeg b/sample/db/samples/images/ror_ringer.jpeg new file mode 100644 index 00000000000..c26f9922b6b Binary files /dev/null and b/sample/db/samples/images/ror_ringer.jpeg differ diff --git a/sample/db/samples/images/ror_ringer_back.jpeg b/sample/db/samples/images/ror_ringer_back.jpeg new file mode 100644 index 00000000000..b8aa9d6cd69 Binary files /dev/null and b/sample/db/samples/images/ror_ringer_back.jpeg differ diff --git a/sample/db/samples/images/ror_stein.jpeg b/sample/db/samples/images/ror_stein.jpeg new file mode 100644 index 00000000000..126072f4d74 Binary files /dev/null and b/sample/db/samples/images/ror_stein.jpeg differ diff --git a/sample/db/samples/images/ror_stein_back.jpeg b/sample/db/samples/images/ror_stein_back.jpeg new file mode 100644 index 00000000000..cba4f27faaf Binary files /dev/null and b/sample/db/samples/images/ror_stein_back.jpeg differ diff --git a/sample/db/samples/images/ror_tote.jpeg b/sample/db/samples/images/ror_tote.jpeg new file mode 100644 index 00000000000..38eb0f54f61 Binary files /dev/null and b/sample/db/samples/images/ror_tote.jpeg differ diff --git a/sample/db/samples/images/ror_tote_back.jpeg b/sample/db/samples/images/ror_tote_back.jpeg new file mode 100644 index 00000000000..0410ebbc670 Binary files /dev/null and b/sample/db/samples/images/ror_tote_back.jpeg differ diff --git a/sample/db/samples/images/ruby_baseball.png b/sample/db/samples/images/ruby_baseball.png new file mode 100644 index 00000000000..9801dcb3486 Binary files /dev/null and b/sample/db/samples/images/ruby_baseball.png differ diff --git a/sample/db/samples/images/spree_bag.jpeg b/sample/db/samples/images/spree_bag.jpeg new file mode 100644 index 00000000000..af60cee5bf0 Binary files /dev/null and b/sample/db/samples/images/spree_bag.jpeg differ diff --git a/sample/db/samples/images/spree_jersey.jpeg b/sample/db/samples/images/spree_jersey.jpeg new file mode 100644 index 00000000000..f58ebb1c389 Binary files /dev/null and b/sample/db/samples/images/spree_jersey.jpeg differ diff --git a/sample/db/samples/images/spree_jersey_back.jpeg b/sample/db/samples/images/spree_jersey_back.jpeg new file mode 100644 index 00000000000..3e48587dea4 Binary files /dev/null and b/sample/db/samples/images/spree_jersey_back.jpeg differ diff --git a/sample/db/samples/images/spree_mug.jpeg b/sample/db/samples/images/spree_mug.jpeg new file mode 100644 index 00000000000..478cd9f2626 Binary files /dev/null and b/sample/db/samples/images/spree_mug.jpeg differ diff --git a/sample/db/samples/images/spree_mug_back.jpeg b/sample/db/samples/images/spree_mug_back.jpeg new file mode 100644 index 00000000000..6c54d2a4f18 Binary files /dev/null and b/sample/db/samples/images/spree_mug_back.jpeg differ diff --git a/sample/db/samples/images/spree_ringer_t.jpeg b/sample/db/samples/images/spree_ringer_t.jpeg new file mode 100644 index 00000000000..69c641ac69f Binary files /dev/null and b/sample/db/samples/images/spree_ringer_t.jpeg differ diff --git a/sample/db/samples/images/spree_ringer_t_back.jpeg b/sample/db/samples/images/spree_ringer_t_back.jpeg new file mode 100644 index 00000000000..76f18a50e99 Binary files /dev/null and b/sample/db/samples/images/spree_ringer_t_back.jpeg differ diff --git a/sample/db/samples/images/spree_spaghetti.jpeg b/sample/db/samples/images/spree_spaghetti.jpeg new file mode 100644 index 00000000000..64723e45e80 Binary files /dev/null and b/sample/db/samples/images/spree_spaghetti.jpeg differ diff --git a/sample/db/samples/images/spree_stein.jpeg b/sample/db/samples/images/spree_stein.jpeg new file mode 100644 index 00000000000..e5ebe3b2b79 Binary files /dev/null and b/sample/db/samples/images/spree_stein.jpeg differ diff --git a/sample/db/samples/images/spree_stein_back.jpeg b/sample/db/samples/images/spree_stein_back.jpeg new file mode 100644 index 00000000000..7b1e9419529 Binary files /dev/null and b/sample/db/samples/images/spree_stein_back.jpeg differ diff --git a/sample/db/samples/images/spree_tote_back.jpeg b/sample/db/samples/images/spree_tote_back.jpeg new file mode 100644 index 00000000000..846adc8fd9a Binary files /dev/null and b/sample/db/samples/images/spree_tote_back.jpeg differ diff --git a/sample/db/samples/images/spree_tote_front.jpeg b/sample/db/samples/images/spree_tote_front.jpeg new file mode 100644 index 00000000000..5c56a066c8a Binary files /dev/null and b/sample/db/samples/images/spree_tote_front.jpeg differ diff --git a/sample/db/samples/option_types.rb b/sample/db/samples/option_types.rb new file mode 100644 index 00000000000..f332bf63d03 --- /dev/null +++ b/sample/db/samples/option_types.rb @@ -0,0 +1,12 @@ +Spree::OptionType.create!([ + { + :name => "tshirt-size", + :presentation => "Size", + :position => 1 + }, + { + :name => "tshirt-color", + :presentation => "Color", + :position => 2 + } +]) diff --git a/sample/db/samples/option_values.rb b/sample/db/samples/option_values.rb new file mode 100644 index 00000000000..a16c0d82e26 --- /dev/null +++ b/sample/db/samples/option_values.rb @@ -0,0 +1,49 @@ +Spree::Sample.load_sample("option_types") + +size = Spree::OptionType.find_by_presentation!("Size") +color = Spree::OptionType.find_by_presentation!("Color") + +Spree::OptionValue.create!([ + { + :name => "Small", + :presentation => "S", + :position => 1, + :option_type => size + }, + { + :name => "Medium", + :presentation => "M", + :position => 2, + :option_type => size + }, + { + :name => "Large", + :presentation => "L", + :position => 3, + :option_type => size + }, + { + :name => "Extra Large", + :presentation => "XL", + :position => 4, + :option_type => size + }, + { + :name => "Red", + :presentation => "Red", + :position => 1, + :option_type => color, + }, + { + :name => "Green", + :presentation => "Green", + :position => 2, + :option_type => color, + }, + { + :name => "Blue", + :presentation => "Blue", + :position => 3, + :option_type => color + } +]) \ No newline at end of file diff --git a/sample/db/samples/orders.rb b/sample/db/samples/orders.rb new file mode 100644 index 00000000000..e7b486148a1 --- /dev/null +++ b/sample/db/samples/orders.rb @@ -0,0 +1,38 @@ +Spree::Sample.load_sample("addresses") + +orders = [] +orders << Spree::Order.create!( + :number => "R123456789", + :email => "spree@example.com", + :item_total => 150.95, + :adjustment_total => 150.95, + :total => 301.90, + :shipping_address => Spree::Address.first, + :billing_address => Spree::Address.last) + +orders << Spree::Order.create!( + :number => "R987654321", + :email => "spree@example.com", + :item_total => 15.95, + :adjustment_total => 15.95, + :total => 31.90, + :shipping_address => Spree::Address.first, + :billing_address => Spree::Address.last) + +orders[0].line_items.create!( + :variant => Spree::Product.find_by_name!("Ruby on Rails Tote").master, + :quantity => 1, + :price => 15.99) + +orders[1].line_items.create!( + :variant => Spree::Product.find_by_name!("Ruby on Rails Bag").master, + :quantity => 1, + :price => 22.99) + +orders.each(&:create_proposed_shipments) + +orders.each do |order| + order.state = "complete" + order.completed_at = Time.now - 1.day + order.save! +end diff --git a/sample/db/samples/payment_methods.rb b/sample/db/samples/payment_methods.rb new file mode 100644 index 00000000000..3211b4cdb9a --- /dev/null +++ b/sample/db/samples/payment_methods.rb @@ -0,0 +1,43 @@ +Spree::Gateway::Bogus.create!( + { + :name => "Credit Card", + :description => "Bogus payment gateway for development.", + :environment => "development", + :active => true + } +) + +Spree::Gateway::Bogus.create!( + { + :name => "Credit Card", + :description => "Bogus payment gateway for production.", + :environment => "production", + :active => true + } +) + +Spree::Gateway::Bogus.create!( + { + :name => "Credit Card", + :description => "Bogus payment gateway for staging.", + :environment => "staging", + :active => true + } +) + +Spree::Gateway::Bogus.create!( + { + :name => "Credit Card", + :description => "Bogus payment gateway for test.", + :environment => "test", + :active => true + } +) + +Spree::PaymentMethod::Check.create!( + { + :name => "Check", + :description => "Pay by check.", + :active => true + } +) diff --git a/sample/db/samples/payments.rb b/sample/db/samples/payments.rb new file mode 100644 index 00000000000..74075ae718e --- /dev/null +++ b/sample/db/samples/payments.rb @@ -0,0 +1,22 @@ +# create payments based on the totals since they can't be known in YAML (quantities are random) +method = Spree::PaymentMethod.where(:name => 'Credit Card', :active => true).first + +# Hack the current method so we're able to return a gateway without a RAILS_ENV +Spree::Gateway.class_eval do + def self.current + Spree::Gateway::Bogus.new + end +end + +# This table was previously called spree_creditcards, and older migrations +# reference it as such. Make it explicit here that this table has been renamed. +Spree::CreditCard.table_name = 'spree_credit_cards' + +creditcard = Spree::CreditCard.create(:cc_type => 'visa', :month => 12, :year => 2.years.from_now.year, :last_digits => '1111', + :name => 'Sean Schofield', :gateway_customer_profile_id => 'BGS-1234') + +Spree::Order.all.each_with_index do |order, index| + order.update! + payment = order.payments.create!(:amount => order.total, :source => creditcard.clone, :payment_method => method) + payment.update_columns(:state => 'pending', :response_code => '12345') +end diff --git a/sample/db/samples/product_option_types.rb b/sample/db/samples/product_option_types.rb new file mode 100644 index 00000000000..2ba42afbf95 --- /dev/null +++ b/sample/db/samples/product_option_types.rb @@ -0,0 +1,12 @@ +Spree::Sample.load_sample("products") + +size = Spree::OptionType.find_by_presentation!("Size") +color = Spree::OptionType.find_by_presentation!("Color") + +ror_baseball_jersey = Spree::Product.find_by_name!("Ruby on Rails Baseball Jersey") +ror_baseball_jersey.option_types = [size, color] +ror_baseball_jersey.save! + +spree_baseball_jersey = Spree::Product.find_by_name!("Spree Baseball Jersey") +spree_baseball_jersey.option_types = [size, color] +spree_baseball_jersey.save! diff --git a/sample/db/samples/product_properties.rb b/sample/db/samples/product_properties.rb new file mode 100644 index 00000000000..1ddb2c6ed13 --- /dev/null +++ b/sample/db/samples/product_properties.rb @@ -0,0 +1,118 @@ +products = + { + "Ruby on Rails Baseball Jersey" => + { + "Manufacturer" => "Wilson", + "Brand" => "Wannabe Sports", + "Model" => "JK1002", + "Shirt Type" => "Baseball Jersey", + "Sleeve Type" => "Long", + "Made from" => "100% cotton", + "Fit" => "Loose", + "Gender" => "Men's" + }, + "Ruby on Rails Jr. Spaghetti" => + { + "Manufacturer" => "Jerseys", + "Brand" => "Resiliance", + "Model" => "TL174", + "Shirt Type" => "Jr. Spaghetti T", + "Sleeve Type" => "None", + "Made from" => "90% Cotton, 10% Nylon", + "Fit" => "Form", + "Gender" => "Women's" + }, + "Ruby on Rails Ringer T-Shirt" => + { + "Manufacturer" => "Jerseys", + "Brand" => "Conditioned", + "Model" => "TL9002", + "Shirt Type" => "Ringer T", + "Sleeve Type" => "Short", + "Made from" => "100% Vellum", + "Fit" => "Loose", + "Gender" => "Men's" + }, + "Ruby on Rails Tote" => + { + "Type" => "Tote", + "Size" => %Q{15" x 18" x 6"}, + "Material" => "Canvas" + }, + "Ruby on Rails Bag" => + { + "Type" => "Messenger", + "Size" => %Q{14 1/2" x 12" x 5"}, + "Material" => "600 Denier Polyester" + }, + "Ruby on Rails Mug" => + { + "Type" => "Mug", + "Size" => %Q{4.5" tall, 3.25" dia.} + }, + "Ruby on Rails Stein" => + { + "Type" => "Stein", + "Size" => %Q{6.75" tall, 3.75" dia. base, 3" dia. rim} + }, + "Spree Stein" => + { + "Type" => "Stein", + "Size" => %Q{6.75" tall, 3.75" dia. base, 3" dia. rim} + }, + "Spree Mug" => + { + "Type" => "Mug", + "Size" => %Q{4.5" tall, 3.25" dia.} + }, + "Spree Tote" => + { + "Type" => "Tote", + "Size" => %Q{15" x 18" x 6"} + }, + "Spree Bag" => + { + "Type" => "Messenger", + "Size" => %Q{14 1/2" x 12" x 5"} + }, + "Spree Baseball Jersey" => + { + "Manufacturer" => "Wilson", + "Brand" => "Wannabe Sports", + "Model" => "JK1002", + "Shirt Type" => "Baseball Jersey", + "Sleeve Type" => "Long", + "Made from" => "100% cotton", + "Fit" => "Loose", + "Gender" => "Men's" + }, + "Spree Jr. Spaghetti" => + { + "Manufacturer" => "Jerseys", + "Brand" => "Resiliance", + "Model" => "TL174", + "Shirt Type" => "Jr. Spaghetti T", + "Sleeve Type" => "None", + "Made from" => "90% Cotton, 10% Nylon", + "Fit" => "Form", + "Gender" => "Women's" + }, + "Spree Ringer T-Shirt" => + { + "Manufacturer" => "Jerseys", + "Brand" => "Conditioned", + "Model" => "TL9002", + "Shirt Type" => "Ringer T", + "Sleeve Type" => "Short", + "Made from" => "100% Vellum", + "Fit" => "Loose", + "Gender" => "Men's" + }, + } + +products.each do |name, properties| + product = Spree::Product.find_by_name(name) + properties.each do |prop_name, prop_value| + product.set_property(prop_name, prop_value) + end +end diff --git a/sample/db/samples/products.rb b/sample/db/samples/products.rb new file mode 100644 index 00000000000..ac05ffee921 --- /dev/null +++ b/sample/db/samples/products.rb @@ -0,0 +1,137 @@ +Spree::Sample.load_sample("tax_categories") +Spree::Sample.load_sample("shipping_categories") + +clothing = Spree::TaxCategory.find_by_name!("Clothing") +shipping_category = Spree::ShippingCategory.find_by_name!("Default") + +default_attrs = { + :description => Faker::Lorem.paragraph, + :available_on => Time.zone.now +} + +products = [ + { + :name => "Ruby on Rails Tote", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 15.99, + :eur_price => 14, + }, + { + :name => "Ruby on Rails Bag", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 22.99, + :eur_price => 19, + }, + { + :name => "Ruby on Rails Baseball Jersey", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 19.99, + :eur_price => 16 + }, + { + :name => "Ruby on Rails Jr. Spaghetti", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 19.99, + :eur_price => 16 + + }, + { + :name => "Ruby on Rails Ringer T-Shirt", + :shipping_category => shipping_category, + :tax_category => clothing, + :price => 19.99, + :eur_price => 16 + }, + { + :name => "Ruby Baseball Jersey", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 19.99, + :eur_price => 16 + }, + { + :name => "Apache Baseball Jersey", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 19.99, + :eur_price => 16 + }, + { + :name => "Spree Baseball Jersey", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 19.99, + :eur_price => 16 + }, + { + :name => "Spree Jr. Spaghetti", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 19.99, + :eur_price => 16 + }, + { + :name => "Spree Ringer T-Shirt", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 19.99, + :eur_price => 16 + }, + { + :name => "Spree Tote", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 15.99, + :eur_price => 14, + }, + { + :name => "Spree Bag", + :tax_category => clothing, + :shipping_category => shipping_category, + :price => 22.99, + :eur_price => 19 + }, + { + :name => "Ruby on Rails Mug", + :shipping_category => shipping_category, + :price => 13.99, + :eur_price => 12 + }, + { + :name => "Ruby on Rails Stein", + :shipping_category => shipping_category, + :price => 16.99, + :eur_price => 14 + }, + { + :name => "Spree Stein", + :shipping_category => shipping_category, + :price => 16.99, + :eur_price => 14, + }, + { + :name => "Spree Mug", + :shipping_category => shipping_category, + :price => 13.99, + :eur_price => 12 + } +] + +products.each do |product_attrs| + eur_price = product_attrs.delete(:eur_price) + Spree::Config[:currency] = "USD" + + default_shipping_category = Spree::ShippingCategory.find_by_name!("Default") + product = Spree::Product.create!(default_attrs.merge(product_attrs)) + Spree::Config[:currency] = "EUR" + product.reload + product.price = eur_price + product.shipping_category = default_shipping_category + product.save! +end + +Spree::Config[:currency] = "USD" diff --git a/sample/db/samples/prototypes.rb b/sample/db/samples/prototypes.rb new file mode 100644 index 00000000000..3da0356f377 --- /dev/null +++ b/sample/db/samples/prototypes.rb @@ -0,0 +1,21 @@ +prototypes = [ + { + :name => "Shirt", + :properties => ["Manufacturer", "Brand", "Model", "Shirt Type", "Sleeve Type", "Material", "Fit", "Gender"] + }, + { + :name => "Bag", + :properties => ["Type", "Size", "Material"] + }, + { + :name => "Mugs", + :properties => ["Size", "Type"] + } +] + +prototypes.each do |prototype_attrs| + prototype = Spree::Prototype.create!(:name => prototype_attrs[:name]) + prototype_attrs[:properties].each do |property| + prototype.properties << Spree::Property.find_by_name!(property) + end +end diff --git a/sample/db/samples/shipping_categories.rb b/sample/db/samples/shipping_categories.rb new file mode 100644 index 00000000000..aa9fbe5cee1 --- /dev/null +++ b/sample/db/samples/shipping_categories.rb @@ -0,0 +1 @@ +Spree::ShippingCategory.find_or_create_by!(:name => "Default") diff --git a/sample/db/samples/shipping_methods.rb b/sample/db/samples/shipping_methods.rb new file mode 100644 index 00000000000..e0c702cbf2e --- /dev/null +++ b/sample/db/samples/shipping_methods.rb @@ -0,0 +1,60 @@ +begin +north_america = Spree::Zone.find_by_name!("North America") +rescue ActiveRecord::RecordNotFound + puts "Couldn't find 'North America' zone. Did you run `rake db:seed` first?" + puts "That task will set up the countries, states and zones required for Spree." + exit +end + +europe_vat = Spree::Zone.find_by_name!("EU_VAT") +shipping_category = Spree::ShippingCategory.find_or_create_by!(name: 'Default') + +Spree::ShippingMethod.create!([ + { + :name => "UPS Ground (USD)", + :zones => [north_america], + :calculator => Spree::Calculator::Shipping::FlatRate.create!, + :shipping_categories => [shipping_category] + }, + { + :name => "UPS Two Day (USD)", + :zones => [north_america], + :calculator => Spree::Calculator::Shipping::FlatRate.create!, + :shipping_categories => [shipping_category] + }, + { + :name => "UPS One Day (USD)", + :zones => [north_america], + :calculator => Spree::Calculator::Shipping::FlatRate.create!, + :shipping_categories => [shipping_category] + }, + { + :name => "UPS Ground (EU)", + :zones => [europe_vat], + :calculator => Spree::Calculator::Shipping::FlatRate.create!, + :shipping_categories => [shipping_category] + }, + { + :name => "UPS Ground (EUR)", + :zones => [europe_vat], + :calculator => Spree::Calculator::Shipping::FlatRate.create!, + :shipping_categories => [shipping_category] + } +]) + +{ + "UPS Ground (USD)" => [5, "USD"], + "UPS Ground (EU)" => [5, "USD"], + "UPS One Day (USD)" => [15, "USD"], + "UPS Two Day (USD)" => [10, "USD"], + "UPS Ground (EUR)" => [8, "EUR"] +}.each do |shipping_method_name, (price, currency)| + shipping_method = Spree::ShippingMethod.find_by_name!(shipping_method_name) + shipping_method.calculator.preferences = { + amount: price, + currency: currency + } + shipping_method.calculator.save! + shipping_method.save! +end + diff --git a/sample/db/samples/stock.rb b/sample/db/samples/stock.rb new file mode 100644 index 00000000000..c55d80fc387 --- /dev/null +++ b/sample/db/samples/stock.rb @@ -0,0 +1,12 @@ +Spree::Sample.load_sample("variants") + +country = Spree::Country.find_by(iso: 'US') +location = Spree::StockLocation.first_or_create! name: 'default', address1: 'Example Street', city: 'City', zipcode: '12345', country: country, state: country.states.first +location.active = true +location.save! + +Spree::Variant.all.each do |variant| + variant.stock_items.each do |stock_item| + Spree::StockMovement.create(:quantity => 10, :stock_item => stock_item) + end +end diff --git a/sample/db/samples/tax_categories.rb b/sample/db/samples/tax_categories.rb new file mode 100644 index 00000000000..96116ebd905 --- /dev/null +++ b/sample/db/samples/tax_categories.rb @@ -0,0 +1,2 @@ +Spree::TaxCategory.create!(:name => "Clothing") +Spree::TaxCategory.create!(:name => "Food") diff --git a/sample/db/samples/tax_rates.rb b/sample/db/samples/tax_rates.rb new file mode 100644 index 00000000000..b98feb891ed --- /dev/null +++ b/sample/db/samples/tax_rates.rb @@ -0,0 +1,9 @@ +north_america = Spree::Zone.find_by_name!("North America") +clothing = Spree::TaxCategory.find_by_name!("Clothing") +tax_rate = Spree::TaxRate.create( + :name => "North America", + :zone => north_america, + :amount => 0.05, + :tax_category => clothing) +tax_rate.calculator = Spree::Calculator::DefaultTax.create! +tax_rate.save! diff --git a/sample/db/samples/taxonomies.rb b/sample/db/samples/taxonomies.rb new file mode 100644 index 00000000000..6caff3eead0 --- /dev/null +++ b/sample/db/samples/taxonomies.rb @@ -0,0 +1,8 @@ +taxonomies = [ + { :name => "Categories" }, + { :name => "Brand" } +] + +taxonomies.each do |taxonomy_attrs| + Spree::Taxonomy.create!(taxonomy_attrs) +end diff --git a/sample/db/samples/taxons.rb b/sample/db/samples/taxons.rb new file mode 100644 index 00000000000..a737177dc8d --- /dev/null +++ b/sample/db/samples/taxons.rb @@ -0,0 +1,145 @@ +Spree::Sample.load_sample("taxonomies") +Spree::Sample.load_sample("products") + +categories = Spree::Taxonomy.find_by_name!("Categories") +brands = Spree::Taxonomy.find_by_name!("Brand") + +products = { + :ror_tote => "Ruby on Rails Tote", + :ror_bag => "Ruby on Rails Bag", + :ror_mug => "Ruby on Rails Mug", + :ror_stein => "Ruby on Rails Stein", + :ror_baseball_jersey => "Ruby on Rails Baseball Jersey", + :ror_jr_spaghetti => "Ruby on Rails Jr. Spaghetti", + :ror_ringer => "Ruby on Rails Ringer T-Shirt", + :spree_stein => "Spree Stein", + :spree_mug => "Spree Mug", + :spree_ringer => "Spree Ringer T-Shirt", + :spree_baseball_jersey => "Spree Baseball Jersey", + :spree_tote => "Spree Tote", + :spree_bag => "Spree Bag", + :spree_jr_spaghetti => "Spree Jr. Spaghetti", + :apache_baseball_jersey => "Apache Baseball Jersey", + :ruby_baseball_jersey => "Ruby Baseball Jersey", +} + + +products.each do |key, name| + products[key] = Spree::Product.find_by_name!(name) +end + +taxons = [ + { + :name => "Categories", + :taxonomy => categories, + :position => 0 + }, + { + :name => "Bags", + :taxonomy => categories, + :parent => "Categories", + :position => 1, + :products => [ + products[:ror_tote], + products[:ror_bag], + products[:spree_tote], + products[:spree_bag] + ] + }, + { + :name => "Mugs", + :taxonomy => categories, + :parent => "Categories", + :position => 2, + :products => [ + products[:ror_mug], + products[:ror_stein], + products[:spree_stein], + products[:spree_mug] + ] + }, + { + :name => "Clothing", + :taxonomy => categories, + :parent => "Categories" + }, + { + :name => "Shirts", + :taxonomy => categories, + :parent => "Clothing", + :position => 0, + :products => [ + products[:ror_jr_spaghetti], + products[:spree_jr_spaghetti] + ] + }, + { + :name => "T-Shirts", + :taxonomy => categories, + :parent => "Clothing" , + :products => [ + products[:ror_baseball_jersey], + products[:ror_ringer], + products[:apache_baseball_jersey], + products[:ruby_baseball_jersey], + products[:spree_baseball_jersey], + products[:spree_ringer] + ], + :position => 0 + }, + { + :name => "Brands", + :taxonomy => brands + }, + { + :name => "Ruby", + :taxonomy => brands, + :parent => "Brand", + :products => [ + products[:ruby_baseball_jersey] + ] + }, + { + :name => "Apache", + :taxonomy => brands, + :parent => "Brand", + :products => [ + products[:apache_baseball_jersey] + ] + }, + { + :name => "Spree", + :taxonomy => brands, + :parent => "Brand", + :products => [ + products[:spree_stein], + products[:spree_mug], + products[:spree_ringer], + products[:spree_baseball_jersey], + products[:spree_tote], + products[:spree_bag], + products[:spree_jr_spaghetti], + ] + }, + { + :name => "Rails", + :taxonomy => brands, + :parent => "Brand", + :products => [ + products[:ror_tote], + products[:ror_bag], + products[:ror_mug], + products[:ror_stein], + products[:ror_baseball_jersey], + products[:ror_jr_spaghetti], + products[:ror_ringer], + ] + }, +] + +taxons.each do |taxon_attrs| + if taxon_attrs[:parent] + taxon_attrs[:parent] = Spree::Taxon.find_by_name!(taxon_attrs[:parent]) + Spree::Taxon.create!(taxon_attrs) + end +end diff --git a/sample/db/samples/variants.rb b/sample/db/samples/variants.rb new file mode 100644 index 00000000000..73061a530f3 --- /dev/null +++ b/sample/db/samples/variants.rb @@ -0,0 +1,164 @@ +Spree::Sample.load_sample("option_values") +Spree::Sample.load_sample("products") + +ror_baseball_jersey = Spree::Product.find_by_name!("Ruby on Rails Baseball Jersey") +ror_tote = Spree::Product.find_by_name!("Ruby on Rails Tote") +ror_bag = Spree::Product.find_by_name!("Ruby on Rails Bag") +ror_jr_spaghetti = Spree::Product.find_by_name!("Ruby on Rails Jr. Spaghetti") +ror_mug = Spree::Product.find_by_name!("Ruby on Rails Mug") +ror_ringer = Spree::Product.find_by_name!("Ruby on Rails Ringer T-Shirt") +ror_stein = Spree::Product.find_by_name!("Ruby on Rails Stein") +spree_baseball_jersey = Spree::Product.find_by_name!("Spree Baseball Jersey") +spree_stein = Spree::Product.find_by_name!("Spree Stein") +spree_jr_spaghetti = Spree::Product.find_by_name!("Spree Jr. Spaghetti") +spree_mug = Spree::Product.find_by_name!("Spree Mug") +spree_ringer = Spree::Product.find_by_name!("Spree Ringer T-Shirt") +spree_tote = Spree::Product.find_by_name!("Spree Tote") +spree_bag = Spree::Product.find_by_name!("Spree Bag") +ruby_baseball_jersey = Spree::Product.find_by_name!("Ruby Baseball Jersey") +apache_baseball_jersey = Spree::Product.find_by_name!("Apache Baseball Jersey") + +small = Spree::OptionValue.find_by_name!("Small") +medium = Spree::OptionValue.find_by_name!("Medium") +large = Spree::OptionValue.find_by_name!("Large") +extra_large = Spree::OptionValue.find_by_name!("Extra Large") + +red = Spree::OptionValue.find_by_name!("Red") +blue = Spree::OptionValue.find_by_name!("Blue") +green = Spree::OptionValue.find_by_name!("Green") + +variants = [ + { + :product => ror_baseball_jersey, + :option_values => [small, red], + :sku => "ROR-00001", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [small, blue], + :sku => "ROR-00002", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [small, green], + :sku => "ROR-00003", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [medium, red], + :sku => "ROR-00004", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [medium, blue], + :sku => "ROR-00005", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [medium, green], + :sku => "ROR-00006", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [large, red], + :sku => "ROR-00007", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [large, blue], + :sku => "ROR-00008", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [large, green], + :sku => "ROR-00009", + :cost_price => 17 + }, + { + :product => ror_baseball_jersey, + :option_values => [extra_large, green], + :sku => "ROR-00010", + :cost_price => 17 + }, +] + +masters = { + ror_baseball_jersey => { + :sku => "ROR-001", + :cost_price => 17, + }, + ror_tote => { + :sku => "ROR-00011", + :cost_price => 17 + }, + ror_bag => { + :sku => "ROR-00012", + :cost_price => 21 + }, + ror_jr_spaghetti => { + :sku => "ROR-00013", + :cost_price => 17 + }, + ror_mug => { + :sku => "ROR-00014", + :cost_price => 11 + }, + ror_ringer => { + :sku => "ROR-00015", + :cost_price => 17 + }, + ror_stein => { + :sku => "ROR-00016", + :cost_price => 15 + }, + apache_baseball_jersey => { + :sku => "APC-00001", + :cost_price => 17 + }, + ruby_baseball_jersey => { + :sku => "RUB-00001", + :cost_price => 17 + }, + spree_baseball_jersey => { + :sku => "SPR-00001", + :cost_price => 17 + }, + spree_stein => { + :sku => "SPR-00016", + :cost_price => 15 + }, + spree_jr_spaghetti => { + :sku => "SPR-00013", + :cost_price => 17 + }, + spree_mug => { + :sku => "SPR-00014", + :cost_price => 11 + }, + spree_ringer => { + :sku => "SPR-00015", + :cost_price => 17 + }, + spree_tote => { + :sku => "SPR-00011", + :cost_price => 13 + }, + spree_bag => { + :sku => "SPR-00012", + :cost_price => 21 + } +} + +Spree::Variant.create!(variants) + +masters.each do |product, variant_attrs| + product.master.update_attributes!(variant_attrs) +end diff --git a/sample/lib/spree/sample.rb b/sample/lib/spree/sample.rb new file mode 100644 index 00000000000..f0a84a2ad4e --- /dev/null +++ b/sample/lib/spree/sample.rb @@ -0,0 +1,23 @@ +module Spree + module Sample + def self.load_sample(file) + # If file is exists within application it takes precendence. + if File.exists?(File.join(Rails.root, 'db', 'samples', "#{file}.rb")) + path = File.expand_path(File.join(Rails.root, 'db', 'samples', "#{file}.rb")) + else + # Otherwise we will use this gems default file. + path = File.expand_path(samples_path + "#{file}.rb") + end + # Check to see if the specified file has been loaded before + if !$LOADED_FEATURES.include?(path) + require path + puts "Loaded #{file.titleize} samples" + end + end + + private + def self.samples_path + Pathname.new(File.join(File.dirname(__FILE__), '..', '..', 'db', 'samples')) + end + end +end diff --git a/sample/lib/spree_sample.rb b/sample/lib/spree_sample.rb index 78a9b8b978a..30e1872ac53 100644 --- a/sample/lib/spree_sample.rb +++ b/sample/lib/spree_sample.rb @@ -1,7 +1,31 @@ require 'spree_core' +require 'spree/sample' module SpreeSample class Engine < Rails::Engine engine_name 'spree_sample' + + # Needs to be here so we can access it inside the tests + def self.load_samples + Spree::Sample.load_sample("payment_methods") + Spree::Sample.load_sample("shipping_categories") + Spree::Sample.load_sample("shipping_methods") + Spree::Sample.load_sample("tax_categories") + Spree::Sample.load_sample("tax_rates") + + Spree::Sample.load_sample("products") + Spree::Sample.load_sample("taxons") + Spree::Sample.load_sample("option_values") + Spree::Sample.load_sample("product_option_types") + Spree::Sample.load_sample("product_properties") + Spree::Sample.load_sample("prototypes") + Spree::Sample.load_sample("variants") + Spree::Sample.load_sample("stock") + Spree::Sample.load_sample("assets") + + Spree::Sample.load_sample("orders") + Spree::Sample.load_sample("adjustments") + Spree::Sample.load_sample("payments") + end end end diff --git a/sample/lib/tasks/sample.rake b/sample/lib/tasks/sample.rake index d27b811eb29..91d89bdbf8c 100644 --- a/sample/lib/tasks/sample.rake +++ b/sample/lib/tasks/sample.rake @@ -1,11 +1,24 @@ require 'ffaker' +require 'pathname' +require 'spree/sample' namespace :spree_sample do desc 'Loads sample data' - task :load do - sample_path = File.join(File.dirname(__FILE__), '..', '..', 'db', 'sample') + task :load => :environment do + if ARGV.include?("db:migrate") + puts %Q{ +Please run db:migrate separately from spree_sample:load. - Rake::Task['db:load_dir'].reenable - Rake::Task['db:load_dir'].invoke(sample_path) +Running db:migrate and spree_sample:load at the same time has been known to +cause problems where columns may be not available during sample data loading. + +Migrations have been run. Please run "rake spree_sample:load" by itself now. + } + exit(1) + end + + SpreeSample::Engine.load_samples end end + + diff --git a/sample/spec/lib/load_sample_spec.rb b/sample/spec/lib/load_sample_spec.rb new file mode 100644 index 00000000000..a2702c1faed --- /dev/null +++ b/sample/spec/lib/load_sample_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe "Load samples" do + before do + # Seeds are only run for rake test_app so to allow this spec to pass without + # rerunning rake test_app every time we must load them in if not already. + unless Spree::Zone.find_by_name("North America") + load Rails.root + 'Rakefile' + load Rails.root + 'db/seeds.rb' + end + end + + it "doesn't raise any error" do + expect { + SpreeSample::Engine.load_samples + }.to_not raise_error + end +end diff --git a/sample/spec/spec_helper.rb b/sample/spec/spec_helper.rb new file mode 100644 index 00000000000..9de79761883 --- /dev/null +++ b/sample/spec/spec_helper.rb @@ -0,0 +1,21 @@ +# This file is copied to ~/spec when you run 'ruby script/generate rspec' +# from the project root directory. +ENV["RAILS_ENV"] ||= 'test' +require File.expand_path("../dummy/config/environment", __FILE__) +require 'rspec/rails' +require 'ffaker' +require 'spree_sample' + +RSpec.configure do |config| + config.color = true + config.infer_spec_type_from_file_location! + config.mock_with :rspec + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, comment the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + config.include FactoryGirl::Syntax::Methods + config.fail_fast = ENV['FAIL_FAST'] || false +end diff --git a/sample/spree_sample.gemspec b/sample/spree_sample.gemspec index acefce248bc..1054351c1a1 100644 --- a/sample/spree_sample.gemspec +++ b/sample/spree_sample.gemspec @@ -8,10 +8,11 @@ Gem::Specification.new do |s| s.summary = 'Sample data (including images) for use with Spree.' s.description = 'Required dependency for Spree' - s.required_ruby_version = '>= 1.8.7' + s.required_ruby_version = '>= 1.9.3' s.author = 'Sean Schofield' s.email = 'sean@spreecommerce.com' s.homepage = 'http://spreecommerce.com' + s.license = %q{BSD-3} s.files = Dir['LICENSE', 'README.md', 'lib/**/*', 'db/**/*'] s.require_path = 'lib' diff --git a/spree.gemspec b/spree.gemspec index 8ccfe699130..ddea0e86f50 100644 --- a/spree.gemspec +++ b/spree.gemspec @@ -1,5 +1,5 @@ # encoding: UTF-8 -version = File.read(File.expand_path("../SPREE_VERSION",__FILE__)).strip +version = File.read(File.expand_path('../SPREE_VERSION',__FILE__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -11,17 +11,18 @@ Gem::Specification.new do |s| s.files = Dir['README.md', 'lib/**/*'] s.require_path = 'lib' s.requirements << 'none' - s.required_ruby_version = '>= 1.8.7' - s.required_rubygems_version = ">= 1.3.6" + s.required_ruby_version = '>= 1.9.3' + s.required_rubygems_version = '>= 1.8.23' s.author = 'Sean Schofield' s.email = 'sean@spreecommerce.com' s.homepage = 'http://spreecommerce.com' + s.license = %q{BSD-3} s.add_dependency 'spree_core', version s.add_dependency 'spree_api', version - s.add_dependency 'spree_dash', version + s.add_dependency 'spree_backend', version + s.add_dependency 'spree_frontend', version s.add_dependency 'spree_sample', version - s.add_dependency 'spree_promo', version s.add_dependency 'spree_cmd', version end