diff --git a/config/initializers/time_formats.rb b/config/initializers/time_formats.rb
new file mode 100644
index 000000000..f5636aa0b
--- /dev/null
+++ b/config/initializers/time_formats.rb
@@ -0,0 +1,162 @@
+module DateTimeConstants
+ Seasons = ['Season: MAM', 'Season: JJA', 'Season: SON', 'Season: DJF']
+ SeasonRepresentativeMonths = { 'Season: MAM' => 4, 'Season: JJA' => 7, 'Season: SON' => 10, 'Season: DJF' => 1 }
+ TimesOfDay = ['morning', 'mid-day', 'afternoon', 'night']
+ TimesOfDayRepresentativeHours = { 'morning' => 9, 'mid-day' => 12, 'afternoon' => 15, 'night' => 0 }
+
+ # dummy values to use when date/times are not fully explicit
+ DummyYear = 9996
+ DummyMonth = 1
+ DummyDay = 1
+ DummyHour = 0
+ DummyMinute = 0
+ DummySecond = 0
+
+ Months = 1..12
+ Days = 1..31
+ Hours = 0..23
+ Minutes = 0..59
+end
+
+module DateTimeUtilityMethods
+
+ private
+
+ # Convert the Trait date attribute (which is an ActiveSupport::TimeWithZone
+ # object) to a new TimeWithZone object representing the time in site_timezone.
+ # This is used in various methods used for presenting the date and time to the
+ # user, which is always done in local (site) time.
+ def date_in_site_timezone
+ date.in_time_zone(site_timezone)
+ end
+
+ # Returns the time zone of the associated site or "UTC" if no there is no
+ # associated site or if its time_zone attribute is blank.
+ def site_timezone
+ begin
+ if respond_to? :site
+ zone = site.time_zone
+ else
+ zone = (Site.find(site_id)).time_zone
+ end
+
+ if zone.blank?
+ zone = 'UTC'
+ end
+ rescue
+ zone = 'UTC' # site not found
+ end
+ return zone
+ end
+
+ def date_format
+ case dateloc
+ when 9
+ :no_date_data
+ when 8
+ :year_only
+ when 7
+ :season_and_year
+ when 6
+ :month_and_year
+ when 5.5
+ :week_of_year
+ when 5
+ :year_month_day
+ when 97
+ :season_only
+ when 96
+ :month_only
+ when 95
+ :month_day
+ when nil
+ :unspecified_dateloc
+ else
+ :unrecognized_dateloc
+ end
+ end
+
+end
+
+NEW_FORMATS = {
+
+ # Date Formats
+
+ # dateloc 9
+ no_date_data: '[date unspecified or unknown]',
+
+ # dateloc 8
+ year_only: '%Y',
+
+ # dateloc 7
+ season_and_year: ->(date) do
+ date.strftime("#{date.to_s(:season_only)} %Y")
+ end,
+
+ # dateloc 6
+ month_and_year: '%B %Y',
+
+ # dateloc 5.5:
+ week_of_year: 'Week of %b %-d, %Y',
+
+ # dateloc 5:
+ year_month_day: '%Y %b %-d',
+
+ # dateloc 97:
+ season_only: ->(date) do
+ DateTimeConstants::SeasonRepresentativeMonths.key(date.month) ||
+ '[Invalid season designation]'
+ end,
+
+ # dateloc 96:
+ month_only: '%B',
+
+ # dateloc 95:
+ month_day: ->(date) { date.strftime("%B #{date.day.ordinalize}") },
+
+ unspecified_dateloc: 'Date Level of Confidence Unknown',
+
+ unrecognized_dateloc: 'Unrecognized Value for Date Level of Confidence',
+
+
+ # Time Formats
+
+ # timeloc 9:
+ no_time_data: '[time unspecified or unknown]',
+
+ # timeloc 4:
+ time_of_day: ->(time) do
+ case time.hour
+ when 0
+ 'night'
+ when 9
+ 'morning'
+ when 12
+ 'mid-day'
+ when 15
+ 'afternoon'
+ else
+ '[Invalid time-of-day designation]'
+ end
+ end,
+
+ # timeloc 3:
+ hour_only: '%-l %p',
+
+ # timeloc 2:
+ hour_minutes: '%H:%M',
+
+ # timeloc 1:
+ hour_minutes_seconds: '%H:%M:%S',
+
+ unspecified_timeloc: 'Time Level of Confidence Unknown',
+
+ unrecognized_timeloc: 'Unrecognized Value for Time Level of Confidence'
+
+
+
+}
+
+
+Time::DATE_FORMATS.merge!(NEW_FORMATS)
+
diff --git a/config/routes.rb b/config/routes.rb
index 9e03e72fe..911b74bdc 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -168,6 +168,7 @@
get :linked
get :rem_citations_sites
get :add_citations_sites
+ get :get_timezone
end
end
diff --git a/spec/factories/sites.rb b/spec/factories/sites.rb
new file mode 100644
index 000000000..c260fa4b8
--- /dev/null
+++ b/spec/factories/sites.rb
@@ -0,0 +1,10 @@
+FactoryGirl.define do
+ factory :site do
+
+ sitename "Mordor"
+ lat 0
+ lon 0
+ masl 0
+
+ end
+end
diff --git a/spec/factories/traits.rb b/spec/factories/traits.rb
new file mode 100644
index 000000000..6481b9a8d
--- /dev/null
+++ b/spec/factories/traits.rb
@@ -0,0 +1,25 @@
+FactoryGirl.define do
+ factory :trait do
+ site
+ mean 1
+ variable
+ access_level 4
+
+ factory :trait_with_inconsistent_date_attributes do
+
+ date "2005-07-25 09:31:00"
+ dateloc 9
+ timeloc 9
+
+ end
+
+ factory :trait_with_consistent_date_attributes do
+
+ date "2005-07-25 09:31:35"
+ dateloc 5
+ timeloc 1
+
+ end
+
+ end
+end
diff --git a/spec/factories/variables.rb b/spec/factories/variables.rb
new file mode 100644
index 000000000..a05d7260a
--- /dev/null
+++ b/spec/factories/variables.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :variable do
+ name "congeniality"
+ end
+end
diff --git a/spec/features/trait_integration_spec.rb b/spec/features/trait_integration_spec.rb
index cf0494b4b..57ea930e1 100644
--- a/spec/features/trait_integration_spec.rb
+++ b/spec/features/trait_integration_spec.rb
@@ -33,14 +33,14 @@
end
it 'should allow creation of new traits', js:true do
- # Create Citation association
- visit '/citations'
- first(:xpath,".//a[@alt='use' and contains(@href,'/use_citation/')]").click
- expect(page).to have_content 'Sites already associated with this citation'
+ # Create Citation association
+ visit '/citations'
+ first(:xpath,".//a[@alt='use' and contains(@href,'/use_citation/')]").click
+ expect(page).to have_content 'Sites already associated with this citation'
- # Verify the trait creation
- visit '/traits/new'
+ # Verify the trait creation
+ visit '/traits/new'
expect(page).to have_content 'New Trait'
@@ -50,6 +50,7 @@
fill_in 'trait_stat', :with => '7.76'
fill_in 'trait_n', :with => '3'
fill_in 'trait_notes', :with => 'Research Interwebs Papers Research Interwebs PapersResearch Interwebs PapersResearch Interwebs Papers'
+ select('1: Aliartos, Greece - Aliartos, Greece', :from => 'trait[site_id]')
click_button 'Create'
diff --git a/spec/features/yield_integration_spec.rb b/spec/features/yield_integration_spec.rb
index 8818be80b..635c28afc 100644
--- a/spec/features/yield_integration_spec.rb
+++ b/spec/features/yield_integration_spec.rb
@@ -32,9 +32,9 @@
fill_in 'yield[stat]', :with => '98736.0'
fill_in 'yield[n]', :with => '100'
fill_in 'Notes', :with => 'In some technical publications, appendices are so long and important as part of the book that they are a creative endeavour of the author'
- select('10', :from => 'yield[date(3i)]')
- select('10', :from => 'yield[date(2i)]')
- select('1800', :from => 'yield[date(1i)]')
+ select('10', :from => 'yield[d_day]')
+ select('10', :from => 'yield[d_month]')
+ select('1800', :from => 'yield[d_year]')
fill_in 'species_query', :with => 'sacc'
select('3', :from => 'yield[specie_id]')
@@ -60,9 +60,9 @@
visit '/yields/new'
fill_in 'yield[mean]', :with => 'asdf'
fill_in 'yield[stat]', :with => '98736.0'
- select('1', :from => 'yield[date(3i)]')
- select('1 - January', :from => 'yield[date(2i)]')
- select('1800', :from => 'yield[date(1i)]')
+ select('1', :from => 'yield[d_day]')
+ select('1', :from => 'yield[d_month]')
+ select('1800', :from => 'yield[d_year]')
fill_in 'species_query', :with => 'sacc'
select('3', :from => 'yield[specie_id]')
@@ -80,9 +80,9 @@
# Required attributes:
fill_in 'yield[mean]', :with => '10.0'
- select('10', :from => 'yield[date(3i)]')
- select('10', :from => 'yield[date(2i)]')
- select('1800', :from => 'yield[date(1i)]')
+ select('10', :from => 'yield[d_day]')
+ select('10', :from => 'yield[d_month]')
+ select('1800', :from => 'yield[d_year]')
fill_in 'species_query', :with => 'Abar'
select('2', :from => 'yield[specie_id]')
@@ -159,9 +159,9 @@
# Required attributes:
fill_in 'yield[mean]', :with => '10.0'
- select('10', :from => 'yield[date(3i)]')
- select('10', :from => 'yield[date(2i)]')
- select('1800', :from => 'yield[date(1i)]')
+ select('10', :from => 'yield[d_day]')
+ select('10', :from => 'yield[d_month]')
+ select('1800', :from => 'yield[d_year]')
fill_in 'species_query', :with => 'Abar'
select('2', :from => 'yield[specie_id]')
diff --git a/spec/models/trait_interface_spec.rb b/spec/models/trait_interface_spec.rb
new file mode 100644
index 000000000..b0cd8ec4e
--- /dev/null
+++ b/spec/models/trait_interface_spec.rb
@@ -0,0 +1,108 @@
+# Override the default path "#{::Rails.root}/spec/fixtures"
+RSpec.configure do |config|
+ config.fixture_path = "#{::Rails.root}/test/fixtures"
+end
+
+require 'support/factory_girl'
+
+
+describe "Trait" do
+
+ specify "Saving an unchanged trait shouldn't change it." do
+
+ t = create :trait
+
+ # A bug having to do with method nsec is averted by using reload here:
+ expect { t.save }.not_to change { t.updated_at }
+
+ end
+
+ specify "Saving an unchanged trait shouldn't change the date, even if date attributes are inconsistent." do
+
+ t = create :trait_with_inconsistent_date_attributes
+ # A bug having to do with method nsec is averted by using reload here:
+ expect { t.save; t.reload }.not_to change { t.date }
+
+ end
+
+ context "Updating a non-date attribute of a trait having consistent date attributes" do
+
+ before(:example) do
+ @trait = create(:trait)
+ @trait.notes = "New note"
+ end
+
+ it "should save the change to the changed attribute" do
+ @trait.save
+ @trait.reload # ensure we get the saved version
+ expect(@trait.notes).to eq("New note")
+ end
+
+ it "shouldn't change the date attribute" do
+ # A bug having to do with method nsec is averted by using reload here:
+ expect { @trait.save; @trait.reload }.not_to change { @trait.date }
+ end
+
+ it "shouldn't change the dateloc attribute" do
+ # A bug having to do with method nsec is averted by using reload here:
+ expect { @trait.save }.not_to change { @trait.dateloc }
+ end
+
+ it "shouldn't change the timeloc attribute" do
+ expect { @trait.save }.not_to change { @trait.timeloc }
+ end
+
+ end
+
+ context "Adding 1 to the d_year virtual attribute of a valid date should" do
+ before(:example) do
+ @trait = create(:trait_with_consistent_date_attributes)
+ @starting_d_year = @trait.d_year
+ @trait.d_year += 1
+ end
+
+ it "increase the year by one" do
+ expect { @trait.save; @trait.reload }.to change { @trait.date.year }.by(1)
+ end
+
+ it "not change the month" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.date.month }
+ end
+
+ it "not change the day" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.date.day }
+ end
+
+ it "not change the hour" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.date.hour }
+ end
+
+ it "not change the minute" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.date.min }
+ end
+
+
+ it "increase the year virtual attribute by one" do
+ @trait.save
+ @trait.reload
+ expect(@trait.d_year - @starting_d_year).to eq(1)
+ end
+
+ it "not change the month virtual attribute" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.d_month }
+ end
+
+ it "not change the day virtual attribute" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.d_day }
+ end
+
+ it "not change the hour virtual attribute" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.t_hour }
+ end
+
+ it "not change the minute virtual attribute" do
+ expect { @trait.save; @trait.reload }.not_to change { @trait.t_minute }
+ end
+
+ end
+end
diff --git a/spec/models/trait_spec.rb b/spec/models/trait_spec.rb
index 552ec4b8b..d9c08467d 100644
--- a/spec/models/trait_spec.rb
+++ b/spec/models/trait_spec.rb
@@ -1,19 +1,199 @@
-describe Trait do
- it 'should be invalid if no attributes are given' do
-
- t = Trait.new
- expect(t.invalid?).to eq(true)
- end
-
- it 'should be valid if a valid mean, variable_id, and access_level are given' do
- t = Trait.new mean: 6, access_level: 1, variable_id: 1
- expect(t.invalid?).to eq(false)
- end
-
- it 'should be invalid if date_year has the wrong format' do
- t = Trait.new mean: 6, access_level: 1, variable_id: 1, date_year: "783"
- expect(t.invalid?).to eq(true)
- end
+### SAMPLE DATA ###
+
+# samples for all possible dateloc values
+TEST_DATE_SAMPLES = [
+ { dateloc: 9, date_attributes: {}, description: "no date information is given" },
+ { dateloc: 8, date_attributes: { d_year: 2001 }, description: "only a year is given" },
+ { dateloc: 7, date_attributes: { d_year: 2001, d_month: "Season: SON" }, description: "a year and season are given" },
+ { dateloc: 6, date_attributes: { d_year: 2001, d_month: 7 }, description: "a year and month are given" },
+ { dateloc: 5, date_attributes: { d_year: 2001, d_month: 7, d_day: 23 }, description: "a year, month, and day are given" },
+ { dateloc: 97, date_attributes: { d_month: "Season: DJF" }, description: "only a season is given" },
+ { dateloc: 96, date_attributes: { d_month: 11 }, description: "only a month is given" },
+ { dateloc: 95, date_attributes: { d_month: 11, d_day: 9 }, description: "only a month and day are given" }
+ ]
+
+# samples for all possible timeloc values
+TEST_TIME_SAMPLES = [
+ { timeloc: 9, time_attributes: {}, description: "no time information is given" },
+ { timeloc: 4, time_attributes: { t_hour: 'afternoon' }, description: "only the approximate time of day is given" },
+ { timeloc: 3, time_attributes: { t_hour: 17 }, description: "only the hour is given" },
+ { timeloc: 2, time_attributes: { t_hour: 19, t_minute: 25 }, description: "both hour and minutes are given" }
+ ]
+
+TEST_SITES = [
+ { site_id: 1 },
+ { site_id: 2 },
+ { site_id: 3 },
+ { site_id: 4 }
+]
+
+ABERRANT_DATA = [
+ { dateloc: nil, description: "no dateloc is given" },
+ { dateloc: 5.5, description: "the obsolescent week dateloc is given" },
+ { dateloc: 10, description: "an invalid dateloc is given" }
+]
+
+### CONVENIENCE METHODS ###
+
+# Compare result of calling Trait#pretty_print with calling the PL/pgSQL
+# function pretty_print. (The latter comprises the normative interpretation of
+# trait date/dateloc information.)
+def rails_pp_output_agrees_with_sql_pp_output(t)
+ sql_call = "SELECT pretty_date(date, dateloc, timeloc, site_id) FROM traits WHERE id = #{t.id}"
+
+ sql_text = ActiveRecord::Base.connection.select_all(sql_call).first.fetch("pretty_date")
+ rails_text = t.pretty_date
+ expect(sql_text).to eq(rails_text), < { self.raw_update }
+end
+
+
+### TESTS ###
+
+
+describe "Trait" do
+ describe "basic validity constraints" do
+ it 'should be invalid if no attributes are given' do
+
+ t = Trait.new
+ expect(t.invalid?).to eq(true)
+ end
+
+ it 'should be valid if a valid mean, variable_id, site_id, and access_level are given' do
+ t = Trait.new mean: 6, access_level: 1, variable_id: 1, site_id: 1
+ expect(t.invalid?).to eq(false)
+ end
+
+ it 'should be invalid if date_year has the wrong format' do
+ t = Trait.new mean: 6, access_level: 1, variable_id: 1, date_year: "783"
+ expect(t.invalid?).to eq(true)
+ end
+
+ end # describe "basic validity constraints"
+
+ describe "date and time semantics" do
+ let(:sample_trait) do
+ Trait.create mean: 1, variable_id: 1, access_level: 1, site_id:1,
+ d_year: '', d_month: '', d_day: '', t_hour: '', t_minute: ''
+ end
+
+ describe "automatic dateloc setting" do
+ TEST_DATE_SAMPLES.each do |sample|
+ it "should set dateloc to #{sample.fetch(:dateloc)} if #{sample.fetch(:description)}" do
+ sample_trait.update_attributes(sample.fetch(:date_attributes))
+ expect(sample_trait.dateloc).to eq sample.fetch(:dateloc)
+ end
+ end
+ end
+
+ describe "pretty-printing date should pretty print the date and time according to the normative SQL function" do
+
+ TEST_DATE_SAMPLES.each do |sample_date|
+ TEST_SITES.each do |sample_site|
+ it "when #{sample_date.fetch(:description)} and site timezone is #{Site.find(sample_site.fetch(:site_id)).time_zone || 'nil'}" do
+
+ # set the site first so that timezone is taken into consideration when date is set
+ sample_trait.update_attributes(sample_site)
+
+ sample_trait.update_attributes(sample_date.fetch(:date_attributes))
+
+ rails_pp_output_agrees_with_sql_pp_output(sample_trait)
+
+ end # end it
+ end # inner each loop
+ end # outer each loop
+
+ describe "pretty_date should display aberrant date information according to the normative SQL function" do
+ let(:sample_trait_with_date) do
+ t = Trait.create(mean: 1, variable_id: 1, access_level: 1, site_id: 1, d_year: 2001, d_month: 7, d_day: 23)
+ t.raw_update = true # circumvent before_save callback so we can set dateloc at will
+ return t
+ end # let
+
+ ABERRANT_DATA.each do |facet|
+ it "when #{facet.fetch(:description)}" do
+ sample_trait_with_date.update_attributes(dateloc: facet.fetch(:dateloc))
+
+ rails_pp_output_agrees_with_sql_pp_output(sample_trait_with_date)
+ end
+ end # ABERRANT_DATA.each
+ end # describe "aberrant cases"
+
+ end # describe "pretty-printing date"
+
+
+
+
+
+ describe "pretty-printing date should pretty print the date and time according to the normative SQL function" do
+
+ TEST_TIME_SAMPLES.each do |sample_time|
+ TEST_SITES.each do |sample_site|
+ it "when #{sample_time.fetch(:description)} and site timezone is #{Site.find(sample_site.fetch(:site_id)).time_zone || 'nil'}" do
+
+ # set the site first so that timezone is taken into consideration when date is set
+ sample_trait.update_attributes(sample_site)
+
+ sample_trait.update_attributes(sample_time.fetch(:time_attributes))
+
+ rails_pretty_time_output_agrees_with_sql_pretty_time_output(sample_trait)
+
+ end # end it
+ end # inner each loop
+ end # outer each loop
+=begin
+ describe "pretty_date should display aberrant date information according to the normative SQL function" do
+ let(:sample_trait_with_date) do
+ t = Trait.create(mean: 1, variable_id: 1, access_level: 1, site_id: 1, d_year: 2001, d_month: 7, d_day: 23)
+ t.raw_update = true # circumvent before_save callback so we can set dateloc at will
+ return t
+ end # let
+
+ ABERRANT_DATA.each do |facet|
+ it "when #{facet.fetch(:description)}" do
+ sample_trait_with_date.update_attributes(dateloc: facet.fetch(:dateloc))
+
+ rails_pp_output_agrees_with_sql_pp_output(sample_trait_with_date)
+ end
+ end # ABERRANT_DATA.each
+ end # describe "aberrant cases"
+=end
+ end # describe "pretty-printing date"
+
+ end # describe "date and time semantics"
+end # describe "Trait"
diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb
new file mode 100644
index 000000000..eec437fb3
--- /dev/null
+++ b/spec/support/factory_girl.rb
@@ -0,0 +1,3 @@
+RSpec.configure do |config|
+ config.include FactoryGirl::Syntax::Methods
+end
diff --git a/test/fixtures/traits.yml b/test/fixtures/traits.yml
index 3d60a9359..8737ff418 100644
--- a/test/fixtures/traits.yml
+++ b/test/fixtures/traits.yml
@@ -4,11 +4,11 @@ test_trait:
citation_id: 1
cultivar_id: 1
treatment_id: 1
- date: NULL
- dateloc: NULL
+ date: "2005-07-25 09:31:00"
+ dateloc: 5
time: NULL
- timeloc: NULL
- mean: NULL
+ timeloc: 2
+ mean: 1
n: NULL
stat: NULL
created_at: NULL
@@ -25,6 +25,17 @@ test_trait:
time_hour: NULL
time_minute: NULL
+trait_with_inconsistent_date_attributes:
+ site_id: 1
+ specie_id: 1
+ date: "2005-07-25 09:31:00"
+ dateloc: 5
+ time: NULL
+ timeloc: 9
+ mean: 1
+ variable_id: 1
+ access_level: 4
+
test_edit_trait:
id: 2
site_id: 1
@@ -32,10 +43,10 @@ test_edit_trait:
citation_id: 1
cultivar_id: 417
treatment_id: 1
- date: NULL
- dateloc: 9.00
+ date: "2015-10-01 04:00:00"
+ dateloc: 7
time: NULL
- timeloc: 9.00
+ timeloc: 3
mean: '5'
n: NULL
statname: ""