Complete course and modules using Rails 5 assign to user
Asked Answered
S

2

6

Edit #2

Here is the courses controller

class CoursesController < ApplicationController
  layout proc { user_signed_in? ? "dashboard" : "application" }

  before_action :set_course, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, except: [:index, :show]
  before_action :authorize_admin, except: [:index, :show, :complete]

  def index
    @courses = Course.all.order(created_at: :asc)
  end

  def show
    course = Course.friendly.find(params[:id])
    @course_modules = course.course_modules.order(created_at: :asc)
  end

  def new
    @course = Course.new
  end

  def edit
  end

  def create
    @course = Course.new(course_params)

    respond_to do |format|
      if @course.save
        format.html { redirect_to courses_path, notice: 'Course was successfully created.' }
        format.json { render :show, status: :created, location: courses_path }
      else
        format.html { render :new }
        format.json { render json: @course.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @course.update(course_params)
        format.html { redirect_to @course, notice: 'Course was successfully updated.' }
        format.json { render :show, status: :ok, location: @course }
      else
        format.html { render :edit }
        format.json { render json: @course.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @course.destroy
    respond_to do |format|
      format.html { redirect_to courses_url, notice: 'Course was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private

  def set_course
    @course = Course.friendly.find(params[:id])
  end

  def course_params
    params.require(:course).permit(:title, :summary, :description, :trailer, :price)
  end
end

Edit #1

So going off Jagdeep's answer below I have now done the following:

course.rb

class Course < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :slugged

  has_many :course_modules

  validates :title, :summary, :description, :trailer, :price, presence: true

  def complete?
    self.update_attribute(:complete, true)
  end
end

course_modules_user.rb

class CourseModulesUser < ApplicationRecord
  belongs_to :course_module
  belongs_to :user

  def complete!
    self.update_attribute(:complete, true)
  end
end

courses_user.rb

class CoursesUser < ApplicationRecord
  belongs_to :course
  belongs_to :user
end

user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  has_one_attached :avatar

  has_many :courses_users
  has_many :courses, through: :courses_users

  has_many :course_modules_users
  has_many :course_modules, through: :course_modules_users

  def mark_course_module_complete!(course_module)
    self.course_modules_users
      .where(course_module_id: course_module.id)
      .first
      .complete!
    end

  def after_confirmation
    welcome_email
    super
  end

  protected

  def welcome_email
    UserMailer.welcome_email(self).deliver
  end
end

Migrations

class CreateCoursesUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :courses_users do |t|
      t.integer :course_id
      t.integer :user_id
      t.boolean :complete

      t.timestamps
    end
  end
end

class CreateCourseModulesUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :course_modules_users do |t|
      t.integer :course_module_id
      t.integer :user_id
      t.boolean :complete

      t.timestamps
    end
  end
end

However, I'm getting errors like this

image

Original Question

So this is a continuation of a previous question, however, this will stray off from the topic of that so here is a new one.

After this, I got roughly what I wanted to get working which is allowing people to mark off modules and make the course complete if all modules are complete. However, upon testing a new user the modules and courses are being marked as complete (obviously a new user isn't going to complete the course on sign-in, nor are any modules going to be complete) so I need for all users to be separate in terms of what is marked as complete and what isn't.

Previously a user by the name of @engineersmnky mentioned HABTM relationship, however, I've not dealt with this previously.

Here is how I have things setup thus far:

user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  has_one_attached :avatar

  has_many :courses

  def after_confirmation
    welcome_email
    super
  end

  protected

  def welcome_email
    UserMailer.welcome_email(self).deliver
  end
end

course.rb

class Course < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :slugged

  has_many :users
  has_many :course_modules

  validates :title, :summary, :description, :trailer, :price, presence: true

  def complete!
    update_attribute(:complete, true)
  end
end

course_module.rb

class CourseModule < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :slugged

  belongs_to :course
  has_many :course_exercises

  validates :title, :course_id, presence: true

  scope :completed, -> { where(complete: true) }
  after_save :update_course, if: :complete?

  private

  def update_course
    course.complete! if course.course_modules.all?(&:complete?)
  end
end

if the course is complete conditional courses/index.html.erb

<% if course.complete? %>
  <%= link_to "Completed", course, class: "block text-lg w-full text-center text-white px-4 py-2 bg-green hover:bg-green-dark border-2 border-green-dark leading-none no-underline" %>
<% else %>
  <%= link_to "View Modules", course, class: "block text-lg w-full text-center text-grey-dark hover:text-darker px-4 py-2 border-2 border-grey leading-none no-underline hover:border-2 hover:border-grey-dark" %>
<% end %>

if course module is complete conditional courses/show.html.erb

<% if course_module.complete? %>
  <i class="fas fa-check text-green float-left mr-1"></i>
  <span class="text-xs mr-2">Completed</span>
<% else %>
  <%= link_to complete_course_module_path(course_module), method: :put do %>
  <i class="fas fa-check text-grey-darkest float-left mr-2"></i>
<% end %>

Databases

Course Modules

database

Courses

courses

Susurration answered 22/8, 2018 at 17:26 Comment(8)
@JagdeepSingh The question is currently when modules and courses are being completed by one user. It's applying to all users, which is incorrect. So is there a way to sort of separate per user if something is complete or not?Susurration
What is the current problem you are facing? Do you still getting the same error?Hypoacidity
@Hypoacidity So I've currently tried to implement Jagdeep's solution so I can have users complete course modules (and only that user), however, whilst implementing it, the completing functionality are now failing and I can no longer complete modules.Susurration
So, did it fail with that error you have mentioned in the question, right?Hypoacidity
Correct @Hypoacidity but I'm sure further errors will follow once this one has been fixed.Susurration
Ok, Can update the question with show action of courses_controller?Hypoacidity
@Hypoacidity Ok I've updated the question.Susurration
You're getting undefined method errors, so you need to define that method in course module model. But that's not really the solution to what you want. The thing is, you really want to decouple the concept of completion from your course and course module models. A course cannot be completed itself, a user must complete a course. This means you need another relationship, as Jagdeep pointed out. So really what you need is a way to check if course module user or course user has completed the course/module, and then check those where applicable.Bookstall
C
1

You will need to create new tables courses_users and course_modules_users to distinguish between courses/course_modules of different users.

Remove field complete from tables courses and course_modules. We don't want to mark a course/course_module as completed globally. See this for how to use migrations to do that.

Further, define has_many :through associations between users and course/course_modules as below:

class User < ApplicationRecord
  has_many :courses_users
  has_many :courses, through: :courses_users

  has_many :course_modules_users
  has_many :course_modules, through: :course_modules_users
end

class Course < ApplicationRecord
  has_many :course_modules
end

class CoursesUser < ApplicationRecord
  # Fields:
  #   :course_id
  #   :user_id
  #   :complete

  belongs_to :course
  belongs_to :user
end

class CourseModule < ApplicationRecord
  belongs_to :course
end

class CourseModulesUser < ApplicationRecord
  # Fields:
  #   :course_module_id
  #   :user_id
  #   :complete

  belongs_to :course_module
  belongs_to :user
end

Now, the queries can be made this way:

Course.all
 => All courses

Course.find(1).course_modules
 => All course modules of a course

user = User.find(1)
course = Course.find(1)

# Assign `course` to `user`
user.courses_users.create(course_id: course.id)

user.courses
 => [course]

course_module = CourseModule.find(1)

# Assign `course_module` to `user`
user.course_modules_users.create(course_module_id: course_module.id)

user.course_modules
 => [course_module]

Now, to mark a course module complete for a user, do this:

class User < ApplicationRecord
  def mark_course_module_complete!(course_module)
    self.course_modules_users
        .where(course_module_id: course_module.id)
        .first
        .complete!
  end
end

class CourseModulesUser < ApplicationRecord
  def complete!
    self.update_attribute(:complete, true)
  end
end

course_module = CourseModule.find(1)

user.mark_course_module_complete!(course_module)

Similarly for courses:

class User < ApplicationRecord
  def mark_course_complete!(course)
    self.courses_users
        .where(course_id: course.id)
        .first
        .complete!
  end
end

class CoursesUser < ApplicationRecord
  def complete!
    self.update_attribute(:complete, true)
  end
end

This should solve your issue of marking courses and course modules as completed on user basis.

There are other things to consider to make it completely functional which i will leave for you to implement e.g. marking a user's course complete automatically when all course modules of a user are complete (Yes, you need to fix that again), marking a user's course incomplete if at least one of its course modules get incompleted, etc.

SO is always open, if you get stuck again.

Cholula answered 23/8, 2018 at 12:26 Comment(4)
just from a naming standpoint I think UserCourse and UserCourseModule might make more sense and associating a User to the UserCourseModules through the UserCourse to make maintaining relational integrity easier.Carniola
I don't know what naming conventions rails uses nowadays, earlier the join table was named alphabetically sorted e.g. aaas_bbbs.Cholula
@JagdeepSingh Ok so I've given this a shot, I've updated the question with the changes I have done so far.Susurration
Hey @JagdeepSingh so I have the database and queries set up in using the rails console. However, how would I set these in the view? So when a user buys the course it assigns the course to the user and same goes when the module is ticked off in the browser.Susurration
H
1

Undefined method complete? for CourseModule:0x000..

I will focus on the error and explain the reason for it. You are calling complete? on a course_module here <% if course_module.complete? %>. But you don't have a method called complete? in the CourseModule model. That explains why the error has triggered.

You should define it in the CourseModule to avoid the error

class CourseModule < ApplicationRecord

 def complete?
   #your logic here
 end
end

Note:

If you are willing to try a different approach, I will recommend you to have a go with enums. enums are very powerful and serves with in-built methods which comes very handy.

For example, you can change the CourseModel to below with enums

class CourseModule < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :slugged

  enum status: [ :completed, :not_completed ]
  belongs_to :course
  has_many :course_exercises
  .......
end

By this you can simply call course_module.completed? which returns true or false based on the status of the course_module. And to update a course_module status as completed, just call course_module.completed!

Hypoacidity answered 12/9, 2018 at 14:20 Comment(9)
If you take a look at Jagdeep's solution, you can see what we are trying to accomplish, but I just need to get the completes to work, I believe I have everything set up correctly in terms of the migrations, models etc.Susurration
@B.J.B My answer is focused on the error. Did the error is fixed? I will try to more details later to help with your ultimate goalHypoacidity
I added the complete? method previously but I'm unsure on where best to place it, I'll add the models in there current form your question, just so there easier to access.Susurration
@B.J.B The best place to put complete? method would be the model.Hypoacidity
Of course but I need it to have it so only completed is showing for an individual user not all users, because the way I had this setup previously it was setting it for all users. That's why I created the new database tables, and associations that Jagdeep suggested.Susurration
Have you considered that model attributes should get an method assigned to them to check if they are present or not? This method is named <attribute_name>? and returns true or false based if they are present or not, with the exception of the value 0 that returns false whereas it would normally evaluate to true. See: api.rubyonrails.org/classes/ActiveRecord/…Verner
@JohanWentholt You are totally true about that. But, if that so, I wonder why the error has raised in the first place.Hypoacidity
@Hypoacidity I'm with you on that. The question has quite a lot of edits. In my opinion the best approach is to create an mcve that we can clone and install, to see the exact working code for ourself.Verner
The error is because he moved the complete property to the course module user and course user models in order to allow for individual completion of modules as per Jagdeep's answer, which is the right thing to do. As I said in my comment above, he needs to check for the course user /course module user models completion to ensure that the rendered completion is user dependent.Bookstall

© 2022 - 2024 — McMap. All rights reserved.