如何在 habits.rb 中将 :missed 天数与 :committed 天数整合?

How to integrate :missed days with :committed days in habits.rb?

我们如何将 t.integer :missedt.text :committed 整合,以便

  1. 当用户在 :level:missed 3 :committed 天勾选后,他必须重新启动 :level?

  2. 他每检查 :missed 天,就会在 :level 中添加额外的 :committed 天,这样他必须在前进之前补上它?

每个习惯在 "Mastery" 达到之前有 5 个级别!

class Habit < ActiveRecord::Base
 belongs_to :user
 before_save :set_level
 acts_as_taggable
 serialize :committed, Array

  def self.comitted_for_today
    today_name = Date::DAYNAMES[Date.today.wday].downcase
    ids = all.select { |h| h.committed.include? today_name }.map(&:id)
    where(id: ids)
  end

 def levels
   committed_wdays = committed.map { |day| Date::DAYNAMES.index(day.titleize) }
   n_days = ((date_started.to_date)..Date.today).count { |date| committed_wdays.include? date.wday }

  case n_days   
   when 0..9
     1
   when 10..24
     2
   when 25..44
     3
   when 45..69
     4
   when 70..99
     5
   else
     "Mastery"
  end
 end

private
 def set_level
  self.level = levels
 end 
end

我猜我们必须在这里区分 :missed:missed,这取决于它所指的级别。

habits/_form.html.erb

  <label> Missed: </label>
  <div>
  <label> Level 1: </label>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  </div>
  <div>
  <label> Level 2: </label>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  </div>
  <div>
  <label> Level 3: </label>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  </div>
  <div>
  <label> Level 4: </label>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  </div>
  <div>
  <label> Level 5: </label>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  <%= f.check_box :missed %>
  </div>

habits_controller.rb

class HabitsController < ApplicationController
  before_action :set_habit, only: [:show, :edit, :update, :destroy]
  before_action :logged_in_user, only: [:create, :destroy]

  def index
    if params[:tag]
      @habits = Habit.tagged_with(params[:tag])
    else
      @habits = Habit.all.order("date_started DESC")
      @habits = current_user.habits
    end
  end
                                              
private
                                              
    def habit_params
      params.require(:habit).permit(:missed, :left, :level, :date_started, :trigger, :target, :positive, :negative, :tag_list, :committed => [])
    end
end

_create_habits.rb

class CreateHabits < ActiveRecord::Migration
  def change
    create_table :habits do |t|
      t.integer :missed
      t.integer :level
      t.text :committed
      t.datetime :date_started
      t.string :trigger
      t.string :target
      t.string :positive
      t.string :negative
      t.references :user, index: true

      t.timestamps null: false
    end
    add_foreign_key :habits, :users
    add_index :habits, [:user_id, :created_at]
  end
end

:committed 完美运行,但现在 :missed 毫无用处。请帮助我添加适当的逻辑以将 :missed:committed.

集成

非常感谢您的宝贵时间!

更新

@Dimitry_N's answer 并没有达到这个问题的 1) 或 2),就像我试图让它工作一样。也许你会更幸运地融入他的逻辑。通过他的回答,我也得到了这个错误:How to fix level.rb to work with :committed days?

我觉得程序设计要稍微重新评价一下。我相信 levelsdays 应该是单独的模型,具有像 levelmissed 这样的列(遵循 SRP 作为@的概念dgilperez 在他的评论中提到)。因此,我们最终得到四个模型:UserHabitLevelDay,具有以下关联:

  • 用户: has_many :habits, has_many :levels
  • 习惯belongs_to:userhas_many :levelshas_many :days, through: :levels #for being able to access Habit.find(*).days
  • 级别belongs_to :userbelongs_to :habithas_many :days
  • : belongs_to :level, belongs_to :habit

通过这些关联,您可以创建具有嵌套属性 的表单。有一个awesome RailCast explaining nested forms

<%= form_for @habit do |habit| %>
  <% 5.times.each_with_index do |number, index| %> 
    <h1>Level <%= index + 1 %></h1>
    <%= habit.fields_for :levels do |level| %>
      <%= level.fields_for :days do |day| %>
        <%= day.label :missed %>
        <%= day.check_box :missed %> <br/>
      <% end %>
    <% end %>
  <% end %>
  <%= habit.submit "submit" %>
<% end %>

"magic" 出现在 habits_controller 中,看起来像这样:

class HabitsController < ApplicationController
  ...
  def new
    @habit = @user.habits.new
    @level = @habit.levels.new
    3.times { @level.days.build }
  end

  def create
    @habit = @user.habits.new(habit_params)
    @levels = @habit.levels

    if @habit.save
      @habit.evaluate(@user) 
      redirect_to ...
    else
      ...
    end
  end

...
  private

  def habit_params 
    params.require(:habit).permit(
      :user_id,
      levels_attributes:[
      :passed,
      days_attributes:[
      :missed,:level_id]])
  end
...  
end

请注意 nested strong params@habit.evalulate(@user) 方法(我将在下面展示)和 3.times { @level.days.build } 调用,它在您的视图中为嵌套表单构建字段。

habit.evauate(用户)方法: 在保存新的 Habit 后调用此方法。评估属性并将错过的天数和级别的 ID 分别附加到用户的 missed_daysmissed_levels 属性。逻辑有点笨拙,因为您会将一个数组附加到另​​一个数组,因此您可能会想出更有效的方法。同时:

  def evaluate(user)
    levels.each { |level| level.evaluate }
    user.missed_levels << levels.where(passed: false).ids 
    user.missed_days << days.where(missed: true).ids 
    user.save
  end

注意对 level.evaluate 的调用,它看起来像这样:

  def evaluate
    if days.where(missed: true ).count == 3
      update_attributes(passed: false)
    else
      update_attributes(passed: true)
    end
  end

架构如下所示:

  create_table "days", force: true do |t|
    t.integer "level_id"
    t.integer "habit_id"
    t.boolean "missed",   default: false
  end

  create_table "habits", force: true do |t|
    ...
    t.integer "user_id"
    ...
  end

  create_table "levels", force: true do |t|
    t.integer "user_id"
    t.integer "habit_id"
    t.boolean "passed",   default: false
  end

  create_table "users", force: true do |t|
    ...
    t.string   "name"
    t.text     "missed_days" #serialize to Array #serialize to Array in model
    t.text     "missed_levels" #serialize to Array in model
    ...
  end

并且不要忘记对 Habit 模型使用 accepts_nested_attributes_for :levels, :days,并且 accepts_nested_attributes_for :days UserHere is a git with all my code. 告诉我。

你应该把问题打断,因为在一个问题中有很多要求。 Dimitry_N 似乎走在了正确的轨道上,但您现在需要将一些逻辑添加到关卡模型中。如果您想详细了解,请与我聊天。