aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--INSTALL.md2
-rw-r--r--app/controllers/merges_controller.rb34
-rw-r--r--app/controllers/referentials_controller.rb2
-rw-r--r--app/controllers/workbench_outputs_controller.rb9
-rw-r--r--app/javascript/vehicle_journeys/components/VehicleJourney.js1
-rw-r--r--app/javascript/vehicle_journeys/components/VehicleJourneys.js1
-rw-r--r--app/models/chouette/journey_pattern.rb2
-rw-r--r--app/models/chouette/line.rb1
-rw-r--r--app/models/chouette/time_table.rb51
-rw-r--r--app/models/chouette/time_table_period.rb7
-rw-r--r--app/models/chouette/vehicle_journey.rb2
-rw-r--r--app/models/concerns/checksum_support.rb7
-rw-r--r--app/models/generic_attribute_control/min_max.rb4
-rw-r--r--app/models/merge.rb426
-rw-r--r--app/models/referential.rb50
-rw-r--r--app/models/referential_cloning.rb15
-rw-r--r--app/models/referential_suite.rb6
-rw-r--r--app/models/vehicle_journey_control/delta.rb2
-rw-r--r--app/models/vehicle_journey_control/speed.rb4
-rw-r--r--app/models/workbench.rb3
-rw-r--r--app/policies/merge_policy.rb15
-rw-r--r--app/views/merges/_form.html.slim7
-rw-r--r--app/views/merges/new.html.slim7
-rw-r--r--app/views/merges/show.html.slim13
-rw-r--r--app/views/stif/dashboards/_dashboard.html.slim5
-rw-r--r--app/views/workbench_outputs/show.html.slim33
-rw-r--r--app/views/workbenches/show.html.slim1
-rw-r--r--app/workers/merge_worker.rb7
-rw-r--r--app/workers/referential_cloning_worker.rb2
-rw-r--r--config/breadcrumbs.rb15
-rw-r--r--config/initializers/apartment.rb3
-rw-r--r--config/locales/calendars.fr.yml24
-rw-r--r--config/locales/merges.yml17
-rw-r--r--config/locales/workbenches.fr.yml5
-rw-r--r--config/routes.rb3
-rw-r--r--db/migrate/20171212152452_create_merges.rb16
-rw-r--r--db/schema.rb15
-rw-r--r--lib/line_periods.rb35
-rw-r--r--lib/range_ext.rb18
-rw-r--r--lib/stif/permission_translator.rb1
-rw-r--r--spec/factories/chouette_time_table.rb1
-rw-r--r--spec/factories/chouette_vehicle_journey.rb1
-rw-r--r--spec/lib/range_ext_spec.rb51
-rw-r--r--spec/models/chouette/time_table_spec.rb95
-rw-r--r--spec/models/merge_spec.rb58
-rw-r--r--spec/models/referential_cloning_spec.rb30
-rw-r--r--spec/models/referential_spec.rb15
-rw-r--r--spec/support/permissions.rb1
-rw-r--r--spec/workers/referential_cloning_worker_spec.rb2
49 files changed, 1069 insertions, 56 deletions
diff --git a/INSTALL.md b/INSTALL.md
index 28ffdeb4d..5ed8ca9f1 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -137,7 +137,7 @@ RAILS_ENV=test bundle exec rake db:create db:migrate
#### Load seed datas
```sh
-bundle exec rake db:seed
+bundle exec rake db:seed:stif
```
#### Synchronise datas with lines and stop areas referentials
diff --git a/app/controllers/merges_controller.rb b/app/controllers/merges_controller.rb
new file mode 100644
index 000000000..26e2c2e3c
--- /dev/null
+++ b/app/controllers/merges_controller.rb
@@ -0,0 +1,34 @@
+class MergesController < ChouetteController
+ # include PolicyChecker
+
+ defaults resource_class: Merge
+ belongs_to :workbench
+
+ respond_to :html
+
+ before_action :set_mergeable_controllers, only: [:new]
+
+ private
+
+ def set_mergeable_controllers
+ @mergeable_referentials ||= parent.referentials.ready.not_in_referential_suite
+ Rails.logger.debug "Mergeables: #{@mergeable_referentials.inspect}"
+ end
+
+ # def build_resource
+ # @import ||= WorkbenchImport.new(*resource_params) do |import|
+ # import.workbench = parent
+ # import.creator = current_user.name
+ # end
+ # end
+
+ def merge_params
+ params.require(:merge).permit(
+ referentials: []
+ # :name,
+ # :file,
+ # :type,
+ # :referential_id
+ )
+ end
+end
diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb
index 83e3bc56a..436d5ccb5 100644
--- a/app/controllers/referentials_controller.rb
+++ b/app/controllers/referentials_controller.rb
@@ -66,7 +66,7 @@ class ReferentialsController < ChouetteController
def validate
ComplianceControlSetCopyWorker.perform_async(params[:compliance_control_set], params[:id])
flash[:notice] = t('notice.referentials.validate')
- redirect_to(referential_path)
+ redirect_to workbench_compliance_check_sets_path(referential.workbench_id)
end
def destroy
diff --git a/app/controllers/workbench_outputs_controller.rb b/app/controllers/workbench_outputs_controller.rb
new file mode 100644
index 000000000..67ed7569e
--- /dev/null
+++ b/app/controllers/workbench_outputs_controller.rb
@@ -0,0 +1,9 @@
+class WorkbenchOutputsController < ChouetteController
+ respond_to :html, only: [:show]
+ defaults resource_class: Workbench
+
+ def show
+ @workbench = current_organisation.workbenches.find params[:workbench_id]
+ @workbench_merges = @workbench.merges.order("created_at desc").paginate(page: params[:page], per_page: 10)
+ end
+end
diff --git a/app/javascript/vehicle_journeys/components/VehicleJourney.js b/app/javascript/vehicle_journeys/components/VehicleJourney.js
index 5f6281487..7ac2a7ce7 100644
--- a/app/javascript/vehicle_journeys/components/VehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/VehicleJourney.js
@@ -69,6 +69,7 @@ export default class VehicleJourney extends Component {
>
<div className='strong mb-xs'>{this.props.value.short_id || '-'}</div>
<div>{this.props.value.published_journey_name && this.props.value.published_journey_name != "non renseigné" ? this.props.value.published_journey_name : '-'}</div>
+ <div>{this.props.value.company ? this.props.value.company.name : '-'}</div>
<div>{this.props.value.journey_pattern.short_id || '-'}</div>
<div>
{time_tables.slice(0,3).map((tt, i)=>
diff --git a/app/javascript/vehicle_journeys/components/VehicleJourneys.js b/app/javascript/vehicle_journeys/components/VehicleJourneys.js
index dc480d6b4..0cac0344c 100644
--- a/app/javascript/vehicle_journeys/components/VehicleJourneys.js
+++ b/app/javascript/vehicle_journeys/components/VehicleJourneys.js
@@ -117,6 +117,7 @@ export default class VehicleJourneys extends Component {
<div className='strong mb-xs'>ID course</div>
<div>Nom course</div>
<div>ID mission</div>
+ <div>Transporteur</div>
<div>Calendriers</div>
{ this.hasFeature('purchase_windows') && <div>Calendriers Commerciaux</div> }
</div>
diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb
index 367b00449..366fde188 100644
--- a/app/models/chouette/journey_pattern.rb
+++ b/app/models/chouette/journey_pattern.rb
@@ -27,7 +27,7 @@ module Chouette
def checksum_attributes
values = self.slice(*['name', 'published_name', 'registration_number']).values
- values << self.stop_points.map(&:stop_area).map(&:user_objectid)
+ values << self.stop_points.sort_by(&:position).map(&:stop_area).map(&:user_objectid)
values.flatten
end
diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb
index 2d776e94b..389240ec7 100644
--- a/app/models/chouette/line.rb
+++ b/app/models/chouette/line.rb
@@ -21,6 +21,7 @@ module Chouette
has_many :journey_patterns, :through => :routes
has_many :vehicle_journeys, :through => :journey_patterns
has_many :routing_constraint_zones, through: :routes
+ has_many :time_tables, -> { distinct }, :through => :vehicle_journeys
has_and_belongs_to_many :group_of_lines, :class_name => 'Chouette::GroupOfLine', :order => 'group_of_lines.name'
diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb
index 74c20f061..db97dd2fa 100644
--- a/app/models/chouette/time_table.rb
+++ b/app/models/chouette/time_table.rb
@@ -569,5 +569,56 @@ module Chouette
tt.comment = I18n.t("activerecord.copy", :name => self.comment)
tt
end
+
+ def intersect_periods!(mask_periods)
+ dates.each do |date|
+ unless mask_periods.any? { |p| p.include? date.date }
+ dates.delete date
+ end
+ end
+
+ periods.each do |period|
+ mask_periods_with_common_part = mask_periods.select { |p| p.intersect? period.range }
+
+ if mask_periods_with_common_part.empty?
+ self.periods.delete period
+ else
+ mask_periods_with_common_part.each do |mask_period|
+ intersection = (mask_period & period.range)
+ period.period_start, period.period_end = intersection.begin, intersection.end
+ end
+ end
+ end
+ end
+
+ def remove_periods!(removed_periods)
+ dates.each do |date|
+ if removed_periods.any? { |p| p.include? date.date }
+ dates.delete date
+ end
+ end
+
+ periods.each do |period|
+ modified_ranges = removed_periods.inject([period.range]) do |period_ranges, removed_period|
+ period_ranges.map { |p| p.remove removed_period }.flatten
+ end
+
+ unless modified_ranges.empty?
+ modified_ranges.each_with_index do |modified_range, index|
+ new_period = index == 0 ? period : periods.build
+
+ new_period.period_start, new_period.period_end =
+ modified_range.min, modified_range.max
+ end
+ else
+ periods.delete period
+ end
+ end
+ end
+
+ def empty?
+ dates.empty? && periods.empty?
+ end
+
end
end
diff --git a/app/models/chouette/time_table_period.rb b/app/models/chouette/time_table_period.rb
index ab3e79d7e..d9b707675 100644
--- a/app/models/chouette/time_table_period.rb
+++ b/app/models/chouette/time_table_period.rb
@@ -42,5 +42,10 @@ module Chouette
def contains?(p)
(p.period_start >= self.period_start && p.period_end <= self.period_end)
end
+
+ def range
+ period_start..period_end
+ end
+
end
-end \ No newline at end of file
+end
diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb
index d84bebf18..d4dc82a56 100644
--- a/app/models/chouette/vehicle_journey.rb
+++ b/app/models/chouette/vehicle_journey.rb
@@ -70,7 +70,7 @@ module Chouette
attrs << self.published_journey_identifier
attrs << self.try(:company).try(:get_objectid).try(:local_id)
attrs << self.footnotes.map(&:checksum).sort
- attrs << self.vehicle_journey_at_stops.map(&:checksum).sort
+ attrs << self.vehicle_journey_at_stops.sort_by { |s| s.stop_point&.position }.map(&:checksum).sort
end
end
diff --git a/app/models/concerns/checksum_support.rb b/app/models/concerns/checksum_support.rb
index c95e23bcf..b700ef286 100644
--- a/app/models/concerns/checksum_support.rb
+++ b/app/models/concerns/checksum_support.rb
@@ -26,4 +26,11 @@ module ChecksumSupport
self.checksum = Digest::SHA256.new.hexdigest(self.checksum_source)
end
end
+
+ def update_checksum!
+ set_current_checksum_source
+ if checksum_source_changed?
+ update checksum: Digest::SHA256.new.hexdigest(checksum_source)
+ end
+ end
end
diff --git a/app/models/generic_attribute_control/min_max.rb b/app/models/generic_attribute_control/min_max.rb
index 1c429b9a4..18873b683 100644
--- a/app/models/generic_attribute_control/min_max.rb
+++ b/app/models/generic_attribute_control/min_max.rb
@@ -2,8 +2,8 @@ module GenericAttributeControl
class MinMax < ComplianceControl
store_accessor :control_attributes, :minimum, :maximum, :target
- validates :minimum, numericality: true, allow_nil: true
- validates :maximum, numericality: true, allow_nil: true
+ validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0
+ validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0
validates :target, presence: true
include MinMaxValuesValidation
diff --git a/app/models/merge.rb b/app/models/merge.rb
new file mode 100644
index 000000000..4cbbd18ab
--- /dev/null
+++ b/app/models/merge.rb
@@ -0,0 +1,426 @@
+class Merge < ActiveRecord::Base
+ extend Enumerize
+
+ belongs_to :workbench
+ validates :workbench, presence: true
+
+ enumerize :status, in: %w[new pending successful failed running], default: :new
+
+ has_array_of :referentials, class_name: 'Referential'
+
+ delegate :output, to: :workbench
+
+ after_commit :merge, :on => :create
+
+ def merge
+ MergeWorker.perform_async(id)
+ end
+
+ def name
+ "Dummy" # FIXME
+ end
+
+ attr_reader :new
+
+ def merge!
+ update started_at: Time.now, status: :running
+
+ prepare_new
+
+ referentials.each do |referential|
+ merge_referential referential
+ end
+
+ save_current
+ rescue => e
+ Rails.logger.error "Merge failed: #{e} #{e.backtrace.join("\n")}"
+ update status: :failed
+ raise e if Rails.env.test?
+ ensure
+ attributes = { ended_at: Time.now }
+ attributes[:status] = :successful if status == :running
+ update attributes
+ end
+
+ def prepare_new
+ new =
+ if workbench.output.current
+ Rails.logger.debug "Clone current output"
+ Referential.new_from(workbench.output.current, fixme_functional_scope).tap do |clone|
+ clone.inline_clone = true
+ end
+ else
+ Rails.logger.debug "Create a new output"
+ # 'empty' one
+ attributes = {
+ workbench: workbench,
+ organisation: workbench.organisation, # TODO could be workbench.organisation by default
+ name: I18n.t("merges.referential_name"),
+ }
+ workbench.output.referentials.new attributes
+ end
+
+ new.referential_suite = output
+ new.organisation = workbench.organisation
+ new.slug = "output_#{workbench.id}_#{Time.now.to_i}"
+
+ unless new.valid?
+ Rails.logger.error "New referential isn't valid : #{new.errors.inspect}"
+ end
+
+ new.save!
+
+ output.update new: new
+ @new = new
+ end
+
+ def merge_referential(referential)
+ Rails.logger.debug "Merge #{referential.slug}"
+
+ metadata_merger = MetadatasMerger.new new, referential
+ metadata_merger.merge
+
+ new.metadatas.delete metadata_merger.empty_metadatas
+
+ new.save!
+
+ line_periods = LinePeriods.from_metadatas(referential.metadatas)
+
+ new.switch do
+ line_periods.each do |line_id, periods|
+ Rails.logger.debug "Clean data for #{line_id} #{periods.inspect}"
+
+ new.lines.find(line_id).time_tables.find_each do |time_table|
+ time_table.remove_periods! periods
+ unless time_table.empty?
+ puts "Remove period on #{time_table.inspect}"
+ time_table.save!
+ else
+ puts "Remove TimeTable #{time_table.inspect}"
+ time_table.destroy
+ end
+ end
+ end
+ end
+
+ # let's merge data :)
+
+ # Routes
+
+ # Always the same pattern :
+ # - load models from original Referential
+ # - load associated datas (children, checksum for associated models)
+ # - switch to new Referential
+ # - enumerate loaded models
+ # - skip model if its checksum exists "in the same line"
+ # - prepare attributes for a fresh model
+ # - remove all primary keys
+ # - compute an ObjectId (TODO)
+ # - process children models as nested attributes
+ # - associated other models (by line/checksum)
+ # - save! and next one
+
+ referential_routes = referential.switch do
+ referential.routes.all.to_a
+ end
+
+ referential_routes_checksums = Hash[referential_routes.map { |r| [ r.id, r.checksum ] }]
+
+ referential_stop_points = referential.switch do
+ referential.stop_points.all.to_a
+ end
+
+ referential_stop_points_by_route = referential_stop_points.group_by(&:route_id)
+
+ new.switch do
+ referential_routes.each do |route|
+ existing_route = new.routes.find_by line_id: route.line_id, checksum: route.checksum
+ unless existing_route
+ attributes = route.attributes.merge(
+ id: nil,
+ objectid: "merge:route:#{route.checksum}", #FIXME
+ # line_id is the same
+ # all other primary must be changed
+ opposite_route_id: nil #FIXME
+ )
+ new_route = new.routes.build attributes
+
+ route_stop_points = referential_stop_points_by_route[route.id]
+
+ # Stop Points
+ route_stop_points.each do |stop_point|
+ attributes = stop_point.attributes.merge(
+ id: nil,
+ route_id: nil,
+ objectid: "merge:stop_point:#{route.checksum}-#{stop_point.position}", #FIXME
+ )
+
+ new_route.stop_points.build attributes
+ end
+
+ new_route.save!
+
+ if new_route.checksum != route.checksum
+ raise "Checksum has changed: #{route.inspect} #{new_route.inspect}"
+ end
+ end
+ end
+ end
+
+ # JourneyPatterns
+
+ referential_journey_patterns, referential_journey_patterns_stop_areas_objectids = referential.switch do
+ journey_patterns = referential.journey_patterns.includes(:stop_points)
+
+ journey_patterns_stop_areas_objectids = Hash[
+ journey_patterns.map do |journey_pattern|
+ [ journey_pattern.id, journey_pattern.stop_points.map(&:stop_area).map(&:objectid)]
+ end
+ ]
+
+ [journey_patterns, journey_patterns_stop_areas_objectids]
+ end
+
+ referential_journey_patterns_checksums = Hash[referential_journey_patterns.map { |j| [ j.id, j.checksum ] }]
+
+ new.switch do
+ referential_journey_patterns.each do |journey_pattern|
+ # find parent route by checksum
+ # TODO add line_id for security
+ associated_route_checksum = referential_routes_checksums[journey_pattern.route_id]
+ existing_associated_route = new.routes.find_by checksum: associated_route_checksum
+
+ existing_journey_pattern = new.journey_patterns.find_by route_id: existing_associated_route.id, checksum: journey_pattern.checksum
+
+ unless existing_journey_pattern
+ attributes = journey_pattern.attributes.merge(
+ id: nil,
+
+ objectid: "merge:journey_pattern:#{existing_associated_route.checksum}-#{journey_pattern.checksum}", #FIXME
+
+ # all other primary must be changed
+ route_id: existing_associated_route.id,
+
+ departure_stop_point_id: nil, # FIXME
+ arrival_stop_point_id: nil
+ )
+
+ stop_areas_objectids = referential_journey_patterns_stop_areas_objectids[journey_pattern.id]
+
+ stop_points = existing_associated_route.stop_points.joins(:stop_area).where("stop_areas.objectid": stop_areas_objectids).order(:position)
+ attributes.merge!(stop_points: stop_points)
+
+ new_journey_pattern = new.journey_patterns.create! attributes
+ if new_journey_pattern.checksum != journey_pattern.checksum
+ raise "Checksum has changed: #{journey_pattern.checksum_source} #{new_journey_pattern.checksum_source}"
+ end
+ end
+ end
+ end
+
+ # Vehicle Journeys
+
+ referential_vehicle_journeys = referential.switch do
+ referential.vehicle_journeys.includes(:vehicle_journey_at_stops).all.to_a
+ end
+
+ new.switch do
+ referential_vehicle_journeys.each do |vehicle_journey|
+ # find parent journey pattern by checksum
+ # TODO add line_id for security
+ associated_journey_pattern_checksum = referential_journey_patterns_checksums[vehicle_journey.journey_pattern_id]
+ existing_associated_journey_pattern = new.journey_patterns.find_by checksum: associated_journey_pattern_checksum
+
+ existing_vehicle_journey = new.vehicle_journeys.find_by journey_pattern_id: existing_associated_journey_pattern.id, checksum: vehicle_journey.checksum
+
+ unless existing_vehicle_journey
+ attributes = vehicle_journey.attributes.merge(
+ id: nil,
+
+ objectid: "merge:vehicle_journey:#{existing_associated_journey_pattern.checksum}-#{vehicle_journey.checksum}", #FIXME
+
+ # all other primary must be changed
+ route_id: existing_associated_journey_pattern.route_id,
+ journey_pattern_id: existing_associated_journey_pattern.id,
+ )
+ new_vehicle_journey = new.vehicle_journeys.build attributes
+
+ # Create VehicleJourneyAtStops
+
+ vehicle_journey.vehicle_journey_at_stops.each_with_index do |vehicle_journey_at_stop, index|
+ at_stop_attributes = vehicle_journey_at_stop.attributes.merge(
+ id: nil,
+ stop_point_id: existing_associated_journey_pattern.stop_points[index].id
+ )
+ new_vehicle_journey.vehicle_journey_at_stops.build at_stop_attributes
+ end
+
+ new_vehicle_journey.save!
+
+ if new_vehicle_journey.checksum != vehicle_journey.checksum
+ raise "Checksum has changed: #{vehicle_journey.checksum_source} #{new_vehicle_journey.checksum_source}"
+ end
+ end
+
+ end
+ end
+
+ # Time Tables
+
+ referential_time_tables_by_id, referential_time_tables_with_lines = referential.switch do
+ time_tables_by_id = Hash[referential.time_tables.includes(:dates, :periods).all.to_a.map { |t| [t.id, t] }]
+
+ time_tables_with_associated_lines =
+ referential.time_tables.joins(vehicle_journeys: {route: :line}).pluck("lines.id", :id, "vehicle_journeys.checksum")
+
+ # Because TimeTables will be modified according metadata periods
+ # we're loading timetables per line (line is associated to a period list)
+ #
+ # line_id: [ { time_table.id, vehicle_journey.checksum } ]
+ time_tables_by_lines = time_tables_with_associated_lines.inject(Hash.new { |h,k| h[k] = [] }) do |hash, row|
+ hash[row.shift] << {id: row.first, vehicle_journey_checksum: row.second}
+ hash
+ end
+
+ [ time_tables_by_id, time_tables_by_lines ]
+ end
+
+ new.switch do
+ referential_time_tables_with_lines.each do |line_id, time_tables_properties|
+ # Because TimeTables will be modified according metadata periods
+ # we're loading timetables per line (line is associated to a period list)
+ line = workbench.line_referential.lines.find(line_id)
+
+ time_tables_properties.each do |properties|
+ time_table = referential_time_tables_by_id[properties[:id]]
+
+ # we can't test if TimeTable already exist by checksum
+ # because checksum is modified by intersect_periods!
+
+ attributes = time_table.attributes.merge(
+ id: nil,
+ comment: "Ligne #{line.name} - #{time_table.comment}",
+ calendar_id: nil
+ )
+ candidate_time_table = new.time_tables.build attributes
+
+ time_table.dates.each do |date|
+ date_attributes = date.attributes.merge(
+ id: nil,
+ time_table_id: nil
+ )
+ candidate_time_table.dates.build date_attributes
+ end
+ time_table.periods.each do |period|
+ period_attributes = period.attributes.merge(
+ id: nil,
+ time_table_id: nil
+ )
+ candidate_time_table.periods.build period_attributes
+ end
+
+ candidate_time_table.intersect_periods! line_periods.periods(line_id)
+
+ # FIXME
+ candidate_time_table.set_current_checksum_source
+ candidate_time_table.update_checksum
+
+ # after intersect_periods!, the checksum is the expected one
+ # we can search an existing TimeTable
+
+ existing_time_table = line.time_tables.find_by checksum: candidate_time_table.checksum
+
+ unless existing_time_table
+ # FIXME use real ObjectId
+ # Referential id is (temporary) used because the "same" TimeTable can be defined in several merged Referentials
+ # and checksum are modified by clean/remove_periods! but this temporary object id is constant
+ candidate_time_table.objectid = "merge:time_table:#{line.id}-#{candidate_time_table.checksum}-#{referential.id}:LOC"
+
+ candidate_time_table.save!
+
+ # Checksum is changed by #intersect_periods
+ # if new_time_table.checksum != time_table.checksum
+ # raise "Checksum has changed: #{time_table.checksum_source} #{new_time_table.checksum_source}"
+ # end
+
+ existing_time_table = candidate_time_table
+ end
+
+ # associate VehicleJourney
+
+ associated_vehicle_journey = line.vehicle_journeys.find_by!(checksum: properties[:vehicle_journey_checksum])
+ associated_vehicle_journey.time_tables << existing_time_table
+ end
+ end
+ end
+ end
+
+ def save_current
+ output.update current: new, new: nil
+ output.current.update referential_suite: output
+ end
+
+ def fixme_functional_scope
+ if attribute = workbench.organisation.sso_attributes.try(:[], "functional_scope")
+ JSON.parse(attribute)
+ end
+ end
+
+ def child_change
+
+ end
+
+ class MetadatasMerger
+
+ attr_reader :merge_metadatas, :referential
+ def initialize(merge_referential, referential)
+ @merge_metadatas = merge_referential.metadatas
+ @referential = referential
+ end
+
+ delegate :metadatas, to: :referential, prefix: :referential
+
+ def merge
+ referential_metadatas.each do |metadata|
+ merge_one metadata
+ end
+ end
+
+ def merged_line_metadatas(line_id)
+ merge_metadatas.select do |m|
+ m.line_ids.include? line_id
+ end
+ end
+
+ def merge_one(metadata)
+ metadata.line_ids.each do |line_id|
+ line_metadatas = merged_line_metadatas(line_id)
+
+ metadata.periodes.each do |period|
+ line_metadatas.each do |m|
+ m.periodes = m.periodes.map do |existing_period|
+ existing_period.remove period
+ end.flatten
+ end
+
+ attributes = {
+ line_ids: [line_id],
+ periodes: [period],
+ referential_source_id: referential.id,
+ created_at: metadata.created_at # TODO check required dates
+ }
+
+ # line_metadatas should not contain conflicted metadatas
+ merge_metadatas << ReferentialMetadata.new(attributes)
+ end
+ end
+ end
+
+ def empty_metadatas
+ merge_metadatas.select { |m| m.periodes.empty? }
+ end
+
+
+ end
+
+end
diff --git a/app/models/referential.rb b/app/models/referential.rb
index 1cdda9e6a..a5d5acbf9 100644
--- a/app/models/referential.rb
+++ b/app/models/referential.rb
@@ -61,6 +61,7 @@ class Referential < ActiveRecord::Base
scope :include_metadatas_lines, ->(line_ids) { where('referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids) }
scope :order_by_validity_period, ->(dir) { joins(:metadatas).order("unnest(periodes) #{dir}") }
scope :order_by_lines, ->(dir) { joins(:metadatas).group("referentials.id").order("sum(array_length(referential_metadata.line_ids,1)) #{dir}") }
+ scope :not_in_referential_suite, -> { where referential_suite_id: nil }
def save_with_table_lock_timeout(options = {})
save_without_table_lock_timeout(options)
@@ -144,6 +145,22 @@ class Referential < ActiveRecord::Base
Chouette::PurchaseWindow.all
end
+ def routes
+ Chouette::Route.all
+ end
+
+ def journey_patterns
+ Chouette::JourneyPattern.all
+ end
+
+ def stop_points
+ Chouette::StopPoint.all
+ end
+
+ def compliance_check_sets
+ ComplianceCheckSet.all
+ end
+
before_validation :define_default_attributes
def define_default_attributes
@@ -151,10 +168,22 @@ class Referential < ActiveRecord::Base
self.objectid_format ||= workbench.objectid_format if workbench
end
- def switch
+ def switch(&block)
raise "Referential not created" if new_record?
- Apartment::Tenant.switch!(slug)
- self
+
+ unless block_given?
+ Rails.logger.debug "Referential switch to #{slug}"
+ Apartment::Tenant.switch! slug
+ self
+ else
+ result = nil
+ Apartment::Tenant.switch slug do
+ Rails.logger.debug "Referential switch to #{slug}"
+ result = yield
+ end
+ Rails.logger.debug "Referential back"
+ result
+ end
end
def self.new_from(from, functional_scope)
@@ -296,7 +325,7 @@ class Referential < ActiveRecord::Base
overlapped_referential_ids.present?
end
- validate :detect_overlapped_referentials
+ validate :detect_overlapped_referentials, unless: :in_referential_suite?
def detect_overlapped_referentials
self.class.where(id: overlapped_referential_ids).each do |referential|
@@ -305,8 +334,19 @@ class Referential < ActiveRecord::Base
end
end
+ def in_referential_suite?
+ referential_suite_id.present?
+ end
+
+ attr_accessor :inline_clone
def clone_schema
- ReferentialCloning.create(source_referential: created_from, target_referential: self)
+ cloning = ReferentialCloning.new source_referential: created_from, target_referential: self
+
+ if inline_clone
+ cloning.clone!
+ else
+ cloning.save!
+ end
end
def create_schema
diff --git a/app/models/referential_cloning.rb b/app/models/referential_cloning.rb
index 24117e6c8..a2b23e819 100644
--- a/app/models/referential_cloning.rb
+++ b/app/models/referential_cloning.rb
@@ -8,19 +8,22 @@ class ReferentialCloning < ActiveRecord::Base
ReferentialCloningWorker.perform_async(id)
end
- def clone!
+ def clone_with_status!
run!
-
- AF83::SchemaCloner
- .new(source_referential.slug, target_referential.slug)
- .clone_schema
-
+ clone!
successful!
rescue Exception => e
Rails.logger.error "Clone failed : #{e}"
+ Rails.logger.error e.backtrace.join('\n')
failed!
end
+ def clone!
+ AF83::SchemaCloner
+ .new(source_referential.slug, target_referential.slug)
+ .clone_schema
+ end
+
private
aasm column: :status do
diff --git a/app/models/referential_suite.rb b/app/models/referential_suite.rb
index 93c2c3f36..4f825628c 100644
--- a/app/models/referential_suite.rb
+++ b/app/models/referential_suite.rb
@@ -1,7 +1,7 @@
class ReferentialSuite < ActiveRecord::Base
belongs_to :new, class_name: 'Referential'
validate def validate_consistent_new
- return true if new_id.nil?
+ return true if new_id.nil? || new.nil?
return true if new.referential_suite_id == id
errors.add(:inconsistent_new,
I18n.t('referential_suites.errors.inconsistent_new', name: new.name))
@@ -9,11 +9,11 @@ class ReferentialSuite < ActiveRecord::Base
belongs_to :current, class_name: 'Referential'
validate def validate_consistent_current
- return true if current_id.nil?
+ return true if current_id.nil? || current.nil?
return true if current.referential_suite_id == id
errors.add(:inconsistent_current,
I18n.t('referential_suites.errors.inconsistent_current', name: current.name))
end
- has_many :referentials
+ has_many :referentials, -> { order "created_at desc" }
end
diff --git a/app/models/vehicle_journey_control/delta.rb b/app/models/vehicle_journey_control/delta.rb
index 077dd6c4a..f061b9fdd 100644
--- a/app/models/vehicle_journey_control/delta.rb
+++ b/app/models/vehicle_journey_control/delta.rb
@@ -3,7 +3,7 @@ module VehicleJourneyControl
store_accessor :control_attributes, :maximum
- validates :maximum, numericality: true, allow_nil: true
+ validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0
def self.default_code; "3-VehicleJourney-3" end
end
diff --git a/app/models/vehicle_journey_control/speed.rb b/app/models/vehicle_journey_control/speed.rb
index 14fad9139..e5e331b50 100644
--- a/app/models/vehicle_journey_control/speed.rb
+++ b/app/models/vehicle_journey_control/speed.rb
@@ -2,8 +2,8 @@ module VehicleJourneyControl
class Speed < ComplianceControl
store_accessor :control_attributes, :minimum, :maximum
- validates :minimum, numericality: true, allow_nil: true
- validates :maximum, numericality: true, allow_nil: true
+ validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0
+ validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0
include MinMaxValuesValidation
def self.default_code; "3-VehicleJourney-2" end
diff --git a/app/models/workbench.rb b/app/models/workbench.rb
index e36589210..3190246ae 100644
--- a/app/models/workbench.rb
+++ b/app/models/workbench.rb
@@ -14,6 +14,7 @@ class Workbench < ActiveRecord::Base
has_many :workbench_imports
has_many :compliance_check_sets
has_many :compliance_control_sets
+ has_many :merges
validates :name, presence: true
validates :organisation, presence: true
@@ -29,7 +30,7 @@ class Workbench < ActiveRecord::Base
if line_ids.empty?
Referential.none
else
- Referential.joins(:metadatas).where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids]).ready
+ Referential.joins(:metadatas).where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids]).ready.not_in_referential_suite
end
end
diff --git a/app/policies/merge_policy.rb b/app/policies/merge_policy.rb
new file mode 100644
index 000000000..82eb72e08
--- /dev/null
+++ b/app/policies/merge_policy.rb
@@ -0,0 +1,15 @@
+class MergePolicy < ApplicationPolicy
+ class Scope < Scope
+ def resolve
+ scope
+ end
+ end
+
+ def create?
+ user.has_permission?('merges.create')
+ end
+
+ def update?
+ user.has_permission?('merges.update')
+ end
+end
diff --git a/app/views/merges/_form.html.slim b/app/views/merges/_form.html.slim
new file mode 100644
index 000000000..ff85ad76b
--- /dev/null
+++ b/app/views/merges/_form.html.slim
@@ -0,0 +1,7 @@
+= simple_form_for merge, as: :merge, url: workbench_merges_path(workbench), html: {class: 'form-horizontal', id: 'wb_merge_form'}, wrapper: :horizontal_form do |form|
+
+ .row
+ .col-lg-12
+ = form.input :referentials, :collection => @mergeable_referentials, include_blank: false, input_html: { multiple: true, 'data-select2ed': true }
+
+ = form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'wb_merge_form'
diff --git a/app/views/merges/new.html.slim b/app/views/merges/new.html.slim
new file mode 100644
index 000000000..dab4bdf4e
--- /dev/null
+++ b/app/views/merges/new.html.slim
@@ -0,0 +1,7 @@
+- breadcrumb :merges, @workbench
+
+.page_content
+ .container-fluid
+ .row
+ .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1
+ = render 'form', merge: @merge, workbench: @workbench
diff --git a/app/views/merges/show.html.slim b/app/views/merges/show.html.slim
new file mode 100644
index 000000000..579995ebf
--- /dev/null
+++ b/app/views/merges/show.html.slim
@@ -0,0 +1,13 @@
+- breadcrumb :merge, @merge
+- page_header_content_for @merge
+
+.page_content
+ .container-fluid
+ .row
+ .col-lg-6.col-md-6.col-sm-12.col-xs-12
+ = definition_list t('metadatas'),
+ { @merge.class.human_attribute_name(:referentials) => @merge.referentials.map(&:name).join(', '),
+ @merge.class.human_attribute_name(:status) => @merge.status,
+ @merge.class.human_attribute_name(:created_at) => @merge.created_at,
+ @merge.class.human_attribute_name(:started_at) => @merge.started_at,
+ @merge.class.human_attribute_name(:ended_at) => @merge.ended_at }
diff --git a/app/views/stif/dashboards/_dashboard.html.slim b/app/views/stif/dashboards/_dashboard.html.slim
index f3cd01f46..64e7d4f96 100644
--- a/app/views/stif/dashboards/_dashboard.html.slim
+++ b/app/views/stif/dashboards/_dashboard.html.slim
@@ -56,9 +56,8 @@
.panel.panel-default
.panel-heading
h3.panel-title.with_actions
- div
- = t('.calendars')
- span.badge.ml-xs = @dashboard.calendars.count if @dashboard.calendars.present?
+ = I18n.t("calendars.index.title")
+ span.badge.ml-xs = @dashboard.calendars.count if @dashboard.calendars.present?
div
= link_to '', calendars_path, class: ' fa fa-chevron-right pull-right', title: t('.see')
diff --git a/app/views/workbench_outputs/show.html.slim b/app/views/workbench_outputs/show.html.slim
new file mode 100644
index 000000000..67dc6e8d4
--- /dev/null
+++ b/app/views/workbench_outputs/show.html.slim
@@ -0,0 +1,33 @@
+/ PageHeader
+
+- breadcrumb :workbench_output, @workbench
+- content_for :page_header_title, t('.title')
+- content_for :page_header_content do
+ .row.mb-sm
+ .col-lg-12.text-right
+ = link_to t('.see_current_output'), referential_path(@workbench.output.current), class: 'btn btn-primary' if @workbench.output&.current
+ = link_to t('merges.actions.create'), new_workbench_merge_path(@workbench), class: 'btn btn-primary'
+
+.page_content
+ .container-fluid
+ .row
+ .col-lg-12
+ = table_builder_2 @workbench_merges,
+ [ \
+ TableBuilderHelper::Column.new( \
+ key: :status, \
+ attribute: Proc.new { |n| import_status(n.status) }, \
+ ), \
+ TableBuilderHelper::Column.new( \
+ key: :started_at, \
+ attribute: Proc.new { |n| l(n.started_at, format: :long) if n.started_at }, \
+ ), \
+ TableBuilderHelper::Column.new( \
+ key: :creator, \
+ attribute: 'creator' \
+ ) \
+ ],
+ links: [],
+ cls: 'table has-search'
+
+ = new_pagination @workbench_merges, 'pull-right'
diff --git a/app/views/workbenches/show.html.slim b/app/views/workbenches/show.html.slim
index 1c82c34b7..fe0b05330 100644
--- a/app/views/workbenches/show.html.slim
+++ b/app/views/workbenches/show.html.slim
@@ -6,6 +6,7 @@
- if policy(Referential).create?
= link_to t('actions.import'), workbench_imports_path(@workbench), class: 'btn btn-primary'
= link_to t('actions.add'), new_referential_path(workbench_id: @workbench), class: 'btn btn-primary'
+ = link_to t('workbenches.actions.show_output'), workbench_output_path(@workbench), class: 'btn btn-primary'
.page_content
.container-fluid
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
new file mode 100644
index 000000000..8a085a25a
--- /dev/null
+++ b/app/workers/merge_worker.rb
@@ -0,0 +1,7 @@
+class MergeWorker
+ include Sidekiq::Worker
+
+ def perform(id)
+ Merge.find(id).merge!
+ end
+end
diff --git a/app/workers/referential_cloning_worker.rb b/app/workers/referential_cloning_worker.rb
index e20148055..e24baa90c 100644
--- a/app/workers/referential_cloning_worker.rb
+++ b/app/workers/referential_cloning_worker.rb
@@ -2,6 +2,6 @@ class ReferentialCloningWorker
include Sidekiq::Worker
def perform(id)
- ReferentialCloning.find(id).clone!
+ ReferentialCloning.find(id).clone_with_status!
end
end
diff --git a/config/breadcrumbs.rb b/config/breadcrumbs.rb
index 3f6503308..ce5cf5b0f 100644
--- a/config/breadcrumbs.rb
+++ b/config/breadcrumbs.rb
@@ -6,6 +6,21 @@ crumb :workbench do |workbench|
link workbench.name, workbench_path(workbench)
end
+crumb :workbench_output do |workbench|
+ link I18n.t('workbench_outputs.show.title'), workbench_output_path(workbench)
+ parent :workbench, current_offer_workbench
+end
+
+crumb :merges do |workbench|
+ link I18n.t('merges.index.title'), workbench_output_path(workbench)
+ parent :workbench, workbench
+end
+
+crumb :merge do |merge|
+ link breadcrumb_name(merge), workbench_merge_path(merge.workbench, merge)
+ parent :merges, merge.workbench
+end
+
crumb :referential do |referential|
link breadcrumb_name(referential), referential_path(referential)
parent :workbench, current_offer_workbench
diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb
index 69204a5d7..8becd23c2 100644
--- a/config/initializers/apartment.rb
+++ b/config/initializers/apartment.rb
@@ -77,7 +77,8 @@ Apartment.configure do |config|
'ComplianceCheckSet',
'ComplianceCheckBlock',
'ComplianceCheckResource',
- 'ComplianceCheckMessage'
+ 'ComplianceCheckMessage',
+ 'Merge'
]
# use postgres schemas?
diff --git a/config/locales/calendars.fr.yml b/config/locales/calendars.fr.yml
index 88cb275ff..f9eaf1be5 100644
--- a/config/locales/calendars.fr.yml
+++ b/config/locales/calendars.fr.yml
@@ -25,28 +25,28 @@ fr:
standard_calendars: Calendriers standards
standard_calendar: Calendrier standard
actions:
- new: Ajouter un calendrier
- edit: Editer cet calendrier
- destroy: Supprimer cet calendrier
- destroy_confirm: Etes vous sûr de supprimer cet calendrier ?
+ new: Ajouter un modèle de calendrier
+ edit: Editer ce modèle de calendrier
+ destroy: Supprimer ce modèle de calendrier
+ destroy_confirm: Etes vous sûr de supprimer ce modèle de calendrier ?
errors:
overlapped_periods: Une autre période chevauche cette période
short_period: "Une période doit être d'une durée de deux jours minimum"
index:
- title: Calendriers
+ title: Modèles de calendrier
all: Tous
shared: Partagées
not_shared: Non partagées
- search_no_results: Aucun calendrier ne correspond à votre recherche
+ search_no_results: Aucun modèle de calendrier ne correspond à votre recherche
date: Date
new:
- title: Ajouter un calendrier
+ title: Ajouter un modèle de calendrier
create:
- title: Ajouter un calendrier
+ title: Ajouter un modèle de calendrier
edit:
- title: Editer le calendrier %{name}
+ title: Editer le modèle de calendrier %{name}
show:
- title: Calendrier %{name}
+ title: Modèle de calendrier %{name}
simple_form:
labels:
calendar:
@@ -59,8 +59,8 @@ fr:
activerecord:
models:
calendar:
- one: "calendrier"
- other: "calendriers"
+ one: "modèle de calendrier"
+ other: "modèles de calendrier"
attributes:
calendar:
name: Nom
diff --git a/config/locales/merges.yml b/config/locales/merges.yml
new file mode 100644
index 000000000..1e2df2459
--- /dev/null
+++ b/config/locales/merges.yml
@@ -0,0 +1,17 @@
+fr:
+ merges:
+ index:
+ title: "Finalisations de l'offre"
+ new:
+ title: "Nouvelle finalisation de l'offre"
+ activerecord:
+ models:
+ merge: "Finalisation de l'offre"
+ attributes:
+ merge:
+ created_at: "Créé le"
+ started_at: Démarrage
+ ended_at: Achevé à
+ status: "Etat"
+ creator: "Opérateur"
+ referentials: "Jeux de données"
diff --git a/config/locales/workbenches.fr.yml b/config/locales/workbenches.fr.yml
index d76255e86..eff53c2d6 100644
--- a/config/locales/workbenches.fr.yml
+++ b/config/locales/workbenches.fr.yml
@@ -6,6 +6,11 @@ fr:
zero: "Aucun jeu de données dans cet espace de travail"
one: "1 jeu de données dans cet espace de travail"
other: "#{count} jeux de données dans cet espace de travail"
+ actions:
+ show_output: "Offre finalisée"
+ workbench_outputs:
+ show:
+ title: "Finalisations de l'offre"
activerecord:
models:
workbench:
diff --git a/config/routes.rb b/config/routes.rb
index bf796a385..8b5faff03 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -15,6 +15,9 @@ ChouetteIhm::Application.routes.draw do
get :executed, on: :member
resources :compliance_checks, only: [:show]
end
+
+ resource :output, controller: :workbench_outputs
+ resources :merges
end
devise_for :users, :controllers => {
diff --git a/db/migrate/20171212152452_create_merges.rb b/db/migrate/20171212152452_create_merges.rb
new file mode 100644
index 000000000..7915bd91b
--- /dev/null
+++ b/db/migrate/20171212152452_create_merges.rb
@@ -0,0 +1,16 @@
+class CreateMerges < ActiveRecord::Migration
+ def change
+ create_table :merges do |t|
+ t.bigint :workbench_id, index: true, foreign_key: true
+ t.bigint :referential_ids, array: true
+
+ t.string :creator
+ t.string :status
+
+ t.datetime :started_at
+ t.datetime :ended_at
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 67c42f568..667b95c84 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -15,8 +15,8 @@ ActiveRecord::Schema.define(version: 20171227113809) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
- enable_extension "hstore"
enable_extension "postgis"
+ enable_extension "hstore"
enable_extension "unaccent"
create_table "access_links", id: :bigserial, force: :cascade do |t|
@@ -522,6 +522,19 @@ ActiveRecord::Schema.define(version: 20171227113809) do
add_index "lines", ["registration_number"], name: "lines_registration_number_key", using: :btree
add_index "lines", ["secondary_company_ids"], name: "index_lines_on_secondary_company_ids", using: :gin
+ create_table "merges", id: :bigserial, force: :cascade do |t|
+ t.integer "workbench_id", limit: 8
+ t.integer "referential_ids", limit: 8, array: true
+ t.string "creator"
+ t.string "status"
+ t.datetime "started_at"
+ t.datetime "ended_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "merges", ["workbench_id"], name: "index_merges_on_workbench_id", using: :btree
+
create_table "networks", id: :bigserial, force: :cascade do |t|
t.string "objectid", null: false
t.integer "object_version", limit: 8
diff --git a/lib/line_periods.rb b/lib/line_periods.rb
new file mode 100644
index 000000000..c176a7a08
--- /dev/null
+++ b/lib/line_periods.rb
@@ -0,0 +1,35 @@
+class LinePeriods
+
+ def initialize
+ @periods_by_line = Hash.new { |h,k| h[k] = [] }
+ end
+
+ def add(line_id, period)
+ @periods_by_line[line_id] << period
+ end
+
+ def each(&block)
+ @periods_by_line.each do |line_id, periods|
+ yield line_id, periods
+ end
+ end
+
+ def periods(line_id)
+ @periods_by_line[line_id]
+ end
+
+ def self.from_metadatas(metadatas)
+ line_periods = new
+
+ metadatas.each do |metadata|
+ metadata.line_ids.each do |line_id|
+ metadata.periodes.each do |period|
+ line_periods.add(line_id, period)
+ end
+ end
+ end
+
+ line_periods
+ end
+
+end
diff --git a/lib/range_ext.rb b/lib/range_ext.rb
index f1df5e70d..e7e0e903f 100644
--- a/lib/range_ext.rb
+++ b/lib/range_ext.rb
@@ -1,8 +1,24 @@
class Range
def intersection(other)
- return nil if (self.max < other.min or other.max < self.min)
+ return nil unless intersect?(other)
[self.min, other.min].max..[self.max, other.max].min
end
alias_method :&, :intersection
+
+ def intersect?(other)
+ self.max > other.min and other.max > self.min
+ end
+
+ def remove(other)
+ return self if (self.max < other.min or other.max < self.min)
+
+ [].tap do |remaining|
+ remaining << (self.min..other.min-1) if self.min < other.min
+ remaining << (other.max+1..self.max) if other.max < self.max
+ remaining.compact!
+ end
+ end
+ alias_method :-, :remove
+
end
diff --git a/lib/stif/permission_translator.rb b/lib/stif/permission_translator.rb
index 4acf42884..9e0feb9b8 100644
--- a/lib/stif/permission_translator.rb
+++ b/lib/stif/permission_translator.rb
@@ -21,6 +21,7 @@ module Stif
calendars
footnotes
imports
+ merges
journey_patterns
referentials
routes
diff --git a/spec/factories/chouette_time_table.rb b/spec/factories/chouette_time_table.rb
index a3ff63b2f..81a08ca2a 100644
--- a/spec/factories/chouette_time_table.rb
+++ b/spec/factories/chouette_time_table.rb
@@ -25,6 +25,7 @@ FactoryGirl.define do
end_date = start_date + 10
end
time_table.save_shortcuts
+ time_table.update_checksum!
end
end
diff --git a/spec/factories/chouette_vehicle_journey.rb b/spec/factories/chouette_vehicle_journey.rb
index 5f64bd502..7d63a2e58 100644
--- a/spec/factories/chouette_vehicle_journey.rb
+++ b/spec/factories/chouette_vehicle_journey.rb
@@ -30,6 +30,7 @@ FactoryGirl.define do
:arrival_time => "2000-01-01 #{arrival_time} UTC",
:departure_time => "2000-01-01 #{departure_time} UTC")
end
+ vehicle_journey.update_checksum!
end
factory :vehicle_journey_odd do
diff --git a/spec/lib/range_ext_spec.rb b/spec/lib/range_ext_spec.rb
index 9c44608b9..eee488c91 100644
--- a/spec/lib/range_ext_spec.rb
+++ b/spec/lib/range_ext_spec.rb
@@ -1,6 +1,6 @@
require 'range_ext'
RSpec.describe Range do
- context "intersection" do
+ describe "#intersection" do
it "is nil (sic) for two distinct ranges" do
expect( (1..2).intersection(3..4) ).to be_nil
end
@@ -15,4 +15,53 @@ RSpec.describe Range do
expect( (2..4) & (1..3) ).to eq 2..3
end
end
+
+ describe "intersect?" do
+ it 'is true when the given range includes begin' do
+ expect( (2..4).intersect? (1..3) ).to be_truthy
+ end
+
+ it 'is true when the given range includes end' do
+ expect( (2..4).intersect? (3..5) ).to be_truthy
+ end
+
+ it 'is true when the given range includes both begin and end' do
+ expect( (2..4).intersect? (1..5) ).to be_truthy
+ end
+
+ it 'is true when the given range is the same' do
+ expect( (2..4).intersect? (2..4) ).to be_truthy
+ end
+
+ it 'is false when the given range is after' do
+ expect( (2..4).intersect? (5..7) ).to be_falsey
+ end
+
+ it 'is false when the given range is before' do
+ expect( (2..4).intersect? (0..2) ).to be_falsey
+ end
+ end
+
+ context "remove" do
+ it "is unchanged when the given range has no intersection" do
+ expect( (1..2).remove(3..4) ).to eq 1..2
+ expect( (3..4).remove(1..2) ).to eq 3..4
+ end
+
+ it "is nil for two equal ranges" do
+ expect( (1..2).remove(1..2) ).to be_empty
+ end
+
+ it "is the begin of the range when given range intersect the end" do
+ expect( (5..10).remove(8..15) ).to eq [5..7]
+ end
+
+ it "is the end of the range when given range intersect the begin" do
+ expect( (5..10).remove(1..6) ).to eq [7..10]
+ end
+
+ it "is the two remaing ranges when given range is the middle" do
+ expect( (1..10).remove(4..6) ).to eq [1..3, 7..10]
+ end
+ end
end
diff --git a/spec/models/chouette/time_table_spec.rb b/spec/models/chouette/time_table_spec.rb
index 677308fc8..d4a726740 100644
--- a/spec/models/chouette/time_table_spec.rb
+++ b/spec/models/chouette/time_table_spec.rb
@@ -1187,4 +1187,99 @@ end
expect(subject.tag_list.size).to eq(2)
end
end
+
+ describe "#intersect_periods!" do
+ let(:time_table) { Chouette::TimeTable.new }
+ let(:periods) do
+ [
+ Date.new(2018, 1, 1)..Date.new(2018, 2, 1),
+ ]
+ end
+
+ it "remove a date not included in given periods" do
+ time_table.dates.build date: Date.new(2017,12,31)
+ time_table.intersect_periods! periods
+ expect(time_table.dates).to be_empty
+ end
+
+ it "keep a date included in given periods" do
+ time_table.dates.build date: Date.new(2018,1,15)
+ expect{time_table.intersect_periods! periods}.to_not change(time_table, :dates)
+ end
+
+ it "remove a period not included in given periods" do
+ time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2017,12,31)
+ time_table.intersect_periods! periods
+ expect(time_table.periods).to be_empty
+ end
+
+ it "modify a start period if not included in given periods" do
+ period = time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2018,1,15)
+ time_table.intersect_periods! periods
+ expect(period.period_start).to eq(Date.new(2018, 1, 1))
+ end
+
+ it "modify a end period if not included in given periods" do
+ period = time_table.periods.build period_start: Date.new(2018,1,15), period_end: Date.new(2018,3,1)
+ time_table.intersect_periods! periods
+ expect(period.period_end).to eq(Date.new(2018, 2, 1))
+ end
+
+ it "keep a period included in given periods" do
+ time_table.periods.build period_start: Date.new(2018,1,10), period_end: Date.new(2018,1,20)
+ expect{time_table.intersect_periods! periods}.to_not change(time_table, :periods)
+ end
+
+ end
+
+ describe "#remove_periods!" do
+ let(:time_table) { Chouette::TimeTable.new }
+ let(:periods) do
+ [
+ Date.new(2018, 1, 1)..Date.new(2018, 2, 1),
+ ]
+ end
+
+ it "remove a date included in given periods" do
+ time_table.dates.build date: Date.new(2018,1,15)
+ time_table.remove_periods! periods
+ expect(time_table.dates).to be_empty
+ end
+
+ it "keep a date not included in given periods" do
+ time_table.dates.build date: Date.new(2017,12,31)
+ expect{time_table.remove_periods! periods}.to_not change(time_table, :dates)
+ end
+
+ it "modify a end period if included in given periods" do
+ period = time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2018,1,15)
+ time_table.remove_periods! periods
+ expect(period.period_end).to eq(Date.new(2017, 12, 31))
+ end
+
+ it "modify a start period if included in given periods" do
+ period = time_table.periods.build period_start: Date.new(2018,1,15), period_end: Date.new(2018,3,1)
+ time_table.remove_periods! periods
+ expect(period.period_start).to eq(Date.new(2018, 2, 2))
+ end
+
+ it "remove a period included in given periods" do
+ time_table.periods.build period_start: Date.new(2018,1,10), period_end: Date.new(2018,1,20)
+ time_table.remove_periods! periods
+ expect(time_table.periods).to be_empty
+ end
+
+ it "split a period including a given period" do
+ time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2018,3,1)
+ time_table.remove_periods! periods
+
+ expected_ranges = [
+ Date.new(2017,12,1)..Date.new(2017,12,31),
+ Date.new(2018,2,2)..Date.new(2018,3,1)
+ ]
+ expect(time_table.periods.map(&:range)).to eq(expected_ranges)
+ end
+
+ end
+
end
diff --git a/spec/models/merge_spec.rb b/spec/models/merge_spec.rb
new file mode 100644
index 000000000..92f8f74b1
--- /dev/null
+++ b/spec/models/merge_spec.rb
@@ -0,0 +1,58 @@
+require "rails_helper"
+
+RSpec.describe Merge do
+
+ it "should work" do
+ stop_area_referential = FactoryGirl.create :stop_area_referential
+ 10.times { FactoryGirl.create :stop_area, stop_area_referential: stop_area_referential }
+
+ line_referential = FactoryGirl.create :line_referential
+ company = FactoryGirl.create :company, line_referential: line_referential
+ 10.times { FactoryGirl.create :line, line_referential: line_referential, company: company, network: nil }
+
+ workbench = FactoryGirl.create :workbench, line_referential: line_referential, stop_area_referential: stop_area_referential
+
+ referential_metadata = FactoryGirl.create(:referential_metadata, lines: line_referential.lines.limit(3))
+
+ referential = FactoryGirl.create :referential,
+ workbench: workbench,
+ organisation: workbench.organisation,
+ metadatas: [referential_metadata]
+
+ factor = 1
+
+ referential.switch do
+ line_referential.lines.each do |line|
+ factor.times do
+ stop_areas = stop_area_referential.stop_areas.order("random()").limit(5)
+ FactoryGirl.create :route, line: line, stop_areas: stop_areas, stop_points_count: 0
+ end
+ end
+
+ referential.routes.each do |route|
+ factor.times do
+ FactoryGirl.create :journey_pattern, route: route, stop_points: route.stop_points.sample(3)
+ end
+ end
+
+ referential.journey_patterns.each do |journey_pattern|
+ factor.times do
+ FactoryGirl.create :vehicle_journey, journey_pattern: journey_pattern, company: company
+ end
+ end
+
+ shared_time_table = FactoryGirl.create :time_table
+
+ referential.vehicle_journeys.each do |vehicle_journey|
+ vehicle_journey.time_tables << shared_time_table
+
+ specific_time_table = FactoryGirl.create :time_table
+ vehicle_journey.time_tables << specific_time_table
+ end
+ end
+
+ merge = Merge.create!(workbench: referential.workbench, referentials: [referential, referential])
+ merge.merge!
+ end
+
+end
diff --git a/spec/models/referential_cloning_spec.rb b/spec/models/referential_cloning_spec.rb
index 4327c98aa..815e05a67 100644
--- a/spec/models/referential_cloning_spec.rb
+++ b/spec/models/referential_cloning_spec.rb
@@ -36,40 +36,50 @@ RSpec.describe ReferentialCloning, :type => :model do
let(:cloner) { double }
- before do
- allow(AF83::SchemaCloner).to receive(:new).and_return cloner
- allow(cloner).to receive(:clone_schema)
- end
-
it 'creates a schema cloner with source and target schemas and clone schema' do
expect(AF83::SchemaCloner).to receive(:new).with(source_referential.slug, target_referential.slug).and_return(cloner)
expect(cloner).to receive(:clone_schema)
referential_cloning.clone!
end
+ end
+
+ describe '#clone_with_status!' do
+ let(:referential_cloning) do
+ ReferentialCloning.new(target_referential: Referential.new(slug: "target"))
+ end
+
+ before do
+ allow(referential_cloning).to receive(:clone!)
+ end
+
+ it 'invokes clone! method' do
+ expect(referential_cloning).to receive(:clone!)
+ referential_cloning.clone_with_status!
+ end
context 'when clone_schema is performed without error' do
it "should have successful status" do
- referential_cloning.clone!
+ referential_cloning.clone_with_status!
expect(referential_cloning.status).to eq("successful")
end
end
context 'when clone_schema raises an error' do
it "should have failed status" do
- expect(cloner).to receive(:clone_schema).and_raise("#fail")
- referential_cloning.clone!
+ expect(referential_cloning).to receive(:clone!).and_raise("#fail")
+ referential_cloning.clone_with_status!
expect(referential_cloning.status).to eq("failed")
end
end
it "defines started_at" do
- referential_cloning.clone!
+ referential_cloning.clone_with_status!
expect(referential_cloning.started_at).not_to be_nil
end
it "defines ended_at" do
- referential_cloning.clone!
+ referential_cloning.clone_with_status!
expect(referential_cloning.ended_at).not_to be_nil
end
diff --git a/spec/models/referential_spec.rb b/spec/models/referential_spec.rb
index 7816e7232..45881333f 100644
--- a/spec/models/referential_spec.rb
+++ b/spec/models/referential_spec.rb
@@ -125,4 +125,19 @@ describe Referential, :type => :model do
end
end
+ context "used in a ReferentialSuite" do
+ before do
+ ref.referential_suite_id = 42
+ end
+
+ it "return true to in_referential_suite?" do
+ expect(ref.in_referential_suite?).to be(true)
+ end
+
+ it "don't use detect_overlapped_referentials in validation" do
+ expect(ref).to_not receive(:detect_overlapped_referentials)
+ ref.valid?
+ end
+ end
+
end
diff --git a/spec/support/permissions.rb b/spec/support/permissions.rb
index dde530871..95afd6c1c 100644
--- a/spec/support/permissions.rb
+++ b/spec/support/permissions.rb
@@ -18,6 +18,7 @@ module Support
calendars
footnotes
imports
+ merges
journey_patterns
referentials
routes
diff --git a/spec/workers/referential_cloning_worker_spec.rb b/spec/workers/referential_cloning_worker_spec.rb
index 2b9a54805..74e83c3b2 100644
--- a/spec/workers/referential_cloning_worker_spec.rb
+++ b/spec/workers/referential_cloning_worker_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe ReferentialCloningWorker do
it "invokes the clone! method of the associated ReferentialCloning" do
expect(ReferentialCloning).to receive(:find).with(id).and_return(referential_cloning)
- expect(referential_cloning).to receive(:clone!)
+ expect(referential_cloning).to receive(:clone_with_status!)
worker.perform(id)
end