Merge branch 'dev'

This commit is contained in:
Mathieu Magnin 2018-01-10 15:52:05 +01:00
commit 8b4afac54e
28 changed files with 602 additions and 32 deletions

View file

@ -109,6 +109,8 @@ gem 'prawn_rails', '~> 0.0.11'
gem 'chunky_png' gem 'chunky_png'
gem 'sentry-raven' gem 'sentry-raven'
gem "administrate"
gem 'rack-mini-profiler' gem 'rack-mini-profiler'
group :test do group :test do

View file

@ -59,6 +59,18 @@ GEM
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.5.1) addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2) public_suffix (~> 2.0, >= 2.0.2)
administrate (0.4.0)
autoprefixer-rails (~> 6.0)
bourbon (~> 4.2)
datetime_picker_rails (~> 0.0.7)
jquery-rails (~> 4.0)
kaminari (~> 0.16)
momentjs-rails (~> 2.8)
neat (~> 1.1)
normalize-rails (~> 3.0)
rails (>= 4.2, < 5.1)
sass-rails (~> 5.0)
selectize-rails (~> 0.6)
apipie-rails (0.3.7) apipie-rails (0.3.7)
json json
arel (7.1.4) arel (7.1.4)
@ -79,6 +91,9 @@ GEM
sass (>= 3.3.4) sass (>= 3.3.4)
bootstrap-wysihtml5-rails (0.3.3.8) bootstrap-wysihtml5-rails (0.3.3.8)
railties (>= 3.0) railties (>= 3.0)
bourbon (4.3.4)
sass (~> 3.4)
thor (~> 0.19)
brakeman (3.7.0) brakeman (3.7.0)
browser (2.3.0) browser (2.3.0)
builder (3.2.3) builder (3.2.3)
@ -116,6 +131,8 @@ GEM
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
daemons (1.2.4) daemons (1.2.4)
database_cleaner (1.5.3) database_cleaner (1.5.3)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
debug_inspector (0.0.2) debug_inspector (0.0.2)
deep_cloneable (2.2.2) deep_cloneable (2.2.2)
activerecord (>= 3.1.0, < 5.2.0) activerecord (>= 3.1.0, < 5.2.0)
@ -396,15 +413,21 @@ GEM
mimemagic (0.3.2) mimemagic (0.3.2)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.10.3) minitest (5.10.3)
momentjs-rails (2.17.1)
railties (>= 3.1)
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
mustermann (1.0.1) mustermann (1.0.1)
neat (1.9.0)
sass (>= 3.3)
thor (~> 0.19)
nenv (0.3.0) nenv (0.3.0)
netrc (0.11.0) netrc (0.11.0)
nio4r (1.2.1) nio4r (1.2.1)
nokogiri (1.8.1) nokogiri (1.8.1)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
normalize-rails (3.0.3)
notiffany (0.1.1) notiffany (0.1.1)
nenv (~> 0.1) nenv (~> 0.1)
shellany (~> 0.0) shellany (~> 0.0)
@ -589,6 +612,7 @@ GEM
securecompare (1.0.0) securecompare (1.0.0)
select2-rails (4.0.3) select2-rails (4.0.3)
thor (~> 0.14) thor (~> 0.14)
selectize-rails (0.12.4.1)
sentry-raven (2.2.0) sentry-raven (2.2.0)
faraday (>= 0.7.6, < 1.0) faraday (>= 0.7.6, < 1.0)
sexp_processor (4.10.0) sexp_processor (4.10.0)
@ -693,6 +717,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers active_model_serializers
administrate
apipie-rails apipie-rails
bootstrap-datepicker-rails bootstrap-datepicker-rails
bootstrap-sass (~> 3.3.5) bootstrap-sass (~> 3.3.5)

View file

@ -0,0 +1,21 @@
module Manager
class AdministrateursController < Manager::ApplicationController
# To customize the behavior of this controller,
# simply overwrite any of the RESTful actions. For example:
#
# def index
# super
# @resources = Administrateur.
# page(params[:page]).
# per(10)
# end
# Define a custom finder by overriding the `find_resource` method:
# def find_resource(param)
# Administrateur.find_by!(slug: param)
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,23 @@
# All Administrate controllers inherit from this `Admin::ApplicationController`,
# making it the ideal place to put authentication logic or other
# before_actions.
#
# If you want to add pagination or other controller-level concerns,
# you're free to overwrite the RESTful controller actions.
module Manager
class ApplicationController < Administrate::ApplicationController
before_action :authenticate_administration!
before_action :default_params
# Override this value to specify the number of elements to display at a time
# on index pages. Defaults to 20.
# def records_per_page
# params[:per_page] || 20
# end
def default_params
params[:order] ||= "created_at"
params[:direction] ||= "desc"
end
end
end

View file

@ -0,0 +1,21 @@
module Manager
class ProceduresController < Manager::ApplicationController
# To customize the behavior of this controller,
# simply overwrite any of the RESTful actions. For example:
#
# def index
# super
# @resources = Procedure.
# page(params[:page]).
# per(10)
# end
# Define a custom finder by overriding the `find_resource` method:
# def find_resource(param)
# Procedure.find_by!(slug: param)
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -0,0 +1,50 @@
require "administrate/base_dashboard"
class AdministrateurDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
email: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
procedures: Field::HasMany,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = [
:email,
:created_at,
:procedures,
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = [
:id,
:email,
:created_at,
:updated_at,
:procedures,
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = [].freeze
# Overwrite this method to customize how procedures are displayed
# across all pages of the admin dashboard.
#
def display_resource(administrateur)
administrateur.email
end
end

View file

@ -0,0 +1,77 @@
require "administrate/base_dashboard"
class ProcedureDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
types_de_piece_justificative: TypesDePieceJustificativeCollectionField,
types_de_champ: TypesDeChampCollectionField,
dossiers: Field::HasMany,
procedure_path: Field::HasOne,
administrateur: Field::BelongsTo,
id: Field::Number,
libelle: Field::String,
description: Field::String,
organisation: Field::String,
direction: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
for_individual: Field::Boolean,
individual_with_siret: Field::Boolean,
auto_archive_on: Field::DateTime,
published_at: Field::DateTime,
hidden_at: Field::DateTime,
archived_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = [
:id,
:created_at,
:libelle,
:organisation,
:dossiers,
:published_at,
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = [
:id,
:administrateur,
:libelle,
:description,
:organisation,
:direction,
:created_at,
:updated_at,
:published_at,
:hidden_at,
:archived_at,
:types_de_champ,
:types_de_piece_justificative,
:for_individual,
:individual_with_siret,
:auto_archive_on,
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = [].freeze
# Overwrite this method to customize how procedures are displayed
# across all pages of the admin dashboard.
#
# def display_resource(procedure)
# "Procedure ##{procedure.id}"
# end
end

View file

@ -0,0 +1,7 @@
require "administrate/field/base"
class TypesDeChampCollectionField < Administrate::Field::Base
def to_s
data
end
end

View file

@ -0,0 +1,7 @@
require "administrate/field/base"
class TypesDePieceJustificativeCollectionField < Administrate::Field::Base
def to_s
data
end
end

View file

@ -2,7 +2,9 @@ class TypesDeChampService
def self.create_update_procedure_params(params, private=false) def self.create_update_procedure_params(params, private=false)
attributes = (private ? 'types_de_champ_private_attributes' : 'types_de_champ_attributes') attributes = (private ? 'types_de_champ_private_attributes' : 'types_de_champ_attributes')
parameters = params params_with_ordered_champs = order_champs(params, attributes)
parameters = params_with_ordered_champs
.require(:procedure) .require(:procedure)
.permit("#{attributes}" => [:libelle, :description, :order_place, :type_champ, :id, :mandatory, :type, .permit("#{attributes}" => [:libelle, :description, :order_place, :type_champ, :id, :mandatory, :type,
drop_down_list_attributes: [:value, :id]]) drop_down_list_attributes: [:value, :id]])
@ -22,6 +24,40 @@ class TypesDeChampService
private private
def self.order_champs(params, attributes)
tdcas = params[:procedure][attributes].to_a
.map { |_hash_index, tdca| tdca }
tdcas
.select { |tdca| !is_number?(tdca[:custom_order_place]) }
.each { |tdca| tdca[:custom_order_place] = (tdca[:order_place].to_i + 1).to_s }
changed_order_tdcas, ordered_tdcas = tdcas.partition { |tdca| tdca_order_changed?(tdca) }
go_up_tdcas, go_down_tdcas = changed_order_tdcas
.partition { |tdca| tdca[:custom_order_place].to_i < (tdca[:order_place].to_i + 1) }
# needed to make the sort_by work properly
tdcas = go_up_tdcas + ordered_tdcas + go_down_tdcas
ordered_tdcas = tdcas
.sort_by { |tdca| tdca[:custom_order_place].to_i }
.each_with_index { |tdca, index| tdca[:order_place] = index.to_s }
.each_with_index.reduce({}) { |acc, (tdca, hash_index)| acc[hash_index.to_s] = tdca; acc }
params[:procedure][attributes] = ActionController::Parameters.new(ordered_tdcas)
params
end
def self.is_number?(value)
(value =~ /^[0-9]+$/) == 0
end
def self.tdca_order_changed?(tdca)
(tdca[:order_place].to_i + 1) != tdca[:custom_order_place].to_i
end
def self.clean_value value def self.clean_value value
value.split("\r\n").map{ |v| v.strip }.join("\r\n") value.split("\r\n").map{ |v| v.strip }.join("\r\n")
end end

View file

@ -38,6 +38,10 @@
= ff.object.button_up(index: ff.index, url: @types_de_champ_facade.move_up_url(ff), private: @types_de_champ_facade.private) = ff.object.button_up(index: ff.index, url: @types_de_champ_facade.move_up_url(ff), private: @types_de_champ_facade.private)
= ff.object.button_down(index: ff.index, url: @types_de_champ_facade.move_down_url(ff), private: @types_de_champ_facade.private) = ff.object.button_down(index: ff.index, url: @types_de_champ_facade.move_down_url(ff), private: @types_de_champ_facade.private)
.form-group
%h4 position
= ff.number_field :custom_order_place, value: (ff.index + 1), length: 999, class: 'form-control', style: 'width: 100px;'
.form-group .form-group
%br &nbsp; %br &nbsp;
- if ff.object.id.nil? - if ff.object.id.nil?

View file

@ -0,0 +1,4 @@
.field-unit__label
= f.label field.attribute
.field-unit__field
= f.text_field field.attribute

View file

@ -0,0 +1 @@
= field.to_s

View file

@ -0,0 +1,15 @@
- if field.data.any?
%table.collection-data{ "aria-labelledby": "page-title" }
%thead
%tr
%td.cell-label Libelle
%td.cell-label Type de champ
%tbody
- field.data.order(:order_place).each do |f|
%tr
%td.cell-data
= f.libelle
%td.cell-data
= I18n.t("activerecord.attributes.type_de_champ.type_champs.#{f.type_champ}")
- else
Aucun

View file

@ -0,0 +1,4 @@
.field-unit__label
= f.label field.attribute
.field-unit__field
= f.text_field field.attribute

View file

@ -0,0 +1 @@
= field.to_s

View file

@ -0,0 +1,12 @@
- if field.data.any?
%table.collection-data{ "aria-labelledby": "page-title" }
%thead
%tr
%td.cell-label Libelle
%tbody
- field.data.order(:order_place).each do |f|
%tr
%td.cell-data
= f.libelle
- else
Aucun

View file

@ -0,0 +1,32 @@
-# # Application Layout
-#
-# This view template is used as the layout
-# for every page that Administrate generates.
-#
-# By default, it renders:
-# - Sidebar for navigation
-# - Content for a search bar
-# (if provided by a `content_for` block in a nested page)
-# - Flashes
-# - Links to stylesheets and Javascripts
!!!
%html{ lang: I18n.locale }
%head
%meta{ charset: "utf-8" }
%meta{ content: "NOODP", :name => "ROBOTS" }
%meta{ content: "initial-scale=1", :name => "viewport" }
%title
= content_for(:title)
| #{Rails.application.class.parent_name.titlecase}
= render "stylesheet"
= csrf_meta_tags
%body
.app-container
.sidebar
= render "sidebar"
%main.main-content{ role: "main" }
= content_for(:search)
= render "flashes"
= yield
= render "javascript"

View file

@ -0,0 +1,38 @@
-# # Collection
-#
-# This partial is used on the `index` and `show` pages
-# to display a collection of resources in an HTML table.
-#
-# ## Local variables:
-#
-# - `collection_presenter`:
-# An instance of [Administrate::Page::Collection][1].
-# The table presenter uses `ResourceDashboard::COLLECTION_ATTRIBUTES` to determine
-# the columns displayed in the table
-# - `resources`:
-# An ActiveModel::Relation collection of resources to be displayed in the table.
-# By default, the number of resources is limited by pagination
-# or by a hard limit to prevent excessive page load times
-#
-# [1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%table.collection-data{ "aria-labelledby": "page-title" }
%thead
%tr
- collection_presenter.attribute_types.each do |attr_name, attr_type|
%th.cell-label{ class: "cell-label--#{attr_type.html_class} cell-label--#{collection_presenter.ordered_html_class(attr_name)}", scope: "col" }
= link_to(sanitized_order_params.merge(collection_presenter.order_params_for(attr_name))) do
= t("helpers.label.#{resource_name}.#{attr_name}", default: attr_name.to_s).titleize
- if collection_presenter.ordered_by?(attr_name)
%span.cell-label__sort-indicator{ class: "cell-label__sort-indicator--#{collection_presenter.ordered_html_class(attr_name)}" }
= svg_tag("administrate/sort_arrow.svg", "sort_arrow", width: "13", height: "13")
%th{ colspan: "2", scope: "col" }
%tbody
- resources.each do |resource|
%tr.table__row{ role: "link", tabindex: "0", "data-url": polymorphic_path([namespace, resource]) }
- collection_presenter.attributes_for(resource).each do |attribute|
%td.cell-data{ class: "cell-data--#{attribute.html_class}" }
= link_to polymorphic_path([namespace, resource]), class: "action-show table__link-plain" do
= render_field attribute

View file

@ -0,0 +1,16 @@
-# # Flash Partial
-#
-# This partial renders flash messages on every page.
-#
-# ## Relevant Helpers:
-#
-# - `flash`:
-# Returns a hash,
-# where the keys are the type of flash (alert, error, notice, etc)
-# and the values are the message to be displayed.
- if flash.any?
.flashes
- flash.each do |key, value|
.flash{ class: "flash--#{key}" }
= value

View file

@ -0,0 +1,16 @@
-# # Javascript Partial
-#
-# This partial imports the necessary javascript on each page.
-# By default, it includes the application JS,
-# but each page can define additional JS sources
-# by providing a `content_for(:javascript)` block.
- Administrate::Engine.javascripts.each do |js_path|
= javascript_include_tag js_path
= yield :javascript
- if Rails.env.test?
= javascript_tag do
$.fx.off = true;
$.ajaxSetup({ async: false });

View file

@ -0,0 +1,17 @@
-# # Sidebar
-#
-# This partial is used to display the sidebar in Administrate.
-# By default, the sidebar contains navigation links
-# for all resources in the admin dashboard,
-# as defined by the routes in the `admin/` namespace
%ul.sidebar__list
%li
= link_to "Se déconnecter", administrations_sign_out_path, method: :delete, class: "sidebar__link"
%hr{ style: "margin-bottom: 0;" }
%ul.sidebar__list
- Administrate::Namespace.new(namespace).resources.each do |resource|
%li
= link_to(display_resource_name(resource), [namespace, resource], class: "sidebar__link sidebar__link--#{nav_link_state(resource)}")

View file

@ -0,0 +1,38 @@
-# # Index
-#
-# This view is the template for the index page.
-# It is responsible for rendering the search bar, header and pagination.
-# It renders the `_table` partial to display details about the resources.
-#
-# ## Local variables:
-#
-# - `page`:
-# An instance of [Administrate::Page::Collection][1].
-# Contains helper methods to help display a table,
-# and knows which attributes should be displayed in the resource's table.
-# - `resources`:
-# An instance of `ActiveRecord::Relation` containing the resources
-# that match the user's search criteria.
-# By default, these resources are passed to the table partial to be displayed.
-# - `search_term`:
-# A string containing the term the user has searched for, if any.
-# - `show_search_bar`:
-# A boolean that determines if the search bar should be shown.
-#
-# [1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
- content_for(:title) do
= display_resource_name(page.resource_name)
- content_for(:search) do
- if show_search_bar
= render "search", search_term: search_term
%header.header
%h1.header__heading#page-title
= content_for(:title)
.header__actions
= render "collection", collection_presenter: page, resources: resources
= paginate resources

View file

@ -0,0 +1,29 @@
-# # Show
-#
-# This view is the template for the show page.
-# It renders the attributes of a resource,
-# as well as a link to its edit page.
-#
-# ## Local variables:
-#
-# - `page`:
-# An instance of [Administrate::Page::Show][1].
-# Contains methods for accessing the resource to be displayed on the page,
-# as well as helpers for describing how each attribute of the resource
-# should be displayed.
-#
-# [1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Show
- content_for(:title) { page.page_title }
%header.header
%h1.header__heading= content_for(:title)
.header__actions
%dl
- page.attributes.each do |attribute|
%dt.attribute-label
= t("helpers.label.#{resource_name}.#{attribute.name}", default: attribute.name.titleize)
%dd.attribute-data{ class: "attribute-data--#{attribute.html_class}" }
= render_field attribute

View file

@ -134,7 +134,7 @@ task :deploy => :environment do
invoke :'deploy:link_shared_paths' invoke :'deploy:link_shared_paths'
invoke :'bundle:install' invoke :'bundle:install'
invoke :'rails:db_migrate' invoke :'rails:db_migrate'
invoke :'rails:assets_precompile' invoke :'rails:assets_precompile:force'
to :launch do to :launch do
queue "/etc/init.d/#{user} upgrade " queue "/etc/init.d/#{user} upgrade "

View file

@ -1,4 +1,11 @@
Rails.application.routes.draw do Rails.application.routes.draw do
namespace :manager do
resources :procedures, only: [:index, :show]
resources :administrateurs, only: [:index, :show]
root to: "procedures#index"
end
get "/ping" => "ping#index", :constraints => {:ip => /127.0.0.1/} get "/ping" => "ping#index", :constraints => {:ip => /127.0.0.1/}
devise_for :administrations, devise_for :administrations,

View file

@ -1,30 +0,0 @@
require 'spec_helper'
describe TypesDeChampService do
let(:params) do
ActionController::Parameters.new({
procedure: {
types_de_champ_attributes: {
"0" => {
libelle: 'top',
drop_down_list_attributes: {
value: "un\r\n deux\r\n -- commentaire --\r\n trois",
id: '5218'
}
}
}
}
})
end
let(:result) { TypesDeChampService.create_update_procedure_params(params) }
describe 'self.create_update_procedure_params' do
describe 'the drop down list attributes' do
subject { result['types_de_champ_attributes']['0']['drop_down_list_attributes'] }
it 'has its value stripped' do
expect(subject['value']).to eq("un\r\ndeux\r\n-- commentaire --\r\ntrois")
end
end
end
end

View file

@ -0,0 +1,97 @@
require 'spec_helper'
describe TypesDeChampService do
let(:params) { ActionController::Parameters.new({ procedure: { types_de_champ_attributes: types_de_champ_attributes } }) }
let(:result) { TypesDeChampService.create_update_procedure_params(params) }
describe 'self.create_update_procedure_params' do
describe 'the drop down list attributes' do
let(:types_de_champ_attributes) do
{
"0" => {
libelle: 'top',
drop_down_list_attributes: {
value: "un\r\n deux\r\n -- commentaire --\r\n trois",
id: '5218'
}
}
}
end
subject { result['types_de_champ_attributes']['0']['drop_down_list_attributes'] }
it 'has its value stripped' do
expect(subject['value']).to eq("un\r\ndeux\r\n-- commentaire --\r\ntrois")
end
end
describe 'reorder the fields' do
let(:types_de_champ_attributes) do
{
'0' => { 'libelle' => 'a', 'order_place' => '0', 'custom_order_place' => '1' },
'1' => { 'libelle' => 'b', 'order_place' => '1', 'custom_order_place' => '2' }
}
end
subject { result['types_de_champ_attributes'].to_unsafe_hash }
it { is_expected.to match({ "0" => { 'libelle' => 'a', 'order_place' => '0' }, "1" => { 'libelle' => 'b', 'order_place' => '1' } }) }
context 'when the user specifies a position on one element' do
let(:types_de_champ_attributes) do
{
'0' => { 'libelle' => 'a', 'order_place' => '1', 'custom_order_place' => '1' },
'1' => { 'libelle' => 'b', 'order_place' => '10', 'custom_order_place' => '10' },
'2' => { 'libelle' => 'c', 'order_place' => '11', 'custom_order_place' => '2' }
}
end
it do
is_expected.to match({
'0' => { 'libelle' => 'a', 'order_place' => '0' },
'1' => { 'libelle' => 'c', 'order_place' => '1' },
'2' => { 'libelle' => 'b', 'order_place' => '2' }
})
end
end
context 'when the user puts a champ down' do
let(:types_de_champ_attributes) do
{
'0' => { 'libelle' => 'a', 'order_place' => '0', 'custom_order_place' => '2' },
'1' => { 'libelle' => 'b', 'order_place' => '1', 'custom_order_place' => '2' },
'2' => { 'libelle' => 'c', 'order_place' => '2', 'custom_order_place' => '3' }
}
end
it do
is_expected.to match({
'0' => { 'libelle' => 'b', 'order_place' => '0' },
'1' => { 'libelle' => 'a', 'order_place' => '1' },
'2' => { 'libelle' => 'c', 'order_place' => '2' }
})
end
end
context 'when the user uses not a number' do
let(:types_de_champ_attributes) do
{
'0' => { 'libelle' => 'a', 'order_place' => '0', 'custom_order_place' => '1' },
'1' => { 'libelle' => 'b', 'order_place' => '1', 'custom_order_place' => '2' },
'2' => { 'libelle' => 'c', 'order_place' => '2', 'custom_order_place' => '' },
'3' => { 'libelle' => 'd', 'order_place' => '3', 'custom_order_place' => 'a' }
}
end
it 'does not change the natural order' do
is_expected.to match({
'0' => { 'libelle' => 'a', 'order_place' => '0' },
'1' => { 'libelle' => 'b', 'order_place' => '1' },
'2' => { 'libelle' => 'c', 'order_place' => '2' },
'3' => { 'libelle' => 'd', 'order_place' => '3' }
})
end
end
end
end
end