diff --git a/Gemfile b/Gemfile index 2d9206e10..244d393bd 100644 --- a/Gemfile +++ b/Gemfile @@ -66,6 +66,7 @@ group :test do gem "capybara", "~> 2.8" gem "database_cleaner" gem "rails-controller-testing" # TO-DO: rewrite the specs that rely on this + gem "factory_girl_rails", "~> 4.0" end # If you have difficulty installing or don't wish to install capybara-webkit, diff --git a/Gemfile.lock b/Gemfile.lock index 00000d1ae..63a9d3051 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -63,7 +63,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (>= 2.0, < 4.0) - capybara-webkit (1.15.0) + capybara-webkit (1.15.1) capybara (>= 2.3, < 4.0) json childprocess (0.9.0) @@ -78,14 +78,19 @@ GEM execjs coffee-script-source (1.12.2) comma (3.0.4) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.3) crass (1.0.4) database_cleaner (1.7.0) diff-lcs (1.3) dynamic_form (1.1.4) erubi (1.7.1) execjs (2.7.0) - ffi (1.9.23) + factory_girl (4.9.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.9.0) + factory_girl (~> 4.9.0) + railties (>= 3.0.0) + ffi (1.9.25) globalid (0.4.1) activesupport (>= 4.2.0) i18n (0.9.5) @@ -97,44 +102,44 @@ GEM jquery-ui-rails (6.0.1) railties (>= 3.2.16) json (2.1.0) - json-schema (2.8.0) + json-schema (2.8.1) addressable (>= 2.4) kgio (2.11.2) - loofah (2.2.2) + loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.0) + mail (2.7.1) mini_mime (>= 0.1.1) memoist (0.16.0) - method_source (0.9.0) - mini_mime (1.0.0) + method_source (0.9.2) + mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.11.3) multi_json (1.13.1) - mustache (1.0.5) + mustache (1.1.0) narray (0.6.0.4) - nio4r (2.3.0) - nokogiri (1.8.2) + nio4r (2.3.1) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) - passenger (5.3.4) + passenger (6.0.0) rack rake (>= 0.8.1) - pg (1.0.0) + pg (1.1.3) protected_attributes_continued (1.3.0) activemodel (~> 5.0) - pry (0.11.3) + pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) pry-byebug (3.6.0) byebug (~> 10.0) pry (~> 0.10) - pry-rails (0.3.6) + pry-rails (0.3.8) pry (>= 0.10.4) - public_suffix (3.0.2) - rabl (0.13.1) + public_suffix (3.0.3) + rabl (0.14.0) activesupport (>= 2.3.14) - rack (2.0.5) - rack-test (1.0.0) + rack (2.0.6) + rack-test (1.1.0) rack (>= 1.0, < 3) railroad (0.5.0) rails (5.1.5) @@ -149,10 +154,10 @@ GEM bundler (>= 1.3.0) railties (= 5.1.5) sprockets-rails (>= 2.0.0) - rails-controller-testing (1.0.2) - actionpack (~> 5.x, >= 5.0.1) - actionview (~> 5.x, >= 5.0.1) - activesupport (~> 5.x) + rails-controller-testing (1.0.4) + actionpack (>= 5.0.1.x) + actionview (>= 5.0.1.x) + activesupport (>= 5.0.1.x) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -166,7 +171,7 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) raindrops (0.19.0) - rake (12.3.1) + rake (12.3.2) rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -176,36 +181,36 @@ GEM rgeo-activerecord (5.1.1) activerecord (~> 5.0) rgeo (~> 0.3) - rspec (3.7.0) - rspec-core (~> 3.7.0) - rspec-expectations (~> 3.7.0) - rspec-mocks (~> 3.7.0) - rspec-core (3.7.1) - rspec-support (~> 3.7.0) - rspec-expectations (3.7.0) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-mocks (3.7.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-rails (3.7.2) + rspec-support (~> 3.8.0) + rspec-rails (3.8.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.7.0) - rspec-expectations (~> 3.7.0) - rspec-mocks (~> 3.7.0) - rspec-support (~> 3.7.0) - rspec-support (3.7.1) - rspec_api_documentation (5.1.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rspec_api_documentation (6.1.0) activesupport (>= 3.0.0) mustache (~> 1.0, >= 0.99.4) rspec (~> 3.0) ruby-graphviz (1.0.8) - rubyzip (1.2.1) + rubyzip (1.2.2) safe_attributes (1.0.10) activerecord (>= 3.0.0) - sass (3.5.6) + sass (3.7.2) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -217,9 +222,9 @@ GEM sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) seer (0.10.0) - selenium-webdriver (3.11.0) + selenium-webdriver (3.141.0) childprocess (~> 0.5) - rubyzip (~> 1.2) + rubyzip (~> 1.2, >= 1.2.2) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -227,25 +232,25 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - thor (0.20.0) + thor (0.20.3) thread_safe (0.3.6) - tilt (2.0.8) - trollop (2.1.2) + tilt (2.0.9) + trollop (2.9.9) tzinfo (1.2.5) thread_safe (~> 0.1) - uglifier (4.1.9) + uglifier (4.1.20) execjs (>= 0.3.0, < 3) - unicorn (5.4.0) + unicorn (5.4.1) kgio (~> 2.6) raindrops (~> 0.7) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) will_paginate (3.1.6) - xpath (3.0.0) + xpath (3.2.0) nokogiri (~> 1.8) yajl-ruby (1.3.1) - yard (0.9.12) + yard (0.9.16) PLATFORMS ruby @@ -262,6 +267,7 @@ DEPENDENCIES comma (= 3.0.4) database_cleaner dynamic_form + factory_girl_rails (~> 4.0) jquery-rails jquery-ui-rails json diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 7e3cc9bb5..be72b4f11 100755 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -223,6 +223,10 @@ font-weight: normal; font-size: 13px; color: #444; } + legend span.inherit { + font-weight: inherit; + font-size: inherit; + color: inherit; } /* #Misc ================================================== */ diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 38902d794..fed20cb4c 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -7,6 +7,29 @@ class SitesController < ApplicationController require 'csv' #AJAX Calls + + # Returns the name of the time zone for the site whose id is passed in the + # HTTP parameter "id"; or returns 'UTC' if no site having that id is found or + # if the time_zone attribute of the site found is NULL. + def get_timezone + begin + site = Site.find(params[:id]) + if site.time_zone.nil? + tz = 'UTC - site timezone unknown' + else + tz = site.time_zone + end + rescue ActiveRecord::RecordNotFound + tz = 'UTC' + end + + respond_to do |format| + format.text { + render(plain: tz) + } + end + end + def linked @citation = Citation.find(session["citation"]) @site = Site.find(params[:id]) diff --git a/app/controllers/yields_controller.rb b/app/controllers/yields_controller.rb index 4d08e5641..a2f2474a5 100644 --- a/app/controllers/yields_controller.rb +++ b/app/controllers/yields_controller.rb @@ -132,12 +132,8 @@ def edit # POST /yields.xml def create - set_default_date_fields(params) - @yield = Yield.new(params[:yield]) - maybe_set_from_julian_date(params) - @yield.user_id = current_user.id logger.info "Current user: #{current_user.id}" @@ -163,11 +159,7 @@ def create def update @yield = Yield.all_limited(current_user).find(params[:id]) - set_default_date_fields(params) - - @yield.update_attributes(params[:yield]) - - maybe_set_from_julian_date(params) + @yield.assign_attributes(params[:yield]) if params['assign_creator'] @yield.user_id = current_user.id @@ -204,39 +196,4 @@ def destroy end end - private - - # If any piece of the date was given in the parameters, set any other pieces - # that may have been missing to default values. - def set_default_date_fields(params) - # If at least on of the date fields is non-blank, assign defaults to the others if they are blank: - if !(params[:yield]['date(1i)'].blank? and params[:yield]['date(2i)'].blank? and params[:yield]['date(3i)'].blank?) - - # Default the empty fields. In Rails 3.2, it seems we must do this explicitly. - params[:yield]['date(1i)'] = "9999" if params[:yield]['date(1i)'].blank? # year default - params[:yield]['date(2i)'] = "1"if params[:yield]['date(2i)'].blank? # month default - params[:yield]['date(3i)'] = "1"if params[:yield]['date(3i)'].blank? # day default - - end - end - - # If either parameter julianyear or julianday was set, update the yield date - # using these parameter values. - def maybe_set_from_julian_date(params) - - # They can also enter the date in julian format, so if they do, overwrite - # the other date field: - if !(params[:julianyear].blank? and params[:julianday].blank?) - params[:julianyear] = "9999" if params[:julianyear].blank? # year default - params[:julianday] = "1" if params[:julianday].blank? # day default - - begin - @yield.date = Date.ordinal(params[:julianyear].to_f, params[:julianday].to_f) - rescue - flash[:error] = "Invalid values in Julian date fields were ignored" - end - end - - end - end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6b5b7ed90..7da8ad08f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -27,18 +27,33 @@ def treatment_check(ty) "7 season" => "7.0" , "7.5 " => "7.5" , "8 year" => "8.0" , - "9 no data" => "9.0" } + "9 no data" => "9.0", + "95 day (year unspecified)" => "95.0", + "96 month (year unspecified)" => "96.0", + "97 season (year unspecified)" => "97.0" } $dateloc_drop_default = "9.0" $timeloc_drop = { "1 second" => "1.0" , "2 minute" => "2.0" , "2.5 quarter-hour" => "2.5" , "3 hour" => "3.0" , + "4 time of day, e.g. morning, afternoon" => "4.0" , "9 no data" => "9.0" } $timeloc_drop_default = "9.0" $statname_list = ["","SD", "SE", "MSE", "95%CI", "LSD", "MSD"] + # Query the database to get all possible timezone offsets in current use: + offsets_query = "SELECT DISTINCT utc_offset FROM pg_timezone_names ORDER BY utc_offset" + $utc_offsets = ActiveRecord::Base.connection.execute(offsets_query).to_a.collect do |item| + offset = item['utc_offset'] + if !offset.match(/\+|-/) + offset = "+#{offset}" + end + offset + end + + # Simple Search def sortable(column, title = nil) title ||= column.titleize diff --git a/app/models/trait.rb b/app/models/trait.rb index 345a88925..718fc9463 100644 --- a/app/models/trait.rb +++ b/app/models/trait.rb @@ -1,9 +1,13 @@ class Trait < ActiveRecord::Base + attr_protected [] # Passed from controller for validation of ability attr_accessor :current_user + #-- + ### Module Usage ### + include Overrides extend DataAccess # provides all_limited @@ -12,6 +16,11 @@ class Trait < ActiveRecord::Base SEARCH_INCLUDES = %w{ citation variable specie site treatment } SEARCH_FIELDS = %w{ traits.id traits.mean traits.n traits.stat traits.statname variables.name species.genus citations.author sites.sitename treatments.name } + include DateTimeConstants + + #-- + ### Associations ### + has_many :covariates has_many :variables, :through => :covariates @@ -25,25 +34,242 @@ class Trait < ActiveRecord::Base belongs_to :entity belongs_to :ebi_method, :class_name => 'Methods', :foreign_key => 'method_id' - validates_presence_of :mean, :access_level - validates_numericality_of :mean - validates_presence_of :variable - validates_inclusion_of :access_level, in: 1..4, message: "You must select an access level" - validates_presence_of :statname, :if => Proc.new { |trait| !trait.stat.blank? } - validates_format_of :date_year, :with => /\A(\d{2}|\d{4})\z/, :allow_blank => true - validates_format_of :date_month, :with => /\A\d{1,2}\z/, :allow_blank => true - validates_format_of :date_day, :with => /\A\d{1,2}\z/, :allow_blank => true - validates_format_of :time_hour, :with => /\A\d{1,2}\z/, :allow_blank => true - validates_format_of :time_minute, :with => /\A\d{1,2}\z/, :allow_blank => true - validate :can_change_checked - validate :mean_in_range - # Only allow admins/managers to change traits marked as failed. + + #-- + ### Scopes ### + + scope :sorted_order, lambda { |order| order(order).includes(SEARCH_INCLUDES).references(SEARCH_INCLUDES) } + scope :search, lambda { |search| where(simple_search(search)) } + scope :exclude_api, -> { where("checked != ?", "-1") } + scope :citation, lambda { |citation| + if citation.nil? + all + else + where("citation_id = ?", citation) + end + } + + + + #-- + ### Callbacks ### + + before_save :process_datetime_input, if: lambda { |obj| + !obj.instance_variable_get(:@d_year).nil? || + !obj.instance_variable_get(:@d_month).nil? || + !obj.instance_variable_get(:@d_day).nil? || + !obj.instance_variable_get(:@d_hour).nil? || + !obj.instance_variable_get(:@d_minute).nil? + } + + + #-- + ### Virtual Attributes ### + + # Passed from controller for validation of ability + attr_accessor :current_user + + attr_writer :d_year + attr_writer :d_month + attr_writer :d_day + attr_writer :t_hour + attr_writer :t_minute + + + #-- + ## Custom accessors for virtual attributes. ## + + # A getter for the +d_year+ virtual attribute. If the @+d_year+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def d_year + if !@d_year.nil? + return @d_year + end + + case dateloc + when 95, 96, 97, 9 + '' + when 5, 5.5, 6, 7, 8 + date.nil? ? '' : date_in_site_timezone.year + when nil + nil + else + raise "In d_year, unrecognized dateloc value." + end + end + + # A getter for the +d_month+ virtual attribute. If the @+d_month+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def d_month + if !@d_month.nil? + return @d_month + end + + case dateloc + when 8, 9 + nil + when 7, 97 + SeasonRepresentativeMonths.key(date_in_site_timezone.month) or + "[bad month value #{date_in_site_timezone.month} doesn't correspond to a season]" + when 5, 5.5, 6, 95, 96 + date.nil? ? '' : date_in_site_timezone.month + when nil + nil + else + raise "In d_month, unrecognized dateloc value." + end + end + + # A getter for the +d_day+ virtual attribute. If the @+d_day+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def d_day + if !@d_day.nil? + return @d_day + end + + case dateloc + when 5.5, 6, 7, 8, 9, 96, 97 + nil + when 5, 95 + date.nil? ? '' : date_in_site_timezone.day + when nil + nil + else + raise "In d_day, unrecognized dateloc value." + end + end + + # A getter for the +t_hour+ virtual attribute. If the @+t_hour+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def t_hour + if !@t_hour.nil? + return @t_hour + end + + case timeloc + when 9 + nil + when 4 + TimesOfDayRepresentativeHours.key(date_in_site_timezone.hour) or + raise "In t_hour, hour is not appropriate for representing a time of day." + when 1, 2, 3 + date.nil? ? '' : date_in_site_timezone.strftime('%H') + when nil + nil + else + raise "In t_hour, unrecognized timeloc value." + end + end + + # A getter for the +t_minute+ virtual attribute. If the @+t_minute+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def t_minute + if !@t_minute.nil? + return @t_minute + end + + case timeloc + when 3, 4, 5, 9 + nil + when 1, 2 + date.nil? ? '' : date_in_site_timezone.strftime('%M') + when nil + nil + else + raise "In t_minute, unrecognized timeloc value." + end + end + + + + #-- + ### VALIDATION ### + + #-- + ## Validation methods ## + + # Validation Method: Check that the five time/date fields represent a valid + # date and time and are consistent with our conventions for allowable partial + # information about dates and times. + def consistent_date_and_time_fields + + # Set defaults for unspecified components and convert the supplied ones to + # integers. + case computed_dateloc + when 9 + year, month, day = DummyYear, DummyMonth, DummyDay + when 8 + year, month, day = d_year.to_i, DummyMonth, DummyDay + when 7 + month = SeasonRepresentativeMonths[d_month] + year, day = d_year.to_i, DummyDay + when 6 + year, month, day = d_year.to_i, d_month.to_i, DummyDay + when 5 + year, month, day = d_year.to_i, d_month.to_i, d_day.to_i + when 97 + month = SeasonRepresentativeMonths[d_month] + year, day = DummyYear, DummyDay + when 96 + year, month, day = DummyYear, d_month.to_i, DummyDay + when 95 + year, month, day = DummyYear, d_month.to_i, d_day.to_i + else + raise "Unexpected computed_dateloc value in Trait#consistent_date_and_time_fields." + end + + case computed_timeloc + when 9 + hour, minute = DummyHour, DummyMinute + when 4 + hour = TimesOfDayRepresentativeHours[t_hour] + minute = DummyMinute + when 3 + hour, minute = t_hour.to_i, DummyMinute + when 2 + hour, minute = t_hour.to_i, t_minute.to_i + else + raise "Unexpected computed_timeloc value in Trait#consistent_date_and_time_fields." + end + + begin + + # This will catch some illegal dates (1900-02-29, for example) that Time.new + # will silently covert to an acceptible date (1900-03-01, in this example). + DateTime.new(year, month, day, hour, minute) + + # Store the computed date for use by the before_save call-back "process_datetime_input". + @computed_date = utctime_from_sitetime(year, month, day, hour, minute) + + rescue ArgumentError => e + errors.add(:date, "is invalid") + end + + end + + # Validation Method: Only allow admins/managers to change traits marked as failed. def can_change_checked errors.add(:checked, "You do not have permission to change") if checked == -1 and current_user.page_access_level > 2 end + # Validation Method: Check that the mean is in the range stipulated for the variable. # To do: change the database type of min and max and constrain to be # non-null so that these tests can be simplified. def mean_in_range @@ -56,16 +282,28 @@ def mean_in_range end end - scope :sorted_order, lambda { |order| order(order).includes(SEARCH_INCLUDES).references(SEARCH_INCLUDES) } - scope :search, lambda { |search| where(simple_search(search)) } - scope :exclude_api, -> { where("checked != ?", "-1") } - scope :citation, lambda { |citation| - if citation.nil? - all - else - where("citation_id = ?", citation) - end - } + + #-- + ## Validations ## + + validates_presence_of :mean, :access_level, :site + validates_numericality_of :mean + validates_presence_of :variable + validates_inclusion_of :access_level, in: 1..4, message: "You must select an access level" + validates_presence_of :statname, :if => Proc.new { |trait| !trait.stat.blank? } + validates_format_of :d_year, :with => /\A(\d{2}|\d{4})\z/, :allow_blank => true + validates_format_of :d_month, :with => /\A(\d{1,2}|#{Seasons.join("|")})\z/, :allow_blank => true + validates_format_of :d_day, :with => /\A\d{1,2}\z/, :allow_blank => true + validates_format_of :t_hour, :with => /\A(\d{2}|#{TimesOfDay.join('|')})\z/, :allow_blank => true + validates_format_of :t_minute, :with => /\A\d{2}\z/, :allow_blank => true + validate :consistent_date_and_time_fields + validate :can_change_checked + validate :mean_in_range + + + + #-- + ### CSV Formats ### comma do id @@ -75,12 +313,15 @@ def mean_in_range cultivar_id treatment_id entity_id - date_year - date_month - date_day + date 'raw date' do |date| date.utc end + pretty_date + d_year + d_month + d_day dateloc - time_hour - time_minute + pretty_time + t_hour + t_minute timeloc mean n @@ -127,15 +368,23 @@ def mean_in_range end site :sitename_state_country => 'Site Name' treatment :name_definition => 'Treatment' - citation :author_year_title => 'Author Year Title' + citation :author_year_title => 'Author Year Title' site :lat => 'Latitude', :lon => 'Longitude' end + + + #-- + ### Presentation Methods ### + def pretty_date - date_string = "" - date_string += "#{date_year} " unless date_year.nil? - date_string += "#{Date::ABBR_MONTHNAMES[date_month]} " unless date_month.nil? - date_string += "#{date_day} " unless date_day.nil? + if date.nil? + '[unspecified]' + else + date_in_site_timezone.to_formatted_s(date_format) + + # show the site timezone next to the date only if the time is unspecified or unknown + (timeloc == 9 && [5, 5.5, 6, 8, 95, 96].include?(dateloc) ? " (#{site_timezone})" : "") + end end def format_statname @@ -155,9 +404,8 @@ def format_stat end def pretty_time - time_string = "" - time_string += "#{time_hour}" unless time_hour.nil? - time_string += ":#{time_minute}" unless time_hour.nil? or time_minute.nil? +# Rails.logger.info("date = #{date.to_s}; date.to_s(time_format) = #{date.to_s(time_format)}") + date.nil? ? '[unspecified]' : date_in_site_timezone.to_s(time_format) + (timeloc == 9 ? '' : " (#{site_timezone})") end def specie_treat_cultivar @@ -178,4 +426,111 @@ def self.search_columns return ["traits.id"] end + + + private + + # Computes the values to store for date, dateloc, and timeloc based on the + # values of the virtual attributes. + def process_datetime_input + self.dateloc = computed_dateloc + self.timeloc = computed_timeloc + self.date = @computed_date # this is set in the consistent_date_and_time_fields validate method + end + + + def computed_dateloc + y = d_year + m = d_month + d = d_day + + if !d.blank? + if m.blank? + errors.add(:base, "If you set a day, you must also set a month.") + 9 + elsif Seasons.include?(m) + errors.add(:base, "If you select a season, day must be blank.") + 9 + else + if y.blank? + 95 + else + 5 + end + end + else # d is blank + if m.blank? + if y.blank? + 9 + else + 8 + end + elsif Seasons.include?(m) + if y.blank? + 97 + else + 7 + end + else # month is a number (a real month) + if y.blank? + 96 + else + 6 + end + end + end + end + + def computed_timeloc + if !t_minute.blank? + if t_hour.blank? + errors.add(:base, "If you specify minutes, you must specify the hour.") + 9 + elsif TimesOfDay.include?(t_hour) + errors.add(:base, "If you select a time of day, minutes must be blank.") + 9 + else + 2 + end + else # t_minute is blank + if t_hour.blank? + 9 + elsif TimesOfDay.include?(t_hour) + 4 + else + 3 + end + end + end + + def time_format + case timeloc + when 9 + :no_time_data + when 4 + :time_of_day + when 3 + :hour_only + when 2 + :hour_minutes + when 1 + :hour_minutes_seconds + when nil + :unspecified_timeloc + else + :unrecognized_timeloc + end + end + + def utctime_from_sitetime(y, m, d, hr, min) + utctime = nil + Time.use_zone site_timezone do + utctime = Time.zone.local(y, m, d, hr, min, DummySecond).utc + end + return utctime.to_datetime + end + + # provides date_in_site_timezone, date_format, and site_timezone + include DateTimeUtilityMethods + end diff --git a/app/models/traits_and_yields_view.rb b/app/models/traits_and_yields_view.rb index c1bf2a852..dd03ed5bb 100644 --- a/app/models/traits_and_yields_view.rb +++ b/app/models/traits_and_yields_view.rb @@ -14,6 +14,10 @@ class TraitsAndYieldsView < ActiveRecord::Base # attr_accessor :current_user self.table_name = 'traits_and_yields_view' + + #-- + ### Module Usage ### + include ActiveModel::Serialization extend CoordinateSearch # provides coordinate_search @@ -28,6 +32,10 @@ class TraitsAndYieldsView < ActiveRecord::Base trait_description method_name city sitename author citation_year cultivar entity } + + #-- + ### Scopes ### + scope :sorted_order, lambda { |order| order(order).includes(SEARCH_INCLUDES).order("id asc").references(SEARCH_INCLUDES) } scope :search, lambda { |search| where(advanced_search(search)) } scope :checked, lambda { |checked_minimum| where("checked >= #{checked_minimum}") } @@ -37,6 +45,12 @@ class TraitsAndYieldsView < ActiveRecord::Base # make NumberHelper available to use inside comma block: extend ActionView::Helpers::NumberHelper + + + + #-- + ### CSV Format ### + comma do checked 'checked' do |num| if num == 1 then @@ -65,6 +79,7 @@ class TraitsAndYieldsView < ActiveRecord::Base author 'author' citation_year 'citation_year' treatment 'treatment' + pretty_date 'date' date 'date' time 'time' month 'month' @@ -85,13 +100,31 @@ class TraitsAndYieldsView < ActiveRecord::Base stat 'stat' do |num| if num.nil? then "[missing]" - else + else TraitsAndYieldsView.number_with_precision(num, precision: 3)#, significant: true) - end + end end notes 'notes' entity 'entity' method_name 'method' end + + + #-- + ### Presentation Methods ### + + def pretty_date + if raw_date.nil? + '[unspecified]' + elsif result_type =~ /traits/ + date_in_site_timezone.to_formatted_s(date_format) + " (#{site_timezone})" + else # for yields, don't mess with time zones + raw_date.to_time.to_formatted_s(date_format) + end + end + + # provides date_in_site_timezone, date_format, and site_timezone + include DateTimeUtilityMethods + end diff --git a/app/models/yield.rb b/app/models/yield.rb index e4c2a81da..113bb91d4 100644 --- a/app/models/yield.rb +++ b/app/models/yield.rb @@ -1,6 +1,9 @@ class Yield < ActiveRecord::Base attr_protected [] + #-- + ### Module Usage ### + include Overrides extend DataAccess # provides all_limited @@ -9,6 +12,11 @@ class Yield < ActiveRecord::Base SEARCH_INCLUDES = %w{ citation specie site treatment cultivar } SEARCH_FIELDS = %w{ species.genus cultivars.name yields.mean yields.n yields.stat yields.statname citations.author sites.sitename treatments.name } + include DateTimeConstants + + #-- + ### Associations ### + belongs_to :citation belongs_to :site belongs_to :specie @@ -17,6 +25,61 @@ class Yield < ActiveRecord::Base belongs_to :user belongs_to :ebi_method, :class_name => 'Methods', :foreign_key => 'method_id' + + + #-- + ### VALIDATION ### + + #-- + ## Validation methods ## + + # Validation Method: Check that the three date fields represent a valid date + # and are consistent with our conventions for allowable partial information + # about dates. + def consistent_date_and_time_fields + + # Set defaults for unspecified components and convert the supplied ones to + # integers. + case computed_dateloc + when 9 + year, month, day = DummyYear, DummyMonth, DummyDay + when 8 + year = julianyear.blank? ? d_year.to_i : julianyear.to_i + month, day = DummyMonth, DummyDay + when 7 + month = SeasonRepresentativeMonths[d_month] + year, day = d_year.to_i, DummyDay + when 6 + year, month, day = d_year.to_i, d_month.to_i, DummyDay + when 5 + if !julianday.blank? + @computed_date = Date.ordinal(julianyear.to_i, julianday.to_i) + return + end + year, month, day = d_year.to_i, d_month.to_i, d_day.to_i + when 97 + month = SeasonRepresentativeMonths[d_month] + year, day = DummyYear, DummyDay + when 96 + year, month, day = DummyYear, d_month.to_i, DummyDay + when 95 + if !julianday.blank? + @computed_date = Date.ordinal(DummyYear, julianday) + return + end + year, month, day = DummyYear, d_month.to_i, d_day.to_i + else + raise "Unexpected computed_dateloc value in Trait#consistent_date_and_time_fields." + end + + # Store the computed date for use by the before_save call-back "process_datetime_input". + @computed_date = Date.new(year, month, day) + + rescue ArgumentError => e + errors.add(:date, "is invalid") + end + + validates_presence_of :mean validates_numericality_of :mean, :greater_than_or_equal_to => 0.0 validates_presence_of :statname, :if => Proc.new { |y| !y.stat.blank? } @@ -26,7 +89,12 @@ class Yield < ActiveRecord::Base validates_presence_of :specie_id validates_presence_of :treatment_id validates_presence_of :access_level - validates_presence_of :date + validate :consistent_date_and_time_fields + + + + #-- + ### Scopes ### scope :all_order, -> { includes(:specie).order('species.genus, species.species') } scope :sorted_order, lambda { |order| order(order).includes(SEARCH_INCLUDES).references(SEARCH_INCLUDES) } @@ -39,6 +107,106 @@ class Yield < ActiveRecord::Base end } + + + #-- + ### Callbacks ### + + before_save :process_date_input + + + + #-- + ### Virtual Attributes ### + + attr_writer :d_year + attr_writer :d_month + attr_writer :d_day + + # We don't need special getter methods for these because we always populate + # the d_* fields, not these, when freshly populating the page form from the + # database. + attr_accessor :julianyear + attr_accessor :julianday + + + #-- + ## Custom accessors for virtual attributes. ## + + # A getter for the +d_year+ virtual attribute. If the @+d_year+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def d_year + if !@d_year.nil? + return @d_year + end + + case dateloc + when 95, 96, 97, 9 + '' + when 5, 5.5, 6, 7, 8 + date.nil? ? '' : date.year + when nil + nil + else + raise "In d_year, unrecognized dateloc value." + end + end + + # A getter for the +d_month+ virtual attribute. If the @+d_month+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def d_month + if !@d_month.nil? + return @d_month + end + + case dateloc + when 8, 9 + nil + when 7, 97 + SeasonRepresentativeMonths.key(date.month) or + "[bad month value #{date_in_site_timezone.month} doesn't correspond to a season]" + when 5, 5.5, 6, 95, 96 + date.nil? ? '' : date.month + when nil + nil + else + raise "In d_month, unrecognized dateloc value." + end + end + + # A getter for the +d_day+ virtual attribute. If the @+d_day+ instance + # variable has been set, it is used. (This occurs, for example, when + # returning to a form that failed validation.) Otherwise, the value is + # computed from these persistent (i.e., database-backed) attributes: date, + # dateloc, and site.time_zone. + def d_day + if !@d_day.nil? + return @d_day + end + + case dateloc + when 5.5, 6, 7, 8, 9, 96, 97 + nil + when 5, 95 + date.nil? ? '' : date.day + when nil + nil + else + raise "In d_day, unrecognized dateloc value." + end + end + + + + #-- + ### CSV Formats ### + comma do id citation_id @@ -46,7 +214,8 @@ class Yield < ActiveRecord::Base specie_id treatment_id cultivar_id - date + date 'raw date' + pretty_date dateloc statname stat @@ -73,6 +242,25 @@ class Yield < ActiveRecord::Base end + + + #-- + ### Presentation Methods ### + + def pretty_date + if date.nil? + '[unspecified]' + else + date.to_time.to_formatted_s(date_format) + end + end + + + + + + + # Now that the access_level column of "yields" has user-defined (domain) type # "level_of_access", we have to ensure it maps to a Ruby Fixnum because Rails # seems to map unknown SQL types to strings by default: @@ -90,4 +278,81 @@ def self.search_columns return ["yields.id"] end + + + + private + def process_date_input + logger.info("computed_dateloc = #{computed_dateloc}") + self.dateloc = computed_dateloc + self.date = @computed_date # this is set in the consistent_date_and_time_fields validate method + end + + + def computed_dateloc + # Convenience variables; since we only use this method in cases where the + # accessor returns exactly the same value as the instance variable, we may + # as well use the latter since it's faster. + y = @d_year + m = @d_month + d = @d_day + + jy = @julianyear + jd = @julianday + + if (!jy.blank? || !jd.blank?) + if (!y.blank? || !m.blank? || !d.blank?) + errors.add(:base, "If you set the Julian year or day, you must leave the other date fields blank.") + return 9 + elsif jy.blank? + return 95 + elsif jd.blank? + return 8 + else + return 5 + end + end + + # We only get here if the Julian date fields are blank. + + if !d.blank? + if m.blank? + errors.add(:base, "If you set a day, you must also set a month.") + 9 + elsif Seasons.include?(m) + errors.add(:base, "If you select a season, day must be blank.") + 9 + else + if y.blank? + 95 + else + 5 + end + end + else # d is blank + if m.blank? + if y.blank? + 9 + else + 8 + end + elsif Seasons.include?(m) + if y.blank? + 97 + else + 7 + end + else # month is a number (a real month) + if y.blank? + 96 + else + 6 + end + end + end + end + + # provides date_format, and site_timezone + include DateTimeUtilityMethods + end diff --git a/app/views/api/v1/base/show.rabl b/app/views/api/v1/base/show.rabl index b93f133f5..60a44c653 100644 --- a/app/views/api/v1/base/show.rabl +++ b/app/views/api/v1/base/show.rabl @@ -3,7 +3,10 @@ object @row => (@row.class.name == "Specie" ? :species : @row) # correct the obj if !root_object.nil? # Show all columns for this model - attributes *root_object.class.column_names.map(&:to_sym) + attributes *root_object.class.column_names.map(&:to_sym) + + (root_object.instance_of?(Trait) ? ['pretty_date', 'pretty_time'] + : (root_object.instance_of?(Yield) ? ['pretty_date'] + : [])) # Now show information for associations diff --git a/app/views/traits/edit.html.erb b/app/views/traits/edit.html.erb index 7f75faf4a..fd7cd0cf9 100644 --- a/app/views/traits/edit.html.erb +++ b/app/views/traits/edit.html.erb @@ -9,6 +9,37 @@ }; <%= javascript_include_tag 'lazy/autocomplete.js' %> + + <% end %>
@@ -55,35 +86,49 @@
-
+
Date - <%= f.select :date_year, [''] + (1800..Time.now.year).to_a %> - <%= f.select :date_month, [''] + (1..12).to_a %> - <%= f.select :date_day, [''] + (1..31).to_a %> + <%= f.select :d_year, (1800..Time.now.year).to_a, include_blank: true %> + <%= f.select :d_month, Trait::Seasons + Trait::Months.to_a, include_blank: true %> + <%= f.select :d_day, Trait::Days.to_a, include_blank: true %>
- <%= f.label :dateloc, "Date Level of Confidence" %> - <%= f.select :dateloc, options_for_select($dateloc_drop.sort, f.object.dateloc || $dateloc_drop_default) %> +
+ Time <%= @trait.site.nil? || !@trait.site.time_zone.blank? ? "at site" : "(UTC)" %> + <%= f.select :t_hour, Trait::TimesOfDay + Trait::Hours.to_a.collect { |i| '%02i' % i }, include_blank: true %> + <%= f.select :t_minute, Trait::Minutes.to_a.collect { |i| '%02i' % i }, include_blank: true %> +
-
+
- Time - <%= f.select :time_hour, [''] + (0..23).to_a %> - <%= f.select :time_minute, [''] + (0..59).to_a %> + Time Zone + <%= @trait.site.nil? ? "(no site selected)" + : @trait.site.time_zone.blank? ? "UTC - site time zone unknown" + : @trait.site.time_zone %>
+
+ +
+
+ <%= f.label :dateloc, "Date Level of Confidence" %> + <%= f.select :dateloc, options_for_select($dateloc_drop.sort, f.object.dateloc || $dateloc_drop_default) %> +
<%= f.label :timeloc, "Time Level of Confidence" %> <%= f.select :timeloc, options_for_select($timeloc_drop.sort, f.object.timeloc || $timeloc_drop_default),{},:class => "input-full" %>
+
+ <--- These will be removed! +
<%= f.label :site_id %> - <%= f.select :site_id, @citation.sites.collect { |p| [ p.select_default, p.id ] }, {}, :class => "input-full" %> + <%= f.select :site_id, @citation.sites.collect { |p| [ p.select_default, p.id ] }, {include_blank: true}, :class => "input-full" %>
<%= f.label :treatment_id %> diff --git a/app/views/traits/new.html.erb b/app/views/traits/new.html.erb index 62e49921a..04e52e077 100644 --- a/app/views/traits/new.html.erb +++ b/app/views/traits/new.html.erb @@ -9,6 +9,37 @@ }; <%= javascript_include_tag 'lazy/autocomplete.js' %> + + <% end %>
@@ -64,35 +95,35 @@
-
+
Date - <%= f.select :date_year, [''] + (1800..Time.now.year).to_a %> - <%= f.select :date_month, [''] + (1..12).to_a %> - <%= f.select :date_day, [''] + (1..31).to_a %> + <%= f.select :d_year, (1800..Time.now.year).to_a, include_blank: true %> + <%= f.select :d_month, Trait::Seasons + Trait::Months.to_a, include_blank: true %> + <%= f.select :d_day, Trait::Days.to_a, include_blank: true %>
-
- <%= f.label :dateloc, "Date Level of Confidence" %> - <%= f.select :dateloc, options_for_select($dateloc_drop.sort, f.object.dateloc || $dateloc_drop_default) %> -
- Time - <%= f.select :time_hour, [''] + (0..23).to_a %> - <%= f.select :time_minute, [''] + (0..59).to_a %> + Time <%= @trait.site.nil? || !@trait.site.time_zone.blank? ? "at site" : "(UTC)" %> + <%= f.select :t_hour, Trait::TimesOfDay + Trait::Hours.to_a.collect { |i| '%02i' % i }, include_blank: true %> + <%= f.select :t_minute, Trait::Minutes.to_a.collect { |i| '%02i' % i }, include_blank: true %>
- <%= f.label :timeloc, "Time Level of Confidence" %> - <%= f.select :timeloc, options_for_select($timeloc_drop.sort, f.object.timeloc || $timeloc_drop_default),{},:class => "input-full" %> +
+ Time Zone + <%= @trait.site.nil? ? "(no site selected)" + : @trait.site.time_zone.blank? ? "UTC - site time zone unknown" + : @trait.site.time_zone %> +
<%= f.label :site_id %> - <%= f.select :site_id, @citation.sites.collect { |p| [ p.select_default, p.id ] }, {}, :class => "input-full" %> + <%= f.select :site_id, @citation.sites.collect { |p| [ p.select_default, p.id ] }, {include_blank: true}, :class => "input-full" %>
<%= f.label :treatment_id %> diff --git a/app/views/traits/show.html.erb b/app/views/traits/show.html.erb index 2b8e66213..7a532f2aa 100644 --- a/app/views/traits/show.html.erb +++ b/app/views/traits/show.html.erb @@ -35,15 +35,9 @@
Date:
<%= @trait.pretty_date %>
-
Date Level of Confidence:
-
<%= $dateloc_drop.invert[@trait.dateloc.to_s] %>
-
Time:
<%= @trait.pretty_time %>
-
Time Level of Confidence:
-
<%= @trait.timeloc %>
-
Mean:
<%= @trait.mean %>
diff --git a/app/views/yields/edit.html.erb b/app/views/yields/edit.html.erb index aaf4c08dc..1f2a61b8e 100644 --- a/app/views/yields/edit.html.erb +++ b/app/views/yields/edit.html.erb @@ -34,15 +34,21 @@
- <%= f.label :date %> - <%= f.date_select :date,:defualt => nil, :order=> [:day, :month, :year], :add_month_numbers=> true, :include_blank=> true, :start_year=> 1800, :end_year=> Time.now.year %> +
+ Date + <%= f.select :d_year, (1800..Time.now.year).to_a, include_blank: true %> + <%= f.select :d_month, Trait::Seasons + Trait::Months.to_a, include_blank: true %> + <%= f.select :d_day, Trait::Days.to_a, include_blank: true %> +
... OR ...
- <%= f.label "julian day (1-365)/year" %> - <%= text_field_tag "julianday" %> / <%= text_field_tag "julianyear" %> +
+ Julian Day (1–366) / Year + <%= f.text_field :julianday %> / <%= f.text_field :julianyear %> +
@@ -74,9 +80,6 @@ <%= f.label :cultivar_id %> <%= f.select :cultivar_id, Cultivar.joins(:specie).order('species.scientificname').collect { |p| [ p.select_default, p.id ] }, { :include_blank => true}, :class => "input-full" %>
- <%= f.label :dateloc, "Date Level of Confidence" %> - <%= f.select :dateloc, options_for_select($dateloc_drop.sort, f.object.dateloc || $dateloc_drop_default), :class => "input-full" %> -
<%= f.label :access_level %> <%= f.select :access_level, ApplicationHelper::DATA_ACCESS_LEVELS, :class => "input-full" %>
diff --git a/app/views/yields/new.html.erb b/app/views/yields/new.html.erb index 1874283ff..fed876bad 100644 --- a/app/views/yields/new.html.erb +++ b/app/views/yields/new.html.erb @@ -33,15 +33,21 @@
- <%= f.label :date %> - <%= f.date_select :date,:defualt => nil, :order=> [:day, :month, :year], :add_month_numbers=> true, :include_blank=> true, :start_year=> 1800, :end_year=> Time.now.year %> +
+ Date + <%= f.select :d_year, (1800..Time.now.year).to_a, include_blank: true %> + <%= f.select :d_month, Trait::Seasons + Trait::Months.to_a, include_blank: true %> + <%= f.select :d_day, Trait::Days.to_a, include_blank: true %> +
... OR ...
- <%= f.label "julian day (1-365)/year" %> - <%= text_field_tag "julianday" %> / <%= text_field_tag "julianyear" %> +
+ Julian Day (1–366) / Year + <%= f.text_field :julianday %> / <%= f.text_field :julianyear %> +
@@ -73,9 +79,6 @@ <%= f.label :cultivar_id %> <%= f.select :cultivar_id, Cultivar.joins(:specie).order('species.scientificname').collect { |p| [ p.select_default, p.id ] }, { :include_blank => true}, :class => "input-full" %>
- <%= f.label :dateloc, "Date Level of Confidence" %> - <%= f.select :dateloc, options_for_select($dateloc_drop.sort, f.object.dateloc || $dateloc_drop_default), :class => "input-full" %> -
<%= f.label :access_level %> <%= f.select :access_level, ApplicationHelper::DATA_ACCESS_LEVELS, :selected => current_user.access_level, :class => "input-full" %>
diff --git a/app/views/yields/show.html.erb b/app/views/yields/show.html.erb index 400835363..9b04e67d9 100644 --- a/app/views/yields/show.html.erb +++ b/app/views/yields/show.html.erb @@ -34,7 +34,7 @@
<%= user_for_view(@yield.user) %>
Date:
-
<%= @yield.date %>
+
<%= @yield.pretty_date %>
Dateloc:
<%= @yield.dateloc %>
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: ""