aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlban Peignier2016-11-22 16:00:24 +0100
committerAlban Peignier2016-11-22 16:00:24 +0100
commit847a3bfc3ecaf0a3fe981359fe7a3eaaae3fac24 (patch)
treeed74741b945ef562a207a0684e73e1a44de093c4
parent58dd80dee1d26890aba89354470304555a54dc29 (diff)
downloadchouette-core-847a3bfc3ecaf0a3fe981359fe7a3eaaae3fac24.tar.bz2
Manage several ReferentialMetadata::Periods in ReferentialMetadata. Refs #2035
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock4
-rw-r--r--app/controllers/referentials_controller.rb2
-rw-r--r--app/models/referential.rb36
-rw-r--r--app/models/referential_metadata.rb146
-rw-r--r--app/views/referentials/_form.html.slim19
-rw-r--r--app/views/referentials/_period_fields.html.slim6
-rw-r--r--app/views/referentials/show.html.slim10
-rw-r--r--spec/models/referential_metadata_spec.rb99
-rw-r--r--spec/models/referential_spec.rb35
10 files changed, 251 insertions, 107 deletions
diff --git a/Gemfile b/Gemfile
index 4d4dabf60..87141a2ea 100644
--- a/Gemfile
+++ b/Gemfile
@@ -106,6 +106,7 @@ gem 'google-analytics-rails'
gem 'will_paginate', '~> 3.0.7'
gem 'ransack'
gem 'squeel'
+gem 'active_attr'
gem 'draper'
diff --git a/Gemfile.lock b/Gemfile.lock
index abfb75692..4fb4e2532 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -49,6 +49,9 @@ GEM
activesupport (= 4.1.10)
builder (~> 3.1)
erubis (~> 2.7.0)
+ active_attr (0.9.0)
+ activemodel (>= 3.0.2, < 5.1)
+ activesupport (>= 3.0.2, < 5.1)
activemodel (4.1.10)
activesupport (= 4.1.10)
builder (~> 3.1)
@@ -572,6 +575,7 @@ DEPENDENCIES
RedCloth
SyslogLogger
aasm
+ active_attr
activerecord-postgis-adapter
acts-as-taggable-on (>= 3)
acts_as_list (~> 0.6.0)
diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb
index b7e6d8031..6a7631894 100644
--- a/app/controllers/referentials_controller.rb
+++ b/app/controllers/referentials_controller.rb
@@ -102,7 +102,7 @@ class ReferentialsController < BreadcrumbController
:archived_at,
:created_from_id,
:workbench_id,
- metadatas_attributes: [:id, :first_period_begin, :first_period_end, :lines => []]
+ metadatas_attributes: [:id, :first_period_begin, :first_period_end, periods_attributes: [:begin, :end, :id, :_destroy], :lines => []]
)
end
diff --git a/app/models/referential.rb b/app/models/referential.rb
index b75ff3ab9..1a4b543b7 100644
--- a/app/models/referential.rb
+++ b/app/models/referential.rb
@@ -204,34 +204,44 @@ class Referential < ActiveRecord::Base
end
def metadatas_period
- # FIXME
- if metadatas.present?
- metadatas.first.periodes.try :first
+ query = "select min(lower), max(upper) from (select lower(unnest(periodes)) as lower, upper(unnest(periodes)) as upper from public.referential_metadata where public.referential_metadata.referential_id = #{id}) bounds;"
+
+ row = self.class.connection.select_one(query)
+ lower, upper = row["min"], row["max"]
+
+ if lower and upper
+ Range.new(Date.parse(lower), Date.parse(upper)-1)
end
end
alias_method :validity_period, :metadatas_period
def metadatas_lines
- # FIXME
- metadatas.present? ? metadatas.first.lines : []
+ if metadatas.present?
+ scope = workbench ? workbench.lines : associated_lines
+ scope.where(id: metadatas.pluck(:line_ids).flatten)
+ else
+ Chouete::Line.none
+ end
end
def overlapped_referential_ids
return [] unless metadatas.present?
line_ids = metadatas.first.line_ids
- period = metadatas.first.periodes.try :first
+ periodes = metadatas.first.periodes
- return [] unless line_ids.present? && period
+ return [] unless line_ids.present? && periodes.present?
not_myself = "and referential_id != #{id}" if persisted?
- query = "SELECT distinct(referential_id) FROM
- (SELECT unnest(public.referential_metadata.line_ids) as line, unnest(public.referential_metadata.periodes) as period, public.referential_metadata.referential_id
- FROM public.referential_metadata
- INNER JOIN public.referentials ON public.referential_metadata.referential_id = public.referentials.id
- WHERE public.referentials.workbench_id = #{workbench_id} and public.referentials.archived_at is null) as metadatas
- WHERE line in (#{line_ids.join(',')}) and period && '#{ActiveRecord::ConnectionAdapters::PostgreSQLColumn.range_to_string(period)}' #{not_myself};"
+ periods_query = periodes.map do |periode|
+ "period && '#{ActiveRecord::ConnectionAdapters::PostgreSQLColumn.range_to_string(periode)}'"
+ end.join(" OR ")
+
+ query = "select distinct(public.referential_metadata.referential_id) FROM public.referential_metadata, unnest(line_ids) line, LATERAL unnest(periodes) period
+ WHERE public.referential_metadata.referential_id
+ IN (SELECT public.referentials.id FROM public.referentials WHERE referentials.workbench_id = #{workbench_id} and referentials.archived_at is null #{not_myself})
+ AND line in (#{line_ids.join(',')}) and (#{periods_query});"
self.class.connection.select_values(query).map(&:to_i)
end
diff --git a/app/models/referential_metadata.rb b/app/models/referential_metadata.rb
index a1066e43b..0c792a9b9 100644
--- a/app/models/referential_metadata.rb
+++ b/app/models/referential_metadata.rb
@@ -7,68 +7,138 @@ class ReferentialMetadata < ActiveRecord::Base
validates :lines, presence: true
validates :periodes, presence: true
- validates :first_period_begin, :first_period_end, presence: true
-
scope :include_lines, -> (line_ids) { where('line_ids && ARRAY[?]', line_ids) }
scope :include_dateranges, -> (dateranges) { where('periodes && ARRAY[?]', dateranges) }
- def first_period
- periodes.first if periodes
+ class Period
+ include ActiveAttr::Model
+
+ attribute :id, type: Integer
+ attribute :begin, type: Date
+ attribute :end, type: Date
+
+ validates :begin, :end, presence: true
+ validate :check_end_greather_than_begin
+
+ def check_end_greather_than_begin
+ if self.begin and self.end and self.begin > self.end
+ errors.add(:end, :invalid)
+ end
+ end
+
+ def self.from_range(index, range)
+ Period.new id: index, begin: range.begin, end: range.end
+ end
+
+ def range
+ if self.begin and self.end and self.begin <= self.end
+ Range.new self.begin, self.end
+ end
+ end
+
+ def intersect?(*other)
+ return false if range.nil?
+
+ other = other.flatten
+ other = other.delete_if { |o| o.id == id } if id
+
+ other.any? do |period|
+ if other_range = period.range
+ (range & other_range).present?
+ end
+ end
+ end
+
+ # Stuff required for coocon
+ def new_record?
+ !persisted?
+ end
+ def persisted?
+ id.present?
+ end
+
+
+ def mark_for_destruction
+ self._destroy = true
+ end
+
+ attribute :_destroy, type: Boolean
+ alias_method :marked_for_destruction?, :_destroy
end
- def first_period_begin
- @first_period_begin or first_period.try(:begin)
+ # Required by coocon
+ def build_period
+ Period.new
end
- def first_period_begin=(date)
- date = (Date.parse(date) rescue nil) if String === date
- periodes_will_change! unless @first_period_begin == date
- @first_period_begin = date
+
+ def periods
+ @periods ||= init_periods
end
- def first_period_end
- if @first_period_end
- @first_period_end
+
+ def init_periods
+ if periodes
+ periodes.each_with_index.map { |r, index| Period.from_range(index, r) }
else
- if first_period
- date = first_period.end
- date -= 1 if first_period.exclude_end?
- date
- end
+ []
end
end
- def first_period_end=(date)
- date = (Date.parse(date) rescue nil) if String === date
- periodes_will_change! unless @first_period_end == date
- @first_period_end = date
- end
+ private :init_periods
- validate :check_first_period_end
+ validate :validate_periods
- def check_first_period_end
- if @first_period_begin and @first_period_end and @first_period_begin > @first_period_end
- errors.add(:first_period_end, :invalid)
+ def validate_periods
+ Rails.logger.debug "Validate periods"
+ unless periods.all?(&:valid?)
+ errors.add(:periods, :invalid)
end
- end
- before_validation :set_first_period
+ periods.each do |period|
+ Rails.logger.debug "Validate period #{period.inspect} : #{period.intersect?(periods)}"
+ if period.intersect?(periods)
+ period.errors.add(:base, :invalid)
+ Rails.logger.debug "period errors #{period.errors}"
+ end
+ end
+ end
- def set_first_period
- if @first_period_begin and @first_period_end and @first_period_begin <= @first_period_end
- self.periodes ||= []
- self.periodes[0] = Range.new @first_period_begin, @first_period_end
+ def periods_attributes=(attributes = {})
+ @periods = []
+ attributes.each do |index, period_attribute|
+ period = Period.new(period_attribute.merge(id: index))
+ @periods << period unless period.marked_for_destruction?
end
+
+ # if self.periodes != @periods.map(&:range).compact
+ periodes_will_change!
+ # end
end
- def column_for_attribute(name)
- if %i{first_period_begin first_period_end}.include?(name.to_sym)
- ActiveRecord::ConnectionAdapters::Column.new(name, nil, "date")
- else
- super name
+ before_validation :fill_periodes
+
+ def fill_periodes
+ if @periods
+ self.periodes = @periods.map(&:range).compact.sort_by(&:begin)
end
end
+ after_save :clear_periods
+
+ def clear_periods
+ @periods = nil
+ end
+ private :clear_periods
+
def self.new_from from
from.dup.tap do |metadata|
metadata.referential_id = nil
end
end
end
+
+class Range
+ def intersection(other)
+ return nil if (self.max < other.begin or other.max < self.begin)
+ [self.begin, other.begin].max..[self.max, other.max].min
+ end
+ alias_method :&, :intersection
+end
diff --git a/app/views/referentials/_form.html.slim b/app/views/referentials/_form.html.slim
index bce8818f2..30bf99632 100644
--- a/app/views/referentials/_form.html.slim
+++ b/app/views/referentials/_form.html.slim
@@ -49,18 +49,19 @@
- if @referential.errors.has_key? :metadatas
.row
- .col-lg-12
- .alert.alert-danger
- - @referential.errors[:metadatas].each do |message|
- p.small = "- #{message}"
= form.simple_fields_for :metadatas do |subform|
.row
- .col-lg-6.col-md-6.col-sm-6.col-xs-6
- = subform.input :first_period_begin, as: :date, html5: true, input_html: { style: 'width: 100%' }
- .col-lg-6.col-md-6.col-sm-6.col-xs-6
- = subform.input :first_period_end, as: :date, html5: true, input_html: { style: 'width: 100%' }
-
+ = subform.simple_fields_for :periods do |period_form|
+ - if period_form.object.errors.has_key? :base
+ .col-lg-12
+ .alert.alert-danger
+ - period_form.object.errors[:base].each do |message|
+ p.small = message
+ = render 'period_fields', f: period_form
+ .links
+ = link_to_add_association 'Ajouter une période', subform, :periods
+
.row
.col-lg-8.col-md-12.col-sm-12.col-xs-12
= subform.input :lines, as: :select, collection: @referential.workbench.lines.includes(:company).order(:name), selected: subform.object.line_ids, label_method: :display_name, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Sélection de lignes', 'multiple': 'multiple', style: 'width: 100%' }
diff --git a/app/views/referentials/_period_fields.html.slim b/app/views/referentials/_period_fields.html.slim
new file mode 100644
index 000000000..68db7be3f
--- /dev/null
+++ b/app/views/referentials/_period_fields.html.slim
@@ -0,0 +1,6 @@
+.nested-fields
+ .col-lg-6.col-md-6.col-sm-6.col-xs-6
+ = f.input :begin, as: :date, html5: true, input_html: { style: 'width: 100%' }
+ .col-lg-6.col-md-6.col-sm-6.col-xs-6
+ = f.input :end, as: :date, html5: true, input_html: { style: 'width: 100%' }
+ = link_to_remove_association "Supprimer", f
diff --git a/app/views/referentials/show.html.slim b/app/views/referentials/show.html.slim
index 94c463d74..7df7efff4 100644
--- a/app/views/referentials/show.html.slim
+++ b/app/views/referentials/show.html.slim
@@ -27,13 +27,13 @@ h2
p
label = "#{Referential.human_attribute_name('validity_period')} : "
- - if Chouette::TimeTable.start_validity_period.nil?
+ - unless period = @referential.metadatas_period
= " #{Referential.human_attribute_name('no_validity_period')}"
- else
- = " #{Referential.human_attribute_name('start_validity_period')}"
- = l Chouette::TimeTable.start_validity_period
- = Referential.human_attribute_name("end_validity_period")
- = l Chouette::TimeTable.end_validity_period
+ => " #{Referential.human_attribute_name('start_validity_period')}"
+ => l period.begin
+ => Referential.human_attribute_name("end_validity_period")
+ = l period.end
/ - if @referential.api_keys.present?
/ h3.api_keys = t('.api_keys')
diff --git a/spec/models/referential_metadata_spec.rb b/spec/models/referential_metadata_spec.rb
index e31caf8a1..50571ad85 100644
--- a/spec/models/referential_metadata_spec.rb
+++ b/spec/models/referential_metadata_spec.rb
@@ -36,49 +36,94 @@ RSpec.describe ReferentialMetadata, :type => :model do
end
- describe "#first_period" do
+ describe "Period" do
- let(:referential_metadata) { create :referential_metadata }
+ subject { period }
- describe "begin" do
- it "should return first period begin" do
- expect(referential_metadata.first_period_begin).to eq(referential_metadata.first_period.begin)
+ def period(attributes = {})
+ return @period if attributes.empty? and @period
+ ReferentialMetadata::Period.new(attributes).tap do |period|
+ @period = period if attributes.empty?
end
end
- describe "begin=" do
- let(:date) { Date.today }
- it "should change the first period begin" do
- referential_metadata.first_period_begin = date
- expect(referential_metadata.first_period_begin).to eq(date)
- end
+ it "should support mark_for_destruction (required by cocoon)" do
+ period.mark_for_destruction
+ expect(period).to be_marked_for_destruction
end
- describe "end" do
- it "should return first period end" do
- expect(referential_metadata.first_period_end).to eq(referential_metadata.first_period.end)
- end
+ it "should support _destroy attribute (required by coocon)" do
+ period._destroy = true
+ expect(period).to be_marked_for_destruction
end
- describe "end=" do
- let(:date) { Date.today }
- it "should change the first period end" do
- referential_metadata.first_period_end = date
- expect(referential_metadata.first_period_end).to eq(date)
- end
+ it "should support new_record? (required by cocoon)" do
+ expect(ReferentialMetadata::Period.new).to be_new_record
+ expect(period(id: 42)).not_to be_new_record
end
- describe "after_validation" do
- it "should define first_period with first_period_begin and first_period_end" do
- referential_metadata.first_period_begin = Date.today
- referential_metadata.first_period_end = Date.tomorrow
+ it "should cast begin as date attribute" do
+ expect(period(begin: "2016-11-22").begin).to eq(Date.new(2016,11,22))
+ end
+
+ it "should cast end as date attribute" do
+ expect(period(end: "2016-11-22").end).to eq(Date.new(2016,11,22))
+ end
- referential_metadata.valid?
+ it { is_expected.to validate_presence_of(:begin) }
+ it { is_expected.to validate_presence_of(:end) }
- expect(referential_metadata.first_period).to eq(Range.new(referential_metadata.first_period_begin, referential_metadata.first_period_end))
+ it "should validate that end is greather than or equlals to begin" do
+ expect(period(begin: "2016-11-21", end: "2016-11-22")).to be_valid
+ expect(period(begin: "2016-11-21", end: "2016-11-21")).to be_valid
+ expect(period(begin: "2016-11-22", end: "2016-11-21")).to_not be_valid
+ end
+
+ describe "intersect?" do
+
+ it "should detect date in common with other periods" do
+ november = period(begin: "2016-11-01", end: "2016-11-30")
+ mid_november_mid_december = period(begin: "2016-11-15", end: "2016-12-15")
+ expect(november.intersect?(mid_november_mid_december)).to be(true)
+ end
+
+ it "should not intersect when no date is in common" do
+ november = period(begin: "2016-11-01", end: "2016-11-30")
+ december = period(begin: "2016-12-01", end: "2016-12-31")
+
+ expect(november.intersect?(december)).to be(false)
+
+ january = period(begin: "2017-01-01", end: "2017-01-31")
+ expect(november.intersect?(december, january)).to be(false)
+ end
+
+ it "should not intersect itself" do
+ period = period(id: 42, begin: "2016-11-01", end: "2016-11-30")
+ expect(period.intersect?(period)).to be(false)
+ end
+
+ end
+
+ end
+
+ describe "before_validation" do
+ let(:referential_metadata) do
+ create(:referential_metadata).tap do |metadata|
+ metadata.periodes = []
end
end
+ it "shoud fill periodes with period ranges" do
+ expected_ranges = [
+ Range.new(Date.today, Date.tomorrow)
+ ]
+ expected_ranges.each_with_index do |range, index|
+ referential_metadata.periods << ReferentialMetadata::Period.from_range(index, range)
+ end
+ referential_metadata.valid?
+
+ expect(referential_metadata.periodes).to eq(expected_ranges)
+ end
end
describe "#includes_lines" do
diff --git a/spec/models/referential_spec.rb b/spec/models/referential_spec.rb
index 2086f66d5..539af4db4 100644
--- a/spec/models/referential_spec.rb
+++ b/spec/models/referential_spec.rb
@@ -52,12 +52,16 @@ describe Referential, :type => :model do
"data_format"=>"neptune",
"metadatas_attributes"=> {
"0"=> {
- "first_period_begin(3i)"=>"19",
- "first_period_begin(2i)"=>"11",
- "first_period_begin(1i)"=>"2016",
- "first_period_end(3i)"=>"19",
- "first_period_end(2i)"=>"12",
- "first_period_end(1i)"=>"2016",
+ "periods_attributes" => {
+ "0" => {
+ "begin"=>"2016-09-19",
+ "end" => "2016-10-19",
+ },
+ "15918593" => {
+ "begin"=>"2016-11-19",
+ "end" => "2016-12-19",
+ },
+ },
"lines"=> [""] + lines.map { |l| l.id.to_s }
}
},
@@ -70,21 +74,24 @@ describe Referential, :type => :model do
let(:new_referential) { Referential.new(attributes) }
let(:first_metadata) { new_referential.metadatas.first }
- it "should create a metadata" do
- expect(new_referential.metadatas.size).to eq(1)
+ let(:expected_ranges) do
+ [
+ Range.new(Date.new(2016,9,19), Date.new(2016,10,19)),
+ Range.new(Date.new(2016,11,19), Date.new(2016,12,19)),
+ ]
end
- it "should define first_period_begin" do
- expect(first_metadata.first_period_begin).to eq(Date.new(2016,11,19))
+ it "should create a metadata" do
+ expect(new_referential.metadatas.size).to eq(1)
end
- it "should define first_period_end" do
- expect(first_metadata.first_period_end).to eq(Date.new(2016,12,19))
+ it "should define metadata periods" do
+ expect(first_metadata.periods.map(&:range)).to eq(expected_ranges)
end
- it "should define period" do
+ it "should define periodes" do
new_referential.save!
- expect(first_metadata.first_period).to eq(Range.new(Date.new(2016,11,19), Date.new(2016,12,19)))
+ expect(first_metadata.periodes).to eq(expected_ranges)
end
it "should define period" do