diff options
| author | Alban Peignier | 2016-11-22 16:00:24 +0100 | 
|---|---|---|
| committer | Alban Peignier | 2016-11-22 16:00:24 +0100 | 
| commit | 847a3bfc3ecaf0a3fe981359fe7a3eaaae3fac24 (patch) | |
| tree | ed74741b945ef562a207a0684e73e1a44de093c4 | |
| parent | 58dd80dee1d26890aba89354470304555a54dc29 (diff) | |
| download | chouette-core-847a3bfc3ecaf0a3fe981359fe7a3eaaae3fac24.tar.bz2 | |
Manage several ReferentialMetadata::Periods in ReferentialMetadata. Refs #2035
| -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 | 
