Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E2447. Reimplement Advice Controller #106

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
49a15b0
Adding a lot of initial files and factories needed
bdevine2 Apr 4, 2024
dfece04
Merge pull request #1 from AdrYne01/add_initial_files
AdrYne01 Apr 4, 2024
217445a
These are all the changes to get rspec running.
bdevine2 Apr 10, 2024
eb3b2c7
Merge pull request #8 from AdrYne01/initial_setup_get_running
bdevine2 Apr 12, 2024
a1e26c0
Reimplemented edit advice method
AdrYne01 Apr 14, 2024
538e7c3
Updated edit_advice method
AdrYne01 Apr 15, 2024
91ac933
Merge pull request #9 from AdrYne01/reimplement-edit-advice-method
bdevine2 Apr 15, 2024
8b5db0c
Updated controller save method
bdevine2 Apr 15, 2024
3be59d5
Update save method rspec tests based on test skeleton provided
bdevine2 Apr 15, 2024
8b6de5f
Gem update for rspec testing
bdevine2 Apr 15, 2024
bebf4a1
Final commit with superclass change for controllers and missing erb f…
bdevine2 Apr 15, 2024
3540db1
Merge pull request #10 from AdrYne01/save_advice_refac_and_tests
bdevine2 Apr 16, 2024
e882f7b
Moved the setting of questionnaire into more modern before action wit…
bdevine2 Apr 16, 2024
364c7a2
Merge pull request #11 from AdrYne01/refactor_set_questionnaire
bdevine2 Apr 16, 2024
9a910d4
Added tests for edit_advice method
AdrYne01 Apr 16, 2024
ba4ce13
Deleted duplicate questionnaire helper file
AdrYne01 Apr 17, 2024
ed4e141
updated edit advice method
AdrYne01 Apr 17, 2024
7e62e6a
Merge pull request #12 from AdrYne01/edit_advice_tests
bdevine2 Apr 17, 2024
5db9a88
refactoring invalid_advice and updating tests
aebartl3 Apr 21, 2024
4cbf064
Merge pull request #15 from AdrYne01/refactor_implement_advice
bdevine2 Apr 21, 2024
e21d158
implementing new invalid_advice change
aebartl3 Apr 21, 2024
3439ac8
Merge pull request #16 from AdrYne01/refactor_implement_advice
bdevine2 Apr 21, 2024
2956063
Final updates for edit_advice method
AdrYne01 Apr 24, 2024
b9eaa2b
Added database.yml.example
AdrYne01 Apr 24, 2024
702d0fc
Merge pull request #17 from AdrYne01/final-update-edit-advice
AdrYne01 Apr 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ group :development, :test do
gem 'rswag-specs'
gem 'rubocop'
gem 'simplecov', require: false, group: :test
gem 'rails-controller-testing'
end

group :development do
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ GEM
racc (~> 1.4)
nokogiri (1.15.2-x64-mingw-ucrt)
racc (~> 1.4)
nokogiri (1.15.2-x86_64-linux)
racc (~> 1.4)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
Expand Down Expand Up @@ -242,6 +244,7 @@ GEM
PLATFORMS
aarch64-linux
x64-mingw-ucrt
x86_64-linux

DEPENDENCIES
bcrypt (~> 3.1.7)
Expand Down
105 changes: 105 additions & 0 deletions app/controllers/advice_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
class AdviceController < ApplicationController
before_action :set_questionnaire, only: %i[ edit_advice save_advice ]
# Advice_controller first checks whether current user has TA privileges or not by implementing action_allowed? method. Secondly it sets the number of advices based on score and sort it in descending order. Then it checks four conditions for the advices.
# 1. If number of advices is not equal to given advices
# 2. If the sorted advices is empty
# 3. If first advice score of sorted advices is NOT equal to max score
# 4. If last advice score of sorted advices is NOT equal to min score
# If any of the above condition are True, the edit_advice method calls adjust_advice_size of the QuestionnaireHelper class which adjust the advice sizes accordingly.
# In the end, save_advice method is called which updates and saves the changes in the advices and displays the success/failure message.

include AuthorizationHelper
# If current user is TA then only current user can edit and update the advice
def action_allowed?
current_user_has_ta_privileges?
end

## This method will return true if the advice and its scores is invalid.
# Validates by utilizing the private methods invalid_advice_length? and invalid_advice_scores?
def invalid_advice?(sorted_advice, num_advices, question)
invalid_advice_length?(num_advices, question, sorted_advice) ||
invalid_advice_scores?(sorted_advice)
end

# Modify the advice associated with a questionnaire
# Separate methods were introduced to calculate the number of advices and sort the advices related to the current question attribute
# This is done to adhere to Single Responsibility Principle
def edit_advice
# For each question in a questionnaire, this method adjusts the advice size if the advice size is <,> number of advices or
# the max or min score of the advices does not correspond to the max or min score of questionnaire respectively.
@questionnaire.questions.each do |question|
# if the question is a scored question, store the number of advices corresponding to that question (max_score - min_score), else 0

num_advices = calculate_num_advices(question)

# sorting question advices in descending order by score

sorted_advice = sort_question_advices(question)

# Checks the condition for adjusting the advice size
if invalid_advice?(sorted_advice, num_advices, question)
# The number of advices for this question has changed.
QuestionnaireHelper.adjust_advice_size(@questionnaire, question)
end
end
end



# save the advice for a questionnaire
def save_advice
begin
# checks if advice is present or not
unless params[:advice].nil?
params[:advice].each do |advice_key, advice_params|
# get existing advice to update by key with the passed in advice param
QuestionAdvice.update(advice_key, advice: advice_params.slice(:advice)[:advice])
end
# we made it here so it was saved
flash[:notice] = 'The advice was successfully saved!'
end
rescue ActiveRecord::RecordNotFound
# If record not found, redirects to edit_advice and sends flash
flash[:notice] = 'The advice record was not found and saved!'
end
# regardless of action above redirect to edit and show flash message if one exists
redirect_to action: 'edit_advice', id: params[:id]
end

private

# Common code for set questionnaire
def set_questionnaire
# Stores the questionnaire with given id in URL
@questionnaire = Questionnaire.find(params[:id])
end

## Checks to see if the advice is the correct length.
# Checks to see if the number of advices is different than the question_advices or advice is empty
def invalid_advice_length?(num_advices, question, sorted_advice)
question.question_advices.length != num_advices ||
sorted_advice.empty?
end

## Checks to see if the scores are valid
# Checks to see if the first and last index of the sorted_advice array are different than expected.
def invalid_advice_scores?(sorted_advice)
sorted_advice[0].score != @questionnaire.max_question_score ||
sorted_advice[sorted_advice.length - 1].score != @questionnaire.min_question_score
end

# Function to calculate number of advices for the current question attribute based on max and min question score.
# Method name is consistent with the functionality
# Refactored the 'if' statement into a ternary operator. This accomplishes the same logic in a more concise manner.
def calculate_num_advices(question)
question.is_a?(ScoredQuestion) ? @questionnaire.max_question_score - @questionnaire.min_question_score + 1 : 0
end


# Function to sort question advices related to the current question attribute
# While sorting questions, sort_by(&:score) is used instead of using a block. It is a shorthand notation and avoids creating a new Proc object for every element in the collection of the questions.
def sort_question_advices(question)
question.question_advices.sort_by(&:score).reverse
end

end
15 changes: 13 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
class ApplicationController < ActionController::API
include JwtToken
class ApplicationController < ActionController::Base
#include JwtToken
def current_user
@current_user ||= session[:user]
end

def current_role_name
current_role.try :name
end

def current_role
current_user.try :role
end
end
36 changes: 36 additions & 0 deletions app/helpers/authorization_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 TA (or higher)
def current_user_has_ta_privileges?
current_user_has_privileges_of?('Teaching Assistant')
end

# Determine if there is a current user
# The application controller method session[:user]
# will return a user even if session[:user] has been explicitly cleared out
# because it is "sticky" in that it uses "@session[:user] ||= session[:user]"
# So, this method can be used to answer a controller's question
# "is anyone CURRENTLY logged in"
def user_logged_in?
!session[:user].nil?
end

# PRIVATE METHODS
private

# 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.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
189 changes: 189 additions & 0 deletions app/models/criterion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Initial commit
class Criterion < ScoredQuestion
include ActionView::Helpers
validates :size, presence: true

# This method returns what to display if an instructor (etc.) is creating or editing a questionnaire (questionnaires_controller.rb)
def edit(_count)
html = '<td align="center"><a rel="nofollow" data-method="delete" href="/questions/' + id.to_s + '">Remove</a></td>'

html += '<td><input size="6" value="' + seq.to_s + '" name="question[' + id.to_s + '][seq]"'
html += ' id="question_' + id.to_s + '_seq" type="text"></td>'

html += '<td><textarea cols="50" rows="1" name="question[' + id.to_s + '][txt]"'
html += ' id="question_' + id.to_s + '_txt" placeholder="Edit question content here">' + txt + '</textarea></td>'

html += '<td><input size="10" disabled="disabled" value="' + type + '" name="question[' + id.to_s + '][type]"'
html += ' id="question_' + id.to_s + '_type" type="text"></td>'

html += '<td><input size="2" value="' + weight.to_s
html += '" name="question[' + id.to_s + '][weight]" id="question_' + id.to_s + '_weight" type="text"></td>'

html += '<td>text area size <input size="3" value="' + size.to_s
html += '" name="question[' + id.to_s + '][size]" id="question_' + id.to_s + '_size" type="text"></td>'

html += '<td> min_label <input size="12" value="' + min_label.to_s
html += '" name="question[' + id.to_s + '][min_label]" id="question_' + id.to_s + '_min_label" type="text"> max_label <input size="10" value="' + max_label.to_s + '" name="question[' + id.to_s
html += '][max_label]" id="question_' + id.to_s + '_max_label" type="text"></td>'

safe_join(['<tr>'.html_safe, '</tr>'.html_safe], html.html_safe)
end

# This method returns what to display if an instructor (etc.) is viewing a questionnaire
def view_question_text
html = '<TD align="left"> ' + txt + ' </TD>'
html += '<TD align="left">' + type + '</TD>'
html += '<td align="center">' + weight.to_s + '</TD>'
questionnaire = self.questionnaire
if !max_label.nil? && !min_label.nil?
html += '<TD align="center"> (' + min_label + ') ' + questionnaire.min_question_score.to_s
html += ' to ' + questionnaire.max_question_score.to_s + ' (' + max_label + ')</TD>'
else
html += '<TD align="center">' + questionnaire.min_question_score.to_s + ' to ' + questionnaire.max_question_score.to_s + '</TD>'
end
safe_join(['<TR>'.html_safe, '</TR>'.html_safe], html.html_safe)
end

# Reduced the number of lines. Removed some redundant if-else statements, and combined some HTML concatenations.
# Display for the students when they are filling the questionnaire
def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale)
html = '<div><label for="responses_' + count.to_s + '">' + txt + '</label></div>'
question_advices = QuestionAdvice.where(question_id: id).sort_by(&:id)
advice_total_length = 0
question_advices.each do |question_advice|
advice_total_length += question_advice.advice.length if question_advice.advice && question_advice.advice != ''
end
# show advice given for different questions
html += advices_criterion_question(count, question_advices) if !question_advices.empty? && (advice_total_length > 0)
# dropdown options to rate a project based on the question
html += dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) if dropdown_or_scale == 'dropdown'
# scale options to rate a project based on the question
html += scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) if dropdown_or_scale == 'scale'
safe_join([''.html_safe, ''.html_safe], html.html_safe)
end

# show advice for each criterion question
def advices_criterion_question(count, question_advices)
html = '<a id="showAdvice_' + id.to_s + '" onclick="showAdvice(' + id.to_s + ')">Show advice</a><script>'
html += 'function showAdvice(i){var element = document.getElementById("showAdvice_" + i.toString());'
html += 'var show = element.innerHTML == "Hide advice";'
html += 'if (show){element.innerHTML="Show advice";} else{element.innerHTML="Hide advice";}toggleAdvice(i);}'
html += 'function toggleAdvice(i) {var elem = document.getElementById(i.toString() + "_myDiv");'
html += 'if (elem.style.display == "none") {elem.style.display = "";} else {elem.style.display = "none";}}</script>'
html += '<div id="' + id.to_s + '_myDiv" style="display: none;">'
# [2015-10-26] Zhewei:
# best to order advices high to low, e.g., 5 to 1
# each level used to be a link;
# clicking on the link caused the dropbox to be filled in with the corresponding number
question_advices.reverse.each_with_index do |question_advice, index|
html += '<a id="changeScore_>' + id.to_s + '" onclick="changeScore(' + count.to_s + ',' + index.to_s + ')">'
html += (questionnaire.max_question_score - index).to_s + ' - ' + question_advice.advice + '</a><br/><script>'
html += 'function changeScore(i, j) {var elem = jQuery("#responses_" + i.toString() + "_score");'
html += 'var opts = elem.children("option").length;'
html += 'elem.val((' + questionnaire.max_question_score.to_s + ' - j).toString());}</script>'
end
html += '</div>'
end

# dropdown options to rate a project based on the question
def dropdown_criterion_question(count, answer = nil, questionnaire_min, questionnaire_max)
current_value = ''
current_value += 'data-current-rating =' + answer.answer.to_s unless answer.nil?
html = '<div><select id="responses_' + count.to_s + '_score" name="responses[' + count.to_s + '][score]" class="review-rating" ' + current_value + '>'
html += "<option value = ''>--</option>"
questionnaire_min.upto(questionnaire_max).each do |j|
html += '<option value=' + j.to_s
html += ' selected="selected"' if !answer.nil? && j == answer.answer
html += '>' + j.to_s
html += '-' + min_label if min_label.present? && j == questionnaire_min
html += '-' + max_label if max_label.present? && j == questionnaire_max
html += '</option>'
end

html += '</select></div><br><br><textarea' + ' id="responses_' + count.to_s + '_comments"'
html += ' name="responses[' + count.to_s + '][comment]" class="tinymce">'
html += answer.comments if !answer.nil? && !answer.comments.nil?
html += '</textarea></td>'
end

# scale options to rate a project based on the question
def scale_criterion_question(count, answer = nil, questionnaire_min, questionnaire_max)
if size.nil? || size.blank?
cols = '70'
rows = '1'
else
cols = size.split(',')[0]
rows = size.split(',')[1]
end
html = '<input id="responses_' + count.to_s + '_score" name="responses[' + count.to_s + '][score]" type="hidden"'
html += 'value="' + answer.answer.to_s + '"' unless answer.nil?
html += '><table><tr><td width="10%"></td>'

(questionnaire_min..questionnaire_max).each do |j|
html += '<td width="10%"><label>' + j.to_s + '</label></td>'
end

html += '<td width="10%"></td></tr><tr><td width="10%">'
html += min_label unless min_label.nil?
html += '</td>'

(questionnaire_min..questionnaire_max).each do |j|
html += '<td width="10%"><input type="radio" id="' + j.to_s + '" value="' + j.to_s + '" name="Radio_' + id.to_s + '"'
html += 'checked="checked"' if (!answer.nil? && answer.answer == j) || (answer.nil? && questionnaire_min == j)
html += '></td>'
end
html += '<script>jQuery("input[name=Radio_' + id.to_s + ']:radio").change(function() {'
html += 'var response_score = jQuery("#responses_' + count.to_s + '_score");'
html += 'var checked_value = jQuery("input[name=Radio_' + id.to_s + ']:checked").val();'
html += 'response_score.val(checked_value);});</script><td width="10%">'
html += max_label unless max_label.nil?
html += '</td><td width="10%"></td></tr></table>'
html += '<textarea cols=' + cols + ' rows=' + rows + ' id="responses_' + count.to_s + '_comments"' \
' name="responses[' + count.to_s + '][comment]" class="tinymce">'
html += answer.comments if !answer.nil? && !answer.comments.nil?
html += '</textarea>'
end

# This method returns what to display if a student is viewing a filled-out questionnaire
def view_completed_question(count, answer, questionnaire_max, tag_prompt_deployments = nil, current_user = nil)
html = '<b>' + count.to_s + '. ' + txt + ' [Max points: ' + questionnaire_max.to_s + ']</b>'
score = answer && !answer.answer.nil? ? answer.answer.to_s : '-'
score_percent = score != '-' ? answer.answer * 1.0 / questionnaire_max : 0
score_color = if score_percent > 0.8
'c5'
elsif score_percent > 0.6
'c4'
elsif score_percent > 0.4
'c3'
elsif score_percent > 0.2
'c2'
else
'c1'
end

html += '<table cellpadding="5"><tr><td>'
html += '<div class="' + score_color + '" style="width:30px; height:30px;' \
' border-radius:50%; font-size:15px; color:black; line-height:30px; text-align:center;">'
html += score + '</div></td>'

if answer && !answer.comments.nil?
html += '<td style="padding-left:10px"><br>' + answer.comments.html_safe + '</td>'
#### start code to show tag prompts ####
if !tag_prompt_deployments.nil? && tag_prompt_deployments.count > 0
# show check boxes for answer tagging
question = Question.find(answer.question_id)
html += '<tr><td colspan="2">'
tag_prompt_deployments.each do |tag_dep|
tag_prompt = TagPrompt.find(tag_dep.tag_prompt_id)
if tag_dep.question_type == question.type && answer.comments.length > tag_dep.answer_length_threshold.to_i
html += tag_prompt.html_control(tag_dep, answer, current_user)
end
end
html += '</td></tr>'
end
#### end code to show tag prompts ####
end
html += '</tr></table>'
safe_join([''.html_safe, ''.html_safe], html.html_safe)
end
end
1 change: 1 addition & 0 deletions app/models/question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Question < ApplicationRecord
before_create :set_seq
belongs_to :questionnaire # each question belongs to a specific questionnaire
has_many :answers, dependent: :destroy
has_many :question_advices, dependent: :destroy # for each question, there is separate advice about each possible score

validates :seq, presence: true, numericality: true # sequence must be numeric
validates :txt, length: { minimum: 0, allow_nil: false, message: "can't be nil" } # user must define text content for a question
Expand Down
Loading