diff options
| -rw-r--r-- | Gemfile | 1 | ||||
| -rw-r--r-- | Gemfile.lock | 4 | ||||
| -rw-r--r-- | app/controllers/referentials_controller.rb | 2 | ||||
| -rw-r--r-- | app/models/referential.rb | 36 | ||||
| -rw-r--r-- | app/models/referential_metadata.rb | 146 | ||||
| -rw-r--r-- | app/views/referentials/_form.html.slim | 19 | ||||
| -rw-r--r-- | app/views/referentials/_period_fields.html.slim | 6 | ||||
| -rw-r--r-- | app/views/referentials/show.html.slim | 10 | ||||
| -rw-r--r-- | spec/models/referential_metadata_spec.rb | 99 | ||||
| -rw-r--r-- | spec/models/referential_spec.rb | 35 |
10 files changed, 251 insertions, 107 deletions
@@ -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 |
