-
Notifications
You must be signed in to change notification settings - Fork 194
Pipeline for report generation #340
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 all commits
6f32448
533cab0
8fd2d66
b9332a6
3e2529e
49871f8
b8f3ebc
a118c02
06e3dc9
36d78be
ea432c5
94cac89
1d96437
eeaa067
10a6a4c
52a6acb
f270833
4871ec9
ec2614a
0e5a061
afb74c5
bbeb1f1
acdbeb3
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,39 @@ | ||||||||||||
| # frozen_string_literal: true | ||||||||||||
|
|
||||||||||||
| class ReportsController < ApplicationController | ||||||||||||
| REPORT_CLASSES = { | ||||||||||||
| 'review_response_map' => Reports::ReviewReport, | ||||||||||||
| 'feedback_response_map' => Reports::FeedbackReport, | ||||||||||||
| 'teammate_review_response_map' => Reports::TeammateReviewReport, | ||||||||||||
| 'bookmark_rating_response_map' => Reports::BookmarkRatingReport, | ||||||||||||
| 'answer_tagging' => Reports::AnswerTaggingReport, | ||||||||||||
| 'basic' => Reports::BasicReport | ||||||||||||
| }.freeze | ||||||||||||
|
|
||||||||||||
| # Only TAs, instructors, and admins may view reports. | ||||||||||||
| def action_allowed? | ||||||||||||
| current_user_has_ta_privileges? | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| # POST /reports/fetch_response_report | ||||||||||||
| # Returns the requested report as JSON. | ||||||||||||
| def fetch_response_report | ||||||||||||
| assignment_id = params[:assignment_id] | ||||||||||||
| type = params[:type] || 'basic' | ||||||||||||
|
|
||||||||||||
| report_class = REPORT_CLASSES[type] | ||||||||||||
| unless report_class | ||||||||||||
| return render json: { | ||||||||||||
| error: "Unknown report type: #{type}. Valid types: #{REPORT_CLASSES.keys.join(', ')}" | ||||||||||||
| }, status: :unprocessable_entity | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| assignment = Assignment.find(assignment_id) | ||||||||||||
| data = report_class.for_assignment(assignment).run | ||||||||||||
| render json: { type: type, assignment_id: assignment_id.to_i }.merge(data) | ||||||||||||
| rescue ActiveRecord::RecordNotFound | ||||||||||||
| render json: { error: 'Assignment not found' }, status: :not_found | ||||||||||||
| rescue StandardError => e | ||||||||||||
| render json: { error: e.message }, status: :internal_server_error | ||||||||||||
|
Comment on lines
+36
to
+37
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. Do not return raw exception messages from the API. Line 36 exposes Proposed fix rescue ActiveRecord::RecordNotFound
render json: { error: 'Assignment not found' }, status: :not_found
rescue StandardError => e
- render json: { error: e.message }, status: :internal_server_error
+ Rails.logger.error("[ReportsController#response_report] #{e.class}: #{e.message}")
+ render json: { error: 'Internal server error' }, status: :internal_server_error
end📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| end | ||||||||||||
| end | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Report logic has been moved to app/services/reports/. | ||
| # This module is retained as a namespace placeholder for any future | ||
| # view-layer formatting helpers. | ||
| module ReportFormatterHelper | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class AnswerTag < ApplicationRecord | ||
| belongs_to :answer | ||
| belongs_to :tag_prompt_deployment | ||
| belongs_to :user | ||
|
|
||
| scope :for_deployment, ->(deployment_id) { where(tag_prompt_deployment_id: deployment_id) } | ||
|
|
||
| validates :answer_id, presence: true | ||
| validates :tag_prompt_deployment_id, presence: true | ||
| validates :user_id, presence: true | ||
| validates :value, presence: true | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,233 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # frozen_string_literal: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module Reports | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Answer-tagging report: shows per-user tagging progress for each | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # TagPromptDeployment on the assignment, plus a cross-deployment summary. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Output shape: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # questionnaire_tagging_report: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # <deployment_id> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # questionnaire_name:, prompt:, question_type:, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # answer_length_threshold:, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # user_stats: [{ user_id:, name:, full_name:, percentage:, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # cnt_tagged:, cnt_not_tagged:, cnt_taggable:, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # tag_update_intervals: }] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # user_tagging_report: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # <user_id> => { user_id:, name:, full_name:, percentage:, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # cnt_tagged:, cnt_not_tagged:, cnt_taggable: } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class AnswerTaggingReport | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def self.for_assignment(assignment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new(assignment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def initialize(assignment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @reportable = assignment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| per_deployment = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The coordinator runs one DeploymentReducer per TagPromptDeployment, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # then passes the results to UserSummaryReducer for cross-deployment totals. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TagPromptDeployment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .where(assignment_id: @reportable.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .includes(:tag_prompt, :questionnaire) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .each do |deployment| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| per_deployment[deployment.id] = DeploymentReducer.new(@reportable, deployment).run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_summary = UserSummaryReducer.new(per_deployment).run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaire_tagging_report: per_deployment, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_tagging_report: user_summary | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Coordinator — runs two streaming reducers for one TagPromptDeployment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # and merges their results: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # TaggableAnswersReducer — streams taggable Answer rows (joined with | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # response_maps, filtered by item type and threshold). Returns per-user | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # lists of taggable answer_ids. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Output: { user_id => [answer_ids] } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # TaggingStatsReducer — streams all AnswerTag rows for this deployment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # (joined with answers to get answer_id). No item/threshold filtering | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # in SQL — filtering happens in finalize by comparing answer_ids. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Output: { user_id => [{ answer_id:, response_id:, updated_at: }] } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Precomputed once, shared across both reducers: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # @item_ids — IDs of items in the deployment's questionnaire (tiny) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # @users_by_team — TeamsUser records grouped by team_id; also used in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # finalize to build per-user name info | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DeploymentReducer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def initialize(reportable, deployment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @reportable = reportable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @deployment = deployment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @item_ids = Item.for_questionnaire_and_type(deployment.questionnaire_id, deployment.question_type).pluck(:id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @users_by_team = TeamsUser.for_assignment(deployment.assignment_id).includes(:user).group_by(&:team_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| taggable_data = TaggableAnswersReducer.new(@reportable, @deployment, @item_ids, @users_by_team).run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagging_stats = TaggingStatsReducer.new(@reportable, @deployment).run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finalize(taggable_data, tagging_stats) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def finalize(taggable_data, tagging_stats) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_info = @users_by_team.values.flatten.each_with_object({}) do |teams_user, info| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info[teams_user.user_id] = { name: teams_user.user.name, full_name: teams_user.user.full_name } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_stats = user_info.map do |user_id, info| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| taggable_answer_ids = taggable_data.fetch(user_id, []).to_set | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cnt_taggable = taggable_answer_ids.size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Filter tags to only those on answers that are taggable for this user. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # TODO: confirm with prof — if a reviewer submits multiple responses for the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # same round, only the latest submitted response should be counted as taggable. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # TaggableAnswersReducer may need to deduplicate by keeping only the most | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # recently submitted response per (reviewer, round). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| matching_tags = tagging_stats.fetch(user_id, []).select { |tag| taggable_answer_ids.include?(tag[:answer_id]) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cnt_tagged = matching_tags.size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cnt_not_tagged = cnt_taggable - cnt_tagged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tag_times_in_order = matching_tags.map { |tag| tag[:updated_at] }.sort | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| intervals_between_tags = tag_times_in_order.each_cons(2).map { |earlier, later| later - earlier } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id: user_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: info[:name], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_name: info[:full_name], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| percentage: cnt_taggable.zero? ? '0.0' : format('%.1f', cnt_tagged.to_f / cnt_taggable * 100), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cnt_tagged: cnt_tagged, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cnt_not_tagged: cnt_not_tagged, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+98
to
+111
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. Count unique tagged answers, not raw tag rows. Line 99 currently counts tag records, so repeated tags on the same answer can inflate Proposed fix- matching_tags = tagging_stats.fetch(user_id, []).select { |tag| taggable_answer_ids.include?(tag[:answer_id]) }
- cnt_tagged = matching_tags.size
- cnt_not_tagged = cnt_taggable - cnt_tagged
+ matching_tags = tagging_stats.fetch(user_id, []).select do |tag|
+ taggable_answer_ids.include?(tag[:answer_id])
+ end
+ cnt_tagged_answers = matching_tags.map { |tag| tag[:answer_id] }.uniq
+ cnt_tagged = cnt_tagged_answers.size
+ cnt_not_tagged = cnt_taggable - cnt_taggedAs per coding guidelines, "app/models/**/*.rb: Focus on ... data integrity, and query efficiency." 📝 Committable suggestion
Suggested change
🧰 Tools🪛 RuboCop (1.86.2)[convention] 98-98: Line is too long. [122/120] (Layout/LineLength) [convention] 106-106: Align the keys of a hash literal if they span more than one line. (Layout/HashAlignment) [convention] 107-107: Align the keys of a hash literal if they span more than one line. (Layout/HashAlignment) [convention] 108-108: Align the keys of a hash literal if they span more than one line. (Layout/HashAlignment) [convention] 109-109: Align the keys of a hash literal if they span more than one line. (Layout/HashAlignment) [convention] 110-110: Align the keys of a hash literal if they span more than one line. (Layout/HashAlignment) [convention] 111-111: Align the keys of a hash literal if they span more than one line. (Layout/HashAlignment) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cnt_taggable: cnt_taggable, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tag_update_intervals: intervals_between_tags | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| questionnaire_name: @deployment.questionnaire.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prompt: @deployment.tag_prompt.prompt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| question_type: @deployment.question_type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| answer_length_threshold: @deployment.answer_length_threshold, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_stats: user_stats | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ----------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Reducer 1 — per-user taggable answer IDs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Streams taggable Answer rows (joined with responses and response_maps, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # filtered by item type and length threshold). Each row is one (answer, team) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # pair — answers.id is unique per row so find_each paginates correctly. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # For each row, adds the answer_id to all users of the team. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Output: { user_id => [answer_ids] } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ----------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class TaggableAnswersReducer < BaseReport | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def initialize(reportable, deployment, item_ids, users_by_team) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(reportable) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @deployment = deployment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @item_ids = item_ids | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @users_by_team = users_by_team | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def source | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Answer.none if @item_ids.empty? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Answer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .taggable_for_assignment( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @deployment.assignment_id, @item_ids, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'ReviewResponseMap', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| threshold: @deployment.answer_length_threshold | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select('answers.id, response_maps.reviewee_id as team_id') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def state_key_for = ->(answer) { answer.team_id } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def initial_state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Hash.new { |state, user_id| state[user_id] = [] } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def accumulate(state, team_id, answer) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (@users_by_team[team_id] || []).each do |teams_user| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state[teams_user.user_id] << answer.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ----------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Reducer 2 — per-user answer tags with answer context. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Streams all AnswerTag rows for this deployment (joined with answers). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # No item or threshold filtering in SQL — the finalize step filters tags | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # by comparing their answer_id against the taggable answer_ids from | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Reducer 1. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Output: { user_id => [{ answer_id:, response_id:, updated_at: }] } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ----------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class TaggingStatsReducer < BaseReport | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def initialize(reportable, deployment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(reportable) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @deployment = deployment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def source | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AnswerTag | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .for_deployment(@deployment.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .joins(:answer) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select('answer_tags.id, answer_tags.user_id, answer_tags.answer_id, answer_tags.updated_at, answers.response_id') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def state_key_for = ->(tag) { tag.user_id } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def initial_state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Hash.new { |state, user_id| state[user_id] = [] } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def accumulate(state, user_id, tag) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state[user_id] << { answer_id: tag.answer_id, response_id: tag.response_id, updated_at: tag.updated_at } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Reducer 3 — cross-deployment per-user summary. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Consumes DeploymentReducer output; no additional DB queries. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class UserSummaryReducer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def initialize(per_deployment_result) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @per_deployment = per_deployment_result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @per_deployment.each_value do |deployment_data| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deployment_data[:user_stats].each do |stat| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key = stat[:user_id] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if summary.key?(key) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry = summary[key] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry[:cnt_tagged] += stat[:cnt_tagged] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry[:cnt_not_tagged] += stat[:cnt_not_tagged] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry[:cnt_taggable] += stat[:cnt_taggable] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry[:percentage] = entry[:cnt_taggable].zero? ? '-' : format('%.1f', entry[:cnt_tagged].to_f / entry[:cnt_taggable] * 100) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary[key] = stat.slice(:user_id, :name, :full_name, :cnt_tagged, :cnt_not_tagged, :cnt_taggable, :percentage) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: expertiza/reimplementation-back-end
Length of output: 286
🏁 Script executed:
Repository: expertiza/reimplementation-back-end
Length of output: 3494
Enforce strong params for
assignment_id/typeand return 400 for missing required inputfetch_response_reportreadsparams[:assignment_id]/params[:type]directly (lines 21-22) and then callsAssignment.find(assignment_id); missing/invalidassignment_idis handled by theActiveRecord::RecordNotFoundrescue (lines 34-35) and returns:not_found(404) instead of a client error (400). Addpermit/requireforassignment_idand rescueActionController::ParameterMissingto return:bad_request.Proposed fix