diff options
77 files changed, 1318 insertions, 180 deletions
@@ -65,6 +65,7 @@ end gem 'activerecord-postgis-adapter', "~> 3.0.0" gem 'polylines' +gem 'activerecord-nulldb-adapter', require: false # Codifligne API gem 'codifligne', af83: 'stif-codifline-api' diff --git a/Gemfile.lock b/Gemfile.lock index 7c305cb8a..9c59016e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,6 +75,8 @@ GEM activemodel (= 4.2.8) activesupport (= 4.2.8) arel (~> 6.0) + activerecord-nulldb-adapter (0.3.7) + activerecord (>= 2.0.0) activerecord-postgis-adapter (3.0.0) activerecord (~> 4.2) rgeo-activerecord (~> 4.0) @@ -564,6 +566,7 @@ DEPENDENCIES SyslogLogger aasm active_attr + activerecord-nulldb-adapter activerecord-postgis-adapter (~> 3.0.0) acts-as-taggable-on (~> 4.0.0) acts_as_list (~> 0.6.0) @@ -680,4 +683,4 @@ DEPENDENCIES will_paginate-bootstrap BUNDLED WITH - 1.15.4 + 1.16.0 diff --git a/app/assets/javascripts/main_menu.coffee b/app/assets/javascripts/main_menu.coffee index a12c47576..1c39be736 100644 --- a/app/assets/javascripts/main_menu.coffee +++ b/app/assets/javascripts/main_menu.coffee @@ -24,6 +24,7 @@ $ -> limit = 51 if $(window).scrollTop() >= limit + data = "" if ($('.page-action .small').length > 0) data = $('.page-action .small')[0].innerHTML diff --git a/app/assets/stylesheets/components/_color_selector.sass b/app/assets/stylesheets/components/_color_selector.sass new file mode 100644 index 000000000..07bfa0c80 --- /dev/null +++ b/app/assets/stylesheets/components/_color_selector.sass @@ -0,0 +1,21 @@ +select.color_selector + option[value='#9B9B9B'] + background-color: #9B9B9B + option[value='#FFA070'] + background-color: #FFA070 + option[value='#C67300'] + background-color: #C67300 + option[value='#7F551B'] + background-color: #7F551B + option[value='#41CCE3'] + background-color: #41CCE3 + option[value='#09B09C'] + background-color: #09B09C + option[value='#3655D7'] + background-color: #3655D7 + option[value='#6321A0'] + background-color: #6321A0 + option[value='#E796C6'] + background-color: #E796C6 + option[value='#DD2DAA'] + background-color: #DD2DAA
\ No newline at end of file diff --git a/app/assets/stylesheets/components/_lists.sass b/app/assets/stylesheets/components/_lists.sass index d8f83d72b..3cce20021 100644 --- a/app/assets/stylesheets/components/_lists.sass +++ b/app/assets/stylesheets/components/_lists.sass @@ -54,3 +54,8 @@ $dlWidth: 40% // Definition .dl-def width: 100% - $dlWidth + + ul + list-style: none + padding-left: 0 + margin-bottom: 0
\ No newline at end of file diff --git a/app/assets/stylesheets/components/_main_nav.sass b/app/assets/stylesheets/components/_main_nav.sass index fdbf5836a..9f7a8e244 100644 --- a/app/assets/stylesheets/components/_main_nav.sass +++ b/app/assets/stylesheets/components/_main_nav.sass @@ -17,6 +17,9 @@ $menuW: 300px line-height: $menuH padding-left: 10px opacity: 0.6 + > a + color: rgba(#fff, 0.9) + text-decoration: none #menu_left position: absolute diff --git a/app/assets/stylesheets/typography/_sboiv.sass b/app/assets/stylesheets/typography/_sboiv.sass index f694306c4..0ed2890fa 100644 --- a/app/assets/stylesheets/typography/_sboiv.sass +++ b/app/assets/stylesheets/typography/_sboiv.sass @@ -89,7 +89,7 @@ .sb-OAS:before content: '\e90f' -.sb-calendar:before +.sb-calendar:before, .sb-purchase_window:before content: '\e910' .sb-journey_pattern:before diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97f5548ae..474277da1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ class ApplicationController < ActionController::Base include PaperTrailSupport include Pundit + include FeatureChecker rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index 2ed10a111..4a752f2b9 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -19,7 +19,7 @@ class CalendarsController < ChouetteController private def calendar_params - permitted_params = [:id, :name, :short_name, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]] + permitted_params = [:id, :name, :short_name, :shared, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]] permitted_params << :shared if policy(Calendar).share? params.require(:calendar).permit(*permitted_params) end diff --git a/app/controllers/concerns/feature_checker.rb b/app/controllers/concerns/feature_checker.rb new file mode 100644 index 000000000..9ca5ed0a7 --- /dev/null +++ b/app/controllers/concerns/feature_checker.rb @@ -0,0 +1,42 @@ +# Check availability of optional features +# +# In your controller, use : +# +# requires_feature :test +# requires_feature :test, only: [:show] +# +# In your view, use : +# +# has_feature? :test +# +module FeatureChecker + extend ActiveSupport::Concern + + module ClassMethods + def requires_feature(feature, options = {}) + before_action options do + check_feature! feature + end + end + end + + included do + helper_method :has_feature? + end + + protected + + def has_feature?(*features) + features.all? do |feature| + current_organisation.has_feature? feature + end + end + + def check_feature!(*features) + unless has_feature?(*features) + raise NotAuthorizedError, "Feature not autorized" + end + end + + class NotAuthorizedError < StandardError; end +end diff --git a/app/controllers/purchase_windows_controller.rb b/app/controllers/purchase_windows_controller.rb new file mode 100644 index 000000000..a70535150 --- /dev/null +++ b/app/controllers/purchase_windows_controller.rb @@ -0,0 +1,58 @@ +class PurchaseWindowsController < ChouetteController + include ReferentialSupport + include RansackDateFilter + include PolicyChecker + before_action :ransack_contains_date, only: [:index] + defaults :resource_class => Chouette::PurchaseWindow, collection_name: 'purchase_windows', instance_name: 'purchase_window' + belongs_to :referential + + def index + index! do + scope = self.ransack_period_range(scope: @purchase_windows, error_message: t('compliance_check_sets.filters.error_period_filter'), query: :overlapping) + @q = scope.ransack(params[:q]) + @purchase_windows = decorate_purchase_windows(@q.result.paginate(page: params[:page], per_page: 30)) + end + end + + def show + show! do + @purchase_window = @purchase_window.decorate(context: { + referential: @referential + }) + end + end + + protected + + def create_resource(purchase_window) + purchase_window.referential = @referential + super + end + + private + + def purchase_window_params + params.require(:purchase_window).permit(:id, :name, :color, :referential_id, periods_attributes: [:id, :begin, :end, :_destroy]) + end + + def decorate_purchase_windows(purchase_windows) + ModelDecorator.decorate( + purchase_windows, + with: PurchaseWindowDecorator, + context: { + referential: @referential + } + ) + end + + def ransack_contains_date + date =[] + if params[:q] && !params[:q]['date_ranges(1i)'].empty? + ['date_ranges(1i)', 'date_ranges(2i)', 'date_ranges(3i)'].each do |key| + date << params[:q][key].to_i + params[:q].delete(key) + end + params[:q]['date_ranges'] = Date.new(*date) rescue nil + end + end +end diff --git a/app/controllers/referential_vehicle_journeys_controller.rb b/app/controllers/referential_vehicle_journeys_controller.rb new file mode 100644 index 000000000..ad08699a5 --- /dev/null +++ b/app/controllers/referential_vehicle_journeys_controller.rb @@ -0,0 +1,17 @@ +# +# Browse all VehicleJourneys of the Referential +# +class ReferentialVehicleJourneysController < ChouetteController + include ReferentialSupport + defaults :resource_class => Chouette::VehicleJourney, collection_name: :vehicle_journeys + + requires_feature :referential_vehicle_journeys + + private + + def collection + @q ||= end_of_association_chain.ransack(params[:q]) + @vehicle_journeys ||= @q.result.includes(:vehicle_journey_at_stops).paginate page: params[:page], per_page: 10 + end + +end diff --git a/app/controllers/stop_areas_controller.rb b/app/controllers/stop_areas_controller.rb index d4d996adb..498493f1e 100644 --- a/app/controllers/stop_areas_controller.rb +++ b/app/controllers/stop_areas_controller.rb @@ -171,7 +171,35 @@ class StopAreasController < ChouetteController helper_method :current_referential def stop_area_params - params.require(:stop_area).permit( :routing_stop_ids, :routing_line_ids, :children_ids, :parent_id, :objectid, :object_version, :name, :comment, :area_type, :registration_number, :nearest_topic_name, :fare_code, :longitude, :latitude, :long_lat_type, :country_code, :street_name, :zip_code, :city_name, :mobility_restricted_suitability, :stairs_availability, :lift_availability, :int_user_needs, :coordinates, :url, :time_zone ) + params.require(:stop_area).permit( + :area_type, + :children_ids, + :city_name, + :comment, + :coordinates, + :country_code, + :fare_code, + :int_user_needs, + :latitude, + :lift_availability, + :long_lat_type, + :longitude, + :mobility_restricted_suitability, + :name, + :nearest_topic_name, + :object_version, + :objectid, + :parent_id, + :registration_number, + :routing_line_ids, + :routing_stop_ids, + :stairs_availability, + :street_name, + :time_zone, + :url, + :waiting_time, + :zip_code, + ) end end diff --git a/app/decorators/purchase_window_decorator.rb b/app/decorators/purchase_window_decorator.rb new file mode 100644 index 000000000..13bc4d666 --- /dev/null +++ b/app/decorators/purchase_window_decorator.rb @@ -0,0 +1,34 @@ +class PurchaseWindowDecorator < Draper::Decorator + decorates Chouette::PurchaseWindow + delegate_all + + def action_links + policy = h.policy(object) + links = [] + + if policy.update? + links << Link.new( + content: I18n.t('actions.edit'), + href: h.edit_referential_purchase_window_path(context[:referential].id, object) + ) + end + + if policy.destroy? + links << Link.new( + content: I18n.t('actions.destroy'), + href: h.referential_purchase_window_path(context[:referential].id, object), + method: :delete, + data: { confirm: h.t('purchase_window.actions.destroy_confirm') } + ) + end + + links + end + + def bounding_dates + unless object.date_ranges.empty? + object.date_ranges.map(&:min).min..object.date_ranges.map(&:max).max + end + end + +end diff --git a/app/decorators/referential_decorator.rb b/app/decorators/referential_decorator.rb index 4103790aa..0863b7f4b 100644 --- a/app/decorators/referential_decorator.rb +++ b/app/decorators/referential_decorator.rb @@ -3,12 +3,19 @@ class ReferentialDecorator < Draper::Decorator def action_links policy = h.policy(object) - links = [ - Link.new( - content: h.t('time_tables.index.title'), - href: h.referential_time_tables_path(object) + links = [] + + if has_feature?(:referential_vehicle_journeys) + links << Link.new( + content: h.t('referential_vehicle_journeys.index.title'), + href: h.referential_vehicle_journeys_path(object) ) - ] + end + + links << Link.new( + content: h.t('time_tables.index.title'), + href: h.referential_time_tables_path(object) + ) if policy.clone? links << Link.new( @@ -63,4 +70,12 @@ class ReferentialDecorator < Draper::Decorator links end + + private + + # TODO move to a base Decorator (ApplicationDecorator) + def has_feature?(*features) + h.has_feature?(*features) rescue false + end + end diff --git a/app/decorators/stop_area_decorator.rb b/app/decorators/stop_area_decorator.rb index 8b2ebf490..cf3612f79 100644 --- a/app/decorators/stop_area_decorator.rb +++ b/app/decorators/stop_area_decorator.rb @@ -31,4 +31,10 @@ class StopAreaDecorator < Draper::Decorator links end + + def waiting_time_text + return '-' if [nil, 0].include? waiting_time + h.t('stop_areas.waiting_time_format', value: waiting_time) + end + end diff --git a/app/helpers/table_builder_helper/url.rb b/app/helpers/table_builder_helper/url.rb index a53ac5620..28f1ade76 100644 --- a/app/helpers/table_builder_helper/url.rb +++ b/app/helpers/table_builder_helper/url.rb @@ -10,7 +10,7 @@ module TableBuilderHelper polymorph_url << item.route.line if item.is_a?(Chouette::RoutingConstraintZone) polymorph_url << item if item.respond_to? :line_referential polymorph_url << item.stop_area if item.respond_to? :stop_area - polymorph_url << item if item.respond_to?(:stop_points) || item.is_a?(Chouette::TimeTable) + polymorph_url << item if item.respond_to?(:stop_points) || item.is_a?(Chouette::TimeTable) || item.is_a?(Chouette::PurchaseWindow) elsif item.respond_to? :referential if item.respond_to? :workbench polymorph_url << item.workbench diff --git a/app/models/calendar.rb b/app/models/calendar.rb index b2e73929f..34ed51374 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -3,22 +3,19 @@ require_relative 'calendar/date_value' require_relative 'calendar/period' class Calendar < ActiveRecord::Base + include DateSupport + include PeriodSupport + has_paper_trail belongs_to :organisation - has_many :time_tables validates_presence_of :name, :short_name, :organisation validates_uniqueness_of :short_name - after_initialize :init_dates_and_date_ranges + has_many :time_tables scope :contains_date, ->(date) { where('date ? = any (dates) OR date ? <@ any (date_ranges)', date, date) } - def init_dates_and_date_ranges - self.dates ||= [] - self.date_ranges ||= [] - end - def self.ransackable_scopes(auth_object = nil) [:contains_date] end @@ -35,144 +32,4 @@ class Calendar < ActiveRecord::Base end end - - ### Calendar::Period - # Required by coocon - def build_period - Calendar::Period.new - end - - def periods - @periods ||= init_periods - end - - def init_periods - (date_ranges || []) - .each_with_index - .map( &Calendar::Period.method(:from_range) ) - end - private :init_periods - - validate :validate_periods - - def validate_periods - periods_are_valid = periods.all?(&:valid?) - - periods.each do |period| - if period.intersect?(periods) - period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods')) - periods_are_valid = false - end - end - - unless periods_are_valid - errors.add(:periods, :invalid) - end - end - - def flatten_date_array attributes, key - date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i } - Date.new(*date_int) - end - - def periods_attributes=(attributes = {}) - @periods = [] - attributes.each do |index, period_attribute| - # Convert date_select to date - ['begin', 'end'].map do |attr| - period_attribute[attr] = flatten_date_array(period_attribute, attr) - end - period = Calendar::Period.new(period_attribute.merge(id: index)) - @periods << period unless period.marked_for_destruction? - end - - date_ranges_will_change! - end - - before_validation :fill_date_ranges - - def fill_date_ranges - if @periods - self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin) - end - end - - after_save :clear_periods - - def clear_periods - @periods = nil - end - - private :clear_periods - - ### Calendar::DateValue - - # Required by coocon - def build_date_value - Calendar::DateValue.new - end - - def date_values - @date_values ||= init_date_values - end - - def init_date_values - if dates - dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) } - else - [] - end - end - private :init_date_values - - validate :validate_date_values - - def validate_date_values - date_values_are_valid = date_values.all?(&:valid?) - - date_values.each do |date_value| - if date_values.count { |d| d.value == date_value.value } > 1 - date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates')) - date_values_are_valid = false - end - date_ranges.each do |date_range| - if date_range.cover? date_value.value - date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges')) - date_values_are_valid = false - end - end - end - - unless date_values_are_valid - errors.add(:date_values, :invalid) - end - end - - def date_values_attributes=(attributes = {}) - @date_values = [] - attributes.each do |index, date_value_attribute| - date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value') - date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index)) - @date_values << date_value unless date_value.marked_for_destruction? - end - - dates_will_change! - end - - before_validation :fill_dates - - def fill_dates - if @date_values - self.dates = @date_values.map(&:value).compact.sort - end - end - - after_save :clear_date_values - - def clear_date_values - @date_values = nil - end - - private :clear_date_values - end diff --git a/app/models/chouette/purchase_window.rb b/app/models/chouette/purchase_window.rb new file mode 100644 index 000000000..8786c7252 --- /dev/null +++ b/app/models/chouette/purchase_window.rb @@ -0,0 +1,27 @@ +require 'range_ext' +require_relative '../calendar/period' + +module Chouette + class PurchaseWindow < Chouette::TridentActiveRecord + # include ChecksumSupport + include ObjectidSupport + include PeriodSupport + extend Enumerize + enumerize :color, in: %w(#9B9B9B #FFA070 #C67300 #7F551B #41CCE3 #09B09C #3655D7 #6321A0 #E796C6 #DD2DAA) + + has_paper_trail + belongs_to :referential + + validates_presence_of :name, :referential + + scope :contains_date, ->(date) { where('date ? <@ any (date_ranges)', date) } + + def local_id + "IBOO-#{self.referential.id}-#{self.id}" + end + + # def checksum_attributes + # end + + end +end
\ No newline at end of file diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb index f216ce449..3a9b44d59 100644 --- a/app/models/chouette/stop_area.rb +++ b/app/models/chouette/stop_area.rb @@ -39,6 +39,8 @@ module Chouette validates_format_of :coordinates, :with => %r{\A *-?(0?[0-9](\.[0-9]*)?|[0-8][0-9](\.[0-9]*)?|90(\.[0]*)?) *\, *-?(0?[0-9]?[0-9](\.[0-9]*)?|1[0-7][0-9](\.[0-9]*)?|180(\.[0]*)?) *\Z}, :allow_nil => true, :allow_blank => true validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true + validates_numericality_of :waiting_time, greater_than_or_equal_to: 0, only_integer: true, if: :waiting_time + def self.nullable_attributes [:registration_number, :street_name, :country_code, :fare_code, :nearest_topic_name, :comment, :long_lat_type, :zip_code, :city_name, :url, :time_zone] diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb index 6f0119e74..6b3c1e7de 100644 --- a/app/models/chouette/vehicle_journey_at_stop.rb +++ b/app/models/chouette/vehicle_journey_at_stop.rb @@ -75,5 +75,14 @@ module Chouette attrs << self.arrival_day_offset.to_s end end + + def departure + departure_time.utc.strftime "%H:%M" if departure_time + end + + def arrival + arrival_time.utc.strftime "%H:%M" if arrival_time + end + end -end
\ No newline at end of file +end diff --git a/app/models/concerns/date_support.rb b/app/models/concerns/date_support.rb new file mode 100644 index 000000000..fbfe19af1 --- /dev/null +++ b/app/models/concerns/date_support.rb @@ -0,0 +1,80 @@ +module DateSupport + extend ActiveSupport::Concern + + included do + after_initialize :init_dates + + def init_dates + self.dates ||= [] + end + + ### Calendar::DateValue + # Required by coocon + def build_date_value + Calendar::DateValue.new + end + + def date_values + @date_values ||= init_date_values + end + + def init_date_values + if dates + dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) } + else + [] + end + end + private :init_date_values + + validate :validate_date_values + + def validate_date_values + date_values_are_valid = date_values.all?(&:valid?) + + date_values.each do |date_value| + if date_values.count { |d| d.value == date_value.value } > 1 + date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates')) + date_values_are_valid = false + end + date_ranges.each do |date_range| + if date_range.cover? date_value.value + date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges')) + date_values_are_valid = false + end + end + end + + unless date_values_are_valid + errors.add(:date_values, :invalid) + end + end + + def date_values_attributes=(attributes = {}) + @date_values = [] + attributes.each do |index, date_value_attribute| + date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value') + date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index)) + @date_values << date_value unless date_value.marked_for_destruction? + end + + dates_will_change! + end + + before_validation :fill_dates + + def fill_dates + if @date_values + self.dates = @date_values.map(&:value).compact.sort + end + end + + after_save :clear_date_values + + def clear_date_values + @date_values = nil + end + + private :clear_date_values + end +end
\ No newline at end of file diff --git a/app/models/concerns/period_support.rb b/app/models/concerns/period_support.rb new file mode 100644 index 000000000..f512c4e89 --- /dev/null +++ b/app/models/concerns/period_support.rb @@ -0,0 +1,80 @@ +module PeriodSupport + extend ActiveSupport::Concern + + included do + after_initialize :init_date_ranges + + def init_date_ranges + self.date_ranges ||= [] + end + + ### Calendar::Period + # Required by coocon + def build_period + Calendar::Period.new + end + + def periods + @periods ||= init_periods + end + + def init_periods + (date_ranges || []) + .each_with_index + .map( &Calendar::Period.method(:from_range) ) + end + private :init_periods + + validate :validate_periods + + def validate_periods + periods_are_valid = periods.all?(&:valid?) + + periods.each do |period| + if period.intersect?(periods) + period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods')) + periods_are_valid = false + end + end + + unless periods_are_valid + errors.add(:periods, :invalid) + end + end + + def flatten_date_array attributes, key + date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i } + Date.new(*date_int) + end + + def periods_attributes=(attributes = {}) + @periods = [] + attributes.each do |index, period_attribute| + # Convert date_select to date + ['begin', 'end'].map do |attr| + period_attribute[attr] = flatten_date_array(period_attribute, attr) + end + period = Calendar::Period.new(period_attribute.merge(id: index)) + @periods << period unless period.marked_for_destruction? + end + + date_ranges_will_change! + end + + before_validation :fill_date_ranges + + def fill_date_ranges + if @periods + self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin) + end + end + + after_save :clear_periods + + def clear_periods + @periods = nil + end + + private :clear_periods + end +end
\ No newline at end of file diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 4343c87af..da7d1fcf3 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -1,3 +1,4 @@ +# coding: utf-8 class Organisation < ActiveRecord::Base include DataFormatEnumerations @@ -75,4 +76,8 @@ class Organisation < ActiveRecord::Base STIF::CodifligneLineId.lines_set_from_functional_scope( functional_scope ) end + def has_feature?(feature) + features && features.include?(feature.to_s) + end + end diff --git a/app/models/referential.rb b/app/models/referential.rb index 851a33653..122af65a1 100644 --- a/app/models/referential.rb +++ b/app/models/referential.rb @@ -128,6 +128,10 @@ class Referential < ActiveRecord::Base Chouette::RoutingConstraintZone.all end + def purchase_windows + Chouette::PurchaseWindow.all + end + before_validation :define_default_attributes def define_default_attributes diff --git a/app/policies/purchase_window_policy.rb b/app/policies/purchase_window_policy.rb new file mode 100644 index 000000000..75143a8bd --- /dev/null +++ b/app/policies/purchase_window_policy.rb @@ -0,0 +1,20 @@ +class PurchaseWindowPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def create? + !archived? && organisation_match? && user.has_permission?('purchase_windows.create') + end + + def update? + !archived? && organisation_match? && user.has_permission?('purchase_windows.update') + end + + def destroy? + !archived? && organisation_match? && user.has_permission?('purchase_windows.destroy') + end + +end
\ No newline at end of file diff --git a/app/views/compliance_control_sets/index.html.slim b/app/views/compliance_control_sets/index.html.slim index 2a5651280..28d2254bf 100644 --- a/app/views/compliance_control_sets/index.html.slim +++ b/app/views/compliance_control_sets/index.html.slim @@ -30,7 +30,7 @@ ), \ TableBuilderHelper::Column.new( \ key: :control_numbers, \ - attribute: 'control_numbers' \ + attribute: Proc.new {|n| n.compliance_controls.count }\ ), \ TableBuilderHelper::Column.new( \ key: :updated_at, \ diff --git a/app/views/dashboards/_dashboard.html.slim b/app/views/dashboards/_dashboard.html.slim index 7d547bf4c..b468fed27 100644 --- a/app/views/dashboards/_dashboard.html.slim +++ b/app/views/dashboards/_dashboard.html.slim @@ -31,13 +31,29 @@ = link_to calendar.name, calendar_path(calendar), class: 'list-group-item' - else .panel-body - em.small.text-muted Aucun modèle de calendrier défini + em.small.text-muted + = t('dasboard.calendars.none') + + .panel.panel-default + .panel-heading + h3.panel-title.with_actions + = t('dasboard.purchase_windows.title') + div + = link_to '', purchase_windows_path, class: ' fa fa-chevron-right pull-right' + - if @dashboard.current_organisation.purchase_windows.present? + .list-group + - @dashboard.current_organisation.purchase_windows.order("updated_at desc").limit(5).each do |purchase_window| + = link_to purchase_window.name, referential_purchase_window_path(purchase_window.referential, purchase_window), class: 'list-group-item' + - else + .panel-body + em.small.text-muted + = t('dasboard.purchase_windows.none') .col-lg-6.col-md-6.col-sm-6.col-xs-12 .panel.panel-default .panel-heading h3.panel-title - = "Référentiels d'arrêts" + = t('dashboard.stop_area_referentials.title') .list-group - @dashboard.current_organisation.stop_area_referentials.each do |referential| = link_to referential.name, stop_area_referential_stop_areas_path(referential), class: 'list-group-item' @@ -45,7 +61,7 @@ .panel.panel-default .panel-heading h3.panel-title - = "Référentiels de lignes" + = t('dashboard.line_referentials.title') .list-group - @dashboard.current_organisation.line_referentials.all.each do |referential| = link_to referential.name, line_referential_lines_path(referential), class: 'list-group-item' diff --git a/app/views/layouts/navigation/_main_nav_top.html.slim b/app/views/layouts/navigation/_main_nav_top.html.slim index 363a89b48..278249e09 100644 --- a/app/views/layouts/navigation/_main_nav_top.html.slim +++ b/app/views/layouts/navigation/_main_nav_top.html.slim @@ -1,5 +1,5 @@ .nav-menu#menu_top - .brandname = t('brandname') + .brandname = link_to t('brandname'), root_path .menu-content .menu-item diff --git a/app/views/purchase_windows/_date_value_fields.html.slim b/app/views/purchase_windows/_date_value_fields.html.slim new file mode 100644 index 000000000..7bde06a94 --- /dev/null +++ b/app/views/purchase_windows/_date_value_fields.html.slim @@ -0,0 +1,13 @@ +.nested-fields + - if f.object.errors.has_key? :base + .row + .col-lg-12 + .alert.alert-danger + - f.object.errors[:base].each do |message| + p.small = message + + .wrapper + div + = f.input :value, as: :date, label: false, wrapper_html: { class: 'date smart_date' } + div + = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/purchase_windows/_filters.html.slim b/app/views/purchase_windows/_filters.html.slim new file mode 100644 index 000000000..9c83d20db --- /dev/null +++ b/app/views/purchase_windows/_filters.html.slim @@ -0,0 +1,15 @@ += search_form_for @q, url: referential_purchase_windows_path, builder: SimpleForm::FormBuilder, html: { method: :get, class: 'form form-filter' } do |f| + .ffg-row + .input-group.search_bar + = f.search_field :name_cont, class: 'form-control', placeholder: t('purchase_windows.index.filter_placeholder') + span.input-group-btn + button.btn.btn-default#search_btn type='submit' + span.fa.fa-search + + .form-group + = f.label Chouette::PurchaseWindow.human_attribute_name(:date), class: 'control-label' + = f.input :date_ranges, as: :date, label: false, wrapper_html: { class: 'date smart_date' }, class: 'form-control', include_blank: true + + .actions + = link_to t('actions.erase'), referential_purchase_windows_path, class: 'btn btn-link' + = f.submit t('actions.filter'), id: 'purchase_window_filter_btn', class: 'btn btn-default' diff --git a/app/views/purchase_windows/_form.html.slim b/app/views/purchase_windows/_form.html.slim new file mode 100644 index 000000000..8f3ba769d --- /dev/null +++ b/app/views/purchase_windows/_form.html.slim @@ -0,0 +1,29 @@ += simple_form_for [@referential, @purchase_window], html: { class: 'form-horizontal', id: 'purchase_window_form' }, wrapper: :horizontal_form do |f| + .row + .col-lg-12 + = f.input :name + = f.input :color, as: :select, boolean_style: :inline, collection: Chouette::PurchaseWindow.color.values, input_html: {class: 'color_selector'} + + .separator + + .row + .col-lg-12 + .subform + .nested-head + .wrapper + div + .form-group + label.control-label + = t('simple_form.labels.purchase_window.ranges.begin') + div + .form-group + label.control-label + = t('simple_form.labels.purchase_window.ranges.end') + div + + = f.simple_fields_for :periods do |period| + = render 'period_fields', f: period + .links.nested-linker + = link_to_add_association t('simple_form.labels.purchase_window.add_a_date_range'), f, :periods, class: 'btn btn-outline-primary' + + = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'purchase_window_form' diff --git a/app/views/purchase_windows/_period_fields.html.slim b/app/views/purchase_windows/_period_fields.html.slim new file mode 100644 index 000000000..95e204554 --- /dev/null +++ b/app/views/purchase_windows/_period_fields.html.slim @@ -0,0 +1,15 @@ +.nested-fields + - if f.object.errors.has_key? :base + .row + .col-lg-12 + .alert.alert-danger + - f.object.errors[:base].each do |message| + p.small = message + + .wrapper + div + = f.input :begin, as: :date, label: false, wrapper_html: { class: 'date smart_date' } + div + = f.input :end, as: :date, label: false, wrapper_html: { class: 'date smart_date' } + div + = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/purchase_windows/edit.html.slim b/app/views/purchase_windows/edit.html.slim new file mode 100644 index 000000000..6354db853 --- /dev/null +++ b/app/views/purchase_windows/edit.html.slim @@ -0,0 +1,7 @@ +- breadcrumb :purchase_window, @referential, @purchase_window +- page_header_content_for @purchase_window +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = render 'form' diff --git a/app/views/purchase_windows/index.html.slim b/app/views/purchase_windows/index.html.slim new file mode 100644 index 000000000..38954b5dc --- /dev/null +++ b/app/views/purchase_windows/index.html.slim @@ -0,0 +1,44 @@ +- breadcrumb :purchase_windows, @referential +- content_for :page_header_actions do + - if policy(Chouette::PurchaseWindow).create? + = link_to(t('actions.add'), new_referential_purchase_window_path(@referential), class: 'btn btn-default') + +.page_content + .container-fluid + - if params[:q].present? or @purchase_windows.any? + .row + .col-lg-12 + = render 'filters' + + - if @purchase_windows.any? + .row + .col-lg-12 + = table_builder_2 @purchase_windows, + [ \ + TableBuilderHelper::Column.new( \ + key: :name, \ + attribute: 'name', \ + link_to: lambda do |purchase_window| \ + referential_purchase_window_path(purchase_window.referential, purchase_window) \ + end \ + ), \ + TableBuilderHelper::Column.new( \ + key: :color, \ + attribute: Proc.new { |tt| tt.color ? content_tag(:span, '', class: 'fa fa-circle', style: "color:#{tt.color}") : '-' }\ + ), \ + TableBuilderHelper::Column.new( \ + key: :bounding_dates, \ + attribute: Proc.new {|w| w.bounding_dates.nil? ? '-' : t('validity_range', debut: l(w.bounding_dates.begin, format: :short), end: l(w.bounding_dates.end, format: :short))} \ + ) \ + ], + links: [:show], + cls: 'table has-filter' + + = new_pagination @purchase_windows, 'pull-right' + + - unless @purchase_windows.any? + .row.mt-xs + .col-lg-12 + = replacement_msg t('purchase_windows.search_no_results') + += javascript_pack_tag 'date_filters' diff --git a/app/views/purchase_windows/new.html.slim b/app/views/purchase_windows/new.html.slim new file mode 100644 index 000000000..402084167 --- /dev/null +++ b/app/views/purchase_windows/new.html.slim @@ -0,0 +1,6 @@ +- breadcrumb :purchase_windows, @referential +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = render 'form' diff --git a/app/views/purchase_windows/show.html.slim b/app/views/purchase_windows/show.html.slim new file mode 100644 index 000000000..9f3abb267 --- /dev/null +++ b/app/views/purchase_windows/show.html.slim @@ -0,0 +1,20 @@ +- breadcrumb :purchase_window, @referential, @purchase_window +- page_header_content_for @purchase_window +- content_for :page_header_content do + .row.mb-sm + .col-lg-12.text-right + - @purchase_window.action_links.each do |link| + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content + +.page_content + .container-fluid + .row + .col-lg-6.col-md-6.col-sm-12.col-xs-12 + = definition_list t('metadatas'), + { Chouette::PurchaseWindow.human_attribute_name(:name) => @purchase_window.try(:name), + 'Organisation' => @purchase_window.referential.organisation.name, + Chouette::PurchaseWindow.human_attribute_name(:date_ranges) => @purchase_window.periods.map{|d| t('validity_range', debut: l(d.begin, format: :short), end: l(d.end, format: :short))}.join('<br>').html_safe } diff --git a/app/views/referential_vehicle_journeys/_filters.html.slim b/app/views/referential_vehicle_journeys/_filters.html.slim new file mode 100644 index 000000000..963da8cea --- /dev/null +++ b/app/views/referential_vehicle_journeys/_filters.html.slim @@ -0,0 +1,11 @@ += search_form_for @q, url: referential_vehicle_journeys_path(@referential), html: {method: :get}, class: 'form form-filter' do |f| + .ffg-row + .input-group.search_bar + = f.search_field :published_journey_name_or_objectid_cont, placeholder: t('.published_journey_name_or_objectid'), class: 'form-control' + span.input-group-btn + button.btn.btn-default#search-btn type='submit' + span.fa.fa-search + + .actions + = link_to 'Effacer', referential_vehicle_journeys_path(@referential), class: 'btn btn-link' + = f.submit 'Filtrer', class: 'btn btn-default' diff --git a/app/views/referential_vehicle_journeys/index.html.slim b/app/views/referential_vehicle_journeys/index.html.slim new file mode 100644 index 000000000..394f4a3f7 --- /dev/null +++ b/app/views/referential_vehicle_journeys/index.html.slim @@ -0,0 +1,58 @@ +- breadcrumb :referential_vehicle_journeys, @referential +- content_for :page_header_title, t('.title') + +.page_content + .container-fluid + - if params[:q].present? or @vehicle_journeys.present? + .row + .col-lg-12 + = render 'filters' + + - if @vehicle_journeys.present? + .row + .col-lg-12 + .select_table + = table_builder_2 @vehicle_journeys, + [ \ + TableBuilderHelper::Column.new( \ + name: t('objectid'), \ + attribute: Proc.new { |n| n.get_objectid.short_id }, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :published_journey_name, \ + attribute: 'published_journey_name', \ + link_to: lambda do |vehicle_journey| \ + referential_line_route_vehicle_journeys_path(@referential, vehicle_journey.route.line, vehicle_journey.route) \ + end, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :line, \ + attribute: Proc.new {|v| v.route.line.name}, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :route, \ + attribute: Proc.new {|v| v.route.name}, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :departure_time, \ + attribute: Proc.new {|v| v.vehicle_journey_at_stops.first&.departure }, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :arrival_time, \ + attribute: Proc.new {|v| v.vehicle_journey_at_stops.last&.arrival }, \ + sortable: false \ + ), \ + ], + cls: 'table has-filter has-search' + + = new_pagination @vehicle_journeys, 'pull-right' + + - unless @vehicle_journeys.any? + .row.mt-xs + .col-lg-12 + = replacement_msg t('.search_no_results') diff --git a/app/views/referentials/_filters.html.slim b/app/views/referentials/_filters.html.slim index c5b6042f0..93fa679df 100644 --- a/app/views/referentials/_filters.html.slim +++ b/app/views/referentials/_filters.html.slim @@ -12,11 +12,11 @@ = f.input :transport_mode_eq_any, collection: @referential.lines.pluck(:transport_mode).uniq.compact, as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + t("enumerize.transport_mode.#{l}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } .form-group.togglable - = f.label Chouette::Line.human_attribute_name(:network), required: false, class: 'control-label' + = f.label t('activerecord.attributes.referential.networks'), required: false, class: 'control-label' = f.input :network_id_eq_any, collection: LineReferential.first.networks.order('name').pluck(:id), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>#{LineReferential.first.networks.find(l).name}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } .form-group.togglable - = f.label Chouette::Line.human_attribute_name(:company), required: false, class: 'control-label' + = f.label t('activerecord.attributes.referential.companies'), required: false, class: 'control-label' = f.input :company_id_eq_any, collection: LineReferential.first.companies.order('name').pluck(:id), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>#{LineReferential.first.companies.find(l).name}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } .actions diff --git a/app/views/stop_areas/_form.html.slim b/app/views/stop_areas/_form.html.slim index ac2cb4e87..e44680499 100644 --- a/app/views/stop_areas/_form.html.slim +++ b/app/views/stop_areas/_form.html.slim @@ -26,6 +26,9 @@ .stop_areas.stop_area.general_info h3 = t("stop_areas.stop_area.general") + - if has_feature?(:stop_area_waiting_time) + = f.input :waiting_time + = f.input :registration_number, required: format_restriction_for_locales(@referential) == '.hub', :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.registration_number")} = f.input :fare_code = f.input :nearest_topic_name, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.nearest_topic_name")} diff --git a/app/views/stop_areas/show.html.slim b/app/views/stop_areas/show.html.slim index 1b1209a68..0c23710b6 100644 --- a/app/views/stop_areas/show.html.slim +++ b/app/views/stop_areas/show.html.slim @@ -15,12 +15,15 @@ .container-fluid .row .col-lg-6.col-md-6.col-sm-12.col-xs-12 - = definition_list t('metadatas'), - { t('id_reflex') => @stop_area.get_objectid.short_id, + - attributes = { t('id_reflex') => @stop_area.get_objectid.short_id, @stop_area.human_attribute_name(:stop_area_type) => Chouette::AreaType.find(@stop_area.area_type).try(:label), @stop_area.human_attribute_name(:registration_number) => @stop_area.registration_number, - 'Coordonnées' => geo_data(@stop_area, @stop_area_referential), + } + - attributes.merge!(@stop_area.human_attribute_name(:waiting_time) => @stop_area.waiting_time_text) if has_feature?(:stop_area_waiting_time) + - attributes.merge!({ "Coordonnées" => geo_data(@stop_area, @stop_area_referential), @stop_area.human_attribute_name(:zip_code) => @stop_area.zip_code, @stop_area.human_attribute_name(:city_name) => @stop_area.city_name, 'Etat' => (@stop_area.deleted_at ? 'Supprimé' : 'Actif'), - @stop_area.human_attribute_name(:comment) => @stop_area.try(:comment) } + @stop_area.human_attribute_name(:comment) => @stop_area.try(:comment), + }) + = definition_list t('metadatas'), attributes diff --git a/config/application.rb b/config/application.rb index 169c13e10..bda582610 100644 --- a/config/application.rb +++ b/config/application.rb @@ -35,6 +35,10 @@ module ChouetteIhm config.active_job.queue_adapter = :sidekiq + config.action_dispatch.rescue_responses.merge!( + 'FeatureChecker::NotAuthorizedError' => :unauthorized + ) + unless Rails.env.production? # Work around sprockets+teaspoon mismatch: Rails.application.config.assets.precompile += %w(spec_helper.js) diff --git a/config/breadcrumbs.rb b/config/breadcrumbs.rb index eb285b731..2cafc4419 100644 --- a/config/breadcrumbs.rb +++ b/config/breadcrumbs.rb @@ -41,6 +41,11 @@ crumb :referential_group_of_line do |referential, group_of_line| parent :referential_group_of_lines, referential end +crumb :referential_vehicle_journeys do |referential| + link I18n.t('referential_vehicle_journeys.index.title'), referential_vehicle_journeys_path(referential) + parent :referential, referential +end + crumb :time_tables do |referential| link I18n.t('time_tables.index.title'), referential_time_tables_path(referential) parent :referential, referential @@ -162,6 +167,16 @@ crumb :line do |line| parent :lines, line.line_referential end +crumb :purchase_windows do |referential| + link I18n.t('purchase_windows.index.title'), referential_purchase_windows_path(referential) + parent :referential, referential +end + +crumb :purchase_window do |referential, purchase_window| + link breadcrumb_name(purchase_window), referential_purchase_window_path(referential, purchase_window) + parent :purchase_windows, referential +end + crumb :calendars do link I18n.t('calendars.index.title'), calendars_path end diff --git a/config/initializers/apartment_null_db.rb b/config/initializers/apartment_null_db.rb new file mode 100644 index 000000000..438f1e58b --- /dev/null +++ b/config/initializers/apartment_null_db.rb @@ -0,0 +1,25 @@ +if ENV['RAILS_DB_ADAPTER'] == 'nulldb' + require 'apartment/adapters/abstract_adapter' + + module Apartment + module Tenant + def adapter + Thread.current[:apartment_adapter] ||= nulldb_adapter(config) + end + + def self.nulldb_adapter(config) + adapter = Adapters::NulldbAdapter + adapter.new(config) + end + end + + module Adapters + # Default adapter when not using Postgresql Schemas + class NulldbAdapter < AbstractAdapter + def initialize config + super + end + end + end + end +end diff --git a/config/locales/calendars.en.yml b/config/locales/calendars.en.yml index d3cc57677..a2110d053 100644 --- a/config/locales/calendars.en.yml +++ b/config/locales/calendars.en.yml @@ -41,6 +41,8 @@ en: date: Date new: title: Add a new calendar + create: + title: Add a new calendar edit: title: Update calendar %{name} show: diff --git a/config/locales/calendars.fr.yml b/config/locales/calendars.fr.yml index fc895bf89..bd3051730 100644 --- a/config/locales/calendars.fr.yml +++ b/config/locales/calendars.fr.yml @@ -41,6 +41,8 @@ fr: date: Date new: title: Ajouter un calendrier + create: + title: Ajouter un calendrier edit: title: Editer le calendrier %{name} show: diff --git a/config/locales/compliance_controls.en.yml b/config/locales/compliance_controls.en.yml index f9d7d23d2..3392afab5 100644 --- a/config/locales/compliance_controls.en.yml +++ b/config/locales/compliance_controls.en.yml @@ -89,7 +89,7 @@ en: vehicle_journey_control/waiting_time: messages: 3_vehiclejourney_1: "On the following vehicle journey %{source_objectid}, the waiting time %{error_value} a this stop point %{target_0_label} (%{target_0_objectid}) is greater than the threshold (%{reference_value})" - description: "The waiting time at a specific stop point cannot be too big" + description: "The waiting time, in minutes, at a specific stop point cannot be too big" vehicle_journey_control/speed: messages: 3_vehiclejourney_2_1: "On the following vehicle journey %{source_objectid}, the computed speed %{error_value} between the stop points %{target_0_label} (%{target_0_objectid}) and %{target_1_label} (%{target_1_objectid}) is greater than the threshold (%{reference_value})" diff --git a/config/locales/compliance_controls.fr.yml b/config/locales/compliance_controls.fr.yml index b77b4e6d4..cde75aaf5 100644 --- a/config/locales/compliance_controls.fr.yml +++ b/config/locales/compliance_controls.fr.yml @@ -88,7 +88,7 @@ fr: vehicle_journey_control/waiting_time: messages: 3_vehiclejourney_1: "Sur la course %{source_objectid}, le temps d'attente %{error_value} à l'arrêt %{target_0_label} (%{target_0_objectid}) est supérieur au seuil toléré (%{reference_value})" - description: "La durée d’attente à un arrêt ne doit pas être trop grande" + description: "La durée d’attente, en minutes, à un arrêt ne doit pas être trop grande" vehicle_journey_control/speed: messages: 3_vehiclejourney_2_1: "Sur la course %{source_objectid}, la vitesse calculée %{error_value} entre les arrêts %{target_0_label} (%{target_0_objectid}) et %{target_1_label} (%{target_1_objectid}) est supérieur au seuil toléré (%{reference_value})" diff --git a/config/locales/dashboard.en.yml b/config/locales/dashboard.en.yml new file mode 100644 index 000000000..8d46ff7aa --- /dev/null +++ b/config/locales/dashboard.en.yml @@ -0,0 +1,16 @@ +en: + dashboards: + show: + title: "Dashboard %{organisation}" + calendars: + title: Calendars + none: No calendar created + purchase_windows: + title: Purchase windows + none: No purchase window created + line_referentials: + title: Line referential + none: No line referential created + stop_area_referentials: + title: Stop area referential + none: No stop area referential created diff --git a/config/locales/dashboard.fr.yml b/config/locales/dashboard.fr.yml index fffb36cd1..d0aa36d61 100644 --- a/config/locales/dashboard.fr.yml +++ b/config/locales/dashboard.fr.yml @@ -2,3 +2,15 @@ fr: dashboards: show: title: "Tableau de bord %{organisation}" + calendars: + title: Modèles de calendrier + none: Aucun calendrier défini + purchase_windows: + title: Calendriers commerciaux + none: Aucun calendrier commercial défini + line_referentials: + title: Référentiels de lignes + none: Aucun référentiels de lignes défini + stop_area_referentials: + title: Référentiels d'arrêts + none: Aucun référentiels d'arrêts défini diff --git a/config/locales/purchase_windows.en.yml b/config/locales/purchase_windows.en.yml new file mode 100644 index 000000000..5ffed305a --- /dev/null +++ b/config/locales/purchase_windows.en.yml @@ -0,0 +1,78 @@ +en: + purchase_windows: + search_no_results: 'No purchase window matching your query' + days: + monday: M + tuesday: Tu + wednesday: W + thursday: Th + friday: F + saturday: Sa + sunday: Su + months: + 1: January + 2: February + 3: March + 4: April + 5: May + 6: June + 7: July + 8: August + 9: September + 10: October + 11: November + 12: December + actions: + new: Add a new purchase window + edit: Edit this purchase window + destroy: Remove this purchase window + destroy_confirm: Are you sure you want destroy this purchase window? + errors: + overlapped_periods: Another period is overlapped with this period + short_period: A period needs to last at least two days + index: + title: purchase windows + all: All + shared: Shared + not_shared: Not shared + search_no_results: No purchase window matching your query + date: Date + filter_placeholder: Put the name of a purchase window... + create: + title: Add a new purchase window + new: + title: Add a new purchase window + edit: + title: Update purchase window %{name} + show: + title: purchase window %{name} + simple_form: + labels: + purchase_windows: + date_value: Date + add_a_date: Add a date + add_a_date_range: Add a date range + ranges: + begin: Beginning + end: End + activerecord: + models: + purchase_window: + zero: purchase window + one: purchase window + other: purchase windows + attributes: + purchase_windows: + name: Name + date_ranges: Date ranges + referential: Referential + color: Color + bounding_dates: Bounding Dates + errors: + models: + purchase_windows: + attributes: + dates: + date_in_date_ranges: A date can not be in Dates and in Date ranges. + date_in_dates: A date can appear only once in the list of dates. + illegal_date: The date %{date} does not exist. diff --git a/config/locales/purchase_windows.fr.yml b/config/locales/purchase_windows.fr.yml new file mode 100644 index 000000000..df5d45d82 --- /dev/null +++ b/config/locales/purchase_windows.fr.yml @@ -0,0 +1,79 @@ +fr: + purchase_windows: + search_no_results: 'Aucun calendrier commercial ne correspond à votre recherche' + days: + monday: L + tuesday: Ma + wednesday: Me + thursday: J + friday: V + saturday: S + sunday: D + months: + 1: Janvier + 2: Février + 3: Mars + 4: Avril + 5: Mai + 6: Juin + 7: Juillet + 8: Août + 9: Septembre + 10: Octobre + 11: Novembre + 12: Décembre + actions: + new: Créer + edit: Editer + destroy: Supprimer + destroy_confirm: Etes vous sûr de supprimer cet calendrier commercial ? + errors: + overlapped_periods: Une autre période chevauche cette période + short_period: Une période doit être d'un duréé de deux jours minimum + index: + title: Calendriers commerciaux + all: Tous + shared: Partagées + not_shared: Non partagées + search_no_results: Aucun calendrier commercial ne correspond à votre recherche + date: Date + filter_placeholder: Indiquez un nom de calendrier commercial... + new: + title: Ajouter un calendrier commercial + create: + title: Ajouter un calendrier commercial + edit: + title: Editer le calendrier commercial %{name} + show: + title: Calendrier commercial %{name} + simple_form: + labels: + calendar: + date_value: Date + add_a_date: Ajouter une date + add_a_date_range: Ajouter un intervalle de dates + ranges: + begin: Début + end: Fin + activerecord: + models: + purchase_window: + zero: "calendrier commercial" + one: "calendrier commercial" + other: "calendriers commerciaux" + attributes: + purchase_window: + name: Nom + short_name: Nom court + date_ranges: Intervalles de dates + referential: Jeu de données + color: Couleur + bounding_dates: Période englobante + errors: + models: + purchase_window: + attributes: + dates: + date_in_date_ranges: Une même date ne peut pas être incluse à la fois dans la liste et dans les intervalles de dates. + date_in_dates: Une même date ne peut pas être incluse plusieurs fois dans la liste. + illegal_date: La date %{date} n'existe pas. diff --git a/config/locales/stop_areas.en.yml b/config/locales/stop_areas.en.yml index 54a5ebae5..3ef3835e2 100644 --- a/config/locales/stop_areas.en.yml +++ b/config/locales/stop_areas.en.yml @@ -1,5 +1,6 @@ en: stop_areas: &en_stop_areas + waiting_time_format: "%{value} minutes" search_no_results: "No stop area matching your query" errors: empty: Aucun stop_area_id @@ -126,6 +127,7 @@ en: city_name: "City" created_at: Created at updated_at: Updated at + waiting_time: Waiting time formtastic: titles: stop_area: diff --git a/config/locales/stop_areas.fr.yml b/config/locales/stop_areas.fr.yml index f96a2e564..69e3ba71e 100644 --- a/config/locales/stop_areas.fr.yml +++ b/config/locales/stop_areas.fr.yml @@ -1,5 +1,6 @@ fr: stop_areas: &fr_stop_areas + waiting_time_format: "%{value} minutes" search_no_results: "Aucun arrêt ne correspond à votre recherche" errors: empty: Aucun stop_area_id @@ -37,7 +38,7 @@ fr: name_or_objectid: "Recherche par nom ou par objectid..." zip_code: Indiquez un code postal... city_name: Indiquez un nom de commune... - area_type: Indiquez un type d'arrêt... + area_type: "Indiquez un type d'arrêt..." new: title: "Ajouter un arrêt" form: @@ -126,6 +127,7 @@ fr: city_name: "Commune" created_at: "Créé le" updated_at: "Edité le" + waiting_time: Temps de desserte formtastic: titles: stop_area: diff --git a/config/locales/vehicle_journeys.en.yml b/config/locales/vehicle_journeys.en.yml index 7f3871fbf..1c1f6c6bd 100644 --- a/config/locales/vehicle_journeys.en.yml +++ b/config/locales/vehicle_journeys.en.yml @@ -106,6 +106,8 @@ en: updated_at: Updated at creator_id: "Created by" footnote_ids: "Footnotes" + departure_time: "Departure" + arrival_time: "Arrival" errors: models: vehicle_journey: @@ -126,3 +128,9 @@ en: hub: vehicle_journey: objectid: "[prefix]:VehicleJourney:[unique_key] : prefix contains only alphanumerical or underscore characters, unique_key accepts also minus character. Maximum length of the unique key = 8." + referential_vehicle_journeys: + index: + title: "Vehicle Journeys" + search_no_results: "No vehicle journey match your search" + filters: + published_journey_name_or_objectid: "Search by name or by ID..." diff --git a/config/locales/vehicle_journeys.fr.yml b/config/locales/vehicle_journeys.fr.yml index ed4b1c30a..5749f8f10 100644 --- a/config/locales/vehicle_journeys.fr.yml +++ b/config/locales/vehicle_journeys.fr.yml @@ -106,6 +106,8 @@ fr: updated_at: "Edité le" creator_id: "Créé par" footnote_ids: "Notes de bas de page" + departure_time: "Départ" + arrival_time: "Arrivée" errors: models: vehicle_journey: @@ -126,3 +128,9 @@ fr: hub: vehicle_journey: objectid: "[prefixe]:VehicleJourney:[clé_unique] caractères autorisés : alphanumériques et 'souligné' pour le préfixe, la clé unique accepte en plus le 'moins'. Longueur maximale de la clé unique = 8." + referential_vehicle_journeys: + index: + title: "Courses" + search_no_results: "Aucune course ne correspond à votre recherche" + filters: + published_journey_name_or_objectid: "Indiquez le nom ou l'ID..." diff --git a/config/routes.rb b/config/routes.rb index 65fa62557..d097d2d71 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -156,6 +156,8 @@ ChouetteIhm::Application.routes.draw do resources :routing_constraint_zones end + resources :vehicle_journeys, controller: 'referential_vehicle_journeys', only: [:index] + resources :import_tasks, :only => [:new, :create] resources :export_tasks, :only => [:new, :create] do collection do @@ -171,6 +173,8 @@ ChouetteIhm::Application.routes.draw do resources :companies, controller: "referential_companies" + resources :purchase_windows + resources :time_tables do collection do get :tags diff --git a/db/migrate/20171214131755_create_business_calendars.rb b/db/migrate/20171214131755_create_business_calendars.rb new file mode 100644 index 000000000..aa7c1ab12 --- /dev/null +++ b/db/migrate/20171214131755_create_business_calendars.rb @@ -0,0 +1,14 @@ +class CreateBusinessCalendars < ActiveRecord::Migration + def change + create_table :business_calendars do |t| + t.string :name + t.string :short_name + t.string :color + t.daterange :date_ranges, array: true + t.date :dates, array: true + t.belongs_to :organisation, index: true + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20171215144543_rename_business_calendars_to_purchase_windows.rb b/db/migrate/20171215144543_rename_business_calendars_to_purchase_windows.rb new file mode 100644 index 000000000..d4467d6a7 --- /dev/null +++ b/db/migrate/20171215144543_rename_business_calendars_to_purchase_windows.rb @@ -0,0 +1,5 @@ +class RenameBusinessCalendarsToPurchaseWindows < ActiveRecord::Migration + def change + rename_table :business_calendars, :purchase_windows + end +end diff --git a/db/migrate/20171215145023_update_purchase_windows_attributes.rb b/db/migrate/20171215145023_update_purchase_windows_attributes.rb new file mode 100644 index 000000000..48dfb15bc --- /dev/null +++ b/db/migrate/20171215145023_update_purchase_windows_attributes.rb @@ -0,0 +1,13 @@ +class UpdatePurchaseWindowsAttributes < ActiveRecord::Migration + def change + add_column :purchase_windows, :objectid, :string + add_column :purchase_windows, :checksum, :string + add_column :purchase_windows, :checksum_source, :text + + remove_column :purchase_windows, :short_name, :string + remove_column :purchase_windows, :dates, :date + remove_column :purchase_windows, :organisation_id, :integer + + add_reference :purchase_windows, :referential, type: :bigint, index: true + end +end diff --git a/db/migrate/20171219170128_add_features_to_organisations.rb b/db/migrate/20171219170128_add_features_to_organisations.rb new file mode 100644 index 000000000..bbec3297b --- /dev/null +++ b/db/migrate/20171219170128_add_features_to_organisations.rb @@ -0,0 +1,5 @@ +class AddFeaturesToOrganisations < ActiveRecord::Migration + def change + add_column :organisations, :features, :string, array: true, default: [] + end +end diff --git a/db/migrate/20171220164059_add_waiting_time_to_stop_areas.rb b/db/migrate/20171220164059_add_waiting_time_to_stop_areas.rb new file mode 100644 index 000000000..369fed3ab --- /dev/null +++ b/db/migrate/20171220164059_add_waiting_time_to_stop_areas.rb @@ -0,0 +1,5 @@ +class AddWaitingTimeToStopAreas < ActiveRecord::Migration + def change + add_column :stop_areas, :waiting_time, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 50ee0dcf4..182df3159 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171218174509) do +ActiveRecord::Schema.define(version: 20171220164059) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -403,9 +403,9 @@ ActiveRecord::Schema.define(version: 20171218174509) do t.string "type" t.integer "parent_id", limit: 8 t.string "parent_type" - t.datetime "notified_parent_at" t.integer "current_step", default: 0 t.integer "total_steps", default: 0 + t.datetime "notified_parent_at" t.string "creator" end @@ -552,6 +552,7 @@ ActiveRecord::Schema.define(version: 20171218174509) do t.datetime "synced_at" t.hstore "sso_attributes" t.string "custom_view" + t.string "features", default: [], array: true end add_index "organisations", ["code"], name: "index_organisations_on_code", unique: true, using: :btree @@ -571,6 +572,20 @@ ActiveRecord::Schema.define(version: 20171218174509) do add_index "pt_links", ["objectid"], name: "pt_links_objectid_key", unique: true, using: :btree + create_table "purchase_windows", id: :bigserial, force: :cascade do |t| + t.string "name" + t.string "color" + t.daterange "date_ranges", array: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "objectid" + t.string "checksum" + t.text "checksum_source" + t.integer "referential_id", limit: 8 + end + + add_index "purchase_windows", ["referential_id"], name: "index_purchase_windows_on_referential_id", using: :btree + create_table "referential_clonings", id: :bigserial, force: :cascade do |t| t.string "status" t.datetime "started_at" @@ -737,6 +752,7 @@ ActiveRecord::Schema.define(version: 20171218174509) do t.datetime "created_at" t.datetime "updated_at" t.string "stif_type" + t.integer "waiting_time" end add_index "stop_areas", ["name"], name: "index_stop_areas_on_name", using: :btree diff --git a/spec/controllers/concerns/feature_checker_spec.rb b/spec/controllers/concerns/feature_checker_spec.rb new file mode 100644 index 000000000..1d289bb15 --- /dev/null +++ b/spec/controllers/concerns/feature_checker_spec.rb @@ -0,0 +1,37 @@ +require "rails_helper" + +RSpec.describe "FeatureChecker", type: :controller do + login_user + + controller do + include FeatureChecker + requires_feature :test, only: :protected + + def protected; render text: "protected"; end + def not_protected; render text: "not protected"; end + + def current_organisation + @organisation ||= Organisation.new + end + end + + before do + routes.draw do + get "protected" => "anonymous#protected" + get "not_protected" => "anonymous#not_protected" + end + end + + it "refuse access when organisation does not have the feature" do + expect{ get(:protected) }.to raise_error(FeatureChecker::NotAuthorizedError) + end + + it "accept access on unprotected action" do + get :not_protected + end + + it 'accept access when organisation has feature' do + controller.current_organisation.features << "test" + get :protected + end +end diff --git a/spec/controllers/referential_vehicle_journeys_controller_spec.rb b/spec/controllers/referential_vehicle_journeys_controller_spec.rb new file mode 100644 index 000000000..842a6665e --- /dev/null +++ b/spec/controllers/referential_vehicle_journeys_controller_spec.rb @@ -0,0 +1,43 @@ +require "rails_helper" + +RSpec.describe ReferentialVehicleJourneysController, type: :controller do + login_user + + before do + @user.organisation.update features: %w{referential_vehicle_journeys} + end + + describe 'GET #index' do + it 'should be successful' do + get :index, referential_id: referential + expect(response).to be_success + end + + it "refuse access when organisation does not have the feature 'referential_vehicle_journeys'" do + @user.organisation.update features: [] + + expect do + get :index, referential_id: referential + end.to raise_error(FeatureChecker::NotAuthorizedError) + end + + it 'define Ransack search (alias @q)' do + get :index, referential_id: referential + expect(assigns[:q]).to be_an_instance_of(Ransack::Search) + end + + it 'define @vehicle_journeys collection' do + vehicle_journey = FactoryGirl.create :vehicle_journey + get :index, referential_id: referential + expect(assigns[:vehicle_journeys]).to include(vehicle_journey) + end + + it 'paginage @vehicle_journeys collection' do + FactoryGirl.create :vehicle_journey + + get :index, referential_id: referential + expect(assigns[:vehicle_journeys].total_entries).to be(1) + end + end + +end diff --git a/spec/decorators/referential_decorator_spec.rb b/spec/decorators/referential_decorator_spec.rb index cbeaf2407..879ab7d4b 100644 --- a/spec/decorators/referential_decorator_spec.rb +++ b/spec/decorators/referential_decorator_spec.rb @@ -1,4 +1,5 @@ RSpec.describe ReferentialDecorator, type: [:helper, :decorator] do + include Support::DecoratorHelpers let( :object ){ build_stubbed :referential } let( :referential ){ object } diff --git a/spec/decorators/stop_area_decorator_spec.rb b/spec/decorators/stop_area_decorator_spec.rb new file mode 100644 index 000000000..fd6aa207a --- /dev/null +++ b/spec/decorators/stop_area_decorator_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +RSpec.describe StopAreaDecorator do + + let(:stop_area) { Chouette::StopArea.new } + let(:decorator) { stop_area.decorate } + + describe '#waiting_time_text' do + it "returns '-' when waiting_time is nil" do + stop_area.waiting_time = nil + expect(decorator.waiting_time_text).to eq('-') + end + + it "returns '-' when waiting_time is zero" do + stop_area.waiting_time = 0 + expect(decorator.waiting_time_text).to eq('-') + end + + it "returns '120 minutes' when waiting_time is 120" do + stop_area.waiting_time = 120 + expect(decorator.waiting_time_text).to eq('120 minutes') + end + end + +end diff --git a/spec/factories/chouette_purchase_windows.rb b/spec/factories/chouette_purchase_windows.rb new file mode 100644 index 000000000..2e2faf4d8 --- /dev/null +++ b/spec/factories/chouette_purchase_windows.rb @@ -0,0 +1,12 @@ +FactoryGirl.define do + factory :purchase_window, class: Chouette::PurchaseWindow do + sequence(:name) { |n| "Purchase Window #{n}" } + sequence(:objectid) { |n| "organisation:PurchaseWindow:#{n}:LOC" } + date_ranges { [generate(:periods)] } + end + + sequence :periods do |n| + date = Date.today + 2*n + date..(date+1) + end +end diff --git a/spec/features/purchase_windows_permission_spec.rb b/spec/features/purchase_windows_permission_spec.rb new file mode 100644 index 000000000..e74fb5c17 --- /dev/null +++ b/spec/features/purchase_windows_permission_spec.rb @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +require 'spec_helper' + +describe "PurchaseWindows", :type => :feature do + login_user + + let(:purchase_window) { create :purchase_window, referential: first_referential} + + describe 'permissions' do + before do + allow_any_instance_of(PurchaseWindowPolicy).to receive(:create?).and_return permission + allow_any_instance_of(PurchaseWindowPolicy).to receive(:destroy?).and_return permission + allow_any_instance_of(PurchaseWindowPolicy).to receive(:update?).and_return permission + visit path + end + + context 'on show view' do + let( :path ){ referential_purchase_window_path(first_referential, purchase_window) } + + context 'if present → ' do + let( :permission ){ true } + it 'view shows the corresponding buttons' do + expect(page).to have_content(I18n.t('purchase_windows.actions.edit')) + expect(page).to have_content(I18n.t('purchase_windows.actions.destroy')) + end + end + + context 'if absent → ' do + let( :permission ){ false } + it 'view does not show the corresponding buttons' do + expect(page).not_to have_content(I18n.t('purchase_windows.actions.edit')) + expect(page).not_to have_content(I18n.t('purchase_windows.actions.destroy')) + end + end + end + + context 'on index view' do + let( :path ){ referential_purchase_windows_path(first_referential) } + + context 'if present → ' do + let( :permission ){ true } + it 'index shows an edit button' do + expect(page).to have_content(I18n.t('purchase_windows.actions.new')) + end + end + + context 'if absent → ' do + let( :permission ){ false } + it 'index does not show any edit button' do + expect(page).not_to have_content(I18n.t('purchase_windows.actions.new')) + end + end + end + end +end diff --git a/spec/models/chouette/purchase_window_spec.rb b/spec/models/chouette/purchase_window_spec.rb new file mode 100644 index 000000000..702a44eeb --- /dev/null +++ b/spec/models/chouette/purchase_window_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe Chouette::PurchaseWindow, :type => :model do + let(:referential) {create(:referential)} + subject { create(:purchase_window, referential: referential) } + + it { should belong_to(:referential) } + it { is_expected.to validate_presence_of(:name) } + + describe 'validations' do + it 'validates and date_ranges do not overlap' do + expect(build(:purchase_window, referential: referential,date_ranges: [Date.today..Date.today + 10.day, Date.yesterday..Date.tomorrow])).to_not be_valid + # expect(build(periods: [Date.today..Date.today + 10.day, Date.yesterday..Date.tomorrow ])).to_not be_valid + end + end + + describe 'before_validation' do + let(:purchase_window) { build(:purchase_window, referential: referential, date_ranges: []) } + + it 'shoud fill date_ranges with date ranges' do + expected_range = Date.today..Date.tomorrow + purchase_window.date_ranges << expected_range + purchase_window.valid? + + expect(purchase_window.date_ranges.map { |period| period.begin..period.end }).to eq([expected_range]) + end + end + +end diff --git a/spec/models/chouette/stop_area_spec.rb b/spec/models/chouette/stop_area_spec.rb index c6aeafaf8..bec8c0868 100644 --- a/spec/models/chouette/stop_area_spec.rb +++ b/spec/models/chouette/stop_area_spec.rb @@ -426,5 +426,30 @@ describe Chouette::StopArea, :type => :model do # end # end + describe '#waiting_time' do + + let(:stop_area) { FactoryGirl.build :stop_area } + + it 'can be nil' do + stop_area.waiting_time = nil + expect(stop_area).to be_valid + end + + it 'can be zero' do + stop_area.waiting_time = 0 + expect(stop_area).to be_valid + end + + it 'can be positive' do + stop_area.waiting_time = 120 + expect(stop_area).to be_valid + end + + it "can't be negative" do + stop_area.waiting_time = -1 + expect(stop_area).to_not be_valid + end + + end end diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index 359417d88..595b08058 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -62,4 +62,26 @@ describe Organisation, :type => :model do expect{Organisation.portail_sync}.to change{ Organisation.count }.by(4) end end + + describe "#has_feature?" do + + let(:organisation) { Organisation.new } + + it 'return false if Organisation features is nil' do + organisation.features = nil + expect(organisation.has_feature?(:dummy)).to be_falsy + end + + it 'return true if Organisation features contains given feature' do + organisation.features = %w{present} + expect(organisation.has_feature?(:present)).to be_truthy + end + + it "return false if Organisation features doesn't contains given feature" do + organisation.features = %w{other} + expect(organisation.has_feature?(:absent)).to be_falsy + end + + end + end diff --git a/spec/policies/purchase_window_policy_spec.rb b/spec/policies/purchase_window_policy_spec.rb new file mode 100644 index 000000000..f078bf288 --- /dev/null +++ b/spec/policies/purchase_window_policy_spec.rb @@ -0,0 +1,15 @@ +RSpec.describe PurchaseWindowPolicy, type: :policy do + + let( :record ){ build_stubbed :purchase_window } + before { stub_policy_scope(record) } + + permissions :create? do + it_behaves_like 'permitted policy and same organisation', "purchase_windows.create", archived: true + end + permissions :destroy? do + it_behaves_like 'permitted policy and same organisation', "purchase_windows.destroy", archived: true + end + permissions :update? do + it_behaves_like 'permitted policy and same organisation', "purchase_windows.update", archived: true + end +end diff --git a/spec/support/decorator_helpers.rb b/spec/support/decorator_helpers.rb index ffedd479b..9d450deb1 100644 --- a/spec/support/decorator_helpers.rb +++ b/spec/support/decorator_helpers.rb @@ -1,5 +1,4 @@ module Support - module DecoratorHelpers def self.included(into) into.instance_eval do @@ -21,7 +20,3 @@ module Support end end end - -RSpec.configure do | c | - c.include Support::DecoratorHelpers, type: :decorator -end diff --git a/spec/views/stop_areas/edit.html.erb_spec.rb b/spec/views/stop_areas/edit.html.erb_spec.rb index 5105bff4b..bfbb0bb55 100644 --- a/spec/views/stop_areas/edit.html.erb_spec.rb +++ b/spec/views/stop_areas/edit.html.erb_spec.rb @@ -6,6 +6,10 @@ describe "/stop_areas/edit", :type => :view do let!(:stop_area) { assign(:stop_area, create(:stop_area)) } let!(:map) { assign(:map, double(:to_html => '<div id="map"/>'.html_safe)) } + before do + allow(view).to receive(:has_feature?) + end + describe "form" do it "should render input for name" do render @@ -13,6 +17,5 @@ describe "/stop_areas/edit", :type => :view do with_tag "input[type=text][name='stop_area[name]'][value=?]", stop_area.name end end - end end diff --git a/spec/views/stop_areas/new.html.erb_spec.rb b/spec/views/stop_areas/new.html.erb_spec.rb index 749782349..23f7387fa 100644 --- a/spec/views/stop_areas/new.html.erb_spec.rb +++ b/spec/views/stop_areas/new.html.erb_spec.rb @@ -5,6 +5,10 @@ describe "/stop_areas/new", :type => :view do let!(:stop_area_referential) { assign :stop_area_referential, stop_area.stop_area_referential } let!(:stop_area) { assign(:stop_area, build(:stop_area)) } + before do + allow(view).to receive(:has_feature?) + end + describe "form" do it "should render input for name" do |
