-
Notifications
You must be signed in to change notification settings - Fork 172
E2550 Response hierarchy and responses_controller back end #227
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
base: main
Are you sure you want to change the base?
Changes from 33 commits
7e593fa
05ce778
21c2f4c
f903ff4
dfba845
a9732e4
1067bc9
a7b4311
ab3abbc
778de56
4c101db
ea4eaae
77138e3
b0aec87
a1c620d
8bb157c
9535b2f
ec5e0ab
e5bb8e4
d268e98
1015d4c
9222eb7
fdc02a1
3178c85
d79fe2a
aff9052
11f4634
b1de8bd
281f33f
02d702d
f49b5d3
fb9b315
afa1c61
005c1ac
391d244
7377636
6de4fe3
8fcb4ae
75bfed7
a166726
921b75e
d0f2c54
d913551
8349a11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class ResponsesController < ApplicationController | ||
| before_action :set_response, only: [:update, :submit, :unsubmit, :destroy] | ||
|
|
||
| # Authorization: determines if current user can perform the action | ||
| def action_allowed? | ||
| case action_name | ||
| when 'create' | ||
| true | ||
| when 'update', 'submit' | ||
| @response = Response.find(params[:id]) | ||
| unless owns_response_or_map? || has_role?('Instructor') || has_role?('Admin') | ||
| render json: { error: 'forbidden' }, status: :forbidden | ||
| end | ||
| when 'unsubmit', 'destroy' | ||
| unless has_role?('Instructor') || has_role?('Admin') | ||
|
||
| render json: { error: 'forbidden' }, status: :forbidden | ||
| end | ||
| else | ||
| render json: { error: 'forbidden' }, status: :forbidden | ||
| end | ||
| true | ||
| end | ||
|
|
||
| # POST /responses | ||
| def create | ||
| @response_map = ResponseMap.find_by(id: params[:response_map_id] || params[:map_id]) | ||
| return render json: { error: 'ResponseMap not found' }, status: :not_found unless @response_map | ||
|
|
||
| @response = Response.new( | ||
| map_id: @response_map.id, | ||
| is_submitted: false, | ||
| created_at: Time.current | ||
| ) | ||
|
|
||
| if @response.save | ||
| render json: { message: 'Response draft created successfully', response: @response }, status: :created | ||
| else | ||
| render json: { error: @response.errors.full_messages.to_sentence }, status: :unprocessable_entity | ||
| end | ||
| end | ||
|
|
||
| # PATCH /responses/:id | ||
| # Reviewer edits existing draft (still unsubmitted) | ||
| def update | ||
| puts "Updating Response ID: #{params[:id]}" | ||
| return render json: { error: 'forbidden' }, status: :forbidden if @response.is_submitted? | ||
|
|
||
| if @response.update(response_params) | ||
| render json: { message: 'Draft updated successfully', response: @response }, status: :ok | ||
| else | ||
| render json: { error: @response.errors.full_messages.to_sentence }, status: :unprocessable_entity | ||
| end | ||
| end | ||
|
|
||
| # PATCH /responses/:id/submit | ||
| # Lock the response and calculate final score | ||
| def submit | ||
| return render json: { error: 'Response not found' }, status: :not_found unless @response | ||
| return render json: { error: 'Response already submitted' }, status: :unprocessable_entity if @response.is_submitted? | ||
|
|
||
| # Check deadline | ||
| return render json: { error: 'Deadline has passed' }, status: :forbidden unless deadline_open?(@response) | ||
|
||
|
|
||
| # Validate rubric completion | ||
| unanswered = @response.scores.select { |a| a.answer.nil? } | ||
|
||
| return render json: { error: 'All rubric items must be answered' }, status: :unprocessable_entity unless unanswered.empty? | ||
|
|
||
| # Lock response | ||
| @response.is_submitted = true | ||
|
|
||
| # Calculate score via ScorableHelper | ||
| total_score = @response.aggregate_questionnaire_score | ||
|
|
||
| if @response.save | ||
| render json: { | ||
| message: 'Response submitted successfully', | ||
| response: @response, | ||
| total_score: total_score | ||
| }, status: :ok | ||
| else | ||
| render json: { error: @response.errors.full_messages.to_sentence }, status: :unprocessable_entity | ||
| end | ||
| end | ||
|
|
||
| # PATCH /responses/:id/unsubmit | ||
| # Instructor/Admin reopens a submitted response | ||
| def unsubmit | ||
| return render json: { error: 'Response not found' }, status: :not_found unless @response | ||
|
|
||
| if @response.is_submitted? | ||
| @response.update(is_submitted: false) | ||
| render json: { message: 'Response reopened for revision', response: @response }, status: :ok | ||
|
||
| else | ||
| render json: { error: 'Response already unsubmitted' }, status: :unprocessable_entity | ||
| end | ||
| end | ||
|
|
||
| # DELETE /responses/:id | ||
| # Instructor/Admin deletes invalid/test response | ||
| def destroy | ||
| return render json: { error: 'Response not found' }, status: :not_found unless @response | ||
|
||
|
|
||
| @response.destroy | ||
| head :no_content | ||
| rescue StandardError => e | ||
| render json: { error: e.message }, status: :unprocessable_entity | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def set_response | ||
| @response = Response.find_by(id: params[:id]) | ||
| end | ||
|
|
||
| def response_params | ||
| params.require(:response).permit( | ||
| :map_id, | ||
| :is_submitted, | ||
| :submitted_at, | ||
| scores_attributes: [:id, :question_id, :answer, :comment] | ||
| ) | ||
| end | ||
|
|
||
| def owns_response_or_map? | ||
|
||
| # Member actions: we have @response from set_response | ||
| return @response.map&.reviewer&.id == current_user.id if @response&.map&.reviewer && current_user | ||
|
|
||
| # Collection actions (create, next_action): check map ownership | ||
| map_id = params[:response_map_id] || params[:map_id] | ||
| return false if map_id.blank? | ||
|
|
||
| map = ResponseMap.find_by(id: map_id) | ||
| return false unless map | ||
|
|
||
| map.reviewer == current_user | ||
| end | ||
|
|
||
| # Returns true if the assignment's due date is in the future or no due date is set | ||
| def deadline_open?(response) | ||
|
||
| assignment = response.respond_to?(:response_map) ? response.response_map&.assignment : nil | ||
| return true if assignment.nil? | ||
| return true if assignment.respond_to?(:due_dates) && assignment.due_dates.nil? | ||
| # if due_date responds to future? use it, otherwise compare to now | ||
| if assignment.respond_to?(:due_dates) && assignment.due_dates.respond_to?(:future?) | ||
| return assignment.due_dates.first.future? | ||
| end | ||
| # fallback: compare | ||
| due = assignment.due_dates | ||
| return true if due.nil? | ||
| puts "Checking deadline: due_at=#{due.inspect}, current_time=#{Time.current}" | ||
| due.first.due_at > Time.current | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,10 +6,43 @@ class Response < ApplicationRecord | |
|
|
||
| belongs_to :response_map, class_name: 'ResponseMap', foreign_key: 'map_id', inverse_of: false | ||
| has_many :scores, class_name: 'Answer', foreign_key: 'response_id', dependent: :destroy, inverse_of: false | ||
| accepts_nested_attributes_for :scores | ||
|
|
||
| alias map response_map | ||
| delegate :questionnaire, :reviewee, :reviewer, to: :map | ||
|
|
||
| # response type to label mapping | ||
| KIND_LABELS = { | ||
|
||
| 'ReviewResponseMap' => 'Review', | ||
| 'TeammateReviewResponseMap' => 'Teammate Review', | ||
| 'BookmarkRatingResponseMap' => 'Bookmark Review', | ||
| 'QuizResponseMap' => 'Quiz', | ||
| 'SurveyResponseMap' => 'Survey', | ||
| 'AssignmentSurveyResponseMap' => 'Assignment Survey', | ||
| 'GlobalSurveyResponseMap' => 'Global Survey', | ||
| 'CourseSurveyResponseMap' => 'Course Survey', | ||
| 'FeedbackResponseMap' => 'Feedback' | ||
| }.freeze | ||
|
|
||
| def kind_name | ||
|
||
| return 'Response' if map.nil? | ||
|
|
||
| klass_name = map.class.name | ||
| # use hash for the mapping first | ||
| if (label = KIND_LABELS[klass_name]).present? | ||
| return label | ||
| end | ||
|
|
||
| # back up plan: use get_title | ||
| if map.respond_to?(:get_title) | ||
| title = map.get_title | ||
| return title if title.present? | ||
| end | ||
|
|
||
| # response type doesn't exist | ||
| 'Unknown Type' | ||
| end | ||
|
|
||
| def reportable_difference? | ||
bestinlalu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| map_class = map.class | ||
| # gets all responses made by a reviewee | ||
|
|
@@ -21,7 +54,7 @@ def reportable_difference? | |
| existing_responses.each do |response| | ||
| unless id == response.id # the current_response is also in existing_responses array | ||
| count += 1 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be in the ReviewAggregator mixin? |
||
| total += response.aggregate_questionnaire_score.to_f / response.maximum_score | ||
| total += response.aggregate_questionnaire_score.to_f / response.maximum_score | ||
|
||
| end | ||
| end | ||
|
|
||
|
|
@@ -35,7 +68,8 @@ def reportable_difference? | |
| score = aggregate_questionnaire_score.to_f / maximum_score | ||
| questionnaire = questionnaire_by_answer(scores.first) | ||
| assignment = map.assignment | ||
| assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id) | ||
| assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, | ||
| questionnaire_id: questionnaire.id) | ||
|
|
||
| # notification_limit can be specified on 'Rubrics' tab on assignment edit page. | ||
| allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disjunction should be replaced by ~ has_privileges_of