Rails:控制器无法正确更新模型

3

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

简短版:

我有一个Meeting模型,其中包含datestart_timeend_time。这些都是时间对象,用户输入时很麻烦,因此我使用虚拟属性来接受字符串,在保存之前由Chronic解析。

我有一个普通的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,在前端无法显示本地时间。

在更新会议时,日期不会更新。时间会更新,但仍保持在UTC 2000-01-01。

更详细的版本

让我感到非常奇怪的是,我有很好的测试覆盖率,表明所有这些在模型层面上都有效。

以下是该模型:

# 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. Was assigning date wrong, thanks AJ! Now using:

    if @date_string.present?
        self.date = Chronic.parse(@date_string).to_date
    elsif self.date.nil?
        self.date = Date.today
    end
    
  2. I was using Chronic correctly, my mistake was at the database layer! I set the fields in the database to time instead of datetime, which ruins everything. Lesson to anyone reading this: never ever use time as a database field (unless you understand exactly what it does and why you're using it instead of datetime).

  3. Same problem as above, changing the fields to datetime fixed the problem.

  4. The problem here has to do with accessing time in the model vs. the view. If I move these time formatting methods into a helper so they're called in the current request scope they will work correctly.

谢谢AJ!你的建议帮助我突破了盲点。

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设置回自身。换句话说,除非它是falsey,否则无法更新日期。


2. 为什么无法从设置的日期中获取年份?这在模型和规范中有效,但在通过控制器提交表单时无效。

不清楚,看起来您正在正确使用Chronic#parse


3. 为什么从表单传递的时区没有设置时间?这些规格再次通过,控制器出了什么问题?

尝试调试time_zone并确保它返回params[:meeting][:time_zone]中的内容。再次检查Chronic是否正确。

顺便说一句:如果您向Time#zone=传递无效字符串,它将崩溃并显示错误。例如,Time.zone = 'utc'是不好的。


4. 为什么前端不显示时区时间?

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

不确定您是否已经这样做了,但是尝试在数据库中明确保存UTC时间,然后以本地时间显示它们。


非常感谢,这不是完整的答案,但您帮助我发现了一些新的调试方法,我已经解决了它!我已经更新了答案,并附上了一些解决此问题的说明。 - Andrew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接