diff options
20 files changed, 418 insertions, 6 deletions
| @@ -43,6 +43,7 @@ gem 'font-awesome-sass'  # Format Output  gem 'json'  gem 'rubyzip', :require => 'zip/zip' +gem 'roo'  # Controller  gem 'inherited_resources' @@ -53,7 +54,7 @@ gem 'will_paginate', '~> 3.0'  gem 'ransack'  gem 'squeel'  #gem 'ninoxe', :git => 'https://github.com/afimb/ninoxe.git' -#gem 'ninoxe',:path => '~/workspace/chouette/ninoxe' +#gem 'ninoxe',:path => '~/projects/ninoxe'  gem 'ninoxe', '~> 1.0.1'  gem 'acts_as_list', '0.1.6' @@ -91,6 +92,7 @@ group :test, :development do    gem 'capybara'    gem 'launchy'    gem 'factory_girl_rails', '1.7' +  # gem 'i18n-tasks'    gem 'rb-inotify', :require => RUBY_PLATFORM.include?('linux') && 'rb-inotify'    gem 'rb-fsevent', :require => RUBY_PLATFORM.include?('darwin') && 'rb-fsevent'  end diff --git a/app/assets/images/icons/file_calc.png b/app/assets/images/icons/file_calc.pngBinary files differ new file mode 100644 index 000000000..6339ff181 --- /dev/null +++ b/app/assets/images/icons/file_calc.png diff --git a/app/assets/images/icons/file_csv.png b/app/assets/images/icons/file_csv.pngBinary files differ new file mode 100644 index 000000000..795cba0a6 --- /dev/null +++ b/app/assets/images/icons/file_csv.png diff --git a/app/assets/images/icons/file_excel.png b/app/assets/images/icons/file_excel.pngBinary files differ new file mode 100644 index 000000000..e60ab1319 --- /dev/null +++ b/app/assets/images/icons/file_excel.png diff --git a/app/assets/stylesheets/vehicle_journey_imports.css.scss b/app/assets/stylesheets/vehicle_journey_imports.css.scss new file mode 100644 index 000000000..6aab362d1 --- /dev/null +++ b/app/assets/stylesheets/vehicle_journey_imports.css.scss @@ -0,0 +1,10 @@ +@import "common/mixins"; + +#workspace.vehicle_journey_imports.new +{ +    .export{ +        margin: 5px 0 15px 0 !important; + +        .file{ margin-left: 5px; } +    } +}
\ No newline at end of file diff --git a/app/controllers/vehicle_journey_exports_controller.rb b/app/controllers/vehicle_journey_exports_controller.rb new file mode 100644 index 000000000..f29cd01aa --- /dev/null +++ b/app/controllers/vehicle_journey_exports_controller.rb @@ -0,0 +1,36 @@ +class VehicleJourneyExportsController < ChouetteController +  belongs_to :referential do +    belongs_to :line, :parent_class => Chouette::Line do +      belongs_to :route, :parent_class => Chouette::Route +    end +  end +   +  respond_to :csv, :only => [:new, :index] +  respond_to :xls, :only => [:new, :index] +   +  def new +    new! do |format| +      @vehicle_journey_export = VehicleJourneyExport.new(:route => @route) +       +      format.csv { render text: @vehicle_journey_export.to_csv } +      format.xls { render text: @vehicle_journey_export.to_csv(col_sep: "\t") } +    end +  end + +   def index +    index! do |format| +      @vehicle_journey_export = VehicleJourneyExport.new(:route => @route) +       +      format.csv { render text: @vehicle_journey_export.to_csv } +      format.xls { render text: @vehicle_journey_export.to_csv(col_sep: "\t") } +    end +  end +   +  protected  +  alias_method :route, :parent + +  def collection +    @vehicle_journey_exports = [] +  end +   +end diff --git a/app/controllers/vehicle_journey_imports_controller.rb b/app/controllers/vehicle_journey_imports_controller.rb new file mode 100644 index 000000000..e3a6dde77 --- /dev/null +++ b/app/controllers/vehicle_journey_imports_controller.rb @@ -0,0 +1,50 @@ +class VehicleJourneyImportsController < ChouetteController +  belongs_to :referential do +    belongs_to :line, :parent_class => Chouette::Line do +      belongs_to :route, :parent_class => Chouette::Route +    end +  end +   +  actions :new, :create +  respond_to :html, :only => :new +   +  def new     +    @vehicle_journey_import = VehicleJourneyImport.new(:route => route) +    flash[:notice] =  "A CSV or Excel file can be used to import records. The first row should be the column name.  +<p> +The following columns are allowed : +<ul> +  <li> +    <strong>stop_point_id</strong> - +    Integer type +  </li> +  <li> +    <strong>stop_area_name</strong> - +    String type +  </li> +  <li> +    <strong>published_journey_name </strong> - +    String type +  </li> +  <li> +    <strong>published_journey_name </strong> - +    String type .... +  </li> +</ul> +</p>" +    new! +  end + +  def create +    @vehicle_journey_import = VehicleJourneyImport.new(params[:vehicle_journey_import].merge({:route => route})) +    if @vehicle_journey_import.save +      redirect_to referential_line_route_path( @referential, @line, @route ), notice: "Import successful"  +    else +      render :new  +    end +  end + +  protected  +  alias_method :route, :parent + +end diff --git a/app/helpers/stop_areas_helper.rb b/app/helpers/stop_areas_helper.rb index da29b2ec6..e654857a8 100644 --- a/app/helpers/stop_areas_helper.rb +++ b/app/helpers/stop_areas_helper.rb @@ -1,7 +1,7 @@  module StopAreasHelper    def genealogical_title -    return t(".genealogical_routing") if @stop_area.stop_area_type == 'itl'     -    t(".genealogical") +    return t("genealogical_routing") if @stop_area.stop_area_type == 'itl'     +    t("genealogical")    end    def show_map? diff --git a/app/models/vehicle_journey_export.rb b/app/models/vehicle_journey_export.rb new file mode 100644 index 000000000..8fe7869f1 --- /dev/null +++ b/app/models/vehicle_journey_export.rb @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +require "csv" + +class VehicleJourneyExport    +  include ActiveModel::Validations +  include ActiveModel::Conversion +  extend  ActiveModel::Naming + +  attr_accessor :route +   +  validates_presence_of :route + +  def initialize(attributes = {})     +    attributes.each { |name, value| send("#{name}=", value) } +  end +   +  def persisted? +    false +  end +   +  def to_csv(options = {}) +    CSV.generate(options) do |csv| +      vehicle_journeys_sorted = route.vehicle_journeys.includes(:vehicle_journey_at_stops).order("vehicle_journey_at_stops.departure_time") +       +      vehicle_journey_at_stops_matrix = (vehicle_journeys_sorted.collect{ |vj| vj.vehicle_journey_at_stops.collect(&:departure_time).collect{|time| time.strftime("%H:%M")} }).transpose +      csv << ["stop_point_id", "stop_area_name"] + vehicle_journeys_sorted.collect(&:objectid) +      route.stop_points.each_with_index do |stop_point, index| +        csv << [stop_point.id, stop_point.stop_area.name] + vehicle_journey_at_stops_matrix[index] +      end +    end +  end + +end diff --git a/app/models/vehicle_journey_import.rb b/app/models/vehicle_journey_import.rb new file mode 100644 index 000000000..d637c9e5f --- /dev/null +++ b/app/models/vehicle_journey_import.rb @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +class VehicleJourneyImport    +  include ActiveModel::Validations +  include ActiveModel::Conversion +  extend  ActiveModel::Naming + +  attr_accessor :file, :route +   +  validates_presence_of :file +  validates_presence_of :route + +  def initialize(attributes = {})     +    attributes.each { |name, value| send("#{name}=", value) } +  end +   +  def persisted? +    false +  end +   +  def save +    begin +      Chouette::VehicleJourney.transaction do        +        if imported_vehicle_journeys.map(&:valid?).all? +          imported_vehicle_journeys.each(&:save!) +          true +        else +          imported_vehicle_journeys.each_with_index do |imported_vehicle_journey, index| +            imported_vehicle_journey.errors.full_messages.each do |message| +              errors.add :base, I18n.t("vehicle_journey_imports.errors.invalid_vehicle_journey", :column => index, :message => message) +            end +          end +          false +        end +      end +    rescue Exception => exception +      errors.add :base, I18n.t("vehicle_journey_imports.errors.exception", :message => exception.message) +      false +    end +  end    +   +  def imported_vehicle_journeys +    @imported_vehicle_journeys ||= load_imported_vehicle_journeys +  end + +  # Find journey pattern on stop points used in vehicle journey at stops +  def find_journey_pattern_schedule(hours_by_stop_point_ids) +    stop_points_used = hours_by_stop_point_ids.reject{ |key,value| value == nil }.keys +    journey_pattern_founded = route.journey_patterns.select{ |jp| jp.stop_points.collect(&:id) == stop_points_used }.first +     +    # If no journey pattern founded, create a new one +    journey_pattern_founded ? journey_pattern_founded : route.journey_patterns.create(:stop_points => Chouette::StopPoint.find(stop_points_used) ) +  end + +  def load_imported_vehicle_journeys +    spreadsheet = open_spreadsheet(file) +    vehicle_journeys = [] +     +    first_column = spreadsheet.column(1) +    stop_point_ids = first_column[1..spreadsheet.last_row].map(&:to_i) +    same_stop_points = route.stop_points.collect(&:id) == stop_point_ids +     +    unless same_stop_points +      errors.add :base, I18n.t("vehicle_journey_imports.errors.not_same_stop_points", :route => route.id) +      return vehicle_journeys +    end     +     +    Chouette::VehicleJourney.transaction do        +      (3..spreadsheet.last_column).each do |i| +        vehicle_journey_objectid = spreadsheet.column(i)[0] +        hours_by_stop_point_ids = Hash[[stop_point_ids, spreadsheet.column(i)[1..spreadsheet.last_row]].transpose] + +        journey_pattern = find_journey_pattern_schedule(hours_by_stop_point_ids) +        vehicle_journey = journey_pattern.vehicle_journeys.where(:objectid => vehicle_journey_objectid, :route_id => route.id, :journey_pattern_id => journey_pattern.id).first_or_create + +        line = 0 +        hours_by_stop_point_ids.each_pair do |key, value| +          line += 1 +          if value.present? # Create a vehicle journey at stop when time is present +            main_time = Time.parse(value) +             +            if main_time.present? +              vjas = Chouette::VehicleJourneyAtStop.where(:vehicle_journey_id => vehicle_journey.id, :stop_point_id => key).first_or_create(:departure_time => main_time, :arrival_time => main_time) +            else +              errors.add :base, I18n.t("vehicle_journey_imports.errors.invalid_vehicle_journey", :column => i, :line => line, :time => value) +            end +          end          +        end +         +        vehicle_journeys << vehicle_journey +      end +    end + +    vehicle_journeys +  end +   +  def open_spreadsheet(file) +    case File.extname(file.original_filename) +    when '.csv' then Roo::CSV.new(file.path) +    when '.xls' then Roo::Excel.new(file.path) +    when '.xlsx' then Roo::Excelx.new(file.path) +    else +      raise "Unknown file type: #{file.original_filename}" +    end +  end +   +end diff --git a/app/views/routes/show.html.erb b/app/views/routes/show.html.erb index 8383366b3..6642f2bd7 100644 --- a/app/views/routes/show.html.erb +++ b/app/views/routes/show.html.erb @@ -91,7 +91,10 @@    <li>      <%= link_to t('vehicle_journeys.actions.index'), [@referential, @line, @route, :vehicle_journeys], :class => "link" %>    </li> -<% end %> +  <% end %> +  <li> +    <%= link_to t('vehicle_journey_imports.new.title'), new_referential_line_route_vehicle_journey_import_path( @referential, @line, @route ), :class => "import" %> +  </li>  </ul>    <%= creation_tag(@route) %> diff --git a/app/views/vehicle_journey_imports/new.html.erb b/app/views/vehicle_journey_imports/new.html.erb new file mode 100644 index 000000000..a1078c88d --- /dev/null +++ b/app/views/vehicle_journey_imports/new.html.erb @@ -0,0 +1,27 @@ +<%= title_tag t('vehicle_journey_imports.new.title') %> + +<p class="export"><%= t('vehicle_journey_imports.new.export_vehicle_journeys') %> +  <span class="file"><%= link_to image_tag("icons/file_csv.png"), referential_line_route_vehicle_journey_exports_path(@referential, @line, @route, :format => :csv) %></span> +  <span class="file"><%= link_to image_tag("icons/file_excel.png"), referential_line_route_vehicle_journey_exports_path(@referential, @line, @route, :format => :xls) %></span> +</p> + +<%= semantic_form_for [@referential, @line, @route, @vehicle_journey_import] do |form| %> +  <% if @vehicle_journey_import.errors.any? %> +    <div id="error_explanation"> +      <h2><%= pluralize(@vehicle_journey_import.errors.count, "error") %> prohibited this import from completing:</h2> +      <ul> +      <% @vehicle_journey_import.errors.full_messages.each do |msg| %> +        <li><%= msg %></li> +      <% end %> +      </ul> +    </div> +  <% end %> +  <br> +  <%= form.inputs do %>  +    <%= form.input :file, :as => :file %> +  <% end %> +  <%= form.actions do %> +    <%= form.action :submit, :as => :button , :label => t( 'formtastic.import' ) %>  +    <%= form.action :cancel, :as => :link %> +  <% end %> +<% end %> diff --git a/config/application.rb b/config/application.rb index 865becd53..8032f9e9a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -40,7 +40,6 @@ module ChouetteIhm      # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.      # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]      config.i18n.default_locale = :fr -    I18n.enforce_available_locales = false      # Configure the default encoding used in templates for Ruby 1.9.      config.encoding = "utf-8" diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index b93a3824f..30b00cb92 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -4,4 +4,5 @@  # Mime::Type.register "text/richtext", :rtf  # Mime::Type.register_alias "text/html", :iphone - Mime::Type.register "application/zip", :zip +Mime::Type.register "application/zip", :zip +Mime::Type.register "application/xls", :xls diff --git a/config/locales/vehicle_journey_imports.yml b/config/locales/vehicle_journey_imports.yml new file mode 100644 index 000000000..2cf1e7b95 --- /dev/null +++ b/config/locales/vehicle_journey_imports.yml @@ -0,0 +1,28 @@ +en: +  vehicle_journey_imports: +    new: +      title: "Import vehicle journey at stops" +      export_vehicle_journeys: "Export existing vehicle journey at stops :" +    form: +      file: "File"   +    errors: +      not_same_stop_points: "Error column 1 : Not same stop points than in route %{route}" +      invalid_vehicle_journey_at_stop: "Error column %{column} line %{line} : vehicle journey at stop invalid %{time}" +      invalid_vehicle_journey: "Error column %{column}, vehicle journey is invalid : %{message}" +      exception: "An exception occured : %{message}" +fr: +  form: +    file: "Fichier" +  vehicle_journey_imports: +    new: +      title: "Import des horaires à l'arrêt" +      export_vehicle_journeys: "Exporter les horaires à l'arrêt existants :" +      file: "Fichier"   +    form: +      file: "Fichier"   +    errors: +      not_same_stop_points: "Erreur colonne 1 : Pas les mêmes points d'arrêt que sur l'itinéraire %{route}" +      invalid_vehicle_journey_at_stop: "Erreur colonne %{column} ligne %{line} : horaire à l'arrêt invalide %{time}" +      invalid_vehicle_journey: "Erreur colonne %{column}, la course est invalide : %{message}" +      exception: "Une exception est survenu : %{message}" + 
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 92c8b0b20..12a83eea1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -92,6 +92,8 @@ ChouetteIhm::Application.routes.draw do            get 'select_journey_pattern', :on => :member            resources :vehicle_translations          end +        resources :vehicle_journey_imports +        resources :vehicle_journey_exports        end      end diff --git a/spec/fixtures/vehicle_journey_imports_valid.csv b/spec/fixtures/vehicle_journey_imports_valid.csv new file mode 100644 index 000000000..2e05cbfc8 --- /dev/null +++ b/spec/fixtures/vehicle_journey_imports_valid.csv @@ -0,0 +1,5 @@ +"stop area id","stop area name","first:VehicleJourney:vehicle_journey_1","first:VehicleJourney:vehicle_journey_2","first:VehicleJourney:vehicle_journey_3" +1,"Arrêt 1","9:00:00","10:05:00","11:10:00" +2,"Arrêt 2","9:05:00",,"11:15:00" +3,"Arrêt 3","9:10:00","10:20:00","11:20:00" +4,"Arrêt 4","9:15:00","10:25:00","11:25:00" diff --git a/spec/fixtures/vehicle_journey_imports_with_vj_invalid.csv b/spec/fixtures/vehicle_journey_imports_with_vj_invalid.csv new file mode 100644 index 000000000..3def5eca1 --- /dev/null +++ b/spec/fixtures/vehicle_journey_imports_with_vj_invalid.csv @@ -0,0 +1,5 @@ +"stop area id","stop area name","first:VehicleJourney:vehicle_journey_1","first:VehicleJourney:vehicle_journey_2","first:VehicleJourney:vehicle_journey_3" +1,"Arrêt 1","9:00:00","10:05:00","11:10:00" +2,"Arrêt 2","11:05:00",,"11:15:00" +3,"Arrêt 3","11:10:00","10:20:00","11:20:00" +4,"Arrêt 4","11:15:00","10:25:00","11:25:00" diff --git a/spec/fixtures/vehicle_journey_imports_with_vjas_invalid.csv b/spec/fixtures/vehicle_journey_imports_with_vjas_invalid.csv new file mode 100644 index 000000000..c63b56432 --- /dev/null +++ b/spec/fixtures/vehicle_journey_imports_with_vjas_invalid.csv @@ -0,0 +1,5 @@ +"stop area id","stop area name","first:VehicleJourney:vehicle_journey_1","first:VehicleJourney:vehicle_journey_2","first:VehicleJourney:vehicle_journey_3" +1,"Arrêt 1",invalid time,"10:05:00","11:10:00" +2,"Arrêt 2","9:05:00",,"11:15:00" +3,"Arrêt 3","9:10:00","10:20:00","11:20:00" +4,"Arrêt 4","9:15:00","10:25:00","11:25:00" diff --git a/spec/models/vehicle_journey_importer_spec.rb b/spec/models/vehicle_journey_importer_spec.rb new file mode 100644 index 000000000..cb8785586 --- /dev/null +++ b/spec/models/vehicle_journey_importer_spec.rb @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +require 'spec_helper' + +describe VehicleJourneyImport do + +  let(:file) { File.open(Rails.root.join("spec", "fixtures", "vehicle_journey_imports_valid.csv").to_s, "r") }   + +  let!(:route) { create(:route) } +  let!(:other_route) { create(:route) } + +  let!(:journey_pattern) { create(:journey_pattern, :route_id => route.id) } +  let!(:other_journey_pattern) { create(:journey_pattern, :route_id => route.id) } + +  let!(:vehicle_journey1) { create(:vehicle_journey, :objectid => "vehicle_journey_1", :route_id => route.id, :journey_pattern_id => journey_pattern.id) } +  let!(:vehicle_journey2) { create(:vehicle_journey, :objectid => "vehicle_journey_2", :route_id => route.id, :journey_pattern_id => other_journey_pattern.id) } +  let!(:vehicle_journey3) { create(:vehicle_journey, :objectid => "vehicle_journey_3", :route_id => route.id, :journey_pattern_id => journey_pattern.id) } +   +  let!(:stop_area1) { create(:stop_area, :name => "Arrêt 1") } +  let!(:stop_area2) { create(:stop_area, :name => "Arrêt 2") } +  let!(:stop_area3) { create(:stop_area, :name => "Arrêt 3") } +  let!(:stop_area4) { create(:stop_area, :name => "Arrêt 4") } +   +  let!(:stop_point1) { create(:stop_point, :id => 1, :stop_area => stop_area1) } +  let!(:stop_point2) { create(:stop_point, :id => 2, :stop_area => stop_area2) } +  let!(:stop_point3) { create(:stop_point, :id => 3, :stop_area => stop_area3) } +  let!(:stop_point4) { create(:stop_point, :id => 4, :stop_area => stop_area4) } +   +  subject { VehicleJourneyImport.new(:route => route, :file => file) } + +  before :each do +    route.stop_points = [stop_point1, stop_point2, stop_point3, stop_point4]  +    journey_pattern.stop_points = [stop_point1, stop_point2, stop_point3, stop_point4] +    other_journey_pattern.stop_points = [stop_point1, stop_point3, stop_point4] +  end + +  describe ".save" do + +    it "should validate presence of route" do +      expect(VehicleJourneyImport.new(:route => route).save).to be_false +    end + +    it "should validate presence of file" do +      expect(VehicleJourneyImport.new(:file => file).save).to be_false +    end + +    it "should import vehicle_journeys and create the right number of objects" do +      expect(VehicleJourneyImport.new(:file => file, :route => route).save).to be_true +    end +     +  end + +  describe ".find_journey_pattern_schedule" do    + +    it "should return journey pattern with same stop points" do           +      expect(subject.find_journey_pattern_schedule( { 1 => "9:00", 2 => "9:05", 3 => "9:10", 4 => "9:15"} )).to eq(journey_pattern) +      expect(subject.find_journey_pattern_schedule( { 1 => "9:00", 2 => nil, 3 => "9:10", 4 => "9:15"} )).to eq(other_journey_pattern) +    end + +    it "should return new journey_pattern if no journey pattern with same stop points is founded" do       +      expect(subject.find_journey_pattern_schedule( { 1 => "9:00", 2 => "9:05", 3 => nil, 4 => "9:15"} )).to be_true +      expect(subject.find_journey_pattern_schedule( { 1 => "9:00", 2 => "9:05", 3 => nil, 4 => "9:15"} ).id).not_to eq(journey_pattern.id) +      expect(subject.find_journey_pattern_schedule( { 1 => "9:00", 2 => "9:05", 3 => nil, 4 => "9:15"} ).id).not_to eq(other_journey_pattern.id) +    end +     +  end + +  describe ".load_imported_vehicle_journeys" do + +    it "should return false when stop points in file are not the same in the route" do +      expect(VehicleJourneyImport.new(:route => other_route, :file => file).load_imported_vehicle_journeys).to eq([]) +      expect(Chouette::VehicleJourney.all.size).to eq(3) +      expect(Chouette::VehicleJourneyAtStop.all.size).to eq(0) +    end + +    it "should return false when vehicle journeys in file are invalid" do +      invalid_file = File.open(Rails.root.join("spec", "fixtures", "vehicle_journey_imports_with_vj_invalid.csv").to_s, "r") +      expect(VehicleJourneyImport.new(:route => other_route, :file => invalid_file).load_imported_vehicle_journeys).to eq([]) +      expect(Chouette::VehicleJourney.all.size).to eq(3) +      expect(Chouette::VehicleJourneyAtStop.all.size).to eq(0) +    end + +    it "should return false when vehicle journey at stops in file are invalid" do +      invalid_file = File.open(Rails.root.join("spec", "fixtures", "vehicle_journey_imports_with_vjas_invalid.csv").to_s, "r") +      expect(VehicleJourneyImport.new(:route => other_route, :file => invalid_file).load_imported_vehicle_journeys).to eq([]) +      expect(Chouette::VehicleJourney.all.size).to eq(3) +      expect(Chouette::VehicleJourneyAtStop.all.size).to eq(0) +    end +     +    it "should load vehicle journeys" do +      expect(subject.load_imported_vehicle_journeys.size).to eq(3) +      expect(Chouette::VehicleJourney.all.collect(&:objectid)).to match_array([vehicle_journey1.objectid, vehicle_journey2.objectid, vehicle_journey3.objectid]) +      expect(Chouette::VehicleJourneyAtStop.all.size).to eq(11) +    end +     +  end + +   +   +end | 
