CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)

From Expertiza_Wiki
Jump to navigation Jump to search

Problem Statement

The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.

Design Goal

  • Enhance Documentation: Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods
  • Reimplement Notification Method: Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, "notification" is a poor name; it should be renamed to a verb that clearly indicates the action being performed
  • Clarify Method Names:
    • Rename reject_suggestion to reject for clarity
    • Rename update_suggestion to update to better reflect its functionality
    • Rename approve_suggestion to approve for clarity
  • Refactor Email Sending Logic: Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code
  • Address DRY Violations: In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability
  • Update Tests: Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality
  • During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor

Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.

Class Diagram

Model associations

In the diagram, the grayed out models are only read by the controller, and never created, updated, or deleted.

Controller and collaborators

Solutions/Details of Changes Made

Enhance Documentation

This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.

Reimplement Notification Method

The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.

The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.

  1. 'approve_suggestion' is now 'approve', and performs the four high-level steps required:
    1. Update the suggestion's status to 'Approved'
    2. Create a topic entry in the database from the suggested topic
    3. Sign the suggester and his team up for the newly created topic
    4. Send out an email informing all teammates that the suggested topic was approved
  2. 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short
  3. 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have
  4. 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved

Clarify Method Names

Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:

Old method name Action New method name
action_allowed? removed -
add_comment no change add_comment
list renamed index
student_view removed -
student_edit removed -
show no change show
update_suggestion renamed update
new removed -
create no change create
submit removed -
send_email renamed send_notice_of_approval!
notification renamed sign_team_up_to_assignment_and_topic!
approve_suggestion merged into approve approve
reject_suggestion renamed reject
suggestion_params removed -
- added destroy
- added create_topic_from_suggestion!

Refactor Email Sending Logic

The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.

In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.

Address DRY Violations

Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.

Update Tests

Currently, this task is being worked on and is not complete yet.

Files Added/Modified

Models:

suggestion.rb (Modified)
Old New
class Suggestion < ApplicationRecord
  validates :title, :description, presence: true
  has_many :suggestion_comments
end
class Suggestion < ApplicationRecord
  has_many :suggestion_comments, dependent: :delete_all

  validates :title, uniqueness: { case_sensitive: false }
  validates :description, presence: true
end
suggestion_comment.rb (Modified)
Old New
class SuggestionComment < ApplicationRecord
  validates :comments, presence: true
  belongs_to :suggestion
end
class SuggestionComment < ApplicationRecord
  belongs_to :suggestion
  belongs_to :user

  validates :comment, presence: true
end
20241029234902_create_suggestions.rb (Added)
class CreateSuggestions < ActiveRecord::Migration[7.0]
  def change
    create_table :suggestions do |t|
      t.string :title
      t.text :description
      t.string :status
      t.boolean :auto_signup
      t.references :assignment, null: false, foreign_key: true
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end
20241029235713_create_suggestion_comments.rb (Added)
class CreateSuggestionComments < ActiveRecord::Migration[7.0]
  def change
    create_table :suggestion_comments do |t|
      t.text :comment
      t.references :suggestion, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.

Controller:

routes.rb (Appended)
resources :suggestions do
  collection do
    post :add_comment
    post :approve
    post :reject
  end
end
suggestions_controller.rb (Reimplemented)
Old New
class SuggestionController < ApplicationController
  include AuthorizationHelper

  def action_allowed?
    case params[:action]
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'
      current_user_has_student_privileges?
    else
      current_user_has_ta_privileges?
    end
  end

  def add_comment
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])
    @suggestion_comment.suggestion_id = params[:id]
    @suggestion_comment.commenter = session[:user].name
    if @suggestion_comment.save
      flash[:notice] = 'Your comment has been successfully added.'
    else
      flash[:error] = 'There was an error in adding your comment.'
    end
    if current_user_has_student_privileges?
      redirect_to action: 'student_view', id: params[:id]
    else
      redirect_to action: 'show', id: params[:id]
    end
  end

  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
  verify method: :post, only: %i[destroy create update],
         redirect_to: { action: :list }

  def list
    @suggestions = Suggestion.where(assignment_id: params[:id])
    @assignment = Assignment.find(params[:id])
  end

  def student_view
    @suggestion = Suggestion.find(params[:id])
  end

  def student_edit
    @suggestion = Suggestion.find(params[:id])
  end

  def show
    @suggestion = Suggestion.find(params[:id])
  end

  def update_suggestion
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],
                                                   description: params[:suggestion][:description],
                                                   signup_preference: params[:suggestion][:signup_preference])
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id
  end

  def new
    @suggestion = Suggestion.new
    session[:assignment_id] = params[:id]
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])
    @assignment = Assignment.find(params[:id])
  end

  def create
    @suggestion = Suggestion.new(suggestion_params)
    @suggestion.assignment_id = session[:assignment_id]
    @assignment = Assignment.find(session[:assignment_id])
    @suggestion.status = 'Initiated'
    @suggestion.unityID = if params[:suggestion_anonymous].nil?
                            session[:user].name
                          else
                            ''
                          end

    if @suggestion.save
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?
    end
    redirect_to action: 'new', id: @suggestion.assignment_id
  end

  def submit
    if !params[:add_comment].nil?
      add_comment
    elsif !params[:approve_suggestion].nil?
      approve_suggestion
    elsif !params[:reject_suggestion].nil?
      reject_suggestion
    end
  end

  # If the user submits a suggestion and gets it approved -> Send email
  # If user submits a suggestion anonymously and it gets approved -> DOES NOT get an email
  def send_email
    proposer = User.find_by(id: @user_id)
    if proposer
      teams_users = TeamsUser.where(team_id: @team_id)
      cc_mail_list = []
      teams_users.each do |teams_user|
        cc_mail_list << User.find(teams_user.user_id).email if teams_user.user_id != proposer.id
      end
      Mailer.suggested_topic_approved_message(
        to: proposer.email,
        cc: cc_mail_list,
        subject: "Suggested topic '#{@suggestion.title}' has been approved",
        body: {
          approved_topic_name: @suggestion.title,
          proposer: proposer.name
        }
      ).deliver_now!
    end
  end

  def notification
    if @suggestion.signup_preference == 'Y'
      if @team_id.nil?
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')
        new_team.create_new_team(@user_id, @signuptopic)
      else
        if @topic_id.nil?
          # clean waitlists
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)
        else
          @signuptopic.private_to = @user_id
          @signuptopic.save
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team
          send_email
        end
      end
    else
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team
      send_email
    end
  end

  def approve_suggestion
    approve
    notification
    redirect_to action: 'show', id: @suggestion
  end

  def reject_suggestion
    @suggestion = Suggestion.find(params[:id])
    if @suggestion.update_attribute('status', 'Rejected')
      flash[:notice] = 'The suggestion has been successfully rejected.'
    else
      flash[:error] = 'An error occurred when rejecting the suggestion.'
    end
    redirect_to action: 'show', id: @suggestion
  end

  private

  def suggestion_params
    params.require(:suggestion).permit(:assignment_id, :title, :description,
                                       :status, :unityID, :signup_preference)
  end

  def approve
    @suggestion = Suggestion.find(params[:id])
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)
    if @user_id
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)
    end
    # After getting topic from user/team, get the suggestion
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)
    # Get success only if the signuptopic object was returned from its class
    if @signuptopic != 'failed'
      flash[:success] = 'The suggestion was successfully approved.'
    else
      flash[:error] = 'An error occurred when approving the suggestion.'
    end
  end
end
class Api::V1::SuggestionsController < ApplicationController
  include PrivilegeHelper

  def add_comment
    render json: SuggestionComment.create!(
      comment: params[:comment],
      suggestion_id: params[:id],
      user_id: @current_user.id
    ), status: :ok
  rescue ActiveRecord::RecordInvalid => e
    render json: e.record.errors, status: :unprocessable_entity
  end

  def approve
    if PrivilegeHelper.current_user_has_ta_privileges?
      transaction do
        @suggestion = Suggestion.find(params[:id])
        @suggestion.update_attribute('status', 'Approved')
        create_topic_from_suggestion!
        unless @suggestion.user_id.nil?
          @suggester = User.find(@suggestion.user_id)
          sign_team_up_to_assignment_and_topic!
          send_notice_of_approval!
        end
        render json: @suggestion, status: :ok
      end
    else
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden
    end
  rescue ActiveRecord::RecordNotFound => e
    render json: e, status: :not_found
  rescue ActiveRecord::RecordInvalid => e
    render json: e.record.errors, status: :unprocessable_entity
  end

  def create
    render json: Suggestion.create!(
      title: params[:title],
      description: params[:description],
      status: 'Initialized',
      auto_signup: params[:auto_signup],
      assignment_id: params[:assignment_id],
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id
    ), status: :ok
  rescue ActiveRecord::RecordInvalid => e
    render json: e.record.errors, status: :unprocessable_entity
  end

  def destroy
    if PrivilegeHelper.current_user_has_ta_privileges?
      Suggestion.find(params[:id]).destroy!
      render json: {}, status: :ok
    else
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden
    end
  rescue ActiveRecord::RecordNotFound => e
    render json: e, status: :not_found
  rescue ActiveRecord::RecordNotDestroyed => e
    render json: e, status: :unprocessable_entity
  end

  def index
    if PrivilegeHelper.current_user_has_ta_privileges?
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok
    else
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden
    end
  end

  def reject
    if PrivilegeHelper.current_user_has_ta_privileges?
      suggestion = Suggestion.find(params[:id])
      if suggestion.status == 'Initialized'
        suggestion.update_attribute('status', 'Rejected')
        render json: suggestion, status: :ok
      else
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity
      end
    else
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden
    end
  rescue ActiveRecord::RecordNotFound => e
    render json: e, status: :not_found
  end

  def show
    @suggestion = Suggestion.find(params[:id])
    puts @suggestion.user_id
    puts @current_user.id
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?
      render json: {
        suggestion: @suggestion,
        comments: SuggestionComment.where(suggestion_id: params[:id])
      }, status: :ok
    else
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden
    end
  rescue ActiveRecord::RecordNotFound => e
    render json: e, status: :not_found
  end

  private

  def create_topic_from_suggestion!
    @signuptopic = SignUpTopic.create!(
      topic_identifier: "S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}",
      topic_name: @suggestion.title,
      assignment_id: @suggestion.assignment_id,
      max_choosers: 1
    )
  end

  def send_notice_of_approval!
    Mailer.send_topic_approved_message(
      to: @suggester.email,
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&:email),
      subject: "Suggested topic '#{@suggestion.title}' has been approved",
      body: {
        approved_topic_name: @suggestion.title,
        suggester: @suggester.name
      }
    )
  end

  def sign_team_up_to_assignment_and_topic!
    return unless @suggestion.auto_signup == true

    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)
                .where(teams_user: { user_id: @suggester.id }).first
    if @team.nil?
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)
    end
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)
    end
    @signuptopic.update_attribute(:private_to, @suggester.id)
  end
end

The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:

  • Responses are in JSON instead of HTML
  • Methods have proper error handling
  • Public method names have been changed to standard Rails controller methods or method naming conventions
  • Private method names describe their public side effects
  • 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation
  • 'submit' method removed entirely as it is a "do-this-or-that" method; instead, the sub-actions are called directly
  • The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries
  • Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method

Helpers:

mailer.rb (Added)
class Mailer < ActionMailer::Base
  default from: 'expertiza.mailer@gmail.com'

  def send_topic_approved_message(defn)
    @body = defn[:body]
    @topic_name = defn[:body][:approved_topic_name]
    @proposer = defn[:body][:proposer]

    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!
  end
end
privilege_helper.rb (Copied)
Old (authorization_helper.rb) New (privilege_helper.rb)
module AuthorizationHelper
  # Notes:
  # We use session directly instead of current_role_name and the like
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb

  # PUBLIC METHODS

  # Determine if the currently logged-in user has the privileges of a Super-Admin
  def current_user_has_super_admin_privileges?
    current_user_has_privileges_of?('Super-Administrator')
  end

  # Determine if the currently logged-in user has the privileges of an Admin (or higher)
  def current_user_has_admin_privileges?
    current_user_has_privileges_of?('Administrator')
  end

  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)
  def current_user_has_instructor_privileges?
    current_user_has_privileges_of?('Instructor')
  end

  # Determine if the currently logged-in user has the privileges of a TA (or higher)
  def current_user_has_ta_privileges?
    current_user_has_privileges_of?('Teaching Assistant')
  end

  # Determine if the currently logged-in user has the privileges of a Student (or higher)
  def current_user_has_student_privileges?
    current_user_has_privileges_of?('Student')
  end

# ...

  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)
  # Let the Role model define this logic for the sake of DRY
  # If there is no currently logged-in user simply return false
  def current_user_has_privileges_of?(role_name)
    current_user_and_role_exist? && session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))
  end

# ...

  def current_user_and_role_exist?
    user_logged_in? && !session[:user].role.nil?
  end
end
module PrivilegeHelper
  # Determine if the currently logged-in user has the privileges of a Super-Admin
  def self.current_user_has_super_admin_privileges?
    current_user_has_privileges_of?('Super-Administrator')
  end

  # Determine if the currently logged-in user has the privileges of an Admin (or higher)
  def self.current_user_has_admin_privileges?
    current_user_has_privileges_of?('Administrator')
  end

  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)
  def self.current_user_has_instructor_privileges?
    current_user_has_privileges_of?('Instructor')
  end

  # Determine if the currently logged-in user has the privileges of a TA (or higher)
  def self.current_user_has_ta_privileges?
    current_user_has_privileges_of?('Teaching Assistant')
  end

  # Determine if the currently logged-in user has the privileges of a Student (or higher)
  def self.current_user_has_student_privileges?
    current_user_has_privileges_of?('Student')
  end

  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)
  # Let the Role model define this logic for the sake of DRY
  # If there is no currently logged-in user simply return false
  def self.current_user_has_privileges_of?(role_name)
    current_user_and_role_exist? && @current_user.role.all_privileges_of?(Role.find_by(name: role_name))
  end

  def self.current_user_and_role_exist?
    !@current_user.nil? && !@current_user.role.nil?
  end
end

The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:

  • The mailer helper, which is responsible for sending emails
  • The privilege helper, which helps determine the permissions level of the current user

Controller spec:

The controller spec file is part of the test plan and explained in the next section of this document.

Test Plan

Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.

Detailed Test Plan

1. Method: show

* Only shows when user is authorized to view the specified suggestion
* Does not show when user is unauthorized

2. Method: add_comment

* adds a comment and then returns showing said comment
* returns an error when comment creation fails

3. Method: approve

* approves the suggestion and shows updated suggestion if user is authorized to do so
* returns a forbidden error when user is unauthorized to approve suggestions

4. Method: reject

* rejects the suggestion and returns to suggestion when user is authorized
* returns a forbidden error when user is unauthorized to reject suggestions

5. Method: edit

* edits suggestion and shows updated suggestion when authorized to edit the suggestion
* returns forbidden error when user is authorized

6. Method: create

* creates suggestion if given suggestion data is valid, otherwise return an error

7. Method: destroy

* deletes the suggestion if user has the permissions to delete suggestions

8. Method: index

* show all suggestions if user has the privileges otherwise returns forbidden error

Current Spec file

suggestions_controller_spec.rb (Modified)
require 'swagger_helper'
require 'rails_helper'

def auth_token_header(user)
  post '/login', params: { user_name: user.name, password: 'password' }
  { Authorization: "Bearer #{JSON.parse(response.body)['token']}" }
end

RSpec.describe 'Suggestions API', type: :request do
  let(:instructor) { instance_double(User, id: 6, role: 'instructor') }
  let(:student) { instance_double(User, id: 1, name: 'student_user', role: 'student', password: 'password') }
  let(:assignment) { instance_double(Assignment, id: 1, instructor:) }
  let(:suggestion) do
    instance_double(Suggestion, id: 1, assignment_id: assignment.id, user_id: student.id, title: 'Test Title')
  end
  let(:suggestion_comment) { instance_double(SuggestionComment) }

  def stub_current_user(user)
    allow_any_instance_of(ApplicationController).to receive(:session).and_return({ user_id: user.id })
  end

  before(:each) do
    allow(Assignment).to receive(:find).with(assignment.id.to_s).and_return(assignment)
    allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion)
    stub_current_user(student)
  end

  describe '#show' do
    context 'when user is authorized' do
      it 'returns suggestion details and comments' do
        student_institution = Institution.create!(name: 'North Carolina State University')
        student_role = Role.create!(name: 'Student')
        student_user = User.create!(name: 'student_user', password: 'password', email: 'example@gmail.com',
                                    full_name: 'Student User', role: student_role, institution: student_institution)
        student_suggestion = Suggestion.create!(title: 'Sample suggestion', description: 'Sample Text',
                                                status: 'Initialized', auto_signup: false, user_id: student_user)

        get "/api/v1/suggestions/#{student_suggestion.id}", headers: auth_token_header(student_user)
        expect(response).to have_http_status(:ok)

        parsed_response = JSON.parse(response.body)
        puts parsed_response
        expect(parsed_response['suggestion']['id']).to eq(student_suggestion.id)
        expect(parsed_response['comments']).to be_an(Array)
      end
    end

    context 'when user is unauthorized' do
      it 'returns a forbidden error' do
        stub_current_user(instance_double(User, id: 2, role: 'student'))

        get "/api/v1/suggestions/#{suggestion.id}"
        expect(response).to have_http_status(:forbidden)

        parsed_response = JSON.parse(response.body)
        expect(parsed_response['error']).to eq('Students can only view their own suggestions.')
      end
    end
  end

  describe '#add_comment' do
    it 'adds a comment and returns the comment JSON' do
      allow(SuggestionComment).to receive(:create!).and_return(suggestion_comment)
      allow(suggestion_comment).to receive(:as_json).and_return({ comment: 'Test comment' })

      stub_current_user(student, 'student', student.role)

      post "/api/v1/suggestions/#{suggestion.id}/comments", params: { comment: 'Test comment' }
      expect(response).to have_http_status(:ok)

      parsed_response = JSON.parse(response.body)
      expect(parsed_response['comment']).to eq('Test comment')
    end

    it 'returns an error when comment creation fails' do
      allow(SuggestionComment).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion_comment))

      stub_current_user(student, 'student', student.role)

      post "/api/v1/suggestions/#{suggestion.id}/comments", params: { comment: '' }
      expect(response).to have_http_status(:unprocessable_entity)

      parsed_response = JSON.parse(response.body)
      expect(parsed_response).to include('errors')
    end
  end

  describe '#approve' do
    context 'when user is authorized' do
      it 'approves the suggestion and returns the updated suggestion JSON' do
        allow(suggestion).to receive(:update_attribute).with('status', 'Approved').and_return(true)

        stub_current_user(instructor, 'instructor', instructor.role)

        post "/api/v1/suggestions/#{suggestion.id}/approve"
        expect(response).to have_http_status(:ok)

        parsed_response = JSON.parse(response.body)
        expect(parsed_response['status']).to eq('Approved')
      end
    end

    context 'when user is unauthorized' do
      it 'returns a forbidden error' do
        stub_current_user(student, 'student', student.role)

        post "/api/v1/suggestions/#{suggestion.id}/approve"
        expect(response).to have_http_status(:forbidden)

        parsed_response = JSON.parse(response.body)
        expect(parsed_response['error']).to eq('Students cannot approve a suggestion.')
      end
    end
  end

  describe '#reject' do
    context 'when user is authorized' do
      it 'rejects the suggestion and returns the updated suggestion JSON' do
        allow(suggestion).to receive(:update_attribute).with('status', 'Rejected').and_return(true)

        stub_current_user(instructor, 'instructor', instructor.role)

        post "/api/v1/suggestions/#{suggestion.id}/reject"
        expect(response).to have_http_status(:ok)

        parsed_response = JSON.parse(response.body)
        expect(parsed_response['status']).to eq('Rejected')
      end
    end

    context 'when user is unauthorized' do
      it 'returns a forbidden error' do
        stub_current_user(student, 'student', student.role)

        post "/api/v1/suggestions/#{suggestion.id}/reject"
        expect(response).to have_http_status(:forbidden)

        parsed_response = JSON.parse(response.body)
        expect(parsed_response['error']).to eq('Students cannot reject a suggestion.')
      end
    end
  end
end

Next Steps

  • Add comments into the code to improve code documentation
  • Update test coverage to cover larger amounts of code
  • Add missing associations to the Suggestion model
  • Add mailer email template

Team

Mentor

  • Piyush Prasad (pprasad3@ncsu.edu)

Members

  • Anthony Spendlove (aspendl@ncsu.edu)
  • Sean McLellan (spmclell@ncsu.edu)
  • Dhananjay Raghu (draghu@ncsu.edu)

References

External Links

See Also