From 6b97872a2580cca391ce0154d2d1149a6df5eb66 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 26 Nov 2012 11:03:44 +1100 Subject: [PATCH 01/31] Create OrderCycles, Exchanges, ExchangeFees and ExchangeVariants --- app/models/exchange.rb | 14 +++++++ app/models/exchange_fee.rb | 4 ++ app/models/exchange_variant.rb | 4 ++ app/models/order_cycle.rb | 9 +++++ .../20121125232613_create_order_cycles.rb | 35 +++++++++++++++++ db/schema.rb | 38 ++++++++++++++++++- spec/factories.rb | 10 +++++ spec/models/exchange_spec.rb | 31 +++++++++++++++ spec/models/order_cycle_spec.rb | 33 ++++++++++++++++ 9 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 app/models/exchange.rb create mode 100644 app/models/exchange_fee.rb create mode 100644 app/models/exchange_variant.rb create mode 100644 app/models/order_cycle.rb create mode 100644 db/migrate/20121125232613_create_order_cycles.rb create mode 100644 spec/models/exchange_spec.rb create mode 100644 spec/models/order_cycle_spec.rb diff --git a/app/models/exchange.rb b/app/models/exchange.rb new file mode 100644 index 0000000000..25e9ee2f37 --- /dev/null +++ b/app/models/exchange.rb @@ -0,0 +1,14 @@ +class Exchange < ActiveRecord::Base + belongs_to :order_cycle + belongs_to :sender, :class_name => 'Enterprise' + belongs_to :receiver, :class_name => 'Enterprise' + belongs_to :payment_enterprise, :class_name => 'Enterprise' + + has_many :exchange_variants + has_many :variants, :through => :exchange_variants + + has_many :exchange_fees + has_many :enterprise_fees, :through => :exchange_fees + + validates_presence_of :order_cycle, :sender, :receiver +end diff --git a/app/models/exchange_fee.rb b/app/models/exchange_fee.rb new file mode 100644 index 0000000000..ff9f12e8dd --- /dev/null +++ b/app/models/exchange_fee.rb @@ -0,0 +1,4 @@ +class ExchangeFee < ActiveRecord::Base + belongs_to :exchange + belongs_to :enterprise_fee +end diff --git a/app/models/exchange_variant.rb b/app/models/exchange_variant.rb new file mode 100644 index 0000000000..c7812845c8 --- /dev/null +++ b/app/models/exchange_variant.rb @@ -0,0 +1,4 @@ +class ExchangeVariant < ActiveRecord::Base + belongs_to :exchange + belongs_to :variant, :class_name => 'Spree::Variant' +end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb new file mode 100644 index 0000000000..fe211f63a0 --- /dev/null +++ b/app/models/order_cycle.rb @@ -0,0 +1,9 @@ +class OrderCycle < ActiveRecord::Base + belongs_to :coordinator, :class_name => 'Enterprise' + belongs_to :coordinator_admin_fee, :class_name => 'EnterpriseFee' + belongs_to :coordinator_sales_fee, :class_name => 'EnterpriseFee' + + has_many :exchanges + + validates_presence_of :name +end diff --git a/db/migrate/20121125232613_create_order_cycles.rb b/db/migrate/20121125232613_create_order_cycles.rb new file mode 100644 index 0000000000..9d0e4bab90 --- /dev/null +++ b/db/migrate/20121125232613_create_order_cycles.rb @@ -0,0 +1,35 @@ +class CreateOrderCycles < ActiveRecord::Migration + def change + create_table :order_cycles do |t| + t.string :name + t.datetime :orders_open_at + t.datetime :orders_close_at + t.references :coordinator + t.references :coordinator_admin_fee + t.references :coordinator_sales_fee + t.timestamps + end + + create_table :exchanges do |t| + t.references :order_cycle + t.references :sender + t.references :receiver + t.references :payment_enterprise + t.datetime :pickup_time + t.string :pickup_instructions + t.timestamps + end + + create_table :exchange_variants do |t| + t.references :exchange + t.references :variant + t.timestamps + end + + create_table :exchange_fees do |t| + t.references :exchange + t.references :enterprise_fee + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f0e2589174..2d3ad2aa42 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20121115010717) do +ActiveRecord::Schema.define(:version => 20121125232613) do create_table "cms_blocks", :force => true do |t| t.integer "page_id", :null => false @@ -158,6 +158,42 @@ ActiveRecord::Schema.define(:version => 20121115010717) do t.datetime "updated_at", :null => false end + create_table "exchange_fees", :force => true do |t| + t.integer "exchange_id" + t.integer "enterprise_fee_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "exchange_variants", :force => true do |t| + t.integer "exchange_id" + t.integer "variant_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "exchanges", :force => true do |t| + t.integer "order_cycle_id" + t.integer "sender_id" + t.integer "receiver_id" + t.integer "payment_enterprise_id" + t.datetime "pickup_time" + t.string "pickup_instructions" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "order_cycles", :force => true do |t| + t.string "name" + t.datetime "orders_open_at" + t.datetime "orders_close_at" + t.integer "coordinator_id" + t.integer "coordinator_admin_fee_id" + t.integer "coordinator_sales_fee_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "product_distributions", :force => true do |t| t.integer "product_id" t.integer "distributor_id" diff --git a/spec/factories.rb b/spec/factories.rb index b643b92f1d..43e2fd6037 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -2,6 +2,16 @@ require 'faker' require 'spree/core/testing_support/factories' FactoryGirl.define do + factory :order_cycle, :class => OrderCycle do + sequence(:name) { |n| "Order Cycle #{n}" } + end + + factory :exchange, :class => Exchange do + order_cycle { OrderCycle.first || FactoryGirl.create(:order_cycle) } + sender { Enterprise.first || FactoryGirl.create(:enterprise) } + receiver { Enterprise.first || FactoryGirl.create(:enterprise) } + end + factory :enterprise, :class => Enterprise do sequence(:name) { |n| "Enterprise #{n}" } description 'enterprise' diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb new file mode 100644 index 0000000000..4a8ea2858e --- /dev/null +++ b/spec/models/exchange_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Exchange do + it "should be valid when built from factory" do + build(:exchange).should be_valid + end + + [:order_cycle, :sender, :receiver].each do |attr| + it "should not be valid without #{attr}" do + e = build(:exchange) + e.send("#{attr}=", nil) + e.should_not be_valid + end + end + + it "has exchange variants" do + e = create(:exchange) + p = create(:product) + + e.exchange_variants.create(:variant => p.master) + e.variants.count.should == 1 + end + + it "has exchange fees" do + e = create(:exchange) + f = create(:enterprise_fee) + + e.exchange_fees.create(:enterprise_fee => f) + e.enterprise_fees.count.should == 1 + end +end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb new file mode 100644 index 0000000000..cfa0e7bbf6 --- /dev/null +++ b/spec/models/order_cycle_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe OrderCycle do + it "should be valid when built from factory" do + build(:order_cycle).should be_valid + end + + it "should not be valid without a name" do + oc = build(:order_cycle) + oc.name = '' + oc.should_not be_valid + end + + it "has a coordinator and associated fees" do + oc = create(:order_cycle) + + oc.coordinator = create(:enterprise) + oc.coordinator_admin_fee = create(:enterprise_fee) + oc.coordinator_sales_fee = create(:enterprise_fee) + + oc.save! + end + + it "has exchanges" do + oc = create(:order_cycle) + + create(:exchange, :order_cycle => oc) + create(:exchange, :order_cycle => oc) + create(:exchange, :order_cycle => oc) + + oc.exchanges.count.should == 3 + end +end From 47c28e65a7d9ed037e3f2535dc408b1b24387621 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 27 Nov 2012 10:09:40 +1100 Subject: [PATCH 02/31] Add detailed order cycle factory, add methods to report on order cycle suppliers, distributors and products/variants exchanged --- app/models/order_cycle.rb | 20 +++++++++- spec/factories.rb | 36 +++++++++++++++-- spec/models/order_cycle_spec.rb | 68 +++++++++++++++++++++++++++++---- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index fe211f63a0..0983695db6 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -3,7 +3,25 @@ class OrderCycle < ActiveRecord::Base belongs_to :coordinator_admin_fee, :class_name => 'EnterpriseFee' belongs_to :coordinator_sales_fee, :class_name => 'EnterpriseFee' - has_many :exchanges + has_many :exchanges, :dependent => :destroy validates_presence_of :name + + + def suppliers + self.exchanges.where(:receiver_id => self.coordinator).map(&:sender).uniq + end + + def distributors + self.exchanges.where(:sender_id => self.coordinator).map(&:receiver).uniq + end + + def variants + self.exchanges.map(&:variants).flatten.uniq + end + + def products + self.variants.map(&:product).uniq + end + end diff --git a/spec/factories.rb b/spec/factories.rb index 43e2fd6037..bfc2665ae3 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -2,8 +2,36 @@ require 'faker' require 'spree/core/testing_support/factories' FactoryGirl.define do - factory :order_cycle, :class => OrderCycle do + factory :order_cycle, :parent => :simple_order_cycle do + after(:create) do |oc| + # Suppliers + create(:exchange, :order_cycle => oc, :receiver => oc.coordinator) + create(:exchange, :order_cycle => oc, :receiver => oc.coordinator) + + # Distributors + create(:exchange, :order_cycle => oc, :sender => oc.coordinator) + create(:exchange, :order_cycle => oc, :sender => oc.coordinator) + + # Products with images + ex = oc.exchanges.first + + 2.times do + product = create(:product) + image = File.open(File.expand_path('../../app/assets/images/logo.jpg', __FILE__)) + Spree::Image.create({:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :alt => "position 1", :attachment => image, :position => 1}) + + ex.variants << product.master + end + end + end + + factory :simple_order_cycle, :class => OrderCycle do sequence(:name) { |n| "Order Cycle #{n}" } + + orders_open_at { Time.zone.now - 1.day } + orders_close_at { Time.zone.now + 1.week } + + coordinator { Enterprise.first || FactoryGirl.create(:enterprise) } end factory :exchange, :class => Exchange do @@ -36,7 +64,7 @@ FactoryGirl.define do name '$0.50 / kg' calculator { FactoryGirl.build(:weight_calculator) } - after_create { |ef| ef.calculator.save! } + after(:create) { |ef| ef.calculator.save! } end factory :product_distribution, :class => ProductDistribution do @@ -54,8 +82,8 @@ FactoryGirl.define do end factory :weight_calculator, :class => OpenFoodWeb::Calculator::Weight do - after_build { |c| c.set_preference(:per_kg, 0.5) } - after_create { |c| c.set_preference(:per_kg, 0.5); c.save! } + after(:build) { |c| c.set_preference(:per_kg, 0.5) } + after(:create) { |c| c.set_preference(:per_kg, 0.5); c.save! } end end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index cfa0e7bbf6..2f504e833e 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -2,17 +2,17 @@ require 'spec_helper' describe OrderCycle do it "should be valid when built from factory" do - build(:order_cycle).should be_valid + build(:simple_order_cycle).should be_valid end it "should not be valid without a name" do - oc = build(:order_cycle) + oc = build(:simple_order_cycle) oc.name = '' oc.should_not be_valid end it "has a coordinator and associated fees" do - oc = create(:order_cycle) + oc = create(:simple_order_cycle) oc.coordinator = create(:enterprise) oc.coordinator_admin_fee = create(:enterprise_fee) @@ -22,12 +22,66 @@ describe OrderCycle do end it "has exchanges" do - oc = create(:order_cycle) + oc = create(:simple_order_cycle) - create(:exchange, :order_cycle => oc) - create(:exchange, :order_cycle => oc) - create(:exchange, :order_cycle => oc) + create(:exchange, order_cycle: oc) + create(:exchange, order_cycle: oc) + create(:exchange, order_cycle: oc) oc.exchanges.count.should == 3 end + + it "reports its suppliers" do + oc = create(:simple_order_cycle) + + e1 = create(:exchange, + order_cycle: oc, receiver: oc.coordinator, sender: create(:enterprise)) + e2 = create(:exchange, + order_cycle: oc, receiver: oc.coordinator, sender: create(:enterprise)) + e3 = create(:exchange, + order_cycle: oc, receiver: oc.coordinator, sender: e2.sender) + + oc.suppliers.sort.should == [e1.sender, e2.sender].sort + end + + it "reports its distributors" do + oc = create(:simple_order_cycle) + + e1 = create(:exchange, + order_cycle: oc, sender: oc.coordinator, receiver: create(:enterprise)) + e2 = create(:exchange, + order_cycle: oc, sender: oc.coordinator, receiver: create(:enterprise)) + e3 = create(:exchange, + order_cycle: oc, sender: oc.coordinator, receiver: e2.receiver) + + oc.distributors.sort.should == [e1.receiver, e2.receiver].sort + end + + describe "product exchanges" do + before(:each) do + @oc = create(:simple_order_cycle) + + e1 = create(:exchange, + order_cycle: @oc, sender: @oc.coordinator, receiver: create(:enterprise)) + e2 = create(:exchange, + order_cycle: @oc, sender: @oc.coordinator, receiver: create(:enterprise)) + + @p1 = create(:product) + @p2 = create(:product) + @p2_v = create(:variant, product: @p2) + + e1.variants << @p1.master + e1.variants << @p2.master + e1.variants << @p2_v + e2.variants << @p1.master + end + + it "reports on the variants exchanged in the order cycle" do + @oc.variants.sort.should == [@p1.master, @p2.master, @p2_v].sort + end + + it "reports on the products exchanged in the order cycle" do + @oc.products.sort.should == [@p1, @p2] + end + end end From d5310452b6a5feda6dc7a389e904de064f7f40ae Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 27 Nov 2012 10:13:12 +1100 Subject: [PATCH 03/31] Admin list order cycles --- .../admin/order_cycles_controller.rb | 10 ++++++ app/models/exchange.rb | 4 +-- app/models/order_cycle_set.rb | 5 +++ app/overrides/add_order_cycles_admin_tab.rb | 4 +++ .../admin/enterprise_fees/index.html.haml | 2 ++ app/views/admin/order_cycles/index.html.haml | 34 +++++++++++++++++++ config/routes.rb | 5 +++ spec/requests/admin/order_cycles_spec.rb | 31 +++++++++++++++++ 8 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 app/controllers/admin/order_cycles_controller.rb create mode 100644 app/models/order_cycle_set.rb create mode 100644 app/overrides/add_order_cycles_admin_tab.rb create mode 100644 app/views/admin/order_cycles/index.html.haml create mode 100644 spec/requests/admin/order_cycles_spec.rb diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb new file mode 100644 index 0000000000..a6ab1375f1 --- /dev/null +++ b/app/controllers/admin/order_cycles_controller.rb @@ -0,0 +1,10 @@ +module Admin + class OrderCyclesController < ResourceController + before_filter :load_order_cycle_set, :only => :index + + private + def load_order_cycle_set + @order_cycle_set = OrderCycleSet.new :collection => collection + end + end +end diff --git a/app/models/exchange.rb b/app/models/exchange.rb index 25e9ee2f37..154824415d 100644 --- a/app/models/exchange.rb +++ b/app/models/exchange.rb @@ -4,10 +4,10 @@ class Exchange < ActiveRecord::Base belongs_to :receiver, :class_name => 'Enterprise' belongs_to :payment_enterprise, :class_name => 'Enterprise' - has_many :exchange_variants + has_many :exchange_variants, :dependent => :destroy has_many :variants, :through => :exchange_variants - has_many :exchange_fees + has_many :exchange_fees, :dependent => :destroy has_many :enterprise_fees, :through => :exchange_fees validates_presence_of :order_cycle, :sender, :receiver diff --git a/app/models/order_cycle_set.rb b/app/models/order_cycle_set.rb new file mode 100644 index 0000000000..f83f40f029 --- /dev/null +++ b/app/models/order_cycle_set.rb @@ -0,0 +1,5 @@ +class OrderCycleSet < ModelSet + def initialize(attributes={}) + super(OrderCycle, OrderCycle.all, nil, attributes) + end +end diff --git a/app/overrides/add_order_cycles_admin_tab.rb b/app/overrides/add_order_cycles_admin_tab.rb new file mode 100644 index 0000000000..1d0dd9b3a8 --- /dev/null +++ b/app/overrides/add_order_cycles_admin_tab.rb @@ -0,0 +1,4 @@ +Deface::Override.new(:virtual_path => "spree/layouts/admin", + :name => "cms_order_cycles_tab", + :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]", + :text => "
  • <%= link_to('Order Cycles', main_app.admin_order_cycles_path) %>
  • ") diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index c52afb060c..82a133fa4f 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -1,3 +1,5 @@ +%h1 Enterprise Fees + = ng_form_for @enterprise_fee_set, :url => main_app.bulk_update_admin_enterprise_fees_path, :html => {'ng-app' => 'enterprise_fees', 'ng-controller' => 'AdminEnterpriseFeesCtrl'} do |enterprise_fee_set_form| - if @enterprise_fee_set.errors.present? %h2 Errors diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml new file mode 100644 index 0000000000..b5bdf735c3 --- /dev/null +++ b/app/views/admin/order_cycles/index.html.haml @@ -0,0 +1,34 @@ +%h1 Order Cycles + += form_for @order_cycle_set, :url => main_app.bulk_update_admin_order_cycles_path do |f| + %table.index#listing_order_cycles + %thead + %tr + %th Name + %th Open + %th Close + %th Coordinator + %th Suppliers + %th Distributors + %th Products + %th + %tbody + = f.fields_for :collection do |order_cycle_form| + - order_cycle = order_cycle_form.object + %tr + %td= link_to order_cycle.name, main_app.edit_admin_order_cycle_path(order_cycle) + %td= order_cycle_form.text_field :orders_open_at, :class => 'datepicker', :value => order_cycle.orders_open_at + %td= order_cycle_form.text_field :orders_close_at, :class => 'datepicker', :value => order_cycle.orders_close_at + %td= order_cycle.coordinator.name + %td + - order_cycle.suppliers.each do |s| + = s.name + %br/ + %td + - order_cycle.distributors.each do |d| + = d.name + %br/ + %td.products + - order_cycle.variants.each do |v| + = image_tag(v.images.first.attachment.url(:mini)) if v.images.present? + %td diff --git a/config/routes.rb b/config/routes.rb index 0ad57b0304..7bbbf022f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,9 +9,14 @@ Openfoodweb::Application.routes.draw do end namespace :admin do + resources :order_cycles do + post :bulk_update, :on => :collection, :as => :bulk_update + end + resources :enterprises do post :bulk_update, :on => :collection, :as => :bulk_update end + resources :enterprise_fees do post :bulk_update, :on => :collection, :as => :bulk_update end diff --git a/spec/requests/admin/order_cycles_spec.rb b/spec/requests/admin/order_cycles_spec.rb new file mode 100644 index 0000000000..73273ed8d6 --- /dev/null +++ b/spec/requests/admin/order_cycles_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +feature %q{ + As an administrator + I want to manage order cycles +}, js: true do + include AuthenticationWorkflow + include WebHelper + + scenario "listing order cycles" do + oc = create(:order_cycle) + + login_to_admin_section + click_link 'Order Cycles' + + # Regular fields + page.should have_selector 'a', text: oc.name + + page.should have_selector "input[value='#{oc.orders_open_at}']" + page.should have_selector "input[value='#{oc.orders_close_at}']" + page.should have_content oc.coordinator.name + + # Suppliers and distributors + oc.suppliers.each { |s| page.should have_content s.name } + oc.distributors.each { |d| page.should have_content d.name } + + # Products + all('td.products img').count.should == 2 + end + +end From 5103ce64ba7a871df198a2993b80e8ed810be53b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 27 Nov 2012 10:51:22 +1100 Subject: [PATCH 04/31] Output new order cycle as JSON --- app/controllers/admin/order_cycles_controller.rb | 8 ++++++++ app/views/admin/order_cycles/new.rep | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 app/views/admin/order_cycles/new.rep diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index a6ab1375f1..6be2bd308c 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -2,6 +2,14 @@ module Admin class OrderCyclesController < ResourceController before_filter :load_order_cycle_set, :only => :index + def new + respond_to do |format| + format.html + format.json + end + end + + private def load_order_cycle_set @order_cycle_set = OrderCycleSet.new :collection => collection diff --git a/app/views/admin/order_cycles/new.rep b/app/views/admin/order_cycles/new.rep new file mode 100644 index 0000000000..4f63b1df42 --- /dev/null +++ b/app/views/admin/order_cycles/new.rep @@ -0,0 +1,7 @@ +r.element :order_cycle, @order_cycle do + r.element :id + r.element :name + r.element :orders_open_at + r.element :orders_close_at + r.element :coordinator_id +end From a19e697556652a3573379a632829581e9dd62c48 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 27 Nov 2012 13:46:59 +1100 Subject: [PATCH 05/31] Add header to admin enterprises page --- app/views/admin/enterprises/index.html.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/admin/enterprises/index.html.erb b/app/views/admin/enterprises/index.html.erb index c1e0869b86..c9c2f76040 100644 --- a/app/views/admin/enterprises/index.html.erb +++ b/app/views/admin/enterprises/index.html.erb @@ -7,6 +7,8 @@
    +

    Enterprises

    + <%= form_for @enterprise_set, :url => main_app.bulk_update_admin_enterprises_path do |f| %> From aacc36ea440d7b84807d5a3b6e6b27a0d3c116d5 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 29 Nov 2012 10:02:23 +1100 Subject: [PATCH 06/31] Create order cycle basic fields --- app/assets/javascripts/admin/order_cycle.js | 20 +++++++++++ .../admin/order_cycles_controller.rb | 15 +++++++++ app/models/order_cycle.rb | 2 +- app/views/admin/order_cycles/index.html.haml | 6 ++++ app/views/admin/order_cycles/new.html.haml | 28 ++++++++++++++++ spec/requests/admin/order_cycles_spec.rb | 33 +++++++++++++++++-- 6 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/admin/order_cycle.js create mode 100644 app/views/admin/order_cycles/new.html.haml diff --git a/app/assets/javascripts/admin/order_cycle.js b/app/assets/javascripts/admin/order_cycle.js new file mode 100644 index 0000000000..4b19b19316 --- /dev/null +++ b/app/assets/javascripts/admin/order_cycle.js @@ -0,0 +1,20 @@ +function AdminOrderCycleCtrl($scope, $http) { + $http.get('/admin/order_cycles/new.json').success(function(data) { + $scope.order_cycle = data; + }); + + $scope.submit = function() { + $http.post('/admin/order_cycles', {order_cycle: $scope.order_cycle}).success(function(data) { + if(data['success']) { + window.location = '/admin/order_cycles'; + } else { + console.log('fail'); + } + }); + }; +} + +angular.module('order_cycle', []). + config(function($httpProvider) { + $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content'); + }); diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 6be2bd308c..f82b1b2b72 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -9,6 +9,21 @@ module Admin end end + def create + @order_cycle = OrderCycle.new(params[:order_cycle]) + respond_to do |format| + if @order_cycle.save + flash[:notice] = 'Your order cycle has been created.' + format.html { redirect_to admin_order_cycles_path } + format.json { render :json => {:success => true} } + else + format.html + format.json { render :json => {:success => false} } + end + end + end + + private def load_order_cycle_set diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 0983695db6..ea580f4f46 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -5,7 +5,7 @@ class OrderCycle < ActiveRecord::Base has_many :exchanges, :dependent => :destroy - validates_presence_of :name + validates_presence_of :name, :coordinator_id def suppliers diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index b5bdf735c3..cb20497993 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -1,3 +1,9 @@ +.toolbar{'data-hook' => "toolbar"} + %ul.actions + %li + = button_link_to "New Order Cycle", main_app.new_admin_order_cycle_path, :icon => 'add', :id => 'admin_new_order_cycle_link' + %br.clear/ + %h1 Order Cycles = form_for @order_cycle_set, :url => main_app.bulk_update_admin_order_cycles_path do |f| diff --git a/app/views/admin/order_cycles/new.html.haml b/app/views/admin/order_cycles/new.html.haml new file mode 100644 index 0000000000..e6b7cf8dcb --- /dev/null +++ b/app/views/admin/order_cycles/new.html.haml @@ -0,0 +1,28 @@ +%h1 New Order Cycle + += form_for [main_app, :admin, @order_cycle], :url => '', :html => {'ng-app' => 'order_cycle', 'ng-controller' => 'AdminOrderCycleCtrl', 'ng-submit' => 'submit()'} do |f| + = f.label :name + = f.text_field :name, 'ng-model' => 'order_cycle.name' + %br/ + + = f.label :orders_open_at, 'Orders open' + = f.text_field :orders_open_at, 'ng-model' => 'order_cycle.orders_open_at' + = f.label :orders_close_at, 'Orders close' + = f.text_field :orders_close_at, 'ng-model' => 'order_cycle.orders_close_at' + %br/ + + %h2 Incoming + %p TODO + + %h2 Coordinator + = f.label :coordinator_id, 'Coordinator' + = f.collection_select :coordinator_id, Enterprise.all, :id, :name, {}, {'ng-model' => 'order_cycle.coordinator_id'} + + %h2 Outgoing + %p TODO + + = f.submit 'Create' + or + = link_to 'Cancel', main_app.admin_order_cycles_path + + %pre {{ order_cycle | json }} diff --git a/spec/requests/admin/order_cycles_spec.rb b/spec/requests/admin/order_cycles_spec.rb index 73273ed8d6..ea2e2401b5 100644 --- a/spec/requests/admin/order_cycles_spec.rb +++ b/spec/requests/admin/order_cycles_spec.rb @@ -8,24 +8,51 @@ feature %q{ include WebHelper scenario "listing order cycles" do + # Given an order cycle oc = create(:order_cycle) + # When I go to the admin order cycles page login_to_admin_section click_link 'Order Cycles' - # Regular fields + # Then I should see the basic fields page.should have_selector 'a', text: oc.name page.should have_selector "input[value='#{oc.orders_open_at}']" page.should have_selector "input[value='#{oc.orders_close_at}']" page.should have_content oc.coordinator.name - # Suppliers and distributors + # And I should see the suppliers and distributors oc.suppliers.each { |s| page.should have_content s.name } oc.distributors.each { |d| page.should have_content d.name } - # Products + # And I should see a thumbnail image for each product all('td.products img').count.should == 2 end + scenario "creating an order cycle" do + # Given a coordinating enterprise + create(:enterprise, name: 'My coordinator') + + # When I go to the new order cycle page + login_to_admin_section + click_link 'Order Cycles' + click_link 'New Order Cycle' + + # And I fill in the basic fields and click Create + fill_in 'order_cycle_name', with: 'Plums & Avos' + fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' + select 'My coordinator', from: 'order_cycle_coordinator_id' + click_button 'Create' + + # Then my order cycle should have been created + page.should have_content 'Your order cycle has been created.' + + page.should have_selector 'a', text: 'Plums & Avos' + + page.should have_selector "input[value='2012-11-06 06:00:00 UTC']" + page.should have_selector "input[value='2012-11-13 17:00:00 UTC']" + page.should have_content 'My coordinator' + end end From 64d4e405dd2038f35d624f3682c8ec25bb07bd82 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 29 Nov 2012 10:25:33 +1100 Subject: [PATCH 07/31] Add datetimepicker for order cycle opening and closing times --- app/assets/javascripts/admin/all.js | 4 +- app/assets/javascripts/admin/util.js.erb | 14 + .../javascripts/{ => shared}/angular.js | 0 .../shared/jquery-ui-timepicker-addon.js | 1882 +++++++++++++++++ app/assets/stylesheets/admin/all.css | 2 + .../shared/jquery-ui-timepicker-addon.css | 10 + app/views/admin/order_cycles/new.html.haml | 4 +- 7 files changed, 1913 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/admin/util.js.erb rename app/assets/javascripts/{ => shared}/angular.js (100%) create mode 100644 app/assets/javascripts/shared/jquery-ui-timepicker-addon.js create mode 100644 app/assets/stylesheets/shared/jquery-ui-timepicker-addon.css diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index e51c920bb8..f7f8d29adb 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -7,7 +7,9 @@ //= require jquery //= require jquery_ujs -//= require angular +//= require jquery-ui +//= require shared/jquery-ui-timepicker-addon +//= require shared/angular //= require admin/spree_core //= require admin/spree_auth //= require admin/spree_promo diff --git a/app/assets/javascripts/admin/util.js.erb b/app/assets/javascripts/admin/util.js.erb new file mode 100644 index 0000000000..5287c5d034 --- /dev/null +++ b/app/assets/javascripts/admin/util.js.erb @@ -0,0 +1,14 @@ +$(document).ready(function() { + $('.datetimepicker').datetimepicker({ + 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: "button", + buttonImage: "<%= asset_path 'datepicker/cal.gif' %>", + buttonImageOnly: true, + stepMinute: 15 + }); +}); \ No newline at end of file diff --git a/app/assets/javascripts/angular.js b/app/assets/javascripts/shared/angular.js similarity index 100% rename from app/assets/javascripts/angular.js rename to app/assets/javascripts/shared/angular.js diff --git a/app/assets/javascripts/shared/jquery-ui-timepicker-addon.js b/app/assets/javascripts/shared/jquery-ui-timepicker-addon.js new file mode 100644 index 0000000000..b8616da378 --- /dev/null +++ b/app/assets/javascripts/shared/jquery-ui-timepicker-addon.js @@ -0,0 +1,1882 @@ +/* + * jQuery timepicker addon + * By: Trent Richardson [http://trentrichardson.com] + * Version 1.1.1 + * Last Modified: 11/07/2012 + * + * Copyright 2012 Trent Richardson + * You may use this project under MIT or GPL licenses. + * http://trentrichardson.com/Impromptu/GPL-LICENSE.txt + * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt + */ + +/*jslint evil: true, white: false, undef: false, nomen: false */ + +(function($) { + + /* + * Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded" + */ + $.ui.timepicker = $.ui.timepicker || {}; + if ($.ui.timepicker.version) { + return; + } + + /* + * Extend jQueryUI, get it started with our version number + */ + $.extend($.ui, { + timepicker: { + version: "1.1.1" + } + }); + + /* + * Timepicker manager. + * Use the singleton instance of this class, $.timepicker, to interact with the time picker. + * Settings for (groups of) time pickers are maintained in an instance object, + * allowing multiple different settings on the same page. + */ + function Timepicker() { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + currentText: 'Now', + closeText: 'Done', + amNames: ['AM', 'A'], + pmNames: ['PM', 'P'], + timeFormat: 'HH:mm', + timeSuffix: '', + timeOnlyTitle: 'Choose Time', + timeText: 'Time', + hourText: 'Hour', + minuteText: 'Minute', + secondText: 'Second', + millisecText: 'Millisecond', + timezoneText: 'Time Zone', + isRTL: false + }; + this._defaults = { // Global defaults for all the datetime picker instances + showButtonPanel: true, + timeOnly: false, + showHour: true, + showMinute: true, + showSecond: false, + showMillisec: false, + showTimezone: false, + showTime: true, + stepHour: 1, + stepMinute: 1, + stepSecond: 1, + stepMillisec: 1, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + timezone: null, + useLocalTimezone: false, + defaultTimezone: "+0000", + hourMin: 0, + minuteMin: 0, + secondMin: 0, + millisecMin: 0, + hourMax: 23, + minuteMax: 59, + secondMax: 59, + millisecMax: 999, + minDateTime: null, + maxDateTime: null, + onSelect: null, + hourGrid: 0, + minuteGrid: 0, + secondGrid: 0, + millisecGrid: 0, + alwaysSetTime: true, + separator: ' ', + altFieldTimeOnly: true, + altTimeFormat: null, + altSeparator: null, + altTimeSuffix: null, + pickerTimeFormat: null, + pickerTimeSuffix: null, + showTimepicker: true, + timezoneIso8601: false, + timezoneList: null, + addSliderAccess: false, + sliderAccessArgs: null, + controlType: 'slider', + defaultValue: null, + parse: 'strict' + }; + $.extend(this._defaults, this.regional['']); + } + + $.extend(Timepicker.prototype, { + $input: null, + $altInput: null, + $timeObj: null, + inst: null, + hour_slider: null, + minute_slider: null, + second_slider: null, + millisec_slider: null, + timezone_select: null, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + timezone: null, + defaultTimezone: "+0000", + hourMinOriginal: null, + minuteMinOriginal: null, + secondMinOriginal: null, + millisecMinOriginal: null, + hourMaxOriginal: null, + minuteMaxOriginal: null, + secondMaxOriginal: null, + millisecMaxOriginal: null, + ampm: '', + formattedDate: '', + formattedTime: '', + formattedDateTime: '', + timezoneList: null, + units: ['hour','minute','second','millisec'], + control: null, + + /* + * Override the default settings for all instances of the time picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function(settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* + * Create a new Timepicker instance + */ + _newInst: function($input, o) { + var tp_inst = new Timepicker(), + inlineSettings = {}, + fns = {}, + overrides, i; + + for (var attrName in this._defaults) { + if(this._defaults.hasOwnProperty(attrName)){ + var attrValue = $input.attr('time:' + attrName); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + } + overrides = { + beforeShow: function (input, dp_inst) { + if ($.isFunction(tp_inst._defaults.evnts.beforeShow)) { + return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst); + } + }, + onChangeMonthYear: function (year, month, dp_inst) { + // Update the time as well : this prevents the time from disappearing from the $input field. + tp_inst._updateDateTime(dp_inst); + if ($.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)) { + tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); + } + }, + onClose: function (dateText, dp_inst) { + if (tp_inst.timeDefined === true && $input.val() !== '') { + tp_inst._updateDateTime(dp_inst); + } + if ($.isFunction(tp_inst._defaults.evnts.onClose)) { + tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst); + } + } + }; + for (i in overrides) { + if (overrides.hasOwnProperty(i)) { + fns[i] = o[i] || null; + } + } + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, overrides, { + evnts:fns, + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); + }); + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { + return val.toUpperCase(); + }); + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { + return val.toUpperCase(); + }); + + // controlType is string - key to our this._controls + if(typeof(tp_inst._defaults.controlType) === 'string'){ + if($.fn[tp_inst._defaults.controlType] === undefined){ + tp_inst._defaults.controlType = 'select'; + } + tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType]; + } + // controlType is an object and must implement create, options, value methods + else{ + tp_inst.control = tp_inst._defaults.controlType; + } + + if (tp_inst._defaults.timezoneList === null) { + var timezoneList = ['-1200', '-1100', '-1000', '-0930', '-0900', '-0800', '-0700', '-0600', '-0500', '-0430', '-0400', '-0330', '-0300', '-0200', '-0100', '+0000', + '+0100', '+0200', '+0300', '+0330', '+0400', '+0430', '+0500', '+0530', '+0545', '+0600', '+0630', '+0700', '+0800', '+0845', '+0900', '+0930', + '+1000', '+1030', '+1100', '+1130', '+1200', '+1245', '+1300', '+1400']; + + if (tp_inst._defaults.timezoneIso8601) { + timezoneList = $.map(timezoneList, function(val) { + return val == '+0000' ? 'Z' : (val.substring(0, 3) + ':' + val.substring(3)); + }); + } + tp_inst._defaults.timezoneList = timezoneList; + } + + tp_inst.timezone = tp_inst._defaults.timezone; + tp_inst.hour = tp_inst._defaults.hour; + tp_inst.minute = tp_inst._defaults.minute; + tp_inst.second = tp_inst._defaults.second; + tp_inst.millisec = tp_inst._defaults.millisec; + tp_inst.ampm = ''; + tp_inst.$input = $input; + + if (o.altField) { + tp_inst.$altInput = $(o.altField).css({ + cursor: 'pointer' + }).focus(function() { + $input.trigger("focus"); + }); + } + + if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) { + tp_inst._defaults.minDate = new Date(); + } + if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) { + tp_inst._defaults.maxDate = new Date(); + } + + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. + if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); + } + if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); + } + if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); + } + if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); + } + tp_inst.$input.bind('focus', function() { + tp_inst._onFocus(); + }); + + return tp_inst; + }, + + /* + * add our sliders to the calendar + */ + _addTimePicker: function(dp_inst) { + var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val(); + + this.timeDefined = this._parseTime(currDT); + this._limitMinMaxDateTime(dp_inst, false); + this._injectTimePicker(); + }, + + /* + * parse the time string from input value or _setTime + */ + _parseTime: function(timeString, withDate) { + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + if (withDate || !this._defaults.timeOnly) { + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); + try { + var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); + if (!parseRes.timeObj) { + return false; + } + $.extend(this, parseRes.timeObj); + } catch (err) { + $.datepicker.log("Error parsing the date/time string: " + err + + "\ndate/time string = " + timeString + + "\ntimeFormat = " + this._defaults.timeFormat + + "\ndateFormat = " + dp_dateFormat); + return false; + } + return true; + } else { + var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); + if (!timeObj) { + return false; + } + $.extend(this, timeObj); + return true; + } + }, + + /* + * generate and inject html for timepicker into ui datepicker + */ + _injectTimePicker: function() { + var $dp = this.inst.dpDiv, + o = this.inst.settings, + tp_inst = this, + litem = '', + uitem = '', + max = {}, + gridSize = {}, + size = null; + + // Prevent displaying twice + if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) { + var noDisplay = ' style="display:none;"', + html = '
    ' + '
    ' + o.timeText + '
    ' + + '
    '; + + // Create the markup + for(var i=0,l=this.units.length; i' + o[litem +'Text'] + '' + + '
    '; + + if (o['show'+uitem] && o[litem+'Grid'] > 0) { + html += '
    '; + + if(litem == 'hour'){ + for (var h = o[litem+'Min']; h <= max[litem]; h += parseInt(o[litem+'Grid'], 10)) { + gridSize[litem]++; + var tmph = $.datepicker.formatTime(useAmpm(o.pickerTimeFormat || o.timeFormat)? 'hht':'HH', {hour:h}, o); + html += ''; + } + } + else{ + for (var m = o[litem+'Min']; m <= max[litem]; m += parseInt(o[litem+'Grid'], 10)) { + gridSize[litem]++; + html += ''; + } + } + + html += '
    ' + tmph + '' + ((m < 10) ? '0' : '') + m + '
    '; + } + html += ''; + } + + // Timezone + html += '
    ' + o.timezoneText + '
    '; + html += '
    '; + + // Create the elements from string + html += ''; + var $tp = $(html); + + // if we only want time picker... + if (o.timeOnly === true) { + $tp.prepend('
    ' + '
    ' + o.timeOnlyTitle + '
    ' + '
    '); + $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); + } + + // add sliders, adjust grids, add events + for(var i=0,l=tp_inst.units.length; i 0) { + size = 100 * gridSize[litem] * o[litem+'Grid'] / (max[litem] - o[litem+'Min']); + $tp.find('.ui_tpicker_'+litem+' table').css({ + width: size + "%", + marginLeft: o.isRTL? '0' : ((size / (-2 * gridSize[litem])) + "%"), + marginRight: o.isRTL? ((size / (-2 * gridSize[litem])) + "%") : '0', + borderCollapse: 'collapse' + }).find("td").click(function(e){ + var $t = $(this), + h = $t.html(), + n = parseInt(h.replace(/[^0-9]/g),10), + ap = h.replace(/[^apm]/ig), + f = $t.data('for'); // loses scope, so we use data-for + + if(f == 'hour'){ + if(ap.indexOf('p') !== -1 && n < 12){ + n += 12; + } + else{ + if(ap.indexOf('a') !== -1 && n === 12){ + n = 0; + } + } + } + + tp_inst.control.value(tp_inst, tp_inst[f+'_slider'], litem, n); + + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }) + .css({ + cursor: 'pointer', + width: (100 / gridSize[litem]) + '%', + textAlign: 'center', + overflow: 'hidden' + }); + } // end if grid > 0 + } // end for loop + + // Add timezone options + this.timezone_select = $tp.find('.ui_tpicker_timezone').append('').find("select"); + $.fn.append.apply(this.timezone_select, + $.map(o.timezoneList, function(val, idx) { + return $("