How to integrate :missed days with :committed days

2019-06-03 22:03发布

How can we integrate t.integer :missed with t.text :committed so that

  1. when a User checks off he :missed 3 :committed days in a :level he has to restart the :level?

  2. for each :missed day he checks off, an additional :committed day is added back into the :level so that he must make it up before advancing?

Each habit has 5 levels before "Mastery" is achieved!

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

I'm guessing we would have to distinguish :missed from :missed here depending on what level it is referring to.

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 works perfectly, but right now :missed serves no purpose. Please help me add the appropriate logic to integrate :missed with :committed.

Thank you so much for your time!

UPDATE

@Dimitry_N's answer doesn't achieve either 1) or 2) of this question as much as I've tried to make it work. Maybe you'll have a better luck incorporating his logic. With his answer I also get this error: How to fix level.rb to work with :committed days?

2条回答
祖国的老花朵
2楼-- · 2019-06-03 22:37

I think the program design has to be slightly re-evaluated. I believe that levels and days should be separate models with columns like level and missed (following the concepts of SRP as @dgilperez mentioned in his comment). Thus, we end up with four models: User, Habit, Level and Day, with the following associations:

  • User: has_many :habits, has_many :levels
  • Habit: belongs_to:user, has_many :levels and has_many :days, through: :levels #for being able to access Habit.find(*).days
  • Level: belongs_to :user, belongs_to :habit and has_many :days
  • Day: belongs_to :level, belongs_to :habit

With these associations, you can create a form with nested attributes. There is an 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 %>

And the "magic" happens in the habits_controller, which looks like this:

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

Note the nested strong params, the @habit.evalulate(@user) method, which I'll show below, and the 3.times { @level.days.build } call, which builds the fields for the nested form in your view.

habit.evauate(user) method: This method is called after a new Habit is saved. Attributes are evaluated and ids of missed days and levels get appended to user's missed_days and missed_levels attributes respectively. The logic is a bit clunky since you'll be appending one Array to another, so you can probably come up with something more efficient. Meanwhile:

  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

note the call to level.evaluate, which looks like this:

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

The schema would look like this:

  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

And don't forget to use accepts_nested_attributes_for :levels, :days for the Habit model, and accepts_nested_attributes_for :days User. Here is a git with all my code. Let me know.

查看更多
我想做一个坏孩纸
3楼-- · 2019-06-03 22:42

You should break the question done because this is a lot to ask for in one question. Dimitry_N seemed to be on the right track, but you'll need to add some of your logic to the levels model now. Please chat with me if you want to go over the details.

查看更多
登录 后发表回答