aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/assets/stylesheets/components/_tables.sass4
-rw-r--r--app/assets/stylesheets/modules/_vj_collection.sass62
-rw-r--r--app/controllers/referential_vehicle_journeys_controller.rb21
-rw-r--r--app/controllers/vehicle_journeys_controller.rb6
-rw-r--r--app/javascript/helpers/stop_area_header_manager.js6
-rw-r--r--app/javascript/vehicle_journeys/actions/index.js30
-rw-r--r--app/javascript/vehicle_journeys/components/VehicleJourney.js38
-rw-r--r--app/javascript/vehicle_journeys/components/VehicleJourneys.js62
-rw-r--r--app/javascript/vehicle_journeys/reducers/vehicleJourneys.js25
-rw-r--r--app/models/chouette/company.rb1
-rw-r--r--app/models/chouette/journey_pattern.rb16
-rw-r--r--app/models/chouette/line.rb9
-rw-r--r--app/models/chouette/purchase_window.rb1
-rw-r--r--app/models/chouette/route.rb6
-rw-r--r--app/models/chouette/stop_area.rb8
-rw-r--r--app/models/chouette/time_table.rb4
-rw-r--r--app/models/chouette/vehicle_journey.rb6
-rw-r--r--app/models/chouette/vehicle_journey_at_stop.rb8
-rw-r--r--app/models/chouette/vehicle_journey_at_stops_day_offset.rb16
-rw-r--r--app/models/referential.rb2
-rw-r--r--app/models/simple_importer.rb422
-rw-r--r--app/views/referential_vehicle_journeys/index.html.slim10
-rw-r--r--app/views/referentials/_overview.html.slim2
-rw-r--r--app/views/vehicle_journeys/show.rabl6
-rw-r--r--config/initializers/apartment.rb1
-rw-r--r--config/secrets.yml.docker2
-rw-r--r--db/migrate/20180129210928_create_simple_importers.rb10
-rw-r--r--db/schema.rb11
-rw-r--r--lib/tasks/imports.rake97
-rw-r--r--spec/fixtures/simple_importer/lines_mapping.csv11
-rw-r--r--spec/fixtures/simple_importer/stop_area.csv2
-rw-r--r--spec/fixtures/simple_importer/stop_area_full.csv3
-rw-r--r--spec/fixtures/simple_importer/stop_area_full_reverse.csv3
-rw-r--r--spec/fixtures/simple_importer/stop_area_incomplete.csv3
-rw-r--r--spec/fixtures/simple_importer/stop_area_missing_street_name.csv2
-rw-r--r--spec/fixtures/simple_importer/stop_points_full.csv11
-rw-r--r--spec/javascript/vehicle_journeys/components/__snapshots__/VehicleJourneys_spec.js.snap6
-rw-r--r--spec/javascript/vehicle_journeys/reducers/vehicleJourneys_spec.js8
-rw-r--r--spec/models/chouette/journey_pattern_spec.rb24
-rw-r--r--spec/models/chouette/vehicle_journey_at_stop_spec.rb2
-rw-r--r--spec/models/simple_importer_spec.rb394
41 files changed, 1294 insertions, 67 deletions
diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass
index 1e02ad586..ef19bd538 100644
--- a/app/assets/stylesheets/components/_tables.sass
+++ b/app/assets/stylesheets/components/_tables.sass
@@ -374,6 +374,8 @@
.th
text-align: right
border-top-color: transparent
+ > div:not(.btn-group)
+ min-height: 20px
.td > .headlined
&:before
@@ -408,7 +410,7 @@
.th
> div:not(.btn-group)
- min-height: 19px
+ min-height: 20px
> *:first-child
padding-right: 30px
diff --git a/app/assets/stylesheets/modules/_vj_collection.sass b/app/assets/stylesheets/modules/_vj_collection.sass
index d99c67bd7..d9079daa2 100644
--- a/app/assets/stylesheets/modules/_vj_collection.sass
+++ b/app/assets/stylesheets/modules/_vj_collection.sass
@@ -86,6 +86,20 @@
&:after
bottom: 50%
+
+ .table-2entries .t2e-head
+ .detailed-timetables
+ .fa
+ margin-right: 5px
+ .detailed-timetables-bt
+ text-decoration: none
+ .fa
+ margin-right: 5px
+ color: $red
+ transition: transform 0.1s
+ &.active .fa
+ transform: rotate(180deg)
+
.table-2entries .t2e-head > .td:nth-child(2) > div,
.table-2entries .t2e-head > .td:last-child > div,
.table-2entries.no_result .t2e-head > .td:last-child > div
@@ -103,6 +117,54 @@
top: 50%
margin-top: -8px
+ .detailed-timetables
+ padding-top: 10px
+ text-align: left
+ margin-bottom: -5px
+
+ & > div
+ position: relative
+ border-left: 1px solid $lightgrey
+ padding-left: 10px
+ a
+ text-decoration: none
+ border: none
+ &:before
+ position: absolute
+ left: 0px
+ top: 0
+ right: -8px
+ content: ""
+ border-top: 1px solid $lightgrey
+ font-size: 0.8em
+ height: 44px
+ position: relative
+ padding-bottom: 5px
+
+ p
+ margin: 0
+ p:first-child
+ padding-top: 8px
+ font-weight: bold
+
+ .t2e-item-list .detailed-timetables > div
+ border-left: none
+ &:after
+ top: 50%
+ left: 50%
+ content: ""
+ border: 1px solid black
+ width: 20px
+ height: 20px
+ margin-left: -10px
+ margin-top: -10px
+ position: absolute
+ border-radius: 20px
+ &.active:after
+ background: black
+ &:before
+ left: -8px
+
// Errors
.table-2entries .t2e-item-list
.t2e-item
diff --git a/app/controllers/referential_vehicle_journeys_controller.rb b/app/controllers/referential_vehicle_journeys_controller.rb
index 2ce28a5cc..e36ef8153 100644
--- a/app/controllers/referential_vehicle_journeys_controller.rb
+++ b/app/controllers/referential_vehicle_journeys_controller.rb
@@ -20,11 +20,30 @@ class ReferentialVehicleJourneysController < ChouetteController
@q = ransack_period_range(scope: @q, error_message: t('vehicle_journeys.errors.purchase_window'), query: :in_purchase_window, prefix: :purchase_window)
@q = ransack_period_range(scope: @q, error_message: t('vehicle_journeys.errors.time_table'), query: :with_matching_timetable, prefix: :time_table)
@q = @q.ransack(params[:q])
- @vehicle_journeys ||= @q.result.order(:published_journey_name).includes(:vehicle_journey_at_stops).paginate page: params[:page], per_page: params[:per_page] || 10
+ @vehicle_journeys ||= @q.result
+ @vehicle_journeys = parse_order @vehicle_journeys
+ @vehicle_journeys = @vehicle_journeys.paginate page: params[:page], per_page: params[:per_page] || 10
@all_companies = Chouette::Company.where("id IN (#{@referential.vehicle_journeys.select(:company_id).to_sql})").distinct
@all_stop_areas = Chouette::StopArea.where("id IN (#{@referential.vehicle_journeys.joins(:stop_areas).select("stop_areas.id").to_sql})").distinct
stop_area_ids = params[:q].try(:[], :stop_area_ids).try(:select, &:present?)
@filters_stop_areas = Chouette::StopArea.find(stop_area_ids) if stop_area_ids.present? && stop_area_ids.size <= 2
end
+ def parse_order scope
+ return scope.order(:published_journey_name) unless params[:sort].present?
+ direction = params[:direction] || "asc"
+ attributes = Chouette::VehicleJourney.column_names.map{|n| "vehicle_journeys.#{n}"}.join(',')
+ case params[:sort]
+ when "line"
+ scope.order("lines.name #{direction}").joins(route: :line)
+ when "route"
+ scope.order("routes.name #{direction}").joins(:route)
+ when "departure_time"
+ scope.joins(:vehicle_journey_at_stops).group(attributes).select(attributes).order("MIN(vehicle_journey_at_stops.departure_time) #{direction}")
+ when "arrival_time"
+ scope.joins(:vehicle_journey_at_stops).group(attributes).select(attributes).order("MAX(vehicle_journey_at_stops.departure_time) #{direction}")
+ else
+ scope.order "#{params[:sort]} #{direction}"
+ end
+ end
end
diff --git a/app/controllers/vehicle_journeys_controller.rb b/app/controllers/vehicle_journeys_controller.rb
index a389f3ab0..14795227c 100644
--- a/app/controllers/vehicle_journeys_controller.rb
+++ b/app/controllers/vehicle_journeys_controller.rb
@@ -154,7 +154,7 @@ class VehicleJourneysController < ChouetteController
private
def load_custom_fields
- @custom_fields = current_workgroup&.custom_fields_definitions || {}
+ @custom_fields = referential.workgroup&.custom_fields_definitions || {}
end
def map_stop_points points
@@ -185,7 +185,9 @@ class VehicleJourneysController < ChouetteController
:long_lat_type => sp.stop_area.try(:long_lat_type),
:country_code => sp.stop_area.try(:country_code),
:country_name => sp.stop_area.try(:country_name),
- :street_name => sp.stop_area.try(:street_name)
+ :street_name => sp.stop_area.try(:street_name),
+ :waiting_time => sp.stop_area.try(:waiting_time),
+ :waiting_time_text => sp.stop_area.decorate.try(:waiting_time_text),
}
end
end
diff --git a/app/javascript/helpers/stop_area_header_manager.js b/app/javascript/helpers/stop_area_header_manager.js
index 5b18e2f63..1003b2cf6 100644
--- a/app/javascript/helpers/stop_area_header_manager.js
+++ b/app/javascript/helpers/stop_area_header_manager.js
@@ -15,11 +15,15 @@ export default class StopAreaHeaderManager {
let index = this.ids_list.indexOf(object_id)
let sp = this.stopPointsList[index]
let showHeadline = this.showHeader(object_id)
+ let title = sp.city_name ? sp.city_name + ' (' + sp.zip_code +')' : ""
+ if(sp.waiting_time > 0){
+ title += " | " + sp.waiting_time_text
+ }
return (
<div
className={(showHeadline) ? 'headlined' : ''}
data-headline={showHeadline}
- title={sp.city_name ? sp.city_name + ' (' + sp.zip_code +')' : ""}
+ title={title}
>
<span>
<span>
diff --git a/app/javascript/vehicle_journeys/actions/index.js b/app/javascript/vehicle_journeys/actions/index.js
index 4ca8bd73b..b398d78fa 100644
--- a/app/javascript/vehicle_journeys/actions/index.js
+++ b/app/javascript/vehicle_journeys/actions/index.js
@@ -348,21 +348,11 @@ const actions = {
var purchaseWindows = []
let tt
for (tt of val.time_tables){
- timeTables.push({
- objectid: tt.objectid,
- comment: tt.comment,
- id: tt.id,
- color: tt.color
- })
+ timeTables.push(tt)
}
if(val.purchase_windows){
for (tt of val.purchase_windows){
- purchaseWindows.push({
- objectid: tt.objectid,
- name: tt.name,
- id: tt.id,
- color: tt.color
- })
+ purchaseWindows.push(tt)
}
}
let vjasWithDelta = val.vehicle_journey_at_stops.map((vjas, i) => {
@@ -527,6 +517,22 @@ const actions = {
minute: actions.simplePad(newArrivalDT.getUTCMinutes())
}
}
+ },
+ addMinutesToTime: (time, minutes) => {
+ let res = {
+ hour: time.hour,
+ minute: time.minute
+ }
+ let delta_hour = parseInt(minutes/60)
+ let delta_minute = minutes - 60*delta_hour
+ res.hour += delta_hour
+ res.minute += delta_minute
+ let extra_hours = parseInt(res.minute/60)
+ res.hour += extra_hours
+ res.minute -= extra_hours*60
+ res.hour = res.hour % 24
+
+ return res
}
}
diff --git a/app/javascript/vehicle_journeys/components/VehicleJourney.js b/app/javascript/vehicle_journeys/components/VehicleJourney.js
index 4a9432231..e11e91497 100644
--- a/app/javascript/vehicle_journeys/components/VehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/VehicleJourney.js
@@ -48,12 +48,24 @@ export default class VehicleJourney extends Component {
}
}
+ hasTimeTable(time_tables, tt) {
+ let found = false
+ time_tables.map((t, index) => {
+ if(t.id == tt.id){
+ found = true
+ return
+ }
+ })
+ return found
+ }
+
isDisabled(bool1, bool2) {
return (bool1 || bool2)
}
render() {
this.previousCity = undefined
+ let detailed_calendars = this.hasFeature('detailed_calendars') && !this.disabled
let {time_tables, purchase_windows} = this.props.value
return (
@@ -68,20 +80,20 @@ export default class VehicleJourney extends Component {
<div>{this.props.value.published_journey_name && this.props.value.published_journey_name != I18n.t('undefined') ? this.props.value.published_journey_name : '-'}</div>
<div>{this.props.value.journey_pattern.short_id || '-'}</div>
<div>{this.props.value.company ? this.props.value.company.name : '-'}</div>
+ { this.hasFeature('purchase_windows') &&
+ <div>
+ {purchase_windows.slice(0,3).map((tt, i)=>
+ <span key={i} className='vj_tt'>{this.purchaseWindowURL(tt)}</span>
+ )}
+ {purchase_windows.length > 3 && <span className='vj_tt'> + {purchase_windows.length - 3}</span>}
+ </div>
+ }
<div>
{time_tables.slice(0,3).map((tt, i)=>
<span key={i} className='vj_tt'>{this.timeTableURL(tt)}</span>
)}
{time_tables.length > 3 && <span className='vj_tt'> + {time_tables.length - 3}</span>}
</div>
- { this.hasFeature('purchase_windows') &&
- <div>
- {purchase_windows.slice(0,3).map((tt, i)=>
- <span key={i} className='vj_tt'>{this.purchaseWindowURL(tt)}</span>
- )}
- {purchase_windows.length > 3 && <span className='vj_tt'> + {purchase_windows.length - 3}</span>}
- </div>
- }
{!this.props.disabled && <div className={(this.props.value.deletable ? 'disabled ' : '') + 'checkbox'}>
<input
id={this.props.index}
@@ -94,7 +106,16 @@ export default class VehicleJourney extends Component {
></input>
<label htmlFor={this.props.index}></label>
</div>}
+
{this.props.disabled && <VehicleJourneyInfoButton vehicleJourney={this.props.value} />}
+
+ { detailed_calendars &&
+ <div className="detailed-timetables hidden">
+ {this.props.allTimeTables.map((tt, i) =>
+ <div key={i} className={(this.hasTimeTable(time_tables, tt) ? "active" : "inactive")}></div>
+ )}
+ </div>
+ }
</div>
{this.props.value.vehicle_journey_at_stops.map((vj, i) =>
<div key={i} className='td text-center'>
@@ -174,4 +195,5 @@ VehicleJourney.propTypes = {
onUpdateTime: PropTypes.func.isRequired,
onSelectVehicleJourney: PropTypes.func.isRequired,
vehicleJourneys: PropTypes.object.isRequired,
+ allTimeTables: PropTypes.array.isRequired,
}
diff --git a/app/javascript/vehicle_journeys/components/VehicleJourneys.js b/app/javascript/vehicle_journeys/components/VehicleJourneys.js
index 01e07ee0c..843aec1a8 100644
--- a/app/javascript/vehicle_journeys/components/VehicleJourneys.js
+++ b/app/javascript/vehicle_journeys/components/VehicleJourneys.js
@@ -12,6 +12,7 @@ export default class VehicleJourneys extends Component {
this.stopPoints(),
this.props.filters.features
)
+ this.toggleTimetables = this.toggleTimetables.bind(this)
}
isReturn() {
@@ -48,9 +49,35 @@ export default class VehicleJourneys extends Component {
return this.headerManager.showHeader(object_id)
}
+ allTimeTables() {
+ if(this._allTimeTables){
+ return this._allTimeTables
+ }
+ let keys = []
+ this._allTimeTables = []
+ this.vehicleJourneysList().map((vj, index) => {
+ vj.time_tables.map((tt, _) => {
+ if(keys.indexOf(tt.id) < 0){
+ keys.push(tt.id)
+ this._allTimeTables.push(tt)
+ }
+ })
+ })
+ return this._allTimeTables
+ }
+
+ toggleTimetables(e) {
+ $('.table-2entries .detailed-timetables').toggleClass('hidden')
+ $('.table-2entries .detailed-timetables-bt').toggleClass('active')
+ this.componentDidUpdate()
+ e.preventDefault()
+ false
+ }
+
componentDidUpdate(prevProps, prevState) {
if(this.props.status.isFetching == false){
$('.table-2entries').each(function() {
+ $(this).find('.th').css('height', 'auto')
var refH = []
var refCol = []
@@ -91,9 +118,19 @@ export default class VehicleJourneys extends Component {
}
}
+ timeTableURL(tt) {
+ let refURL = window.location.pathname.split('/', 3).join('/')
+ let ttURL = refURL + '/time_tables/' + tt.id
+
+ return (
+ <a href={ttURL} title='Voir le calendrier'><span className='fa fa-calendar' style={{color: (tt.color ? tt.color : '#4B4B4B')}}></span>{tt.days || tt.comment}</a>
+ )
+ }
+
render() {
this.previousBreakpoint = undefined
-
+ this._allTimeTables = null
+ let detailed_calendars = this.hasFeature('detailed_calendars') && !this.isReturn() && (this.allTimeTables().length > 0)
if(this.props.status.isFetching == true) {
return (
<div className="isLoading" style={{marginTop: 80, marginBottom: 80}}>
@@ -133,8 +170,28 @@ export default class VehicleJourneys extends Component {
<div>{I18n.attribute_name("vehicle_journey", "name")}</div>
<div>{I18n.attribute_name("vehicle_journey", "journey_pattern_id")}</div>
<div>{I18n.model_name("company")}</div>
- <div>{I18n.model_name("time_table", "plural": true)}</div>
{ this.hasFeature('purchase_windows') && <div>{I18n.model_name("purchase_window", "plural": true)}</div> }
+ <div>
+ { detailed_calendars &&
+ <a href='#' onClick={this.toggleTimetables} className='detailed-timetables-bt'>
+ <span className='fa fa-angle-up'></span>
+ {I18n.model_name("time_table", "plural": true)}
+ </a>
+ }
+ { !detailed_calendars && I18n.model_name("time_table", "plural": true)}
+ </div>
+ { !this.isReturn() &&
+ <div className="detailed-timetables hidden">
+ {this.allTimeTables().map((tt, i)=>
+ <div key={i}>
+ <p>
+ {this.timeTableURL(tt)}
+ </p>
+ <p>{tt.bounding_dates}</p>
+ </div>
+ )}
+ </div>
+ }
</div>
{this.stopPoints().map((sp, i) =>{
return (
@@ -159,6 +216,7 @@ export default class VehicleJourneys extends Component {
onSelectVehicleJourney={this.props.onSelectVehicleJourney}
vehicleJourneys={this}
disabled={this.isReturn()}
+ allTimeTables={this.allTimeTables()}
/>
)}
</div>
diff --git a/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js
index 1a15ec46d..383dea4a0 100644
--- a/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js
+++ b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js
@@ -19,26 +19,17 @@ const vehicleJourney= (state = {}, action, keep) => {
current_time.minute = parseInt(action.data["start_time.minute"].value) || 0
}
_.each(action.stopPointsList, (sp) =>{
+ let inJourney = false
if(action.selectedJourneyPattern.full_schedule && action.selectedJourneyPattern.costs && action.selectedJourneyPattern.costs[prevSp.stop_area_id + "-" + sp.stop_area_id]){
let delta = parseInt(action.selectedJourneyPattern.costs[prevSp.stop_area_id + "-" + sp.stop_area_id].time)
- let delta_hour = parseInt(delta/60)
- let delta_minute = delta - 60*delta_hour
- current_time.hour += delta_hour
- current_time.minute += delta_minute
- let extra_hours = parseInt(current_time.minute/60)
- current_time.hour += extra_hours
- current_time.minute -= extra_hours*60
- current_time.hour = current_time.hour % 24
+ current_time = actions.addMinutesToTime(current_time, delta)
prevSp = sp
+ inJourney = true
}
let offsetHours = sp.time_zone_offset / 3600
let offsetminutes = sp.time_zone_offset/60 - 60*offsetHours
let newVjas = {
delta: 0,
- departure_time:{
- hour: (24 + current_time.hour + offsetHours) % 24,
- minute: current_time.minute + offsetminutes
- },
arrival_time:{
hour: (24 + current_time.hour + offsetHours) % 24,
minute: current_time.minute + offsetminutes
@@ -47,6 +38,16 @@ const vehicleJourney= (state = {}, action, keep) => {
stop_area_cityname: sp.city_name,
dummy: true
}
+
+ if(sp.waiting_time && inJourney){
+ current_time = actions.addMinutesToTime(current_time, parseInt(sp.waiting_time))
+ }
+
+ newVjas.departure_time = {
+ hour: (24 + current_time.hour + offsetHours) % 24,
+ minute: current_time.minute + offsetminutes
+ }
+
if(current_time.hour + offsetHours > 24){
newVjas.departure_day_offset = 1
newVjas.arrival_day_offset = 1
diff --git a/app/models/chouette/company.rb b/app/models/chouette/company.rb
index b3d40ab96..53e412600 100644
--- a/app/models/chouette/company.rb
+++ b/app/models/chouette/company.rb
@@ -15,6 +15,5 @@ module Chouette
[:organizational_unit, :operating_department_name, :code, :phone, :fax, :email, :url, :time_zone]
end
-
end
end
diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb
index aa9fdb810..830e985d9 100644
--- a/app/models/chouette/journey_pattern.rb
+++ b/app/models/chouette/journey_pattern.rb
@@ -170,5 +170,21 @@ module Chouette
end
full
end
+
+ def set_distances distances
+ raise "inconsistent data: #{distances.count} values for #{stop_points.count} stops" unless distances.count == stop_points.count
+ prev = distances[0].to_i
+ _costs = self.costs
+ distances[1..-1].each_with_index do |distance, i|
+ distance = distance.to_i
+ relative = distance - prev
+ prev = distance
+ start, stop = stop_points[i..i+1]
+ key = "#{start.stop_area_id}-#{stop.stop_area_id}"
+ _costs[key] ||= {}
+ _costs[key]["distance"] = relative
+ end
+ self.costs = _costs
+ end
end
end
diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb
index ba2e2755d..d077d5c9d 100644
--- a/app/models/chouette/line.rb
+++ b/app/models/chouette/line.rb
@@ -41,6 +41,7 @@ module Chouette
validates_presence_of :name
+
scope :by_text, ->(text) { where('lower(name) LIKE :t or lower(published_name) LIKE :t or lower(objectid) LIKE :t or lower(comment) LIKE :t or lower(number) LIKE :t',
t: "%#{text.downcase}%") }
@@ -80,6 +81,14 @@ module Chouette
line_referential.companies.where(id: ([company_id] + Array(secondary_company_ids)).compact)
end
+ def deactivate
+ self.deactivated = true
+ end
+
+ def activate
+ self.deactivated = false
+ end
+
def deactivate!
update_attribute :deactivated, true
end
diff --git a/app/models/chouette/purchase_window.rb b/app/models/chouette/purchase_window.rb
index 334493015..157390a21 100644
--- a/app/models/chouette/purchase_window.rb
+++ b/app/models/chouette/purchase_window.rb
@@ -19,6 +19,7 @@ module Chouette
scope :contains_date, ->(date) { where('date ? <@ any (date_ranges)', date) }
scope :overlap_dates, ->(date_range) { where('daterange(?, ?) && any (date_ranges)', date_range.first, date_range.last + 1.day) }
+ scope :matching_dates, ->(date_range) { where('ARRAY[daterange(?, ?)] = date_ranges', date_range.first, date_range.last + 1.day) }
def self.ransackable_scopes(auth_object = nil)
[:contains_date]
diff --git a/app/models/chouette/route.rb b/app/models/chouette/route.rb
index 5cc5d8b0d..3729deb7d 100644
--- a/app/models/chouette/route.rb
+++ b/app/models/chouette/route.rb
@@ -185,6 +185,12 @@ module Chouette
return true
end
+ def full_journey_pattern
+ journey_pattern = journey_patterns.find_or_create_by registration_number: self.number, name: self.name
+ journey_pattern.stop_points = self.stop_points
+ journey_pattern
+ end
+
protected
def self.vehicle_journeys_timeless(stop_point_id)
diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb
index 830fe8b78..7170dd217 100644
--- a/app/models/chouette/stop_area.rb
+++ b/app/models/chouette/stop_area.rb
@@ -369,6 +369,14 @@ module Chouette
!activated?
end
+ def activate
+ self.deleted_at = nil
+ end
+
+ def deactivate
+ self.deleted_at = Time.now
+ end
+
def activate!
update_attribute :deleted_at, nil
end
diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb
index 15b22b671..b76de852a 100644
--- a/app/models/chouette/time_table.rb
+++ b/app/models/chouette/time_table.rb
@@ -44,10 +44,10 @@ module Chouette
attrs << self.int_day_types
dates = self.dates
dates += TimeTableDate.where(time_table_id: self.id)
- attrs << dates.map(&:checksum).map(&:to_s).sort
+ attrs << dates.map(&:checksum).map(&:to_s).uniq.sort
periods = self.periods
periods += TimeTablePeriod.where(time_table_id: self.id)
- attrs << periods.map(&:checksum).map(&:to_s).sort
+ attrs << periods.map(&:checksum).map(&:to_s).uniq.sort
end
end
diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb
index 4a6ba3f75..1a79db823 100644
--- a/app/models/chouette/vehicle_journey.rb
+++ b/app/models/chouette/vehicle_journey.rb
@@ -105,7 +105,7 @@ module Chouette
attrs << self.try(:company).try(:get_objectid).try(:local_id)
attrs << self.footnotes.map(&:checksum).sort
vjas = self.vehicle_journey_at_stops
- vjas += VehicleJourneyAtStop.where(vehicle_journey_id: self.id)
+ vjas += VehicleJourneyAtStop.where(vehicle_journey_id: self.id) unless self.new_record?
attrs << vjas.uniq.sort_by { |s| s.stop_point&.position }.map(&:checksum).sort
end
end
@@ -381,8 +381,8 @@ module Chouette
end
def self.lines
- lines_query = joins(:route).select("routes.line_id").to_sql
- Chouette::Line.where("id IN (#{lines_query})")
+ lines_query = joins(:route).select("routes.line_id").reorder(nil).except(:group).pluck(:'routes.line_id')
+ Chouette::Line.where(id: lines_query)
end
end
end
diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb
index eda711ade..3b4f35f13 100644
--- a/app/models/chouette/vehicle_journey_at_stop.rb
+++ b/app/models/chouette/vehicle_journey_at_stop.rb
@@ -41,7 +41,7 @@ module Chouette
:arrival_day_offset,
I18n.t(
'vehicle_journey_at_stops.errors.day_offset_must_not_exceed_max',
- short_id: vehicle_journey.get_objectid.short_id,
+ short_id: vehicle_journey&.get_objectid&.short_id,
max: DAY_OFFSET_MAX + 1
)
)
@@ -52,7 +52,7 @@ module Chouette
:departure_day_offset,
I18n.t(
'vehicle_journey_at_stops.errors.day_offset_must_not_exceed_max',
- short_id: vehicle_journey.get_objectid.short_id,
+ short_id: vehicle_journey&.get_objectid&.short_id,
max: DAY_OFFSET_MAX + 1
)
)
@@ -69,8 +69,8 @@ module Chouette
def checksum_attributes
[].tap do |attrs|
- attrs << self.departure_time.try(:to_s, :time)
- attrs << self.arrival_time.try(:to_s, :time)
+ attrs << self.departure_time&.utc.try(:to_s, :time)
+ attrs << self.arrival_time&.utc.try(:to_s, :time)
attrs << self.departure_day_offset.to_s
attrs << self.arrival_day_offset.to_s
end
diff --git a/app/models/chouette/vehicle_journey_at_stops_day_offset.rb b/app/models/chouette/vehicle_journey_at_stops_day_offset.rb
index b2cb90d11..7497cd72c 100644
--- a/app/models/chouette/vehicle_journey_at_stops_day_offset.rb
+++ b/app/models/chouette/vehicle_journey_at_stops_day_offset.rb
@@ -11,13 +11,19 @@ module Chouette
@at_stops.inject(nil) do |prior_stop, stop|
next stop if prior_stop.nil?
- if stop.arrival_time < prior_stop.departure_time ||
- stop.arrival_time < prior_stop.arrival_time
+ # we only compare time of the day, not actual times
+ stop_arrival_time = stop.arrival_time - stop.arrival_time.to_date.to_time
+ stop_departure_time = stop.departure_time - stop.departure_time.to_date.to_time
+ prior_stop_arrival_time = prior_stop.arrival_time - prior_stop.arrival_time.to_date.to_time
+ prior_stop_departure_time = prior_stop.departure_time - prior_stop.departure_time.to_date.to_time
+
+ if stop_arrival_time < prior_stop_departure_time ||
+ stop_arrival_time < prior_stop_arrival_time
arrival_offset += 1
end
- if stop.departure_time < stop.arrival_time ||
- stop.departure_time < prior_stop.departure_time
+ if stop_departure_time < stop_arrival_time ||
+ stop_departure_time < prior_stop_departure_time
departure_offset += 1
end
@@ -39,4 +45,4 @@ module Chouette
save
end
end
-end \ No newline at end of file
+end
diff --git a/app/models/referential.rb b/app/models/referential.rb
index 509e0412f..09c2e7d34 100644
--- a/app/models/referential.rb
+++ b/app/models/referential.rb
@@ -51,7 +51,9 @@ class Referential < ActiveRecord::Base
belongs_to :stop_area_referential
validates_presence_of :stop_area_referential
has_many :stop_areas, through: :stop_area_referential
+
belongs_to :workbench
+ delegate :workgroup, to: :workbench, allow_nil: true
belongs_to :referential_suite
diff --git a/app/models/simple_importer.rb b/app/models/simple_importer.rb
new file mode 100644
index 000000000..b824d596d
--- /dev/null
+++ b/app/models/simple_importer.rb
@@ -0,0 +1,422 @@
+class SimpleImporter < ActiveRecord::Base
+ attr_accessor :configuration
+
+ def self.define name
+ @importers ||= {}
+ configuration = Configuration.new name
+ yield configuration
+ configuration.validate!
+ @importers[name.to_sym] = configuration
+ end
+
+ def self.find_configuration name
+ @importers ||= {}
+ configuration = @importers[name.to_sym]
+ raise "Importer not found: #{name}" unless configuration
+ configuration
+ end
+
+ def initialize *args
+ super *args
+ self.configuration = self.class.find_configuration self.configuration_name
+ self.journal ||= []
+ end
+
+ def configure
+ new_config = configuration.duplicate
+ yield new_config
+ new_config.validate!
+ self.configuration = new_config
+ end
+
+ def context
+ self.configuration.context
+ end
+
+ def resolve col_name, value, &block
+ val = block.call(value)
+ return val if val.present?
+ @resolution_queue[[col_name.to_s, value]].push({record: @current_record, attribute: @current_attribute, block: block})
+ nil
+ end
+
+ def import opts={}
+ @verbose = opts.delete :verbose
+
+
+ @resolution_queue = Hash.new{|h,k| h[k] = []}
+ @errors = []
+ @messages = []
+ @number_of_lines = 0
+ @padding = 1
+ @current_line = 0
+ fail_with_error "File not found: #{self.filepath}" do
+ @number_of_lines = CSV.read(self.filepath, self.configuration.csv_options).length
+ @padding = [1, Math.log(@number_of_lines, 10).ceil()].max
+ end
+
+
+ self.configuration.before_actions(:parsing).each do |action| action.call self end
+
+ @statuses = ""
+
+ if ENV["NO_TRANSACTION"]
+ process_csv_file
+ else
+ ActiveRecord::Base.transaction do
+ process_csv_file
+ end
+ end
+ self.status ||= :success
+ rescue FailedImport
+ self.status = :failed
+ ensure
+ self.save!
+ end
+
+ def fail_with_error msg=nil, opts={}
+ begin
+ yield
+ rescue => e
+ msg = msg.call if msg.is_a?(Proc)
+ custom_print "\nFAILED: \n errors: #{msg}\n exception: #{e.message}\n#{e.backtrace.join("\n")}", color: :red unless self.configuration.ignore_failures
+ push_in_journal({message: msg, error: e.message, event: :error, kind: :error})
+ @new_status = colorize("x", :red)
+ if self.configuration.ignore_failures
+ raise FailedRow if opts[:abort_row]
+ else
+ raise FailedImport
+ end
+ end
+ end
+
+ def encode_string s
+ s.encode("utf-8").force_encoding("utf-8")
+ end
+
+ def dump_csv_from_context
+ filepath = "./#{self.configuration_name}_#{Time.now.strftime "%y%m%d%H%M"}.csv"
+ # for some reason, context[:csv].to_csv does not work
+ CSV.open(filepath, 'w') do |csv|
+ header = true
+ context[:csv].each do |row|
+ csv << row.headers if header
+ csv << row.fields
+ header = false
+ end
+ end
+ log "CSV file dumped in #{filepath}"
+ end
+
+ def log msg, opts={}
+ msg = colorize msg, opts[:color] if opts[:color]
+ if opts[:append]
+ @messages[-1] = (@messages[-1] || "") + msg
+ else
+ @messages << msg
+ end
+ print_state
+ end
+
+ protected
+
+ def process_csv_file
+ self.configuration.before_actions(:all).each do |action| action.call self end
+ log "Starting import ...", color: :green
+
+ (context[:csv] || CSV.read(filepath, self.configuration.csv_options)).each do |row|
+ @current_row = row
+ @new_status = nil
+ begin
+ handle_row row
+ fail_with_error ->(){ @current_record.errors.messages } do
+ new_record = @current_record&.new_record?
+ @new_status ||= new_record ? colorize("✓", :green) : colorize("-", :orange)
+ @event = new_record ? :creation : :update
+ self.configuration.before_actions(:each_save).each do |action|
+ action.call self, @current_record
+ end
+ ### This could fail if the record has a mandatory relation which is not yet resolved
+ ### TODO: do not attempt to save if the current record if waiting for resolution
+ ### and fail at the end if there remains unresolved relations
+ if @current_record
+ if self.configuration.ignore_failures
+ unless @current_record.save
+ @new_status = colorize("x", :red)
+ push_in_journal({message: "errors: #{@current_record.errors.messages}", error: "invalid record", event: :error, kind: :error})
+ end
+ else
+ @current_record.save!
+ end
+ end
+ self.configuration.after_actions(:each_save).each do |action|
+ action.call self, @current_record
+ end
+ end
+ rescue FailedRow
+ @new_status = colorize("x", :red)
+ end
+ push_in_journal({event: @event, kind: :log}) if @current_record&.valid?
+ @statuses += @new_status
+ self.configuration.columns.each do |col|
+ if @current_record && col.name && @resolution_queue.any?
+ val = @current_record.send col[:attribute]
+ (@resolution_queue.delete([col.name, val]) || []).each do |res|
+ record = res[:record]
+ attribute = res[:attribute]
+ value = res[:block].call(val, record)
+ record.send "#{attribute}=", value
+ record.save!
+ end
+ end
+ end
+ print_state
+ @current_line += 1
+ end
+
+ begin
+ self.configuration.after_actions(:all).each do |action|
+ action.call self
+ end
+ rescue FailedRow
+ end
+ end
+
+ def handle_row row
+ if self.configuration.get_custom_handler
+ instance_exec(row, &self.configuration.get_custom_handler)
+ else
+ fail_with_error "", abort_row: true do
+ @current_record = self.configuration.find_record row
+ self.configuration.columns.each do |col|
+ @current_attribute = col[:attribute]
+ val = col[:value]
+ if val.nil? || val.is_a?(Proc)
+ if row.has_key? col.name
+ if val.is_a?(Proc)
+ val = instance_exec(row[col.name], &val)
+ else
+ val = row[col.name]
+ end
+ else
+ push_in_journal({event: :column_not_found, message: "Column not found: #{col.name}", kind: :warning})
+ self.status ||= :success_with_warnings
+ end
+ end
+
+ if val.nil? && col.required?
+ raise "MISSING VALUE FOR COLUMN #{col.name}"
+ end
+ val = encode_string(val) if val.is_a?(String)
+ @current_record.send "#{@current_attribute}=", val if val
+ end
+ end
+ end
+ end
+
+ def push_in_journal data
+ line = @current_line + 1
+ line += 1 if configuration.headers
+ self.journal.push data.update(line: line, row: @current_row)
+ if data[:kind] == :error || data[:kind] == :warning
+ @errors.push data
+ end
+ end
+
+ def colorize txt, color
+ color = {
+ red: "31",
+ green: "32",
+ orange: "33",
+ }[color] || "33"
+ "\e[#{color}m#{txt}\e[0m"
+ end
+
+ def print_state
+ return unless @verbose
+
+ @status_width ||= begin
+ term_width = %x(tput cols).to_i
+ term_width - @padding - 10
+ rescue
+ 100
+ end
+
+ @status_height ||= begin
+ term_height = %x(tput lines).to_i
+ term_height - 3
+ rescue
+ 50
+ end
+
+ full_status = @statuses || ""
+ full_status = full_status.last(@status_width*10) || ""
+ padding_size = [(@number_of_lines - @current_line - 1), (@status_width - full_status.size/10)].min
+ full_status = "#{full_status}#{"."*[padding_size, 0].max}"
+
+ msg = "#{"%#{@padding}d" % (@current_line + 1)}/#{@number_of_lines}: #{full_status}"
+
+ lines_count = [(@status_height / 2) - 3, 1].max
+
+ if @messages.any?
+ msg += "\n\n"
+ msg += colorize "=== MESSAGES (#{@messages.count}) ===\n", :green
+ msg += "[...]\n" if @messages.count > lines_count
+ msg += @messages.last(lines_count).join("\n")
+ msg += "\n"*[lines_count-@messages.count, 0].max
+ end
+
+ if @errors.any?
+ msg += "\n\n"
+ msg += colorize "=== ERRORS (#{@errors.count}) ===\n", :red
+ msg += "[...]\n" if @errors.count > lines_count
+ msg += @errors.last(lines_count).map do |j|
+ kind = j[:kind]
+ kind = colorize(kind, kind == :error ? :red : :orange)
+ encode_string "[#{kind}]\t\tL#{j[:line]}\t#{j[:error]}\t\t#{j[:message]}"
+ end.join("\n")
+ end
+ custom_print msg, clear: true
+ end
+
+ def custom_print msg, opts={}
+ return unless @verbose
+ out = ""
+ msg = colorize(msg, opts[:color]) if opts[:color]
+ puts "\e[H\e[2J" if opts[:clear]
+ out += msg
+ print out
+ end
+
+ class FailedImport < RuntimeError
+ end
+
+ class FailedRow < RuntimeError
+ end
+
+ class Configuration
+ attr_accessor :model, :headers, :separator, :key, :context, :encoding, :ignore_failures, :scope
+ attr_reader :columns
+
+ def initialize import_name, opts={}
+ @import_name = import_name
+ @key = opts[:key] || "id"
+ @headers = opts.has_key?(:headers) ? opts[:headers] : true
+ @separator = opts[:separator] || ","
+ @encoding = opts[:encoding]
+ @columns = opts[:columns] || []
+ @model = opts[:model]
+ @custom_handler = opts[:custom_handler]
+ @before = opts[:before]
+ @after = opts[:after]
+ @ignore_failures = opts[:ignore_failures]
+ @context = opts[:context] || {}
+ @scope = opts[:scope]
+ end
+
+ def duplicate
+ Configuration.new @import_name, self.options
+ end
+
+ def options
+ {
+ key: @key,
+ headers: @headers,
+ separator: @separator,
+ encoding: @encoding,
+ columns: @columns.map(&:duplicate),
+ model: model,
+ custom_handler: @custom_handler,
+ before: @before,
+ after: @after,
+ ignore_failures: @ignore_failures,
+ context: @context,
+ scope: @scope
+ }
+ end
+
+ def validate!
+ raise "Incomplete configuration, missing model for #{@import_name}" unless model.present?
+ end
+
+ def attribute_for_col col_name
+ column = self.columns.find{|c| c.name == col_name}
+ column && column[:attribute] || col_name
+ end
+
+ def record_scope
+ _scope = @scope
+ _scope = instance_exec(&_scope) if _scope.is_a?(Proc)
+ _scope || model
+ end
+
+ def find_record attrs
+ record_scope.find_or_initialize_by(attribute_for_col(@key) => attrs[@key.to_s])
+ end
+
+ def csv_options
+ {
+ headers: self.headers,
+ col_sep: self.separator,
+ encoding: self.encoding
+ }
+ end
+
+ def add_column name, opts={}
+ @columns.push Column.new({name: name.to_s}.update(opts))
+ end
+
+ def add_value attribute, value
+ @columns.push Column.new({attribute: attribute, value: value})
+ end
+
+ def before group=:all, &block
+ @before ||= Hash.new{|h, k| h[k] = []}
+ @before[group].push block
+ end
+
+ def after group=:all, &block
+ @after ||= Hash.new{|h, k| h[k] = []}
+ @after[group].push block
+ end
+
+ def before_actions group=:all
+ @before ||= Hash.new{|h, k| h[k] = []}
+ @before[group]
+ end
+
+ def after_actions group=:all
+ @after ||= Hash.new{|h, k| h[k] = []}
+ @after[group]
+ end
+
+ def custom_handler &block
+ @custom_handler = block
+ end
+
+ def get_custom_handler
+ @custom_handler
+ end
+
+ class Column
+ attr_accessor :name
+ def initialize opts={}
+ @name = opts[:name]
+ @options = opts
+ @options[:attribute] ||= @name
+ end
+
+ def duplicate
+ Column.new @options.dup
+ end
+
+ def required?
+ !!@options[:required]
+ end
+
+ def [](key)
+ @options[key]
+ end
+ end
+ end
+end
diff --git a/app/views/referential_vehicle_journeys/index.html.slim b/app/views/referential_vehicle_journeys/index.html.slim
index 69e29597c..ca1b1ecd9 100644
--- a/app/views/referential_vehicle_journeys/index.html.slim
+++ b/app/views/referential_vehicle_journeys/index.html.slim
@@ -25,28 +25,28 @@
link_to: lambda do |vehicle_journey| \
vehicle_journey.published_journey_name ? referential_line_route_vehicle_journeys_path(@referential, vehicle_journey.route.line, vehicle_journey.route) : '' \
end, \
- sortable: false \
+ sortable: true \
), \
TableBuilderHelper::Column.new( \
key: :line, \
attribute: Proc.new {|v| v.route.line.name}, \
- sortable: false \
+ sortable: true \
), \
TableBuilderHelper::Column.new( \
key: :route, \
attribute: Proc.new {|v| v.route.name}, \
- sortable: false \
+ sortable: true \
), \
TableBuilderHelper::Column.new( \
key: :departure_time, \
attribute: Proc.new {|v| v.vehicle_journey_at_stops.first&.departure }, \
- sortable: false \
+ sortable: true \
), \
@filters_stop_areas&.map{|s| table_builder_column_for_stop_area(s)},
TableBuilderHelper::Column.new( \
key: :arrival_time, \
attribute: Proc.new {|v| v.vehicle_journey_at_stops.last&.arrival }, \
- sortable: false \
+ sortable: true \
), \
].flatten.compact,
cls: 'table has-filter has-search'
diff --git a/app/views/referentials/_overview.html.slim b/app/views/referentials/_overview.html.slim
index 143784800..539c25fd4 100644
--- a/app/views/referentials/_overview.html.slim
+++ b/app/views/referentials/_overview.html.slim
@@ -9,7 +9,7 @@
span.fa.fa-search
.form-group.togglable
= f.label Chouette::Line.human_attribute_name(:company_id), required: false, class: 'control-label'
- = f.input :company_id_eq_any, collection: overview.referential_lines.map(&:company).uniq.sort_by(&:name), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l.name + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'}
+ = f.input :company_id_eq_any, collection: overview.referential_lines.map(&:company).compact.uniq.sort_by(&:name), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l.name + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'}
.form-group.togglable
= f.label Chouette::Line.human_attribute_name(:transport_mode), required: false, class: 'control-label'
diff --git a/app/views/vehicle_journeys/show.rabl b/app/views/vehicle_journeys/show.rabl
index 546c851a4..bb26ce797 100644
--- a/app/views/vehicle_journeys/show.rabl
+++ b/app/views/vehicle_journeys/show.rabl
@@ -23,6 +23,12 @@ end
child(:time_tables, :object_root => false) do |time_tables|
attributes :id, :objectid, :comment, :color
+ node(:days) do |tt|
+ tt.display_day_types
+ end
+ node(:bounding_dates) do |tt|
+ tt.presenter.time_table_bounding
+ end
child(:calendar) do
attributes :id, :name, :date_ranges, :dates, :shared
end
diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb
index 2d06fb88b..fc652a2da 100644
--- a/config/initializers/apartment.rb
+++ b/config/initializers/apartment.rb
@@ -81,6 +81,7 @@ Apartment.configure do |config|
'ComplianceCheckMessage',
'Merge',
'CustomField',
+ 'SimpleImporter',
]
# use postgres schemas?
diff --git a/config/secrets.yml.docker b/config/secrets.yml.docker
index 7e7808070..f9bbf5fa0 100644
--- a/config/secrets.yml.docker
+++ b/config/secrets.yml.docker
@@ -14,4 +14,4 @@
secret_key_base: <%= ENV.fetch 'SECRET_KEY_BASE', 'change_this_string_for_something_more_secure' %>
api_endpoint: <%= ENV.fetch 'IEV_API_ENDPOINT', 'http://iev:8080/chouette_iev/' %>
api_token: <%= ENV.fetch 'IEV_API_TOKEN', 'change this according to IEV configuration' %>
- newrelic_licence_key: <%= ENF.fetch 'NR_LICENCE_KEY', 'will_not_work' %>
+ newrelic_licence_key: <%= ENV.fetch 'NR_LICENCE_KEY', 'will_not_work' %>
diff --git a/db/migrate/20180129210928_create_simple_importers.rb b/db/migrate/20180129210928_create_simple_importers.rb
new file mode 100644
index 000000000..c2a918900
--- /dev/null
+++ b/db/migrate/20180129210928_create_simple_importers.rb
@@ -0,0 +1,10 @@
+class CreateSimpleImporters < ActiveRecord::Migration
+ def change
+ create_table :simple_importers do |t|
+ t.string :configuration_name
+ t.string :filepath
+ t.string :status
+ t.json :journal
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 596682ce8..f77961f8d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -15,9 +15,10 @@ ActiveRecord::Schema.define(version: 20180202170009) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
- enable_extension "postgis"
enable_extension "hstore"
+ enable_extension "postgis"
enable_extension "unaccent"
+ enable_extension "objectid"
create_table "access_links", id: :bigserial, force: :cascade do |t|
t.integer "access_point_id", limit: 8
@@ -119,6 +120,7 @@ ActiveRecord::Schema.define(version: 20180202170009) do
t.datetime "updated_at"
t.date "end_date"
t.string "date_type"
+ t.string "mode"
end
add_index "clean_ups", ["referential_id"], name: "index_clean_ups_on_referential_id", using: :btree
@@ -724,6 +726,13 @@ ActiveRecord::Schema.define(version: 20180202170009) do
t.integer "line_id", limit: 8
end
+ create_table "simple_importers", id: :bigserial, force: :cascade do |t|
+ t.string "configuration_name"
+ t.string "filepath"
+ t.string "status"
+ t.json "journal"
+ end
+
create_table "stop_area_referential_memberships", id: :bigserial, force: :cascade do |t|
t.integer "organisation_id", limit: 8
t.integer "stop_area_referential_id", limit: 8
diff --git a/lib/tasks/imports.rake b/lib/tasks/imports.rake
index 02e32fd3d..b91ff7efb 100644
--- a/lib/tasks/imports.rake
+++ b/lib/tasks/imports.rake
@@ -1,3 +1,5 @@
+require 'csv'
+
namespace :import do
desc "Notify parent imports when children finish"
task notify_parent: :environment do
@@ -8,4 +10,99 @@ namespace :import do
task netex_abort_old: :environment do
NetexImport.abort_old
end
+
+ def importer_output_to_csv importer
+ filepath = "./#{importer.configuration_name}_#{Time.now.strftime "%y%m%d%H%M"}_out.csv"
+ cols = %w(line kind event message error)
+ if importer.reload.journal.size > 0
+ keys = importer.journal.first["row"].map(&:first)
+ CSV.open(filepath, "w") do |csv|
+ csv << cols + keys
+ importer.journal.each do |j|
+ csv << cols.map{|c| j[c]} + j["row"].map(&:last)
+ end
+ end
+ puts "Import Output written in #{filepath}"
+ end
+ end
+
+ desc "import the given file with the corresponding importer"
+ task :import, [:configuration_name, :filepath, :referential_id] => :environment do |t, args|
+ importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
+ if args[:referential_id].present?
+ referential = Referential.find args[:referential_id]
+ importer.configure do |config|
+ config.add_value :referential, referential
+ config.context = {referential: referential}
+ end
+ end
+ puts "\e[33m***\e[0m Start importing"
+ begin
+ importer.import(verbose: true)
+ rescue Interrupt
+ raise
+ ensure
+ puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
+ importer_output_to_csv importer
+ end
+ end
+
+ desc "import the given file with the corresponding importer in the given StopAreaReferential"
+ task :import_in_stop_area_referential, [:referential_id, :configuration_name, :filepath] => :environment do |t, args|
+ referential = StopAreaReferential.find args[:referential_id]
+ importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
+ importer.configure do |config|
+ config.add_value :stop_area_referential, referential
+ config.context = {stop_area_referential: referential}
+ end
+ puts "\e[33m***\e[0m Start importing"
+ begin
+ importer.import(verbose: true)
+ rescue Interrupt
+ raise
+ ensure
+ puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
+ importer_output_to_csv importer
+ end
+ end
+
+ desc "import the given routes files"
+ task :import_routes, [:referential_id, :configuration_name, :mapping_filepath, :filepath] => :environment do |t, args|
+ referential = Referential.find args[:referential_id]
+ referential.switch
+ stop_area_referential = referential.stop_area_referential
+ importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
+ importer.configure do |config|
+ config.add_value :stop_area_referential, referential
+ config.context = {stop_area_referential: stop_area_referential, mapping_filepath: args[:mapping_filepath]}
+ end
+ puts "\e[33m***\e[0m Start importing"
+ begin
+ importer.import(verbose: true)
+ rescue Interrupt
+ raise
+ ensure
+ puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
+ importer_output_to_csv importer
+ end
+ end
+
+ desc "import the given file with the corresponding importer in the given LineReferential"
+ task :import_in_line_referential, [:referential_id, :configuration_name, :filepath] => :environment do |t, args|
+ referential = LineReferential.find args[:referential_id]
+ importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
+ importer.configure do |config|
+ config.add_value :line_referential, referential
+ config.context = {line_referential: referential}
+ end
+ puts "\e[33m***\e[0m Start importing"
+ begin
+ importer.import(verbose: true)
+ rescue Interrupt
+ raise
+ ensure
+ puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
+ importer_output_to_csv importer
+ end
+ end
end
diff --git a/spec/fixtures/simple_importer/lines_mapping.csv b/spec/fixtures/simple_importer/lines_mapping.csv
new file mode 100644
index 000000000..b26d0ab59
--- /dev/null
+++ b/spec/fixtures/simple_importer/lines_mapping.csv
@@ -0,0 +1,11 @@
+id;timetable_route_id;route_name;stop_sequence;stop_distance;station_code;station_name;border;Ligne Chouette;Transporteur
+3354;1136;Paris centre - Bercy > Lille > Londres;1;0;XPB;Paris City Center - Bercy;f;Paris <> Londres - OUIBUS;OUIBUS
+3355;1136;Paris centre - Bercy > Lille > Londres;2;232;XDB;Lille;f;Paris <> Londres - OUIBUS;OUIBUS
+3749;1136;Paris centre - Bercy > Lille > Londres;3;350;COF;Coquelles - France;t;Paris <> Londres - OUIBUS;OUIBUS
+4772;1136;Paris centre - Bercy > Lille > Londres;4;350;COU;Coquelles - UK;t;Paris <> Londres - OUIBUS;OUIBUS
+3357;1136;Paris centre - Bercy > Lille > Londres;5;527;ZEP;London;f;Paris <> Londres - OUIBUS;OUIBUS
+3358;1137;Londres > Lille > Paris centre - Bercy;1;0;ZEP;London;f;Paris <> Londres - OUIBUS;OUIBUS
+3559;1137;Londres > Lille > Paris centre - Bercy;2;177;COU;Coquelles - UK;t;Paris <> Londres - OUIBUS;OUIBUS
+3743;1137;Londres > Lille > Paris centre - Bercy;3;177;COF;Coquelles - France;t;Paris <> Londres - OUIBUS;OUIBUS
+3360;1137;Londres > Lille > Paris centre - Bercy;4;295;XDB;Lille;f;Paris <> Londres - OUIBUS;OUIBUS
+3361;1137;Londres > Lille > Paris centre - Bercy;5;527;XPB;Paris City Center - Bercy;f;Paris <> Londres - OUIBUS;OUIBUS
diff --git a/spec/fixtures/simple_importer/stop_area.csv b/spec/fixtures/simple_importer/stop_area.csv
new file mode 100644
index 000000000..9361d022b
--- /dev/null
+++ b/spec/fixtures/simple_importer/stop_area.csv
@@ -0,0 +1,2 @@
+name;lat;long;type;street_name
+Nom du Stop;45.00;12;ZDEP;99 rue des Poissonieres
diff --git a/spec/fixtures/simple_importer/stop_area_full.csv b/spec/fixtures/simple_importer/stop_area_full.csv
new file mode 100644
index 000000000..250caab30
--- /dev/null
+++ b/spec/fixtures/simple_importer/stop_area_full.csv
@@ -0,0 +1,3 @@
+"id";"station_code";"uic_code";"country_code";"province";"district";"county";"station_name";"inactive";"change_timestamp";"longitude";"latitude";"parent_station_code";"additional_info";"external_reference";"timezone";"address";"postal_code";"city"
+5669;"PAR";"PAR";"FRA";"";"";"";"Paris - All stations";f;"2017-07-17 11:56:53.138";2.35222190000002;48.856614;"";"";"{""Région"":""Ile-de-France""}";"Europe/Paris";"";"";""
+5748;"XED";"XED";"FRA";"";"";"";"Paris MLV";t;"2017-05-29 11:24:34.575";2.783409;48.870569;"PAR";"";"{""Région"":""Ile-de-France""}";"Europe/Paris";"";"";""
diff --git a/spec/fixtures/simple_importer/stop_area_full_reverse.csv b/spec/fixtures/simple_importer/stop_area_full_reverse.csv
new file mode 100644
index 000000000..9ea15f6cc
--- /dev/null
+++ b/spec/fixtures/simple_importer/stop_area_full_reverse.csv
@@ -0,0 +1,3 @@
+"id";"station_code";"uic_code";"country_code";"province";"district";"county";"station_name";"inactive";"change_timestamp";"longitude";"latitude";"parent_station_code";"additional_info";"external_reference";"timezone";"address";"postal_code";"city"
+5748;"XED";"XED";"FRA";"";"";"";"Paris MLV";t;"2017-05-29 11:24:34.575";2.783409;48.870569;"PAR";"";"{""Région"":""Ile-de-France""}";"Europe/Paris";"";"";""
+5669;"PAR";"PAR";"FRA";"";"";"";"Paris - All stations";f;"2017-07-17 11:56:53.138";2.35222190000002;48.856614;"";"";"{""Région"":""Ile-de-France""}";"Europe/Paris";"";"";""
diff --git a/spec/fixtures/simple_importer/stop_area_incomplete.csv b/spec/fixtures/simple_importer/stop_area_incomplete.csv
new file mode 100644
index 000000000..9b11aa02c
--- /dev/null
+++ b/spec/fixtures/simple_importer/stop_area_incomplete.csv
@@ -0,0 +1,3 @@
+name;lat;long;type;street_name
+Foo;45.00;12;ZDEP
+;45.00;12;ZDEP
diff --git a/spec/fixtures/simple_importer/stop_area_missing_street_name.csv b/spec/fixtures/simple_importer/stop_area_missing_street_name.csv
new file mode 100644
index 000000000..aa845c3f5
--- /dev/null
+++ b/spec/fixtures/simple_importer/stop_area_missing_street_name.csv
@@ -0,0 +1,2 @@
+name;lat;long;type;foo
+Nom du Stop;45.00;12;ZDEP;blabla
diff --git a/spec/fixtures/simple_importer/stop_points_full.csv b/spec/fixtures/simple_importer/stop_points_full.csv
new file mode 100644
index 000000000..9c5b3c1b7
--- /dev/null
+++ b/spec/fixtures/simple_importer/stop_points_full.csv
@@ -0,0 +1,11 @@
+"id";"timetable_route_id";"route_name";"stop_sequence";"stop_distance";"station_code";"station_name";"border"
+3354;1136;"Paris centre - Bercy > Lille > Londres";1;0;"XPB";"Paris City Center - Bercy";f
+3355;1136;"Paris centre - Bercy > Lille > Londres";2;232;"XDB";"Lille";f
+3749;1136;"Paris centre - Bercy > Lille > Londres";3;350;"COF";"Coquelles - France";t
+4772;1136;"Paris centre - Bercy > Lille > Londres";4;350;"COU";"Coquelles - UK";t
+3357;1136;"Paris centre - Bercy > Lille > Londres";5;527;"ZEP";"London";f
+3358;1137;"Londres > Lille > Paris centre - Bercy";1;0;"ZEP";"London";f
+3559;1137;"Londres > Lille > Paris centre - Bercy";2;177;"COU";"Coquelles - UK";t
+3743;1137;"Londres > Lille > Paris centre - Bercy";3;177;"COF";"Coquelles - France";t
+3360;1137;"Londres > Lille > Paris centre - Bercy";4;295;"XDB";"Lille";f
+3361;1137;"Londres > Lille > Paris centre - Bercy";5;527;"XPB";"Paris City Center - Bercy";f
diff --git a/spec/javascript/vehicle_journeys/components/__snapshots__/VehicleJourneys_spec.js.snap b/spec/javascript/vehicle_journeys/components/__snapshots__/VehicleJourneys_spec.js.snap
index cdd34cbbd..818845ec8 100644
--- a/spec/javascript/vehicle_journeys/components/__snapshots__/VehicleJourneys_spec.js.snap
+++ b/spec/javascript/vehicle_journeys/components/__snapshots__/VehicleJourneys_spec.js.snap
@@ -33,6 +33,9 @@ exports[`stopPointHeader should display the city name 1`] = `
<div>
calendrier
</div>
+ <div
+ className="detailed-timetables hidden"
+ />
</div>
<div
className="td"
@@ -123,6 +126,9 @@ exports[`stopPointHeader with the "long_distance_routes" feature should display
<div>
calendrier
</div>
+ <div
+ className="detailed-timetables hidden"
+ />
</div>
<div
className="td"
diff --git a/spec/javascript/vehicle_journeys/reducers/vehicleJourneys_spec.js b/spec/javascript/vehicle_journeys/reducers/vehicleJourneys_spec.js
index 0d7612a80..389c60add 100644
--- a/spec/javascript/vehicle_journeys/reducers/vehicleJourneys_spec.js
+++ b/spec/javascript/vehicle_journeys/reducers/vehicleJourneys_spec.js
@@ -152,7 +152,7 @@ describe('vehicleJourneys reducer', () => {
},
departure_time : {
hour: 23,
- minute: 2
+ minute: 12
},
departure_day_offset: -1,
arrival_day_offset: -1,
@@ -178,11 +178,11 @@ describe('vehicleJourneys reducer', () => {
delta : 0,
arrival_time : {
hour: 0,
- minute: 32
+ minute: 42
},
departure_time : {
hour: 0,
- minute: 32
+ minute: 42
},
stop_point_objectid: 'test-4',
stop_area_cityname: 'city',
@@ -219,7 +219,7 @@ describe('vehicleJourneys reducer', () => {
type: 'ADD_VEHICLEJOURNEY',
data: fakeData,
selectedJourneyPattern: fakeSelectedJourneyPattern,
- stopPointsList: [{object_id: 'test-1', city_name: 'city', stop_area_id: 1, id: 1, time_zone_offset: 0}, {object_id: 'test-2', city_name: 'city', stop_area_id: 2, id: 2, time_zone_offset: -3600}, {object_id: 'test-3', city_name: 'city', stop_area_id: 3, id: 3, time_zone_offset: 0}, {object_id: 'test-4', city_name: 'city', stop_area_id: 4, id: 4, time_zone_offset: 0}],
+ stopPointsList: [{object_id: 'test-1', city_name: 'city', stop_area_id: 1, id: 1, time_zone_offset: 0, waiting_time: null}, {object_id: 'test-2', city_name: 'city', stop_area_id: 2, id: 2, time_zone_offset: -3600, waiting_time: 10}, {object_id: 'test-3', city_name: 'city', stop_area_id: 3, id: 3, time_zone_offset: 0, waiting_time: 20}, {object_id: 'test-4', city_name: 'city', stop_area_id: 4, id: 4, time_zone_offset: 0}],
selectedCompany: fakeSelectedCompany
})
).toEqual([{
diff --git a/spec/models/chouette/journey_pattern_spec.rb b/spec/models/chouette/journey_pattern_spec.rb
index 19a74a0e7..7c767e4d1 100644
--- a/spec/models/chouette/journey_pattern_spec.rb
+++ b/spec/models/chouette/journey_pattern_spec.rb
@@ -71,6 +71,30 @@ describe Chouette::JourneyPattern, :type => :model do
end
end
+ describe "set_distances" do
+ let(:journey_pattern) { create :journey_pattern }
+ let(:distances){ [] }
+ it "should raise an error" do
+ expect{journey_pattern.set_distances(distances)}.to raise_error
+ end
+
+ context "with consistent data" do
+ let(:distances){ [0, 100, "200", 500, 1000] }
+
+ it "should set costs" do
+ expect{journey_pattern.set_distances(distances)}.to_not raise_error
+ start, stop = journey_pattern.stop_points[0..1]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 100
+ start, stop = journey_pattern.stop_points[1..2]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 100
+ start, stop = journey_pattern.stop_points[2..3]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 300
+ start, stop = journey_pattern.stop_points[3..4]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 500
+ end
+ end
+ end
+
describe "state_update" do
def journey_pattern_to_state jp
jp.attributes.slice('name', 'published_name', 'registration_number').tap do |item|
diff --git a/spec/models/chouette/vehicle_journey_at_stop_spec.rb b/spec/models/chouette/vehicle_journey_at_stop_spec.rb
index a97559a0c..f79d19c88 100644
--- a/spec/models/chouette/vehicle_journey_at_stop_spec.rb
+++ b/spec/models/chouette/vehicle_journey_at_stop_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Chouette::VehicleJourneyAtStop, type: :model do
context '#checksum_attributes' do
it 'should return attributes' do
- expected = [at_stop.departure_time.to_s(:time), at_stop.arrival_time.to_s(:time)]
+ expected = [at_stop.departure_time.utc.to_s(:time), at_stop.arrival_time.utc.to_s(:time)]
expected << at_stop.departure_day_offset.to_s
expected << at_stop.arrival_day_offset.to_s
expect(at_stop.checksum_attributes).to include(*expected)
diff --git a/spec/models/simple_importer_spec.rb b/spec/models/simple_importer_spec.rb
new file mode 100644
index 000000000..60d7b7882
--- /dev/null
+++ b/spec/models/simple_importer_spec.rb
@@ -0,0 +1,394 @@
+RSpec.describe SimpleImporter do
+ describe "#define" do
+ context "with an incomplete configuration" do
+
+ it "should raise an error" do
+ expect do
+ SimpleImporter.define :foo
+ end.to raise_error
+ end
+ end
+ context "with a complete configuration" do
+ before do
+ SimpleImporter.define :foo do |config|
+ config.model = "example"
+ end
+ end
+
+ it "should define an importer" do
+ expect{SimpleImporter.find_configuration(:foo)}.to_not raise_error
+ expect{SimpleImporter.new(configuration_name: :foo, filepath: "")}.to_not raise_error
+ expect{SimpleImporter.find_configuration(:bar)}.to raise_error
+ expect{SimpleImporter.new(configuration_name: :bar, filepath: "")}.to raise_error
+ expect{SimpleImporter.create(configuration_name: :foo, filepath: "")}.to change{SimpleImporter.count}.by 1
+ end
+ end
+ end
+
+ describe "#import" do
+ let(:importer){ importer = SimpleImporter.new(configuration_name: :test, filepath: filepath) }
+ let(:filepath){ fixtures_path 'simple_importer', filename }
+ let(:filename){ "stop_area.csv" }
+ let(:stop_area_referential){ create(:stop_area_referential, objectid_format: :stif_netex) }
+
+ before(:each) do
+ SimpleImporter.define :test do |config|
+ config.model = Chouette::StopArea
+ config.separator = ";"
+ config.key = "name"
+ config.add_column :name
+ config.add_column :lat, attribute: :latitude
+ config.add_column :lat, attribute: :longitude, value: ->(raw){ raw.to_f + 1 }
+ config.add_column :type, attribute: :area_type, value: ->(raw){ raw&.downcase }
+ config.add_column :street_name
+ config.add_column :stop_area_referential, value: stop_area_referential
+ config.add_value :kind, :commercial
+ end
+ end
+
+ it "should import the given file" do
+ expect{importer.import verbose: false}.to change{Chouette::StopArea.count}.by 1
+ expect(importer.status).to eq "success"
+ stop = Chouette::StopArea.last
+ expect(stop.name).to eq "Nom du Stop"
+ expect(stop.latitude).to eq 45.00
+ expect(stop.longitude).to eq 46.00
+ expect(stop.area_type).to eq "zdep"
+ expect(importer.reload.journal.last["event"]).to eq("creation")
+ end
+
+ context "when overriding configuration" do
+ before(:each){
+ importer.configure do |config|
+ config.add_value :latitude, 88
+ end
+ }
+
+ it "should import the given file and not mess with the global configuration" do
+ expect{importer.import}.to change{Chouette::StopArea.count}.by 1
+ expect(importer.status).to eq "success"
+ stop = Chouette::StopArea.last
+ expect(stop.latitude).to eq 88
+ importer = SimpleImporter.new(configuration_name: :test, filepath: filepath)
+ expect{importer.import}.to change{Chouette::StopArea.count}.by 0
+ expect(stop.reload.latitude).to eq 45
+ end
+ end
+
+ context "with an already existing record" do
+ let(:filename){ "stop_area.csv" }
+ before(:each){
+ create :stop_area, name: "Nom du Stop"
+ }
+ it "should only update the record" do
+ expect{importer.import}.to change{Chouette::StopArea.count}.by 0
+ expect(importer.status).to eq "success"
+ stop = Chouette::StopArea.last
+ expect(stop.name).to eq "Nom du Stop"
+ expect(stop.latitude).to eq 45.00
+ expect(stop.longitude).to eq 46.00
+ expect(stop.area_type).to eq "zdep"
+ expect(importer.reload.journal.last["event"]).to eq("update")
+ end
+
+ context "in another scope" do
+ before(:each) do
+ ref = create(:stop_area_referential)
+ importer.configure do |config|
+ config.context = { stop_area_referential: ref }
+ config.scope = ->{ context[:stop_area_referential].stop_areas }
+ end
+ end
+
+ it "should create the record" do
+ expect{importer.import verbose: false}.to change{Chouette::StopArea.count}.by 1
+ expect(importer.status).to eq "success"
+ end
+ end
+ end
+
+ context "with a missing column" do
+ let(:filename){ "stop_area_missing_street_name.csv" }
+ it "should set an error message" do
+ expect{importer.import(verbose: false)}.to_not raise_error
+ expect(importer.status).to eq "success_with_warnings"
+ expect(importer.reload.journal.first["event"]).to eq("column_not_found")
+ end
+ end
+
+ context "with an incomplete dataset" do
+ let(:filename){ "stop_area_incomplete.csv" }
+ it "should fail" do
+ expect{importer.import}.to_not raise_error
+ expect(importer.status).to eq "failed"
+ expect(importer.reload.journal.last["message"]).to eq({"name" => ["doit être rempli(e)"]})
+ end
+
+ it "should be transactional" do
+ expect{importer.import}.to_not change {Chouette::StopArea.count}
+ end
+ end
+
+ context "with a wrong filepath" do
+ let(:filename){ "not_found.csv" }
+ it "should fail" do
+ expect{importer.import}.to_not raise_error
+ expect(importer.status).to eq "failed"
+ expect(importer.reload.journal.first["message"]).to eq "File not found: #{importer.filepath}"
+ end
+ end
+
+ context "with a custom behaviour" do
+ let!(:present){ create :stop_area, name: "Nom du Stop", stop_area_referential: stop_area_referential }
+ let!(:missing){ create :stop_area, name: "Another", stop_area_referential: stop_area_referential }
+ before(:each){
+ importer.configure do |config|
+ config.before do |importer|
+ stop_area_referential.stop_areas.each &:deactivate!
+ end
+
+ config.before(:each_save) do |importer, stop_area|
+ stop_area.activate!
+ end
+ end
+ }
+
+ it "should disable all missing areas" do
+ expect{importer.import}.to change{Chouette::StopArea.count}.by 0
+ expect(present.reload.activated?).to be_truthy
+ expect(missing.reload.activated?).to be_falsy
+ end
+
+ context "with an error" do
+ let(:filename){ "stop_area_incomplete.csv" }
+ it "should do nothing" do
+ expect{importer.import}.to_not change {Chouette::StopArea.count}
+ expect(present.reload.activated?).to be_truthy
+ expect(missing.reload.activated?).to be_truthy
+ end
+ end
+ end
+
+ context "with a full file" do
+ let(:filename){ "stop_area_full.csv" }
+ let!(:missing){ create :stop_area, name: "Another", stop_area_referential: stop_area_referential }
+
+ before(:each) do
+ SimpleImporter.define :test do |config|
+ config.model = Chouette::StopArea
+ config.separator = ";"
+ config.key = "station_code"
+ config.add_column :station_code, attribute: :registration_number
+ config.add_column :country_code
+ config.add_column :station_name, attribute: :name
+ config.add_column :inactive, attribute: :deleted_at, value: ->(raw){ raw == "t" ? Time.now : nil }
+ config.add_column :change_timestamp, attribute: :updated_at
+ config.add_column :longitude
+ config.add_column :latitude
+ config.add_column :parent_station_code, attribute: :parent, value: ->(raw){ raw.present? && resolve(:station_code, raw){|value| Chouette::StopArea.find_by(registration_number: value) } }
+ config.add_column :parent_station_code, attribute: :area_type, value: ->(raw){ raw.present? ? "zdep" : "gdl" }
+ config.add_column :timezone, attribute: :time_zone
+ config.add_column :address, attribute: :street_name
+ config.add_column :postal_code, attribute: :zip_code
+ config.add_column :city, attribute: :city_name
+ config.add_value :stop_area_referential_id, stop_area_referential.id
+ config.add_value :long_lat_type, "WGS84"
+ config.add_value :kind, :commercial
+ config.before do |importer|
+ stop_area_referential.stop_areas.each &:deactivate!
+ end
+
+ config.before(:each_save) do |importer, stop_area|
+ stop_area.activate
+ end
+ end
+ end
+
+ it "should import the given file" do
+ expect{importer.import(verbose: false)}.to change{Chouette::StopArea.count}.by 2
+ expect(importer.status).to eq "success"
+ first = Chouette::StopArea.find_by registration_number: "PAR"
+ last = Chouette::StopArea.find_by registration_number: "XED"
+
+ expect(last.parent).to eq first
+ expect(first.area_type).to eq "gdl"
+ expect(last.area_type).to eq "zdep"
+ expect(first.long_lat_type).to eq "WGS84"
+ expect(first.activated?).to be_truthy
+ expect(last.activated?).to be_truthy
+ expect(missing.reload.activated?).to be_falsy
+ end
+
+ context "with a relation in reverse order" do
+ let(:filename){ "stop_area_full_reverse.csv" }
+
+ it "should import the given file" do
+ expect{importer.import}.to change{Chouette::StopArea.count}.by 2
+ expect(importer.status).to eq "success"
+ first = Chouette::StopArea.find_by registration_number: "XED"
+ last = Chouette::StopArea.find_by registration_number: "PAR"
+ expect(first.parent).to eq last
+ end
+ end
+ end
+
+ context "with a specific importer" do
+ let(:filename){ "stop_points_full.csv" }
+
+ before(:each) do
+ create :line, name: "Paris <> Londres - OUIBUS"
+
+ SimpleImporter.define :test do |config|
+ config.model = Chouette::Route
+ config.separator = ";"
+ config.context = {stop_area_referential: stop_area_referential}
+
+ config.before do |importer|
+ mapping = {}
+ path = Rails.root + "spec/fixtures/simple_importer/lines_mapping.csv"
+ CSV.foreach(path, importer.configuration.csv_options) do |row|
+ if row["Ligne Chouette"].present?
+ mapping[row["timetable_route_id"]] ||= Chouette::Line.find_by(name: importer.encode_string(row["Ligne Chouette"]))
+ end
+ end
+ importer.context[:mapping] = mapping
+ end
+
+ config.custom_handler do |row|
+ line = nil
+ fail_with_error "MISSING LINE FOR ROUTE: #{encode_string row["route_name"]}" do
+ line = context[:mapping][row["timetable_route_id"]]
+ raise unless line
+ end
+ @current_record = Chouette::Route.find_or_initialize_by number: row["timetable_route_id"]
+ @current_record.name = encode_string row["route_name"]
+ @current_record.published_name = encode_string row["route_name"]
+
+ @current_record.line = line
+ if @prev_route != @current_record
+ if @prev_route && @prev_route.valid?
+ journey_pattern = @prev_route.full_journey_pattern
+ fail_with_error "WRONG DISTANCES FOR ROUTE #{@prev_route.name} (#{@prev_route.number}): #{@distances.count} distances for #{@prev_route.stop_points.count} stops" do
+ journey_pattern.stop_points = @prev_route.stop_points
+ journey_pattern.set_distances @distances
+ end
+ fail_with_error ->(){ journey_pattern.errors.messages } do
+ journey_pattern.save!
+ end
+ end
+ @distances = []
+ end
+ @distances.push row["stop_distance"]
+ position = row["stop_sequence"].to_i - 1
+
+ stop_area = context[:stop_area_referential].stop_areas.where(registration_number: row["station_code"]).last
+ unless stop_area
+ stop_area = Chouette::StopArea.new registration_number: row["station_code"]
+ stop_area.name = row["station_name"]
+ stop_area.kind = row["border"] == "f" ? :commercial : :non_commercial
+ stop_area.area_type = row["border"] == "f" ? :zdep : :border
+ stop_area.stop_area_referential = context[:stop_area_referential]
+ fail_with_error ->{p stop_area; "UNABLE TO CREATE STOP_AREA: #{stop_area.errors.messages}" }, abort_row: true do
+ stop_area.save!
+ end
+ end
+ stop_point = @current_record.stop_points.find_by(stop_area_id: stop_area.id)
+ if stop_point
+ stop_point.set_list_position position
+ else
+ stop_point = @current_record.stop_points.build(stop_area_id: stop_area.id, position: position)
+ stop_point.for_boarding = :normal
+ stop_point.for_alighting = :normal
+ end
+
+ @prev_route = @current_record
+ end
+
+ config.after(:each_save) do |importer, route|
+ opposite_route_name = route.name.split(" > ").reverse.join(' > ')
+ opposite_route = Chouette::Route.where(name: opposite_route_name).where('id < ?', route.id).last
+ if opposite_route && opposite_route.line == route.line
+ route.update_attribute :wayback, :inbound
+ opposite_route.update_attribute :wayback, :outbound
+ route.update_attribute :opposite_route_id, opposite_route.id
+ opposite_route.update_attribute :opposite_route_id, route.id
+ end
+ end
+
+ config.after do |importer|
+ prev_route = importer.instance_variable_get "@prev_route"
+ if prev_route && prev_route.valid?
+ journey_pattern = prev_route.full_journey_pattern
+ importer.fail_with_error "WRONG DISTANCES FOR ROUTE #{prev_route.name}: #{importer.instance_variable_get("@distances").count} distances for #{prev_route.stop_points.count} stops" do
+ journey_pattern.set_distances importer.instance_variable_get("@distances")
+ journey_pattern.stop_points = prev_route.stop_points
+ end
+ importer.fail_with_error ->(){ journey_pattern.errors.messages } do
+ journey_pattern.save!
+ end
+ end
+ end
+ end
+ end
+
+ it "should import the given file" do
+ routes_count = Chouette::Route.count
+ journey_pattern_count = Chouette::JourneyPattern.count
+ stop_areas_count = Chouette::StopArea.count
+
+ expect{importer.import(verbose: false)}.to change{Chouette::StopPoint.count}.by 10
+ expect(importer.status).to eq "success"
+ expect(Chouette::Route.count).to eq routes_count + 2
+ expect(Chouette::JourneyPattern.count).to eq journey_pattern_count + 2
+ expect(Chouette::StopArea.count).to eq stop_areas_count + 5
+ route = Chouette::Route.find_by number: 1136
+ expect(route.stop_areas.count).to eq 5
+ expect(route.opposite_route).to eq Chouette::Route.find_by(number: 1137)
+ journey_pattern = route.full_journey_pattern
+ expect(journey_pattern.stop_areas.count).to eq 5
+ start, stop = journey_pattern.stop_points[0..1]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 232
+ start, stop = journey_pattern.stop_points[1..2]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 118
+ start, stop = journey_pattern.stop_points[2..3]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 0
+ start, stop = journey_pattern.stop_points[3..4]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 177
+
+ route = Chouette::Route.find_by number: 1137
+ expect(route.opposite_route).to eq Chouette::Route.find_by(number: 1136)
+ expect(route.stop_areas.count).to eq 5
+ journey_pattern = route.full_journey_pattern
+ expect(journey_pattern.stop_areas.count).to eq 5
+ start, stop = journey_pattern.stop_points[0..1]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 177
+ start, stop = journey_pattern.stop_points[1..2]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 0
+ start, stop = journey_pattern.stop_points[2..3]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 118
+ start, stop = journey_pattern.stop_points[3..4]
+ expect(journey_pattern.costs_between(start, stop)[:distance]).to eq 232
+
+ stop_area = Chouette::StopArea.where(registration_number: "XPB").last
+ expect(stop_area.kind).to eq :commercial
+ expect(stop_area.area_type).to eq :zdep
+
+ stop_area = Chouette::StopArea.where(registration_number: "XDB").last
+ expect(stop_area.kind).to eq :commercial
+ expect(stop_area.area_type).to eq :zdep
+
+ stop_area = Chouette::StopArea.where(registration_number: "COF").last
+ expect(stop_area.kind).to eq :non_commercial
+ expect(stop_area.area_type).to eq :border
+
+ stop_area = Chouette::StopArea.where(registration_number: "COU").last
+ expect(stop_area.kind).to eq :non_commercial
+ expect(stop_area.area_type).to eq :border
+
+ stop_area = Chouette::StopArea.where(registration_number: "ZEP").last
+ expect(stop_area.kind).to eq :commercial
+ expect(stop_area.area_type).to eq :zdep
+ end
+ end
+ end
+end