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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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 |
