• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

expertiza / expertiza / #19892

12 Jun 2026 11:44PM UTC coverage: 9.671% (-5.6%) from 15.224%
#19892

push

web-flow
Merge c64ae4198 into b68bb8f64

1574 of 16276 relevant lines covered (9.67%)

0.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

25.47
/app/models/assignment.rb
1
###
2
####
3
#### We have spent a lot of time on refactoring this file, PLEASE consult with Expertiza development team before putting code in.
4
###
5
###
6

7
class Assignment < ApplicationRecord
1✔
8
  require 'analytic/assignment_analytic'
1✔
9
  include Scoring
1✔
10
  include AssignmentAnalytic
1✔
11
  include ReviewAssignment
1✔
12
  include QuizAssignment
1✔
13
  include AssignmentHelper
1✔
14
  has_paper_trail
1✔
15
  # When an assignment is created, it needs to
16
  # be created as an instance of a subclass of the Assignment (model) class;
17
  # then Rails will "automatically' set the type field to the value that
18
  # designates an assignment of the appropriate type.
19
  belongs_to :course
1✔
20
  belongs_to :instructor, class_name: 'User', inverse_of: :assignments
1✔
21
  has_one :assignment_node, foreign_key: 'node_object_id', dependent: :destroy, inverse_of: :assignment
1✔
22
  has_many :participants, class_name: 'AssignmentParticipant', foreign_key: 'parent_id', dependent: :destroy
1✔
23
  has_many :users, through: :participants, inverse_of: :assignment
1✔
24
  has_many :due_dates, class_name: 'AssignmentDueDate', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :assignment
1✔
25
  has_many :teams, class_name: 'AssignmentTeam', foreign_key: 'parent_id', dependent: :destroy, inverse_of: :assignment
1✔
26
  has_many :invitations, class_name: 'Invitation', foreign_key: 'assignment_id', dependent: :destroy # , inverse_of: :assignment
1✔
27
  has_many :assignment_questionnaires, dependent: :destroy
1✔
28
  has_many :questionnaires, through: :assignment_questionnaires
1✔
29
  has_many :sign_up_topics, foreign_key: 'assignment_id', dependent: :destroy, inverse_of: :assignment
1✔
30
  has_many :response_maps, foreign_key: 'reviewed_object_id', dependent: :destroy, inverse_of: :assignment
1✔
31
  has_many :review_mappings, class_name: 'ReviewResponseMap', foreign_key: 'reviewed_object_id', dependent: :destroy, inverse_of: :assignment
1✔
32
  has_many :plagiarism_checker_assignment_submissions, dependent: :destroy
1✔
33
  has_many :assignment_badges, dependent: :destroy
1✔
34
  has_many :badges, through: :assignment_badges
1✔
35
  validates :name, presence: true
1✔
36
  validates :name, uniqueness: { scope: :course_id }
1✔
37
  validate :valid_num_review
1✔
38
  validates :directory_path, presence: true # E2138 Validation for unique submission directory
1✔
39
  validates :directory_path, uniqueness: { scope: :course_id }
1✔
40

41
  REVIEW_QUESTIONNAIRES = { author_feedback: 0, metareview: 1, review: 2, teammate_review: 3 }.freeze
1✔
42

43
  #  Review Strategy information.
44
  RS_AUTO_SELECTED = 'Auto-Selected'.freeze
1✔
45
  RS_INSTRUCTOR_SELECTED = 'Instructor-Selected'.freeze
1✔
46
  REVIEW_STRATEGIES = [RS_AUTO_SELECTED, RS_INSTRUCTOR_SELECTED].freeze
1✔
47
  DEFAULT_MAX_REVIEWERS = 3
1✔
48
  DEFAULT_MAX_OUTSTANDING_REVIEWS = 2
1✔
49

50
  def user_on_team?(user)
1✔
51
    teams = self.teams
×
52
    users = []
×
53
    teams.each do |team|
×
54
      users << team.users
×
55
    end
56
    users.flatten.include? user
×
57
  end
58

59
  def self.max_outstanding_reviews
1✔
60
    DEFAULT_MAX_OUTSTANDING_REVIEWS
×
61
  end
62

63
  def team_assignment?
1✔
64
    max_team_size > 0
×
65
  end
66
  alias team_assignment team_assignment?
1✔
67

68
  def topics?
1✔
69
    @has_topics ||= sign_up_topics.any?
×
70
  end
71

72
  def calibrated?
1✔
73
    is_calibrated
×
74
  end
75

76
  def self.assign_courses_to_assignment(user)
1✔
77
    @courses = Course.where(instructor_id: user.id).order(:name)
×
78
  end
79

80
  # removes an assignment from course
81
  def remove_assignment_from_course
1✔
82
    oldpath = begin
×
83
                path
×
84
              rescue StandardError
85
                nil
×
86
              end
87
    self.course_id = nil
×
88
    save
×
89
    newpath = begin
×
90
                path
×
91
              rescue StandardError
92
                nil
×
93
              end
94
    FileHelper.update_file_location(oldpath, newpath)
×
95
  end
96

97
  def teams?
1✔
98
    @has_teams ||= teams.any?
×
99
  end
100

101
  # remove empty teams (teams with no users) from assignment
102
  def remove_empty_teams
1✔
103
    empty_teams = teams.reload.select { |team| team.teams_users.empty? }
×
104
    teams.delete(empty_teams)
×
105
  end
106

107
  # checks whether the assignment is getting a valid number of reviews (less than number of reviews allowed)
108
  def valid_num_review
1✔
109
    self.num_reviews = num_reviews_allowed
×
110
    if num_reviews_greater?(num_reviews_required, num_reviews_allowed)
×
111
      errors.add(:message, 'Num of reviews required cannot be greater than number of reviews allowed')
×
112
    elsif num_reviews_greater?(num_metareviews_required, num_metareviews_allowed)
×
113
      errors.add(:message, 'Number of Meta-Reviews required cannot be greater than number of meta-reviews allowed')
×
114
    end
115
  end
116

117
  #--------------------metareview assignment begin
118
  def assign_metareviewer_dynamically(meta_reviewer)
1✔
119
    # The following method raises an exception if not successful which
120
    # has to be captured by the caller (in review_mapping_controller)
121
    response_map = response_map_to_metareview(meta_reviewer)
×
122
    response_map.assign_metareviewer(meta_reviewer)
×
123
  end
124

125
  # Returns a review (response) to metareview if available, otherwise will raise an error
126
  def response_map_to_metareview(metareviewer)
1✔
127
    response_map_set = Array.new(review_mappings)
×
128
    # Reject response maps without responses
129
    response_map_set.reject! { |response_map| response_map.response.empty? }
×
130
    raise 'There are no reviews to metareview at this time for this assignment.' if response_map_set.empty?
×
131

132
    # Reject reviews where the meta_reviewer was the reviewer or the contributor
133
    response_map_set.reject! do |response_map|
×
134
      (response_map.reviewee == metareviewer) || (response_map.reviewer == metareviewer)
×
135
    end
136
    raise 'There are no more reviews to metareview for this assignment.' if response_map_set.empty?
×
137

138
    # Metareviewer can only metareview each review once
139
    response_map_set.reject! { |response_map| response_map.metareviewed_by?(metareviewer) }
×
140
    raise 'You have already metareviewed all reviews for this assignment.' if response_map_set.empty?
×
141

142
    # Reduce to the response maps with the least number of metareviews received
143
    min_metareviews = min_metareview(response_map_set)
×
144
    response_map_set.reject! { |response_map| response_map.metareview_response_maps.count > min_metareviews }
×
145

146
    # Reduce the response maps to the reviewers with the least number of metareviews received
147
    reviewers = reviewer_metareviews_map(response_map_set)
×
148
    min_metareviews = reviewers.first[1]
×
149
    reviewers.reject! { |reviewer| reviewer[1] == min_metareviews }
×
150
    response_map_set.reject! { |response_map| reviewers.member?(response_map.reviewer) }
×
151

152
    # Pick the response map whose most recent meta_reviewer was assigned longest ago
153
    min_metareviews = min_metareview(response_map_set)
×
154
    response_map_set.sort! { |a, b| a.metareview_response_maps.last.id <=> b.metareview_response_maps.last.id } if min_metareviews > 0
×
155
    # The first review_map is the best to metareview
156
    response_map_set.first
×
157
  end
158

159
  def metareview_mappings
1✔
160
    mappings = []
×
161
    review_mappings.each do |map|
×
162
      m_map = MetareviewResponseMap.find_by(reviewed_object_id: map.id)
×
163
      mappings << m_map unless m_map.nil?
×
164
    end
165
    mappings
×
166
  end
167
  #--------------------metareview assignment end
168

169
  def dynamic_reviewer_assignment?
1✔
170
    review_assignment_strategy == RS_AUTO_SELECTED
×
171
  end
172
  alias is_using_dynamic_reviewer_assignment? dynamic_reviewer_assignment?
1✔
173

174
  def path
1✔
175
    if course_id.nil? && instructor_id.nil?
×
176
      raise 'The path cannot be created. The assignment must be associated with either a course or an instructor.'
×
177
    end
178

179
    path_text = if !course_id.nil? && course_id > 0
×
180
                  Rails.root.to_s + '/pg_data/' + FileHelper.clean_path(instructor[:name]) + '/' +
181
                    FileHelper.clean_path(course.directory_path) + '/'
×
182
                else
183
                  Rails.root.to_s + '/pg_data/' + FileHelper.clean_path(instructor[:name]) + '/'
×
184
                end
185
    path_text += FileHelper.clean_path(directory_path)
×
186
    path_text
×
187
  end
188

189
  # Check whether review, metareview, etc.. is allowed
190
  # The permissions of TopicDueDate is the same as AssignmentDueDate.
191
  # Here, column is usually something like 'review_allowed_id'
192
  def check_condition(column, topic_id = nil)
1✔
193
    next_due_date = DueDate.get_next_due_date(id, topic_id)
×
194
    return false if next_due_date.nil?
×
195

196
    right_id = next_due_date.send column
×
197
    right = DeadlineRight.find(right_id)
×
198
    right && (right.name == 'OK' || right.name == 'Late')
×
199
  end
200

201
  # Determine if the next due date from now allows for submissions
202
  def submission_allowed(topic_id = nil)
1✔
203
    check_condition('submission_allowed_id', topic_id)
×
204
  end
205

206
  # Determine if the next due date from now allows to take the quizzes
207
  def quiz_allowed(topic_id = nil)
1✔
208
    check_condition('quiz_allowed_id', topic_id)
×
209
  end
210

211
  # Determine if the next due date from now allows for reviews
212
  def can_review(topic_id = nil)
1✔
213
    check_condition('review_allowed_id', topic_id)
×
214
  end
215

216
  # Determine if the next due date from now allows for metareviews
217
  def metareview_allowed(topic_id = nil)
1✔
218
    check_condition('review_of_review_allowed_id', topic_id)
×
219
  end
220

221
  # Deletes all instances created as part of assignment and finally destroys itself.
222
  def delete(force = nil)
1✔
223
    begin
×
224
      maps = ReviewResponseMap.where(reviewed_object_id: id)
×
225
      maps.each { |map| map.delete(force) }
×
226
    rescue StandardError
227
      raise "There is at least one review response that exists for #{name}."
×
228
    end
229

230
    begin
×
231
      maps = TeammateReviewResponseMap.where(reviewed_object_id: id)
×
232
      maps.each { |map| map.delete(force) }
×
233
    rescue StandardError
234
      raise "There is at least one teammate review response that exists for #{name}."
×
235
    end
236

237
    # destroy instances of invitations, teams, participants, etc, refactored by Rajan, Jasmine, Sreenidhi 3/30/2020
238
    # You can now add the instances to be deleted into the list.
239
    delete_instances = %w[invitations teams participants due_dates assignment_questionnaires]
×
240
    delete_instances.each do |instance|
×
241
      instance_eval(instance).each(&:destroy)
×
242
    end
243

244
    # The size of an empty directory is 2
245
    # Delete the directory if it is empty
246
    directory = begin
×
247
                  Dir.entries(Rails.root + '/pg_data/' + directory_path)
×
248
                rescue StandardError
249
                  nil
×
250
                end
251
    if directory_path.present? && !directory.nil?
×
252
      raise 'The assignment directory is not empty.' unless directory.size == 2
×
253

254
      Dir.delete(Rails.root + '/pg_data/' + directory_path)
×
255
    end
256
    destroy
×
257
  end
258

259
  # Check to see if assignment is a microtask
260
  def microtask?
1✔
261
    microtask.nil? ? false : microtask
×
262
  end
263

264
  # Check to see if assignment has badge
265
  def badge?
1✔
266
    has_badge.nil? ? false : has_badge
×
267
  end
268

269
  # add a new participant to this assignment
270
  # manual addition
271
  # user_name - the user account name of the participant to add
272
  def add_participant(user_name, can_submit, can_review, can_take_quiz, can_mentor)
1✔
273
    user = User.find_by(name: user_name)
×
274
    if user.nil?
×
275
      raise "The user account with the name #{user_name} does not exist. Please <a href='" +
×
276
            url_for(controller: 'users', action: 'new') + "'>create</a> the user first."
277
    end
278
    participant = AssignmentParticipant.find_by(parent_id: id, user_id: user.id)
×
279
    raise "The user #{user.name} is already a participant." if participant
×
280

281
    new_part = AssignmentParticipant.create(parent_id: id,
×
282
                                            user_id: user.id,
283
                                            permission_granted: user.master_permission_granted,
284
                                            can_submit: can_submit,
285
                                            can_review: can_review,
286
                                            can_take_quiz: can_take_quiz,
287
                                            can_mentor: can_mentor)
288
    new_part.set_handle
×
289
  end
290

291
  def create_node
1✔
292
    parent = CourseNode.find_by(node_object_id: course_id)
×
293
    node = AssignmentNode.create(node_object_id: id)
×
294
    node.parent_id = parent.id unless parent.nil?
×
295
    node.save
×
296
  end
297

298
  # if current  stage is submission or review, find the round number
299
  # otherwise, return 0
300
  def number_of_current_round(topic_id)
1✔
301
    next_due_date = DueDate.get_next_due_date(id, topic_id)
×
302
    return 0 if next_due_date.nil?
×
303

304
    next_due_date.round ||= 0
×
305
  end
306

307
  # For varying rubric feature
308
  def current_stage_name(topic_id = nil)
1✔
309
    if staggered_deadline?
×
310
      return (topic_id.nil? ? 'Unknown' : current_stage(topic_id))
×
311
    end
312

313
    due_date = find_current_stage(topic_id)
×
314
    unless due_date == 'Finished' || due_date.nil? || due_date.deadline_name.nil?
×
315
      return due_date.deadline_name
×
316
    end
317

318
    current_stage(topic_id)
×
319
  end
320

321
  # check if this assignment has multiple review phases with different review rubrics
322
  def varying_rubrics_by_round?
1✔
323
    # E-2084 corrected '>=' to '>' to fix logic
324
    #This is a hack, we should actually check if we have more than one rubric of a given type eg, review
325
    AssignmentQuestionnaire.where(assignment_id: id, used_in_round: 2).size >= 1
×
326
  end
327

328
  def link_for_current_stage(topic_id = nil)
1✔
329
    return nil if staggered_and_no_topic?(topic_id)
×
330

331
    due_date = find_current_stage(topic_id)
×
332
    if due_date.nil? || (due_date == 'Finished') || due_date.is_a?(TopicDueDate)
×
333
      return nil
×
334
    end
335

336
    due_date.description_url
×
337
  end
338

339
  def stage_deadline(topic_id = nil)
1✔
340
    return 'Unknown' if staggered_and_no_topic?(topic_id)
×
341

342
    due_date = find_current_stage(topic_id)
×
343
    due_date.nil? || due_date == 'Finished' ? due_date : due_date.due_at.to_s
×
344
  end
345

346
  def num_review_rounds
1✔
347
    due_dates = AssignmentDueDate.where(parent_id: id)
×
348
    rounds = 0
×
349
    due_dates.each do |due_date|
×
350
      rounds = due_date.round if due_date.round > rounds
×
351
    end
352
    rounds
×
353
  end
354

355
  def find_current_stage(topic_id = nil)
1✔
356
    next_due_date = DueDate.get_next_due_date(id, topic_id)
×
357
    return 'Finished' if next_due_date.nil?
×
358

359
    next_due_date
×
360
  end
361

362
  # Zhewei: this method is almost the same as 'stage_deadline'
363
  def current_stage(topic_id = nil)
1✔
364
    return 'Unknown' if staggered_and_no_topic?(topic_id)
×
365

366
    due_date = find_current_stage(topic_id)
×
367
    due_date.nil? || due_date == 'Finished' ? 'Finished' : DeadlineType.find(due_date.deadline_type_id).name
×
368
  end
369

370
  # Find the ID of a review questionnaire for this assignment
371
  def review_questionnaire_id(round_number = nil, topic_id = nil)
1✔
372
    # If round is not given, try to retrieve current round from the next due date
373
    if round_number.nil?
×
374
      next_due_date = DueDate.get_next_due_date(id)
×
375
      round_number = next_due_date.try(:round)
×
376
    end
377
    # Create assignment_form that we can use to retrieve AQ with all the same attributes and questionnaire based on AQ
378
    assignment_form = AssignmentForm.create_form_object(id)
×
379
    assignment_questionnaire = assignment_form.assignment_questionnaire('ReviewQuestionnaire', round_number, topic_id)
×
380
    questionnaire = assignment_form.questionnaire(assignment_questionnaire, 'ReviewQuestionnaire')
×
381
    return questionnaire.id unless questionnaire.id.nil?
×
382

383
    # If correct questionnaire is not found, find it by type
384
    AssignmentQuestionnaire.where(assignment_id: id).select do |aq|
×
385
      !aq.questionnaire_id.nil? && Questionnaire.find(aq.questionnaire_id).type == 'ReviewQuestionnaire'
×
386
      return aq.questionnaire_id
×
387
    end
388
    nil
389
  end
390

391
  def self.export_details(csv, parent_id, detail_options)
1✔
392
    return csv unless detail_options.value?('true')
×
393

394
    @assignment = Assignment.find(parent_id)
×
395
    @answers = {} # Contains all answer objects for this assignment
×
396
    # Find all unique response types
397
    @uniq_response_type = ResponseMap.where.not(type: nil).pluck(:type).uniq
×
398
    # Find all unique round numbers
399
    @uniq_rounds = Response.pluck(:round).uniq
×
400
    # create the nested hash that holds all the answers organized by round # and response type
401
    @uniq_rounds.each do |round_num|
×
402
      @answers[round_num] = {}
×
403
      @uniq_response_type.each do |res_type|
×
404
        @answers[round_num][res_type] = []
×
405
      end
406
    end
407
    @answers = generate_answer(@answers, @assignment)
×
408
    # Loop through each round and response type and construct a new row to be pushed in CSV
409
    @uniq_rounds.each do |round_num|
×
410
      @uniq_response_type.each do |res_type|
×
411
        round_type = check_empty_rounds(@answers, round_num, res_type)
×
412
        csv << [round_type, '---', '---', '---', '---', '---', '---', '---'] unless round_type.nil?
×
413
        @answers[round_num][res_type].each do |answer|
×
414
          csv << csv_row(detail_options, answer)
×
415
        end
416
      end
417
    end
418
  end
419

420
  # This method was refactored to reduce complexity, additional fields could now be added to the list - Rajan, Jasmine, Sreenidhi
421
  # Now you could add your export fields to the hashmap
422
  EXPORT_DETAIL_FIELDS = { team_id: 'Team ID / Author ID', team_name: 'Reviewee (Team / Student Name)', reviewer: 'Reviewer', question: 'Question / Criterion', question_id: 'Question ID', comment_id: 'Answer / Comment ID', comments: 'Answer / Comment', score: 'Score' }.freeze
1✔
423
  def self.export_details_fields(detail_options)
1✔
424
    fields = []
×
425
    EXPORT_DETAIL_FIELDS.each do |key, value|
×
426
      fields << value if detail_options[key.to_s] == 'true'
×
427
    end
428
    fields
×
429
  end
430

431
  def self.handle_nil(csv_field)
1✔
432
    return ' ' if csv_field.nil?
×
433

434
    csv_field
×
435
  end
436

437
  # Generates a single row based on the detail_options selected
438
  def self.csv_row(detail_options, answer)
1✔
439
    teams_csv = []
×
440
    @response = Response.find(answer.response_id)
×
441
    map = ResponseMap.find(@response.map_id)
×
442
    @reviewee = Team.find_by id: map.reviewee_id
×
443
    @reviewee = Participant.find(map.reviewee_id).user if @reviewee.nil?
×
444
    reviewer = Participant.find(map.reviewer_id).user
×
445
    teams_csv << handle_nil(@reviewee.id) if detail_options['team_id'] == 'true'
×
446
    teams_csv << handle_nil(@reviewee.name) if detail_options['team_name'] == 'true'
×
447
    teams_csv << handle_nil(reviewer.name) if detail_options['reviewer'] == 'true'
×
448
    teams_csv << handle_nil(answer.question.txt) if detail_options['question'] == 'true'
×
449
    teams_csv << handle_nil(answer.question.id) if detail_options['question_id'] == 'true'
×
450
    teams_csv << handle_nil(answer.id) if detail_options['comment_id'] == 'true'
×
451
    teams_csv << handle_nil(answer.comments) if detail_options['comments'] == 'true'
×
452
    teams_csv << handle_nil(answer.answer) if detail_options['score'] == 'true'
×
453
    teams_csv
×
454
  end
455

456
  # Populate answers will review information
457
  def self.generate_answer(answers, assignment)
1✔
458
    # get all response maps for this assignment
459
    @response_maps_for_assignment = ResponseMap.find_by_sql(["SELECT * FROM response_maps WHERE reviewed_object_id = #{assignment.id}"])
×
460
    # for each map, get the response & answer associated with it
461
    @response_maps_for_assignment.each do |map|
×
462
      @response_for_this_map = Response.find_by_sql(["SELECT * FROM responses WHERE map_id = #{map.id}"])
×
463
      # for this response, get the answer associated with it
464
      @response_for_this_map.each do |resp|
×
465
        @associated_answers = Answer.find_by_sql(["SELECT * FROM answers WHERE response_id = #{resp.id}"])
×
466
        @associated_answers.each do |answer|
×
467
          answers[resp.round][map.type].push(answer)
×
468
        end
469
      end
470
    end
471
    answers
×
472
  end
473

474
  # Checks if there are rounds with no reviews
475
  def self.check_empty_rounds(answers, round_num, res_type)
1✔
476
    if answers[round_num][res_type].any?
×
477
      round_num.nil? ? 'Round Nil - ' + res_type : 'Round ' + round_num.to_s + ' - ' + res_type.to_s
×
478
    end
479
  end
480

481
  # This method is used to set the headers for the csv like Assignment Name and Assignment Instructor
482
  def self.export_headers(parent_id)
1✔
483
    @assignment = Assignment.find(parent_id)
×
484
    fields = []
×
485
    fields << 'Assignment Name: ' + @assignment.name.to_s
×
486
    fields << 'Assignment Instructor: ' + User.find(@assignment.instructor_id).name.to_s
×
487
    fields
×
488
  end
489

490
  # This method is used for export contents of grade#view.  -Zhewei
491
  def self.export(csv, parent_id, options)
1✔
492
    @assignment = Assignment.find(parent_id)
×
493
    @questions = {}
×
494
    questionnaires = @assignment.questionnaires
×
495
    questionnaires.each do |questionnaire|
×
496
      if @assignment.varying_rubrics_by_round?
×
497
        round = AssignmentQuestionnaire.find_by(assignment_id: @assignment.id, questionnaire_id: @questionnaire.id).used_in_round
×
498
        questionnaire_symbol = round.nil? ? questionnaire.symbol : (questionnaire.symbol.to_s + round.to_s).to_sym
×
499
      else
500
        questionnaire_symbol = questionnaire.symbol
×
501
      end
502
      @questions[questionnaire_symbol] = questionnaire.questions
×
503
    end
504
    @scores = @assignment.review_grades(@assignment, @questions)
×
505
    return csv if @scores[:teams].nil?
×
506

507
    export_data(csv, @scores, options)
×
508
  end
509

510
  def self.export_data(csv, scores, options)
1✔
511
    @scores = scores
×
512
    (0..@scores[:teams].length - 1).each do |index|
×
513
      team = @scores[:teams][index.to_s.to_sym]
×
514
      first_participant = team[:team].participants[0] unless team[:team].participants[0].nil?
×
515
      next if first_participant.nil?
×
516
      participants_score = @scores[:participants][first_participant.id.to_s.to_sym]
×
517
      teams_csv = []
×
518
      teams_csv << team[:team].name
×
519
      names_of_participants = ''
×
520
      team[:team].participants.each do |p|
×
521
        names_of_participants += p.fullname
×
522
        names_of_participants += '; ' unless p == team[:team].participants.last
×
523
      end
524
      teams_csv << names_of_participants
×
525
      export_data_fields(options, team, teams_csv, participants_score)
×
526
      csv << teams_csv
×
527
    end
528
  end
529

530
  def self.export_data_fields(options, team, teams_csv, participants_score)
1✔
531
    if options['team_score'] == 'true'
×
532
      if team[:scores]
×
533
        teams_csv.push(team[:scores][:max], team[:scores][:min], team[:scores][:avg])
×
534
      else
535
        teams_csv.push('---', '---', '---')
×
536
      end
537
    end
538
    review_hype_mapping_hash = { review: 'submitted_score',
×
539
                                 metareview: 'metareview_score',
540
                                 feedback: 'author_feedback_score',
541
                                 teammate: 'teammate_review_score' }
542
    review_hype_mapping_hash.each do |review_type, score_name|
×
543
      export_individual_data_fields(review_type, score_name, teams_csv, participants_score, options)
×
544
    end
545
    teams_csv.push(participants_score[:total_score])
×
546
  end
547

548
  def self.export_individual_data_fields(review_type, score_name, teams_csv, participants_score, options)
1✔
549
    if participants_score[review_type]
×
550
      teams_csv.push(participants_score[review_type][:scores][:max], participants_score[review_type][:scores][:min], participants_score[review_type][:scores][:avg])
×
551
    elsif options[score_name]
×
552
      teams_csv.push('---', '---', '---')
×
553
    end
554
  end
555

556
  # This method was refactored by Rajan, Jasmine, Sreenidhi on 03/31/2020
557
  # Now you can add groups of fields to the hashmap
558
  EXPORT_FIELDS = { team_score: ['Team Max', 'Team Min', 'Team Avg'], submitted_score: ['Submitted Max', 'Submitted Min', 'Submitted Avg'], metareview_score: ['Metareview Max', 'Metareview Min', 'Metareview Avg'], author_feedback_score: ['Author Feedback Max, Author Feedback Min, Author Feedback Avg'], teammate_review_score: ['Teammate Review Max', 'Teammate Review Min', 'Teammate Review Avg'] }.freeze
1✔
559
  def self.export_fields(options)
1✔
560
    fields = []
×
561
    fields << 'Team Name'
×
562
    fields << 'Team Member(s)'
×
563
    EXPORT_FIELDS.each do |key, value|
×
564
      next unless options[key.to_s] == 'true'
×
565

566
      value.each do |f|
×
567
        fields.push(f)
×
568
      end
569
    end
570
    fields.push('Final Score')
×
571
    fields
×
572
  end
573

574
  def find_due_dates(type)
1✔
575
    due_dates.select { |due_date| due_date.deadline_type_id == DeadlineType.find_by(name: type).id }
×
576
  end
577

578
  # Method find_review_period is used in answer_helper.rb to get the start and end dates of a round
579
  def find_review_period(round)
1✔
580
    # If round is nil, it means the same questionnaire is used for every round. Thus, we return all periods.
581
    # If round is not nil, we return only the period of that round.
582

583
    submission_type = DeadlineType.find_by(name: 'submission').id
×
584
    review_type = DeadlineType.find_by(name: 'review').id
×
585

586
    due_dates = []
×
587
    due_dates += find_due_dates('submission')
×
588
    due_dates += find_due_dates('review')
×
589
    due_dates.sort_by!(&:id)
×
590

591
    start_dates = []
×
592
    end_dates = []
×
593

594
    if round.nil?
×
595
      round = 1
×
596
      while self.due_dates.exists?(round: round)
×
597
        start_dates << due_dates.select { |due_date| due_date.deadline_type_id == submission_type && due_date.round == round }.last
×
598
        end_dates << due_dates.select { |due_date| due_date.deadline_type_id == review_type && due_date.round == round }.last
×
599
        round += 1
×
600
      end
601
    else
602
      start_dates << due_dates.select { |due_date| due_date.deadline_type_id == submission_type && due_date.round == round }.last
×
603
      end_dates << due_dates.select { |due_date| due_date.deadline_type_id == review_type && due_date.round == round }.last
×
604
    end
605
    [start_dates, end_dates]
×
606
  end
607

608
  # for program 1 like assignment, if same rubric is used in both rounds,
609
  # the 'used_in_round' field in 'assignment_questionnaires' will be null,
610
  # since one field can only store one integer
611
  # if questionnaire_ids is empty, Expertiza will try to find questionnaire whose type is 'ReviewQuestionnaire'.
612
  def questionnaire_ids(round)
1✔
613
    questionnaire_ids = if round.nil?
×
614
                          AssignmentQuestionnaire.where(assignment_id: id)
×
615
                        else
616
                          AssignmentQuestionnaire.where(assignment_id: id, used_in_round: round)
×
617
                        end
618
    if questionnaire_ids.empty?
×
619
      AssignmentQuestionnaire.where(assignment_id: id).find_each do |aq|
×
620
        questionnaire_ids << aq if aq.questionnaire.type == 'ReviewQuestionnaire'
×
621
      end
622
    end
623
    questionnaire_ids
×
624
  end
625

626
  def pair_programming_enabled?
1✔
627
    self.enable_pair_programming
×
628
  end
629

630
  private
1✔
631

632
  # returns true if assignment has staggered deadline and topic_id is nil
633
  def staggered_and_no_topic?(topic_id)
1✔
634
    staggered_deadline? && topic_id.nil?
×
635
  end
636

637
  # returns true if reviews required is greater than reviews allowed
638
  def num_reviews_greater?(reviews_required, reviews_allowed)
1✔
639
    reviews_allowed && reviews_allowed != -1 && reviews_required > reviews_allowed
×
640
  end
641

642
  def min_metareview(response_map_set)
1✔
643
    response_map_set.sort! { |a, b| a.metareview_response_maps.count <=> b.metareview_response_maps.count }
×
644
    min_metareviews = response_map_set.first.metareview_response_maps.count
×
645
    min_metareviews
×
646
  end
647

648
  # returns a map of reviewer to meta_reviews
649
  def reviewer_metareviews_map(response_map_set)
1✔
650
    reviewers = {}
×
651
    response_map_set.each do |response_map|
×
652
      reviewer = response_map.reviewer
×
653
      reviewers.member?(reviewer) ? reviewers[reviewer] += 1 : reviewers[reviewer] = 1
×
654
    end
655
    reviewers = reviewers.sort_by { |a| a[1] }
×
656
  end
657

658
  #Method to drop all the SignedUpRecords of all topics for that assignment once the drop_topic deadline passes
659
  def drop_waitlisted_teams
1✔
660
    # Find all the topics (sign_up_topics) under the current assignment (self).
661
    topics = SignUpTopic.where(assignment_id: self.id)
×
662
  
663
    # Iterate through each topic to find and drop waitlisted teams.
664
    topics.each do |topic|
×
665
      signed_up_teams = SignedUpTeam.where(topic_id: topic.id, is_waitlisted: true)
×
666
      # Remove all of the waitlisted SignedUpTeam entries for this topic.
667
      signed_up_teams.destroy_all
×
668
    end
669
  end
670

671
end
672

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc