From 357040f67aa063354f60b54659468726befe876b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Jul 2016 15:39:29 +1000 Subject: [PATCH] Enterprise User can create and update schedules via OC index --- .../order_cycles_controller.js.coffee | 13 +- .../order_cycles_selector.js.coffee | 14 +- .../directives/schedule_dialog.js.coffee | 43 ++--- .../services/schedule_resource.js.coffee | 2 + .../order_cycles/services/schedules.js.coffee | 23 ++- .../resources/services/order_cycles.js.coffee | 19 -- .../utils/services/dialog_defaults.js.coffee | 3 +- .../admin/order_cycles_selector.html.haml | 1 + .../templates/admin/schedule_dialog.html.haml | 15 +- .../admin/openfoodnetwork.css.scss | 3 + app/controllers/admin/schedules_controller.rb | 21 ++- app/models/schedule.rb | 2 + app/models/spree/ability_decorator.rb | 3 + .../admin/order_cycles/_header.html.haml | 4 +- app/views/admin/order_cycles/_row.html.haml | 14 +- app/views/admin/order_cycles/index.html.haml | 2 +- config/locales/en.yml | 5 +- config/routes.rb | 2 +- lib/open_food_network/permissions.rb | 7 +- .../admin/schedules_controller_spec.rb | 167 ++++++++++++++++++ spec/factories.rb | 4 + spec/features/admin/schedules_spec.rb | 90 ++++++++++ .../order_cycles_controller_spec.js.coffee | 34 ++-- 23 files changed, 391 insertions(+), 100 deletions(-) create mode 100644 spec/controllers/admin/schedules_controller_spec.rb create mode 100644 spec/features/admin/schedules_spec.rb diff --git a/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee index 2f5d6d9b5c..63f5902efa 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises, Schedules) -> +angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises, Schedules, Dereferencer) -> $scope.RequestMonitor = RequestMonitor $scope.columns = Columns.columns $scope.saveAll = -> OrderCycles.saveChanges($scope.order_cycles_form) @@ -7,10 +7,13 @@ angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, C compileData = -> for schedule in $scope.schedules - Schedules.linkToOrderCycles(schedule) + Dereferencer.dereference(schedule.order_cycles, OrderCycles.orderCyclesByID) for orderCycle in $scope.orderCycles - OrderCycles.linkToEnterprises(orderCycle) - OrderCycles.linkToSchedules(orderCycle) + coordinator = Enterprises.byID[orderCycle.coordinator.id] + orderCycle.coordinator = coordinator if coordinator? + Dereferencer.dereference(orderCycle.producers, Enterprises.byID) + Dereferencer.dereference(orderCycle.shops, Enterprises.byID) + Dereferencer.dereference(orderCycle.schedules, Schedules.byID) orderCycle.involvedEnterpriseIDs = [orderCycle.coordinator.id] orderCycle.producerNames = orderCycle.producers.map((producer) -> orderCycle.involvedEnterpriseIDs.push(producer.id); producer.name).join(", ") orderCycle.shopNames = orderCycle.shops.map((shop) -> orderCycle.involvedEnterpriseIDs.push(shop.id); shop.name).join(", ") @@ -29,8 +32,8 @@ angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, C existingIDs = Object.keys(OrderCycles.orderCyclesByID) RequestMonitor.load (orderCycles = OrderCycles.index(ams_prefix: "index", "q[orders_close_at_gt]": "#{daysFromToday($scope.ordersCloseAtLimit)}", "q[id_not_in][]": existingIDs)).$promise orderCycles.$promise.then -> - compileData() $scope.orderCycles.push(orderCycle) for orderCycle in orderCycles + compileData() daysFromToday = (days) -> now = new Date diff --git a/app/assets/javascripts/admin/order_cycles/directives/order_cycles_selector.js.coffee b/app/assets/javascripts/admin/order_cycles/directives/order_cycles_selector.js.coffee index 6ad521331f..0b472a6b39 100644 --- a/app/assets/javascripts/admin/order_cycles/directives/order_cycles_selector.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/directives/order_cycles_selector.js.coffee @@ -2,14 +2,8 @@ angular.module("admin.orderCycles").directive 'orderCyclesSelector', (OrderCycle restrict: 'C' templateUrl: 'admin/order_cycles_selector.html' link: (scope, element, attr) -> - if scope.scheduleID? - scope.selectedOrderCycles = Schedules.byID[scope.scheduleID].orderCycles - scope.orderCycleIDs = scope.selectedOrderCycles.map (i, orderCycle) -> orderCycle.id - else - scope.selectedOrderCycles = [] - - scope.availableOrderCycles = (orderCycle for id, orderCycle of OrderCycles.orderCyclesByID when orderCycle not in scope.selectedOrderCycles) - + scope.selectedOrderCycles = (orderCycle for id, orderCycle of OrderCycles.orderCyclesByID when orderCycle.id in scope.schedule.order_cycle_ids) + scope.availableOrderCycles = (orderCycle for id, orderCycle of OrderCycles.orderCyclesByID when orderCycle.id not in scope.schedule.order_cycle_ids) element.find('#available-order-cycles .order-cycles').sortable connectWith: '#selected-order-cycles .order-cycles' @@ -17,6 +11,6 @@ angular.module("admin.orderCycles").directive 'orderCyclesSelector', (OrderCycle element.find('#selected-order-cycles .order-cycles').sortable connectWith: '#available-order-cycles .order-cycles' receive: (event, ui) -> - scope.orderCycleIDs = $('#selected-order-cycles .order-cycles').children('.order-cycle').map((i, element) -> $(element).scope().orderCycle.id).get() + scope.schedule.order_cycle_ids = $('#selected-order-cycles .order-cycles').children('.order-cycle').map((i, element) -> $(element).scope().orderCycle.id).get() remove: (event, ui) -> - scope.orderCycleIDs = $('#selected-order-cycles .order-cycles').children('.order-cycle').map((i, element) -> $(element).scope().orderCycle.id).get() + scope.schedule.order_cycle_ids = $('#selected-order-cycles .order-cycles').children('.order-cycle').map((i, element) -> $(element).scope().orderCycle.id).get() diff --git a/app/assets/javascripts/admin/order_cycles/directives/schedule_dialog.js.coffee b/app/assets/javascripts/admin/order_cycles/directives/schedule_dialog.js.coffee index 4d09cfb80c..4c66a5886e 100644 --- a/app/assets/javascripts/admin/order_cycles/directives/schedule_dialog.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/directives/schedule_dialog.js.coffee @@ -1,40 +1,43 @@ -angular.module("admin.orderCycles").directive 'scheduleDialog', ($compile, $injector, $templateCache, DialogDefaults, Schedules) -> +angular.module("admin.orderCycles").directive 'scheduleDialog', ($window, $compile, $injector, $templateCache, DialogDefaults, Schedules) -> restrict: 'A' scope: - scheduleID: '@' + scheduleId: '@' link: (scope, element, attr) -> - scope.submitted = false - scope.name = "" - scope.orderCycleIDs = [] - scope.errors = [] + # Link opening of dialog to click event on element + element.bind 'click', (e) -> + existing = Schedules.byID[scope.scheduleId] + scope.schedule = + id: existing?.id + name: existing?.name || '' + order_cycle_ids: existing?.order_cycle_ids || [] + scope.submitted = false + scope.errors = [] + # Compile modal template + scope.template = $compile($templateCache.get('admin/schedule_dialog.html'))(scope) + # Set Dialog options + settings = angular.copy(DialogDefaults) + scope.template.dialog(angular.extend(settings,{width: $window.innerWidth * 0.6})) + scope.template.dialog(close: -> scope.template.remove()) + scope.template.dialog('open') scope.close = -> scope.template.dialog('close') return - scope.addSchedule = -> + scope.submit = -> scope.schedule_form.$setPristine() scope.submitted = true scope.errors = [] + return scope.errors.push("Please select at least one order cycle") unless scope.schedule.order_cycle_ids.length > 0 if scope.schedule_form.$valid - Schedules.add({name: scope.name, order_cycle_ids: scope.orderCycleIDs}).$promise.then (data) -> + method = if scope.schedule.id? then Schedules.update else Schedules.add + method(scope.schedule).$promise.then (data) -> if data.id - scope.name = "" - scope.orderCycleIDs = "" scope.submitted = false - template.dialog('close') + scope.template.dialog('close') , (response) -> if response.data.errors scope.errors.push(error) for error in response.data.errors else scope.errors.push("Sorry! Could not create '#{scope.name}'") return - - # Link opening of dialog to click event on element - element.bind 'click', (e) -> - # Compile modal template - scope.template = $compile($templateCache.get('admin/schedule_dialog.html'))(scope) - # Set Dialog options - scope.template.dialog(DialogDefaults) - scope.template.dialog(close: -> scope.template.remove()) - scope.template.dialog('open') diff --git a/app/assets/javascripts/admin/order_cycles/services/schedule_resource.js.coffee b/app/assets/javascripts/admin/order_cycles/services/schedule_resource.js.coffee index 8f1443d99d..127f68ca60 100644 --- a/app/assets/javascripts/admin/order_cycles/services/schedule_resource.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/schedule_resource.js.coffee @@ -7,4 +7,6 @@ angular.module("admin.orderCycles").factory 'ScheduleResource', ($resource) -> method: 'POST' 'update': method: 'PUT' + params: + id: '@id' }) diff --git a/app/assets/javascripts/admin/order_cycles/services/schedules.js.coffee b/app/assets/javascripts/admin/order_cycles/services/schedules.js.coffee index ed58981d66..932818ea26 100644 --- a/app/assets/javascripts/admin/order_cycles/services/schedules.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/schedules.js.coffee @@ -1,11 +1,23 @@ -angular.module("admin.orderCycles").factory "Schedules", ($q, RequestMonitor, ScheduleResource) -> +angular.module("admin.orderCycles").factory "Schedules", ($q, RequestMonitor, ScheduleResource, OrderCycles, Dereferencer) -> new class Schedules byID: {} # all: [] - add: (params) -> + add: (params) => ScheduleResource.create params, (schedule) => - @byID[schedule.id] = schedule if schedule.id + @byID[schedule.id] = schedule if schedule.id? + Dereferencer.dereference(schedule.order_cycles, OrderCycles.orderCyclesByID) + orderCycle.schedules.push(schedule) for orderCycle in schedule.order_cycles + + update: (params) => + ScheduleResource.update params, (schedule) => + if schedule.id? + Dereferencer.dereference(schedule.order_cycles, OrderCycles.orderCyclesByID) + for orderCycle in @byID[schedule.id].order_cycles when orderCycle.id not in schedule.order_cycle_ids + orderCycle.schedules.splice(i, 1) for s, i in orderCycle.schedules by -1 when s.id == schedule.id + for orderCycle in schedule.order_cycles when orderCycle.id not in @byID[schedule.id].order_cycle_ids + orderCycle.schedules.push(@byID[schedule.id]) + angular.extend(@byID[schedule.id], schedule) # remove: (schedule) -> # params = id: schedule.id @@ -26,8 +38,3 @@ angular.module("admin.orderCycles").factory "Schedules", ($q, RequestMonitor, Sc # @all = data RequestMonitor.load(request.$promise) request - - linkToOrderCycles: (schedule) -> - for orderCycle, i in schedule.orderCycles - orderCycle = OrderCycles.orderCyclesByID[orderCycle.id] - schedule.orderCycles[i] = orderCycle if orderCycle? diff --git a/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee b/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee index 9dab7ceb98..4feeb980db 100644 --- a/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee +++ b/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee @@ -66,22 +66,3 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy resetAttribute: (order_cycle, attribute) -> order_cycle[attribute] = @pristineByID[order_cycle.id][attribute] - - linkAllToEnterprises: -> - for id, orderCycle of @orderCyclesByID - @linkToEnterprises(orderCycle) - - linkToEnterprises: (orderCycle) -> - coordinator = Enterprises.enterprisesByID[orderCycle.coordinator.id] - orderCycle.coordinator = coordinator if coordinator? - for producer, i in orderCycle.producers - producer = Enterprises.enterprisesByID[producer.id] - orderCycle.producers[i] = producer if producer? - for shop, i in orderCycle.shops - shop = Enterprises.enterprisesByID[shop.id] - orderCycle.shops[i] = shop if shop? - - linkToSchedules: (orderCycle) -> - for schedule, i in orderCycle.schedules - schedule = Schedules.byID[schedule.id] - orderCycle.schedules[i] = schedule if schedule? diff --git a/app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee b/app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee index 7afb65e82d..a7d92d83f9 100644 --- a/app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee +++ b/app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee @@ -3,7 +3,8 @@ angular.module("admin.utils").factory "DialogDefaults", ($window) -> hide: { effect: "fade", duration: 300 } autoOpen: false resizable: false - width: $window.innerWidth * 0.4; + width: $window.innerWidth * 0.4 + position: ['middle', 100] modal: true open: (event, ui) -> $('.ui-widget-overlay').bind 'click', -> diff --git a/app/assets/javascripts/templates/admin/order_cycles_selector.html.haml b/app/assets/javascripts/templates/admin/order_cycles_selector.html.haml index 746a2e3a2f..8ecaceb103 100644 --- a/app/assets/javascripts/templates/admin/order_cycles_selector.html.haml +++ b/app/assets/javascripts/templates/admin/order_cycles_selector.html.haml @@ -8,3 +8,4 @@ .order-cycles .order-cycle{ ng: { repeat: 'orderCycle in selectedOrderCycles' } } {{ orderCycle.name }} +.error{ ng: { repeat: "error in errors", bind: "error" } } diff --git a/app/assets/javascripts/templates/admin/schedule_dialog.html.haml b/app/assets/javascripts/templates/admin/schedule_dialog.html.haml index 9f74c2493f..5fd308bc5d 100644 --- a/app/assets/javascripts/templates/admin/schedule_dialog.html.haml +++ b/app/assets/javascripts/templates/admin/schedule_dialog.html.haml @@ -1,19 +1,20 @@ #schedule-dialog .text-normal.margin-bottom-30.text-center - = t('admin.order_cycles.index.add_a_new_schedule') + %span{ ng: { hide: 'schedule.id' } }= t('admin.order_cycles.index.adding_a_new_schedule') + %span{ ng: { show: 'schedule.id' } }= t('admin.order_cycles.index.updating_a_schedule') - %form{ name: 'schedule_form', novalidate: true, ng: { submit: "addSchedule()" }} + %form{ name: 'schedule_form', novalidate: true, ng: { submit: "submit()" }} .text-center.margin-bottom-20 - %input.fullwidth{ type: 'text', name: 'name', required: true, placeholder: t('admin.order_cycles.index.schedule_name_placeholder'), ng: { model: "name" } } - %div{ ng: { show: "submitted && new_schedule_form.$pristine" } } - .error{ ng: { show: "(new_schedule_form.name.$error.required)" } } + %input.fullwidth{ type: 'text', name: 'name', required: true, placeholder: t('admin.order_cycles.index.schedule_name_placeholder'), ng: { model: "schedule.name" } } + %div{ ng: { show: "submitted && schedule_form.$pristine" } } + .error{ ng: { show: "(schedule_form.name.$error.required)" } } = t('admin.order_cycles.index.name_required_error') - .error{ ng: { repeat: "error in errors", bind: "error" } } .order-cycles-selector.text-center.margin-bottom-30 .text-center - %input.button.icon-plus{ type: 'submit', value: t('admin.order_cycles.index.create_schedule') } + %input.button.icon-plus{ type: 'submit', value: t('admin.order_cycles.index.create_schedule'), ng: { hide: 'schedule.id' } } + %input.button.icon-plus{ type: 'submit', value: t('admin.order_cycles.index.update_schedule'), ng: { show: 'schedule.id' } } or %input.button.red.icon-remove{ type: 'button', value: t('actions.cancel'), ng: { click: 'close()' } } diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 4c520108a9..2f575af1bb 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -66,6 +66,9 @@ input.search { background-color: #fa787e; } +a { + cursor:pointer; +} form.order_cycle { h2 { diff --git a/app/controllers/admin/schedules_controller.rb b/app/controllers/admin/schedules_controller.rb index 4b31004fb2..d4b1242f36 100644 --- a/app/controllers/admin/schedules_controller.rb +++ b/app/controllers/admin/schedules_controller.rb @@ -2,12 +2,16 @@ require 'open_food_network/permissions' module Admin class SchedulesController < ResourceController + before_filter :check_editable_order_cycle_ids, only: [:create, :update] + + respond_to :json respond_override create: { json: { - success: lambda { - binding.pry - render_as_json @schedule, editable_schedule_ids: permissions.editable_schedules.pluck(:id) - }, + success: lambda { render_as_json @schedule, editable_schedule_ids: permissions.editable_schedules.pluck(:id) }, + failure: lambda { render json: { errors: @schedule.errors.full_messages }, status: :unprocessable_entity } + } } + respond_override update: { json: { + success: lambda { render_as_json @schedule, editable_schedule_ids: permissions.editable_schedules.pluck(:id) }, failure: lambda { render json: { errors: @schedule.errors.full_messages }, status: :unprocessable_entity } } } @@ -30,6 +34,15 @@ module Admin [:index] end + def check_editable_order_cycle_ids + return unless params[:schedule][:order_cycle_ids] + requested = params[:schedule][:order_cycle_ids] + existing = @schedule.order_cycle_ids + permitted = OrderCycle.where(id: params[:schedule][:order_cycle_ids] + existing).merge(OrderCycle.managed_by(spree_current_user)).pluck(:id) + params[:schedule][:order_cycle_ids] |= (existing - permitted) + params[:schedule][:order_cycle_ids] -= (requested - permitted) + end + def permissions return @permissions unless @permission.nil? @permissions = OpenFoodNetwork::Permissions.new(spree_current_user) diff --git a/app/models/schedule.rb b/app/models/schedule.rb index 3b46307a55..7bba906903 100644 --- a/app/models/schedule.rb +++ b/app/models/schedule.rb @@ -2,4 +2,6 @@ class Schedule < ActiveRecord::Base has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules' attr_accessible :name, :order_cycle_ids + + validates :order_cycles, presence: true end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 38eed4f408..4fcea53b87 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -186,6 +186,9 @@ class AbilityDecorator OrderCycle.accessible_by(user).include? order_cycle end can [:admin, :index, :create], Schedule + can [:admin, :update], Schedule do |schedule| + OpenFoodNetwork::Permissions.new(user).editable_schedules.include? schedule + end can [:bulk_update, :clone, :destroy, :notify_producers], OrderCycle do |order_cycle| user.enterprises.include? order_cycle.coordinator end diff --git a/app/views/admin/order_cycles/_header.html.haml b/app/views/admin/order_cycles/_header.html.haml index 72e00577f8..0d3f9fba8e 100644 --- a/app/views/admin/order_cycles/_header.html.haml +++ b/app/views/admin/order_cycles/_header.html.haml @@ -1,6 +1,6 @@ %colgroup %col{ ng: { show: 'columns.name.visible' } } - %col{ ng: { show: 'columns.schedule.visible' } } + %col{ ng: { show: 'columns.schedules.visible' } } %col{ ng: { show: 'columns.open.visible' }, style: 'width: 20%;' } %col{ ng: { show: 'columns.close.visible' }, style: 'width: 20%;' } - unless simple_index @@ -16,7 +16,7 @@ %tr %th{ ng: { show: 'columns.name.visible' } } =t :name - %th{ ng: { show: 'columns.schedule.visible' } } + %th{ ng: { show: 'columns.schedules.visible' } } =t('admin.order_cycles.index.schedules') %th{ ng: { show: 'columns.open.visible' } } =t :open diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index c1e80f33ac..81651bd88f 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -1,14 +1,16 @@ %tr{ class: "order-cycle-{{orderCycle.id}} {{orderCycle.status}}", ng: { repeat: 'orderCycle in orderCycles | involving:involvingFilter | filter:{name: query} track by orderCycle.id' } } - %td{ ng: { show: 'columns.name.visible' } } + %td.name{ ng: { show: 'columns.name.visible' } } %a{ ng: { href: '{{orderCycle.edit_path}}' } } {{ orderCycle.name }} - %td{ ng: { show: 'columns.schedule.visible' } } - %a{ href: '#', 'schedule-dialog' => true, 'schedule-id' => 'schedule.id', ng: { repeat: 'schedule in orderCycle.schedules'} } - {{ schedule.name }} - %td{ ng: { show: 'columns.open.visible' } } + %td.schedules{ ng: { show: 'columns.schedules.visible' } } + %span{ ng: { repeat: 'schedule in orderCycle.schedules'} } + %a{ 'schedule-dialog' => true, 'schedule-id' => '{{schedule.id}}' } + {{ schedule.name }} + %span{ ng: { show: 'orderCycle.schedules.length == 0'}} None + %td.orders_open_at{ ng: { show: 'columns.open.visible' } } %input.datetimepicker{ id: 'oc{{::orderCycle.id}}_orders_open_at', name: 'oc{{::orderCycle.id}}[orders_open_at]', type: 'text', ng: { if: 'orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_open_at' }, datetimepicker: 'orderCycle.orders_open_at' } %input{ id: 'oc{{::orderCycle.id}}_orders_open_at', name: 'oc{{::orderCycle.id}}[orders_open_at]', type: 'text', ng: { if: '!orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_open_at'}, disabled: true } - %td{ ng: { show: 'columns.close.visible' } } + %td.orders_close_at{ ng: { show: 'columns.close.visible' } } %input.datetimepicker{ id: 'oc{{::orderCycle.id}}_orders_close_at', name: 'oc{{::orderCycle.id}}[orders_close_at]', type: 'text', ng: { if: 'orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_close_at' }, datetimepicker: 'orderCycle.orders_close_at' } %input{ id: 'oc{{::orderCycle.id}}_orders_close_at', name: 'oc{{::orderCycle.id}}[orders_close_at]', type: 'text', ng: { if: '!orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_close_at'}, disabled: true } diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index 84d98a7aeb..c43d41d3fe 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -6,7 +6,7 @@ = content_for :page_actions do %li - %a.button.icon-plus#new-schedule{ href: "#", "schedule-dialog" => true } + %a.button.icon-plus#new-schedule{ "schedule-dialog" => true } = t('admin.order_cycles.index.new_schedule') %li#new_order_cycle_link = button_link_to t(:new_order_cycle), main_app.new_admin_order_cycle_path, icon: 'icon-plus', id: 'admin_new_order_cycle_link' diff --git a/config/locales/en.yml b/config/locales/en.yml index 187ee534de..76dc0ce2b8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -677,8 +677,11 @@ en: index: involving: Involving schedules: Schedules - add_a_new_schedule: Add a new schedule + adding_a_new_schedule: Adding A New Schedule + updating_a_schedule: Updating A Schedule + new_schedule: New Schedule create_schedule: Create Schedule + update_schedule: Update Schedule schedule_name_placeholder: Schedule Name name_required_error: Please enter a name for this schedule name_and_timing_form: diff --git a/config/routes.rb b/config/routes.rb index c8592da54f..4f33e83678 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -175,7 +175,7 @@ Openfoodnetwork::Application.routes.draw do get :status, on: :collection end - resources :schedules, only: [:index, :create], format: :json + resources :schedules, only: [:index, :create, :update], format: :json end namespace :api do diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 84905385ce..e5101dddd9 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -128,14 +128,11 @@ module OpenFoodNetwork end def editable_schedules - Schedule.joins(:order_cycles).where(coordinator_id: managed_enterprises.pluck(:id)).select("DISTINCT schedules.*") + Schedule.joins(:order_cycles).where(order_cycles: { id: OrderCycle.managed_by(@user).pluck(:id) }).select("DISTINCT schedules.*") end def visible_schedules - managed_enterprise_ids = managed_enterprises.pluck(:id) - Schedule.joins(order_cycles: :exchanges) - .where('exchanges.sender_id IN (?) OR exchanges.receiver_id IN (?)', managed_enterprise_ids, managed_enterprise_ids) - .select("DISTINCT schedules.*") + Schedule.joins(:order_cycles).where(order_cycles: { id: OrderCycle.accessible_by(@user).pluck(:id) }).select("DISTINCT schedules.*") end diff --git a/spec/controllers/admin/schedules_controller_spec.rb b/spec/controllers/admin/schedules_controller_spec.rb new file mode 100644 index 0000000000..bd45cac0b0 --- /dev/null +++ b/spec/controllers/admin/schedules_controller_spec.rb @@ -0,0 +1,167 @@ +require 'spec_helper' + +describe Admin::SchedulesController, type: :controller do + include AuthenticationWorkflow + + describe "index" do + let!(:coordinated_order_cycle) { create(:simple_order_cycle) } + let!(:managed_coordinator) { coordinated_order_cycle.coordinator } + let!(:other_order_cycle) { create(:simple_order_cycle, coordinator: create(:enterprise)) } + let!(:coordinated_schedule) { create(:schedule, order_cycles: [coordinated_order_cycle] ) } + let!(:uncoordinated_schedule) { create(:schedule, order_cycles: [other_order_cycle] ) } + + context "html" do + context "where I manage an order cycle coordinator" do + before do + controller.stub spree_current_user: managed_coordinator.owner + end + + it "returns an empty @collection" do + spree_get :index, format: :html + expect(assigns(:collection)).to eq [] + end + end + end + + context "json" do + context "where I manage an order cycle coordinator" do + before do + controller.stub spree_current_user: managed_coordinator.owner + end + + let(:params) { { format: :json } } + + it "scopes @collection to schedules containing order_cycles coordinated by enterprises I manage" do + spree_get :index, params + expect(assigns(:collection)).to eq [coordinated_schedule] + end + + it "serializes the data" do + expect(ActiveModel::ArraySerializer).to receive(:new) + spree_get :index, params + end + end + + context "where I manage an order cycle coordinator" do + it "returns an empty collection" do + spree_get :index, format: :json + expect(assigns(:collection)).to be_nil + end + end + end + end + + describe "update" do + let(:user) { create(:user, enterprise_limit: 10) } + let!(:managed_coordinator) { create(:enterprise, owner: user) } + let!(:managed_enterprise) { create(:enterprise, owner: user) } + let!(:coordinated_order_cycle) { create(:simple_order_cycle, coordinator: managed_coordinator ) } + let!(:coordinated_order_cycle2) { create(:simple_order_cycle, coordinator: managed_enterprise ) } + let!(:uncoordinated_order_cycle) { create(:simple_order_cycle, coordinator: create(:enterprise) ) } + let!(:uncoordinated_order_cycle2) { create(:simple_order_cycle, coordinator: create(:enterprise)) } + let!(:coordinated_schedule) { create(:schedule, order_cycles: [coordinated_order_cycle, uncoordinated_order_cycle] ) } + let!(:uncoordinated_schedule) { create(:schedule, order_cycles: [uncoordinated_order_cycle] ) } + + context "json" do + context "where I manage at least one of the schedule's coordinators" do + render_views + + before do + controller.stub spree_current_user: user + end + + it "allows me to update basic information" do + spree_put :update, format: :json, id: coordinated_schedule.id, schedule: { name: "my awesome schedule" } + expect(JSON.parse(response.body)["id"]).to eq coordinated_schedule.id + expect(JSON.parse(response.body)["name"]).to eq "my awesome schedule" + expect(assigns(:schedule)).to eq coordinated_schedule + expect(coordinated_schedule.reload.name).to eq 'my awesome schedule' + end + + it "allows me to add/remove only order cycles I coordinate to/from the schedule" do + order_cycle_ids = [coordinated_order_cycle2.id, uncoordinated_order_cycle2.id ] + spree_put :update, format: :json, id: coordinated_schedule.id, schedule: { order_cycle_ids: order_cycle_ids } + expect(assigns(:schedule)).to eq coordinated_schedule + # coordinated_order_cycle2 is added, uncoordinated_order_cycle is NOT removed + expect(coordinated_schedule.reload.order_cycles).to include coordinated_order_cycle2, uncoordinated_order_cycle + # coordinated_order_cycle is removed, uncoordinated_order_cycle2 is NOT added + expect(coordinated_schedule.reload.order_cycles).to_not include coordinated_order_cycle, uncoordinated_order_cycle2 + end + end + + context "where I don't manage any of the schedule's coordinators" do + before do + controller.stub spree_current_user: uncoordinated_order_cycle2.coordinator.owner + end + + it "prevents me from updating the schedule" do + spree_put :update, format: :json, id: coordinated_schedule.id, schedule: { name: "my awesome schedule" } + expect(response).to redirect_to spree.unauthorized_path + expect(assigns(:schedule)).to eq nil + expect(coordinated_schedule.name).to_not eq "my awesome schedule" + end + end + end + end + + describe "create" do + let(:user) { create(:user) } + let!(:managed_coordinator) { create(:enterprise, owner: user) } + let!(:coordinated_order_cycle) { create(:simple_order_cycle, coordinator: managed_coordinator ) } + let!(:uncoordinated_order_cycle) { create(:simple_order_cycle, coordinator: create(:enterprise)) } + + def create_schedule(params) + spree_put :create, params + end + + context "json" do + let(:params) { { format: :json, schedule: { name: 'new schedule' } } } + + context 'as an enterprise user' do + before { allow(controller).to receive(:spree_current_user) { user } } + + context "where no order cycles ids are provided" do + it "does not allow me to create the schedule" do + expect { create_schedule params }.to_not change(Schedule, :count) + end + end + + context "where I manage at least one of the order cycles to be added to the schedules" do + before do + params[:schedule].merge!( order_cycle_ids: [coordinated_order_cycle.id, uncoordinated_order_cycle.id] ) + end + + it "allows me to create the schedule, adding only order cycles that I manage" do + expect { create_schedule params }.to change(Schedule, :count).by(1) + schedule = Schedule.last + expect(schedule.order_cycles).to include coordinated_order_cycle + expect(schedule.order_cycles).to_not include uncoordinated_order_cycle + end + end + + context "where I don't manage any of the order cycles to be added to the schedules" do + before do + params[:schedule].merge!( order_cycle_ids: [uncoordinated_order_cycle.id] ) + end + + it "prevents me from creating the schedule" do + expect { create_schedule params }.to_not change(Schedule, :count) + end + end + end + + context 'as an admin user' do + before do + allow(controller).to receive(:spree_current_user) { create(:admin_user) } + params[:schedule].merge!( order_cycle_ids: [coordinated_order_cycle.id, uncoordinated_order_cycle.id] ) + end + + it "allows me to create a schedule" do + expect { create_schedule params }.to change(Schedule, :count).by(1) + schedule = Schedule.last + expect(schedule.order_cycles).to include coordinated_order_cycle, uncoordinated_order_cycle + end + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 600d0aaa65..fc41b1940f 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -133,6 +133,10 @@ FactoryGirl.define do receiver { incoming ? order_cycle.coordinator : FactoryGirl.create(:enterprise) } end + factory :schedule, class: Schedule do + sequence(:name) { |n| "Schedule #{n}" } + end + factory :variant_override, :class => VariantOverride do price 77.77 count_on_hand 11111 diff --git a/spec/features/admin/schedules_spec.rb b/spec/features/admin/schedules_spec.rb new file mode 100644 index 0000000000..b2f49e93ca --- /dev/null +++ b/spec/features/admin/schedules_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +feature 'Schedules', js: true do + include AuthenticationWorkflow + include WebHelper + + context "as an enterprise user" do + let(:user) { create(:user) } + let(:managed_enterprise) { create(:distributor_enterprise, owner: user) } + let(:unmanaged_enterprise) { create(:distributor_enterprise) } + let!(:weekly_schedule) { create(:schedule, name: 'Weekly') } + let!(:fortnightly_schedule) { create(:schedule, name: 'Fortnightly') } + let!(:oc1) { create(:order_cycle, coordinator: managed_enterprise, name: 'oc1', schedules: [weekly_schedule]) } + let!(:oc2) { create(:order_cycle, coordinator: managed_enterprise, name: 'oc2', schedules: [weekly_schedule]) } + let!(:oc3) { create(:order_cycle, coordinator: managed_enterprise, name: 'oc3', schedules: [weekly_schedule]) } + let!(:oc4) { create(:order_cycle, coordinator: unmanaged_enterprise, name: 'oc4', schedules: [weekly_schedule]) } + + before { login_to_admin_as user } + + describe "Adding a new Schedule" do + it "immediately shows the schedule in the order cycle list once created" do + click_link 'Order Cycles' + expect(page).to have_selector ".order-cycle-#{oc1.id}" + find('a', text: 'NEW SCHEDULE').click + + within "#schedule-dialog" do + expect(page).to have_selector '#available-order-cycles .order-cycle', text: oc1.name + expect(page).to have_selector '#available-order-cycles .order-cycle', text: oc2.name + expect(page).to have_selector '#available-order-cycles .order-cycle', text: oc3.name + expect(page).to have_no_selector '#available-order-cycles .order-cycle', text: oc4.name + fill_in 'name', with: "Fortnightly" + find("#available-order-cycles .order-cycle", text: oc1.name).drag_to find("#selected-order-cycles") + find("#available-order-cycles .order-cycle", text: oc3.name).drag_to find("#selected-order-cycles") + click_button "Create Schedule" + end + + within ".order-cycle-#{oc1.id} td.schedules" do + expect(page).to have_selector "a", text: "Weekly" + expect(page).to have_selector "a", text: "Fortnightly" + end + + within ".order-cycle-#{oc2.id} td.schedules" do + expect(page).to have_selector "a", text: "Weekly" + expect(page).to have_no_selector "a", text: "Fortnightly" + end + + within ".order-cycle-#{oc3.id} td.schedules" do + expect(page).to have_selector "a", text: "Weekly" + expect(page).to have_selector "a", text: "Fortnightly" + end + end + end + + describe "updating existing schedules" do + use_short_wait + before do + oc1.update_attributes(schedule_ids: [weekly_schedule.id, fortnightly_schedule.id]) + oc3.update_attributes(schedule_ids: [weekly_schedule.id, fortnightly_schedule.id]) + end + + it "immediately shows updated schedule lists for order cycles" do + click_link 'Order Cycles' + + within ".order-cycle-#{oc1.id} td.schedules" do + find('a', text: "Weekly").click + end + + within "#schedule-dialog" do + find("#selected-order-cycles .order-cycle", text: oc3.name).drag_to find("#available-order-cycles") + click_button "Update Schedule" + end + + within ".order-cycle-#{oc1.id} td.schedules" do + expect(page).to have_selector "a", text: "Weekly" + expect(page).to have_selector "a", text: "Fortnightly" + end + + within ".order-cycle-#{oc2.id} td.schedules" do + expect(page).to have_selector "a", text: "Weekly" + expect(page).to have_no_selector "a", text: "Fortnightly" + end + + within ".order-cycle-#{oc3.id} td.schedules" do + expect(page).to have_no_selector "a", text: "Weekly" + expect(page).to have_selector "a", text: "Fortnightly" + end + end + end + end +end diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee index 6e65e3abea..ecac5dadff 100644 --- a/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/order_cycles/controllers/order_cycles_controller_spec.js.coffee @@ -1,6 +1,6 @@ describe "OrderCyclesCtrl", -> - ctrl = scope = httpBackend = Enterprises = OrderCycles = null - coordinator = producer = shop = orderCycle = null + ctrl = scope = httpBackend = Enterprises = OrderCycles = Schedules = null + coordinator = producer = shop = orderCycle = schedule = null beforeEach -> module "admin.orderCycles" @@ -13,48 +13,62 @@ describe "OrderCyclesCtrl", -> compare: (actual, expected) -> { pass: angular.equals(actual, expected) } - beforeEach inject(($controller, $rootScope, $httpBackend, _OrderCycles_, _Enterprises_) -> + beforeEach inject(($controller, $rootScope, $httpBackend, _OrderCycles_, _Enterprises_, _Schedules_) -> scope = $rootScope.$new() ctrl = $controller httpBackend = $httpBackend Enterprises = _Enterprises_ OrderCycles = _OrderCycles_ + Schedules = _Schedules_ spyOn(window, "daysFromToday").and.returnValue "SomeDate" coordinator = { id: 3, name: "Coordinator" } producer = { id: 1, name: "Producer" } shop = { id: 5, name: "Shop" } - orderCycle = { id: 4, name: "OC1", coordinator: {id: 3}, shops: [{id: 3},{id: 5}], producers: [{id: 1}] } + schedule = { id: 7, name: 'Weekly', order_cycles: [{id: 4}]} + orderCycle = { id: 4, schedules: [{id: 7}], name: "OC1", coordinator: {id: 3}, shops: [{id: 3},{id: 5}], producers: [{id: 1}] } httpBackend.expectGET("/admin/enterprises/visible.json?ams_prefix=basic").respond [coordinator, producer, shop] + httpBackend.expectGET("/admin/schedules.json").respond [schedule] httpBackend.expectGET("/admin/order_cycles.json?ams_prefix=index&q%5Borders_close_at_gt%5D=SomeDate").respond [orderCycle] - ctrl "OrderCyclesCtrl", {$scope: scope, Enterprises: Enterprises, OrderCycles: OrderCycles} + ctrl "OrderCyclesCtrl", {$scope: scope, Enterprises: Enterprises, OrderCycles: OrderCycles, Schedules: Schedules} ) describe "before data is returned", -> it "the RequestMonitor will have a state of loading", -> expect(scope.RequestMonitor.loading).toBe true + it "has not received/stored any data yet", -> + expect(Enterprises.byID["5"]).toBeUndefined() + expect(OrderCycles.orderCyclesByID["4"]).toBeUndefined() + expect(Schedules.byID["7"]).toBeUndefined() + describe "after data is returned", -> beforeEach -> httpBackend.flush() describe "initialisation", -> - it "gets suppliers, adds a blank option as the first in the list", -> - expect(scope.enterprises).toDeepEqual [ { id : '0', name : 'All' }, coordinator, producer, shop ] + it "gets enterprises", -> + expect(scope.enterprises).toDeepEqual [ coordinator, producer, shop ] - it "stores enterprises in an list that is accessible by id", -> - expect(Enterprises.enterprisesByID["5"]).toDeepEqual shop + it "stores enterprises, order cycle and schedules in a list that is accessible by id", -> + expect(Enterprises.byID["5"]).toBeDefined() + expect(OrderCycles.orderCyclesByID["4"]).toBeDefined() + expect(Schedules.byID["7"]).toBeDefined() - it "gets order cycles, with dereferenced coordinator, shops and producers", -> + it "gets order cycles, with dereferenced coordinator, shops and producers, schedules", -> oc = OrderCycles.orderCyclesByID["4"] + s = Schedules.byID["7"] expect(scope.orderCycles).toDeepEqual [oc] expect(oc.coordinator).toDeepEqual coordinator expect(oc.shops).toDeepEqual [coordinator,shop] expect(oc.producers).toDeepEqual [producer] + expect(oc.schedules).toEqual [s] + expect(s.order_cycles).toEqual [oc] expect(oc.shopNames).toEqual "Coordinator, Shop" expect(oc.producerNames).toEqual "Producer" + it "the RequestMonitor will not longer have a state of loading", -> expect(scope.RequestMonitor.loading).toBe false