问题
How can we integrate t.integer :missed
with t.text :committed
so that
when a User checks off he
:missed
3:committed
days in a:level
he has to restart the:level
?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?
回答1:
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.
回答2:
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
andhas_many :days, through: :levels #for being able to access Habit.find(*).days
- Level:
belongs_to :user
,belongs_to :habit
andhas_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.
来源:https://stackoverflow.com/questions/28799527/how-to-integrate-missed-days-with-committed-days-in-habits-rb