3

我提前道歉,这将是一个很长的问题。

简洁版本:

我有一个会议模型,其中包含datestart_timeend_time。这些是时间对象,用户输入当然很痛苦,所以我使用虚拟属性来接受在保存之前由Chronic解析的字符串。

我有一个普通的 vanilla rails 控制器,它从表单接收这些虚拟属性并将它们传递给模型。这是控制器:

def create
  @meeting = @member.meetings.build(params[:meeting])
  if @meeting.save
    redirect_to member_meetings_path(@member), :notice => "Meeting Added"
  else
    render :new
  end
end

def update
  @meeting = @member.meetings.find(params[:id])
  if @meeting.update_attributes(params[:meeting])
    redirect_to member_meetings_path(@member), :notice => "Meeting Updated"
  else
    render :new
  end
end

我已经验证控制器从表单接收到正确的参数,例如params[:meeting][:date_string]按预期设置。

问题:

创建时,日期设置正确,但时间分配为 2000 年,设置为 UTC,并且不会在前端显示为本地时间。

更新时,日期不会更新。时间更新但在 2000 年 1 月 1 日保持 UTC。

更长的版本

让我觉得这超级奇怪的是我有不错的测试覆盖率,表明所有这些都在模型层工作。

这是模型:

# DEPENDENCIES
require 'chronic'
class Meeting < ActiveRecord::Base
  # MASS ASSIGNMENT PROTECTION
  attr_accessible :name, :location, :description, :contact_id, :member_id, :time_zone, 
                  :date, :start_time, :end_time, :date_string, :start_time_string, :end_time_string

  # RELATIONSHIPS
  belongs_to :member
  belongs_to :contact

  # CALLBACKS
  before_save :parse_time

  # Time IO Formatting
  attr_writer :date_string, :start_time_string, :end_time_string

  # Display time as string, year optional    
  def date_string(year=true)
    if date
      str = "%B %e"
      str += ", %Y" if year
      date.strftime(str).gsub('  ',' ')
    else
      ""
    end
  end

  # Display time as string, AM/PM optional
  def start_time_string(meridian=true)
    if start_time
      str = "%l:%M"
      str += " %p" if meridian
      start_time.strftime(str).lstrip
    else
      ""
    end
  end

  # Display time as string, AM/PM optional    
  def end_time_string(meridian=true)
    if end_time
      str = "%l:%M"
      str += " %p" if meridian
      end_time.strftime(str).lstrip
    else
      ""
    end
  end

  # Display Date and Time for Front-End    
  def time
    date.year == Date.today.year ? y = false : y = true
    start_time.meridian != end_time.meridian ? m = true : m = false
    [date_string(y),'; ',start_time_string(m),' - ',end_time_string].join
  end

  private
    # Time Input Processing, called in `before_save`
    def parse_time
      set_time_zone
      self.date ||= @date_string ? Chronic.parse(@date_string).to_date : Date.today
      self.start_time = Chronic.parse @start_time_string, :now => self.date
      self.end_time = Chronic.parse @end_time_string, :now => self.date
    end

    def set_time_zone
      if time_zone
        Time.zone = time_zone
      elsif member && member.time_zone
        Time.zone = member.time_zone
      end
      Chronic.time_class = Time.zone
    end

end

这是规格。请注意,为了单独测试parse_time回调,@meeting.send(:parse_time)只要我没有实际创建或更新记录,我就会在这些测试中调用。

require "minitest_helper"

describe Meeting do
  before do
    @meeting = Meeting.new
  end

  describe "accepting dates in natural language" do
    it "should recognize months and days" do
      @meeting.date_string = 'December 17'
      @meeting.send(:parse_time)
      @meeting.date.must_equal Date.new(Time.now.year,12,17)
    end

    it "should assume a start time is today" do
      @meeting.start_time_string = '1pm'
      @meeting.send(:parse_time)
      @meeting.start_time.must_equal Time.zone.local(Date.today.year,Date.today.month,Date.today.day, 13,0,0)
    end

    it "should assume an end time is today" do
      @meeting.end_time_string = '3:30'
      @meeting.send(:parse_time)
      @meeting.end_time.must_equal Time.zone.local(Date.today.year,Date.today.month,Date.today.day, 15,30,0)
    end

    it "should set start time to the given date" do
      @meeting.date = Date.new(Time.now.year,12,1)
      @meeting.start_time_string = '4:30 pm'
      @meeting.send(:parse_time)
      @meeting.start_time.must_equal Time.zone.local(Time.now.year,12,1,16,30)
    end

    it "should set end time to the given date" do
      @meeting.date = Date.new(Time.now.year,12,1)
      @meeting.end_time_string = '6pm'
      @meeting.send(:parse_time)
      @meeting.end_time.must_equal Time.zone.local(Time.now.year,12,1,18,0)
    end
  end

  describe "displaying time" do
    before do
      @meeting.date = Date.new(Date.today.year,12,1)
      @meeting.start_time = Time.new(Date.today.year,12,1,16,30)
      @meeting.end_time = Time.new(Date.today.year,12,1,18,0)
    end

    it "should print a friendly time" do
      @meeting.time.must_equal "December 1; 4:30 - 6:00 PM"
    end
  end

  describe "displaying if nil" do
    it "should handle nil date" do
      @meeting.date_string.must_equal ""
    end

    it "should handle nil start_time" do
      @meeting.start_time_string.must_equal ""
    end

    it "should handle nil end_time" do
      @meeting.end_time_string.must_equal ""
    end
  end

  describe "time zones" do
    before do
      @meeting.assign_attributes(
        time_zone: 'Central Time (US & Canada)',
        date_string: "December 1, #{Time.now.year}",
        start_time_string: "4:30 PM",
        end_time_string: "6:00 PM"
      )
      @meeting.save
    end

    it "should set meeting start times in the given time zone" do
      Time.zone = 'Central Time (US & Canada)'
      @meeting.start_time.must_equal Time.zone.local(Time.now.year,12,1,16,30)
    end

    it "should set the correct UTC offset" do
      @meeting.start_time.utc_offset.must_equal -(6*60*60)
    end

    after do
      @meeting.destroy
    end
  end

  describe "updating" do
    before do
      @m = Meeting.create(
        time_zone: 'Central Time (US & Canada)',
        date_string: "December 1, #{Time.now.year}",
        start_time_string: "4:30 PM",
        end_time_string: "6:00 PM"
      )
      @m.update_attributes start_time_string: '2pm', end_time_string: '3pm'
      Time.zone = 'Central Time (US & Canada)'
    end

    it "should update start time via mass assignment" do
      @m.start_time.must_equal Time.zone.local(Time.now.year,12,1,14,00)
    end

    it "should update end time via mass assignment" do
      @m.end_time.must_equal Time.zone.local(Time.now.year,12,1,15,00)
    end

    after do
      @m.destroy
    end
  end

end

我什至在以后的测试方法中专门混合通过批量分配创建和更新记录,以确保这些按预期工作。所有这些测试都通过了。

我很欣赏对以下内容的任何见解:

  1. 为什么控制器#update 操作中的日期不更新?

  2. 为什么时间不是从设定的日期开始计算年份?这在模型和规范中有效,但在通过控制器通过表单提交时无效。

  3. 为什么不将时间设置为从表单传入的时区?同样,这些规格通过了,控制器出了什么问题?

  4. 为什么时间不会在前端显示在他们的时区?

感谢您的帮助,我觉得我必须为这棵树上的树而失去森林,因为我已经做了几个小时了。


更新:

感谢 AJcodez 的帮助,我看到了一些问题:

  1. 指定日期错误,谢谢 AJ!现在使用:

    if @date_string.present?
        self.date = Chronic.parse(@date_string).to_date
    elsif self.date.nil?
        self.date = Date.today
    end
    
  2. 我正确使用了 Chronic,我的错误在数据库层!我将数据库中的字段设置为,time而不是datetime,这会破坏一切。给任何阅读本文的人的教训:永远不要time用作数据库字段(除非您确切了解它的作用以及为什么使用它而不是 datetime)。

  3. 与上述相同的问题,更改字段以datetime解决问题。

  4. 这里的问题与访问模型与视图中的时间有关。如果我将这些时间格式化方法移动到帮助器中,以便在当前请求范围内调用它们,它们将正常工作。

谢谢阿杰!你的建议让我越过了我的盲点。

4

1 回答 1

1

好了就到这里了。。

1. 为什么控制器#update 操作中的日期不更新?

我看到两个潜在的问题。看起来你没有再次解析日期。试试这个:

def update
  @meeting = @member.meetings.find(params[:id])
  @meeting.assign_attributes params[:meeting]
  @meeting.send :parse_time
  if @meeting.save
  ...

assign_attributes设置但不保存新值:http ://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_attributes

此外,在您的 parse_time 方法中,您使用此分配:self.date ||=如果已分配,它将始终将 self.date 设置回自身。换句话说,除非它是虚假的,否则您无法更新日期。


2. 为什么时间不是从设定的日期开始计算年份?这在模型和规范中有效,但在通过控制器通过表单提交时无效。

不知道,看起来你使用Chronic#parse正确。


3. 为什么不将时间设置为从表单传入的时区?同样,这些规格通过了,控制器出了什么问题?

尝试调试time_zone并确保它返回的是params[:meeting][:time_zone]. 再一次,慢性病看起来是正确的。

旁注:如果你传递一个无效的字符串,Time#zone=它会报错。比如Time.zone = 'utc'都是坏的。


4. 为什么时间不会在前端显示在他们的时区?

请参阅Time#in_time_zone http://api.rubyonrails.org/classes/Time.html#method-i-in_time_zone并每次都明确命名您的时区。

不确定您是否已经在执行此操作,但请尝试在数据库中显式保存 UTC 时间,然后以本地时间显示。

于 2012-12-20T05:45:48.490 回答