mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-10 23:07:47 +00:00
PI inventories additions
This commit is contained in:
@@ -53,6 +53,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
$scope.resetProducts()
|
||||
$scope.loading = false
|
||||
|
||||
$timeout ->
|
||||
if $scope.showLatestImport
|
||||
$scope.importDateFilter = $scope.importDates[1].id
|
||||
|
||||
$scope.resetProducts = ->
|
||||
DirtyProducts.clear()
|
||||
|
||||
@@ -25,6 +25,7 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl",
|
||||
|
||||
$scope.resetSelectFilters = ->
|
||||
$scope.producerFilter = 0
|
||||
$scope.importDateFilter = '0'
|
||||
$scope.query = ''
|
||||
|
||||
$scope.resetSelectFilters()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
angular.module("admin.variantOverrides").filter "importDate", ($filter, variantOverrides) ->
|
||||
return (products, hub_id, date) ->
|
||||
return [] if !hub_id
|
||||
return $filter('filter')(products, (product) ->
|
||||
return true if date == 0 or date == undefined or date == '0' or date == ''
|
||||
|
||||
for variant in product.variants
|
||||
for vo in variantOverrides
|
||||
if vo.variant_id == variant.id and vo.import_date == date
|
||||
return true
|
||||
false
|
||||
, true)
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.variantOverrides", ["admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems", 'ngTagsInput'])
|
||||
angular.module("admin.variantOverrides", ["ofn.admin", "admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems", 'ngTagsInput'])
|
||||
|
||||
@@ -3,6 +3,7 @@ require 'open_food_network/spree_api_key_loader'
|
||||
module Admin
|
||||
class VariantOverridesController < ResourceController
|
||||
include OpenFoodNetwork::SpreeApiKeyLoader
|
||||
include EnterprisesHelper
|
||||
|
||||
prepend_before_filter :load_data
|
||||
before_filter :load_collection, only: [:bulk_update]
|
||||
@@ -55,6 +56,10 @@ module Admin
|
||||
variant_override_enterprises_per_hub
|
||||
|
||||
@inventory_items = InventoryItem.where(enterprise_id: @hubs)
|
||||
|
||||
import_dates = [{id: '0', name: 'All'}]
|
||||
inventory_import_dates.map {|i| import_dates.push({id: i, name: i.to_formatted_s(:long)}) }
|
||||
@import_dates = import_dates.to_json
|
||||
end
|
||||
|
||||
def load_collection
|
||||
|
||||
@@ -5,6 +5,7 @@ Spree::Admin::ProductsController.class_eval do
|
||||
include OpenFoodNetwork::SpreeApiKeyLoader
|
||||
include OrderCyclesHelper
|
||||
include EnterprisesHelper
|
||||
before_filter :latest_import, only: [:bulk_edit]
|
||||
before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update]
|
||||
before_filter :load_spree_api_key, :only => [:bulk_edit, :variant_overrides]
|
||||
before_filter :strip_new_properties, only: [:create, :update]
|
||||
@@ -93,6 +94,10 @@ Spree::Admin::ProductsController.class_eval do
|
||||
|
||||
private
|
||||
|
||||
def latest_import
|
||||
@show_latest_import = params[:latest_import] || false
|
||||
end
|
||||
|
||||
def load_form_data
|
||||
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name
|
||||
@taxons = Spree::Taxon.order(:name)
|
||||
|
||||
@@ -53,7 +53,7 @@ class ProductImporter
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false #ActiveModel, not ActiveRecord
|
||||
false # ActiveModel
|
||||
end
|
||||
|
||||
def has_entries?
|
||||
@@ -188,7 +188,7 @@ class ProductImporter
|
||||
rows.each_with_index do |row, i|
|
||||
row_data = Hash[[headers, row].transpose]
|
||||
entry = SpreadsheetEntry.new(row_data)
|
||||
entry.line_number = i+2
|
||||
entry.line_number = i + 2
|
||||
@entries.push entry
|
||||
end
|
||||
@entries
|
||||
@@ -238,12 +238,7 @@ class ProductImporter
|
||||
def create_inventory_item(entry, existing_variant)
|
||||
existing_variant_override = VariantOverride.where(variant_id: existing_variant.id, hub_id: entry.supplier_id).first
|
||||
|
||||
if existing_variant_override
|
||||
variant_override = existing_variant_override
|
||||
else
|
||||
variant_override = VariantOverride.new(variant_id: existing_variant.id, hub_id: entry.supplier_id)
|
||||
end
|
||||
|
||||
variant_override = existing_variant_override || VariantOverride.new(variant_id: existing_variant.id, hub_id: entry.supplier_id)
|
||||
variant_override.assign_attributes(count_on_hand: entry.on_hand, import_date: @import_time)
|
||||
check_on_hand_nil(entry, variant_override)
|
||||
variant_override.assign_attributes(entry.attributes.slice('price', 'on_demand'))
|
||||
@@ -260,7 +255,7 @@ class ProductImporter
|
||||
end
|
||||
|
||||
def mark_as_inventory_item(entry, variant_override)
|
||||
if variant_override.id?
|
||||
if variant_override.id
|
||||
entry.is_a_valid('existing_inventory_item')
|
||||
entry.product_object = variant_override
|
||||
updates_count_per_supplier(entry.supplier_id) unless entry.has_errors?
|
||||
@@ -279,7 +274,8 @@ class ProductImporter
|
||||
where('variant_overrides.hub_id IN (?)', supplier_id).
|
||||
count
|
||||
else
|
||||
products_count = Spree::Variant.joins(:product).
|
||||
products_count = Spree::Variant.
|
||||
joins(:product).
|
||||
where('spree_products.supplier_id IN (?)
|
||||
AND spree_variants.is_master = false
|
||||
AND spree_variants.deleted_at IS NULL', supplier_id).
|
||||
@@ -377,7 +373,7 @@ class ProductImporter
|
||||
@entries.each do |entry|
|
||||
supplier_name = entry.supplier
|
||||
supplier_id = @suppliers_index[supplier_name] ||
|
||||
Enterprise.find_by_name(supplier_name, :select => 'id, name').try(:id)
|
||||
Enterprise.find_by_name(supplier_name, select: 'id, name').try(:id)
|
||||
@suppliers_index[supplier_name] = supplier_id
|
||||
end
|
||||
@suppliers_index
|
||||
@@ -388,7 +384,7 @@ class ProductImporter
|
||||
@entries.each do |entry|
|
||||
producer_name = entry.producer
|
||||
producer_id = @producers_index[producer_name] ||
|
||||
Enterprise.find_by_name(producer_name, :select => 'id, name').try(:id)
|
||||
Enterprise.find_by_name(producer_name, select: 'id, name').try(:id)
|
||||
@producers_index[producer_name] = producer_id
|
||||
end
|
||||
@producers_index
|
||||
@@ -437,6 +433,7 @@ class ProductImporter
|
||||
product = Spree::Product.new()
|
||||
product.assign_attributes(entry.attributes.except('id'))
|
||||
assign_defaults(product, entry)
|
||||
|
||||
if product.save
|
||||
ensure_variant_updated(product, entry)
|
||||
@products_created += 1
|
||||
@@ -473,6 +470,7 @@ class ProductImporter
|
||||
new_item = entry.product_object
|
||||
assign_defaults(new_item, entry)
|
||||
new_item.import_date = @import_time
|
||||
|
||||
if new_item.valid? and new_item.save
|
||||
display_in_inventory(new_item, true)
|
||||
@inventory_created += 1
|
||||
@@ -486,6 +484,7 @@ class ProductImporter
|
||||
existing_item = entry.product_object
|
||||
assign_defaults(existing_item, entry)
|
||||
existing_item.import_date = @import_time
|
||||
|
||||
if existing_item.valid? and existing_item.save
|
||||
display_in_inventory(existing_item)
|
||||
@inventory_updated += 1
|
||||
@@ -499,6 +498,7 @@ class ProductImporter
|
||||
new_variant = entry.product_object
|
||||
assign_defaults(new_variant, entry)
|
||||
new_variant.import_date = @import_time
|
||||
|
||||
if new_variant.valid? and new_variant.save
|
||||
@variants_created += 1
|
||||
@updated_ids.push new_variant.id
|
||||
@@ -511,6 +511,7 @@ class ProductImporter
|
||||
variant = entry.product_object
|
||||
assign_defaults(variant, entry)
|
||||
variant.import_date = @import_time
|
||||
|
||||
if variant.valid? and variant.save
|
||||
@variants_updated += 1
|
||||
@updated_ids.push variant.id
|
||||
@@ -584,7 +585,10 @@ class ProductImporter
|
||||
|
||||
# Otherwise, if a variant exists with matching display_name and unit_value, update it
|
||||
match.variants.each do |existing_variant|
|
||||
if existing_variant.display_name == entry.display_name and existing_variant.unit_value == Float(entry.unit_value)
|
||||
if existing_variant.display_name == entry.display_name \
|
||||
and existing_variant.unit_value == Float(entry.unit_value) \
|
||||
and existing_variant.deleted_at == nil
|
||||
|
||||
mark_as_existing_variant(entry, existing_variant)
|
||||
return
|
||||
end
|
||||
@@ -597,6 +601,7 @@ class ProductImporter
|
||||
def mark_as_new_product(entry)
|
||||
new_product = Spree::Product.new()
|
||||
new_product.assign_attributes(entry.attributes.except('id'))
|
||||
|
||||
if new_product.valid?
|
||||
entry.is_a_valid 'new_product' unless entry.has_errors?
|
||||
else
|
||||
@@ -607,6 +612,7 @@ class ProductImporter
|
||||
def mark_as_existing_variant(entry, existing_variant)
|
||||
existing_variant.assign_attributes(entry.attributes.except('id', 'product_id'))
|
||||
check_on_hand_nil(entry, existing_variant)
|
||||
|
||||
if existing_variant.valid?
|
||||
entry.product_object = existing_variant
|
||||
entry.is_a_valid 'existing_variant' unless entry.has_errors?
|
||||
@@ -620,6 +626,7 @@ class ProductImporter
|
||||
new_variant = Spree::Variant.new(entry.attributes.except('id', 'product_id'))
|
||||
new_variant.product_id = product_id
|
||||
check_on_hand_nil(entry, new_variant)
|
||||
|
||||
if new_variant.valid?
|
||||
entry.product_object = new_variant
|
||||
entry.is_a_valid 'new_variant' unless entry.has_errors?
|
||||
@@ -629,7 +636,8 @@ class ProductImporter
|
||||
end
|
||||
|
||||
def updates_count_per_supplier(supplier_id)
|
||||
if @reset_counts[supplier_id] and @reset_counts[supplier_id][:updates_count]
|
||||
if @reset_counts[supplier_id] \
|
||||
and @reset_counts[supplier_id][:updates_count]
|
||||
@reset_counts[supplier_id][:updates_count] += 1
|
||||
else
|
||||
@reset_counts[supplier_id] = {updates_count: 1}
|
||||
@@ -637,17 +645,17 @@ class ProductImporter
|
||||
end
|
||||
|
||||
def check_on_hand_nil(entry, object)
|
||||
if entry.on_hand.blank?
|
||||
object.on_hand = 0 if object.respond_to?(:on_hand)
|
||||
object.count_on_hand = 0 if object.respond_to?(:count_on_hand)
|
||||
entry.on_hand_nil = true
|
||||
end
|
||||
return unless entry.on_hand.blank?
|
||||
|
||||
object.on_hand = 0 if object.respond_to?(:on_hand)
|
||||
object.count_on_hand = 0 if object.respond_to?(:count_on_hand)
|
||||
entry.on_hand_nil = true
|
||||
end
|
||||
|
||||
def delete_uploaded_file
|
||||
# Only delete if file is in '/tmp/product_import' directory
|
||||
if @file.path == Rails.root.join('tmp', 'product_import').to_s
|
||||
File.delete(@file)
|
||||
end
|
||||
return unless @file.path == Rails.root.join('tmp', 'product_import').to_s
|
||||
|
||||
File.delete(@file)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,10 +46,18 @@
|
||||
- if @importer.errors.count == 0
|
||||
%p All #{@importer.total_saved_count} items saved successfully
|
||||
- else
|
||||
%p #{@importer.total_saved_count} items saved successfully
|
||||
%br
|
||||
%h5 Save errors
|
||||
- @importer.errors.full_messages.each do |error|
|
||||
%p.save-error
|
||||
- #{error}
|
||||
|
||||
%br
|
||||
- if @importer.total_saved_count > 0
|
||||
- if @import_into == 'inventories'
|
||||
%a.button{href: main_app.admin_inventory_path} View Inventory
|
||||
- else
|
||||
%a.button{href: bulk_edit_admin_products_path + '?latest_import=true'} View Products
|
||||
|
||||
%a.button{href: main_app.admin_product_import_path} Back
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
.filters.sixteen.columns.alpha.omega
|
||||
.filter.four.columns.alpha
|
||||
%label{ :for => 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search')
|
||||
%label{for: 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search')
|
||||
%br
|
||||
%input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub_id'} }
|
||||
.two.columns
|
||||
.filter_select.four.columns
|
||||
%label{ :for => 'hub_id', ng: { bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.variant_overrides.index.select_a_shop')}'" } }
|
||||
%input.fullwidth{type: "text", id: 'query', ng: {model: 'query', disabled: '!hub_id'} }
|
||||
.one.columns
|
||||
.filter_select.three.columns
|
||||
%label{for: 'hub_id', ng: {bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.variant_overrides.index.select_a_shop')}'" } }
|
||||
%br
|
||||
%select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs' } }
|
||||
.filter_select.four.columns
|
||||
%label{ :for => 'producer_filter', ng: {class: '{disabled: !hub_id}'} }=t('admin.producer')
|
||||
%select.select2.fullwidth#hub_id{name: 'hub_id', ng: {model: 'hub_id', options: 'hub.id as hub.name for (id, hub) in hubs' } }
|
||||
.filter_select.three.columns
|
||||
%label{for: 'producer_filter', ng: {class: '{disabled: !hub_id}'} }=t('admin.producer')
|
||||
%br
|
||||
%input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: '#{t(:all)}'}", ng: { model: 'producerFilter', disabled: '!hub_id' } }
|
||||
%input.ofn-select2.fullwidth{id: 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: '#{t(:all)}'}", ng: {model: 'producerFilter', disabled: '!hub_id' } }
|
||||
.filter_select.three.columns
|
||||
%label{ :for => 'import_date_filter', ng: {class: '{disabled: !hub_id}'} } #{t('admin.variant_overrides.index.import_date')}
|
||||
%br
|
||||
%select.fullwidth{id: 'import_date_filter', 'ofn-select2-min-search' => 5, ng: {model: 'importDateFilter', options: 'date.id as date.name for date in import_dates', disabled: '!hub_id', init: "import_dates = #{@import_dates}"} }
|
||||
%options{value: '0', selected: 'selected'} #{t(:all)}
|
||||
-# .filter_select{ :class => "three columns" }
|
||||
-# %label{ :for => 'distributor_filter' }Hub
|
||||
-# %br
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
%th.tags{ ng: { show: 'columns.tags.visible' } }=t('admin.tags')
|
||||
%th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.variant_overrides.index.hide')
|
||||
%th.import_date{ ng: { show: 'columns.import_date.visible' } }=t('admin.variant_overrides.index.import_date')
|
||||
%tbody{ ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } }
|
||||
%tbody{ ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | importDate:hub_id:importDateFilter | filter:query) | limitTo:productLimit' } }
|
||||
= render 'admin/variant_overrides/products_product'
|
||||
= render 'admin/variant_overrides/products_variants'
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
.filters.sixteen.columns.alpha.omega
|
||||
.quick_search.three.columns.alpha
|
||||
%label{ :for => 'quick_filter' }
|
||||
%label{ for: 'quick_filter' }
|
||||
%br
|
||||
%input.quick-search.fullwidth{ 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => t('admin.quick_search') }
|
||||
%input.quick-search.fullwidth{ ng: {model: 'query'}, name: "quick_filter", type: 'text', placeholder: t('admin.quick_search') }
|
||||
.one.columns
|
||||
.filter_select.three.columns
|
||||
%label{ :for => 'producer_filter' }= t 'producer'
|
||||
%label{ for: 'producer_filter' }= t 'producer'
|
||||
%br
|
||||
%select.fullwidth{ :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' }
|
||||
%select.fullwidth{ id: 'producer_filter', 'ofn-select2-min-search' => 5, ng: {model: 'producerFilter', options: 'producer.id as producer.name for producer in filterProducers'} }
|
||||
.filter_select.three.columns
|
||||
%label{ :for => 'category_filter' }= t 'category'
|
||||
%label{ for: 'category_filter' }= t 'category'
|
||||
%br
|
||||
%select.fullwidth{ :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'}
|
||||
%select.fullwidth{ id: 'category_filter', 'ofn-select2-min-search' => 5, ng: {model: 'categoryFilter', options: 'taxon.id as taxon.name for taxon in filterTaxons'} }
|
||||
.filter_select.three.columns
|
||||
%label{ :for => 'import_filter' } Import Date
|
||||
%label{ for: 'import_filter' } Import Date
|
||||
%br
|
||||
%select.fullwidth{ :id => 'import_date_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'importDateFilter', 'ng-init' => "import_dates = #{@import_dates}", 'ng-options' => 'import.id as import.name for import in import_dates'}
|
||||
%select.fullwidth{ id: 'import_date_filter', 'ofn-select2-min-search' => 5, ng: {model: 'importDateFilter', init: "importDates = #{@import_dates}; showLatestImport = #{@show_latest_import}"}}
|
||||
%option{value: "{{date.id}}", ng: {repeat: "date in importDates track by date.id" }}
|
||||
{{date.name}}
|
||||
|
||||
%div{ :class => "one column" }
|
||||
.filter_clear.three.columns.omega
|
||||
%label{ :for => 'clear_all_filters' }
|
||||
%label{ for: 'clear_all_filters' }
|
||||
%br
|
||||
%input.fullwidth.red{ :type => 'button', :id => 'clear_all_filters', :value => t('admin.clear_filters'), 'ng-click' => "resetSelectFilters()" }
|
||||
|
||||
@@ -31,7 +31,7 @@ feature "Product Import", js: true do
|
||||
before { quick_login_as_admin }
|
||||
after { File.delete('/tmp/test.csv') }
|
||||
|
||||
it "validates entries and saves them if they are all valid" do
|
||||
it "validates entries and saves them if they are all valid and allows viewing new items in Bulk Products" do
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
|
||||
csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
|
||||
@@ -54,11 +54,19 @@ feature "Product Import", js: true do
|
||||
expect(page).to have_selector '.created-count', text: '2'
|
||||
expect(page).to_not have_selector '.updated-count'
|
||||
|
||||
carrots = Spree::Product.find_by_name('Carrots')
|
||||
potatoes = Spree::Product.find_by_name('Potatoes')
|
||||
potatoes.supplier.should == enterprise
|
||||
potatoes.on_hand.should == 6
|
||||
potatoes.price.should == 6.50
|
||||
potatoes.variants.first.import_date.should be_within(1.minute).of DateTime.now
|
||||
|
||||
click_link 'View Products'
|
||||
|
||||
expect(page).to have_content 'Bulk Edit Products'
|
||||
wait_until { page.find("#p_#{potatoes.id}").present? }
|
||||
expect(page).to have_field "product_name", with: carrots.name
|
||||
expect(page).to have_field "product_name", with: potatoes.name
|
||||
end
|
||||
|
||||
it "displays info about invalid entries but still allows saving of valid entries" do
|
||||
@@ -195,7 +203,7 @@ feature "Product Import", js: true do
|
||||
carrots = Spree::Product.find_by_name('Carrots')
|
||||
carrots.variants.first.import_date.should be_within(1.minute).of DateTime.now
|
||||
|
||||
visit 'admin/products/bulk_edit'
|
||||
click_link 'View Products'
|
||||
|
||||
wait_until { page.find("#p_#{carrots.id}").present? }
|
||||
|
||||
@@ -246,7 +254,6 @@ feature "Product Import", js: true do
|
||||
expect(page).to have_selector '.inv-created-count', text: '2'
|
||||
expect(page).to have_selector '.inv-updated-count', text: '1'
|
||||
|
||||
|
||||
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
|
||||
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
|
||||
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
|
||||
@@ -259,6 +266,17 @@ feature "Product Import", js: true do
|
||||
|
||||
Float(cabbage_override.price).should == 1.50
|
||||
cabbage_override.count_on_hand.should == 2001
|
||||
|
||||
click_link 'View Inventory'
|
||||
expect(page).to have_content 'Inventory'
|
||||
|
||||
select enterprise2.name, from: "hub_id", visible: false
|
||||
|
||||
within '#variant-overrides' do
|
||||
expect(page).to have_content 'Beans'
|
||||
expect(page).to have_content 'Sprouts'
|
||||
expect(page).to have_content 'Cabbage'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -349,8 +367,6 @@ feature "Product Import", js: true do
|
||||
expect(page).to_not have_selector '.invalid-count'
|
||||
expect(page).to have_selector '.inv-create-count', text: "1"
|
||||
|
||||
#expect(page.body).to have_content 'you do not have permission'
|
||||
|
||||
click_button 'Save'
|
||||
|
||||
expect(page).to have_selector '.inv-created-count', text: '1'
|
||||
@@ -379,8 +395,7 @@ feature "Product Import", js: true do
|
||||
expect(page).to_not have_selector '.inv-create-count'
|
||||
|
||||
expect(page.body).to have_content 'you do not have permission'
|
||||
|
||||
|
||||
expect(page).to_not have_selector 'input[type=submit][value="Save"]'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user