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

MarkUsProject / Markus / 12485101215

24 Dec 2024 06:32PM UTC coverage: 91.755% (-0.003%) from 91.758%
12485101215

Pull #7303

github

web-flow
Merge 38bc2f2d4 into 919aac97e
Pull Request #7303: build(deps): bump sinatra from 3.1.0 to 4.1.0

623 of 1357 branches covered (45.91%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

121 existing lines in 4 files now uncovered.

41165 of 44186 relevant lines covered (93.16%)

120.55 hits per line

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

84.4
/app/controllers/groups_controller.rb
1
# Manages actions relating to editing and modifying
2
# groups.
3
class GroupsController < ApplicationController
1✔
4
  # Administrator
5
  before_action { authorize! }
138✔
6
  layout 'assignment_content'
1✔
7

8
  content_security_policy only: [:assign_scans] do |p|
1✔
9
    p.img_src :self, :blob
12✔
10
  end
11

12
  # Group administration functions -----------------------------------------
13
  # Verify that all functions below are included in the authorize filter above
14

15
  def new
1✔
16
    assignment = Assignment.find(params[:assignment_id])
3✔
17
    begin
18
      assignment.add_group(params[:new_group_name])
3✔
19
      flash_now(:success, I18n.t('flash.actions.create.success',
2✔
20
                                 resource_name: Group.model_name.human))
21
    rescue StandardError => e
22
      flash_now(:error, e.message)
1✔
23
    ensure
24
      head :ok
3✔
25
    end
26
  end
27

28
  def remove_group
1✔
29
    # When a success div exists we can return successfully removed groups
30
    groupings = Grouping.where(id: params[:grouping_id])
14✔
31
    errors = []
14✔
32
    @removed_groupings = []
14✔
33
    Repository.get_class.update_permissions_after(only_on_request: true) do
14✔
34
      groupings.each do |grouping|
12✔
35
        grouping.student_memberships.each do |member|
12✔
UNCOV
36
          grouping.remove_member(member.id)
×
37
        end
38
      end
39
    end
40
    groupings.each do |grouping|
14✔
41
      if grouping.has_submission?
14✔
42
        errors.push(grouping.group.group_name)
8✔
43
      else
44
        grouping.delete_grouping
6✔
45
        @removed_groupings.push(grouping)
6✔
46
      end
47
    end
48
    if errors.any?
14✔
49
      err_groups = errors.join(', ')
8✔
50
      flash_message(:error, I18n.t('groups.delete_group_has_submission') + err_groups)
8✔
51
    end
52
    head :ok
14✔
53
  end
54

55
  def rename_group
1✔
56
    @grouping = record
3✔
57
    @assignment = record.assignment
3✔
58
    @group = @grouping.group
3✔
59

60
    # Checking if a group with this name already exists
61
    if (@existing_group = current_course.groups.where(group_name: params[:new_groupname]).first)
3✔
62
      existing = true
2✔
63
      groupexist_id = @existing_group.id
2✔
64
    end
65

66
    if existing
3✔
67

68
      # We link the grouping to the group already existing
69

70
      # We verify there is no other grouping linked to this group on the
71
      # same assignement
72
      params[:groupexist_id] = groupexist_id
2✔
73
      params[:assignment_id] = @assignment.id
2✔
74

75
      if Grouping.exists?(assessment_id: @assignment.id, group_id: groupexist_id)
2✔
UNCOV
76
        flash_now(:error, I18n.t('groups.group_name_already_in_use'))
×
77
      elsif @grouping.has_submitted_files? || @grouping.has_non_empty_submission?
2✔
78
        flash_now(:error, I18n.t('groups.group_name_already_in_use_diff_assignment'))
2✔
79
      else
UNCOV
80
        @grouping.update_attribute(:group_id, groupexist_id)
×
81
      end
82
    else
83
      # We update the group_name
84
      @group.group_name = params[:new_groupname]
1✔
85
      if @group.save
1✔
86
        flash_now(:success, I18n.t('flash.actions.update.success',
1✔
87
                                   resource_name: Group.human_attribute_name(:group_name)))
88
      end
89
    end
90
    head :ok
3✔
91
  end
92

93
  def valid_grouping
1✔
94
    # TODO: make this a member route in a new GroupingsController
95
    assignment = Assignment.find(params[:assignment_id])
2✔
96
    grouping = assignment.groupings.find(params[:grouping_id])
2✔
97
    grouping.validate_grouping
2✔
98
    head :ok
2✔
99
  end
100

101
  def invalid_grouping
1✔
102
    # TODO: make this a member route in a new GroupingsController
103
    assignment = Assignment.find(params[:assignment_id])
2✔
104
    grouping = assignment.groupings.find(params[:grouping_id])
2✔
105
    grouping.invalidate_grouping
2✔
106
    head :ok
2✔
107
  end
108

109
  def index
1✔
110
    @assignment = Assignment.find(params[:assignment_id])
2✔
111
    @clone_assignments = current_course.assignments
2✔
112
                                       .joins(:assignment_properties)
113
                                       .where(assignment_properties: { vcs_submit: true })
114
                                       .where.not(id: @assignment.id)
115
                                       .order(:id)
116

117
    respond_to do |format|
2✔
118
      format.html
2✔
119
      format.json do
2✔
UNCOV
120
        render json: @assignment.all_grouping_data
×
121
      end
122
    end
123
  end
124

125
  def assign_scans
1✔
126
    # TODO: make this a member route in a new GroupingsController
127
    @assignment = Assignment.find(params[:assignment_id])
12✔
128
    if params.key?(:grouping_id)
12✔
129
      next_grouping = @assignment.groupings.find(params[:grouping_id])
6✔
130
    else
131
      next_grouping = Grouping.get_assign_scans_grouping(@assignment)
6✔
132
    end
133
    if next_grouping&.current_submission_used.nil?
12✔
134
      if @assignment.groupings.left_outer_joins(:current_submission_used).where('submissions.id': nil).any?
4✔
135
        flash_message(:warning, I18n.t('exam_templates.assign_scans.not_all_submissions_collected'))
3✔
136
      end
137
      redirect_back(fallback_location: course_assignment_groups_path(current_course, @assignment))
4✔
138
      return
4✔
139
    end
140
    names = next_grouping.non_rejected_student_memberships.map do |u|
8✔
141
      u.user.display_name
4✔
142
    end
143
    num_valid = @assignment.get_num_valid
8✔
144
    num_total = @assignment.groupings.size
8✔
145
    if num_valid == num_total
8✔
146
      flash_message(:success, t('exam_templates.assign_scans.done'))
4✔
147
    end
148
    @data = {
149
      group_name: next_grouping.group.group_name,
8✔
150
      grouping_id: next_grouping.id,
151
      students: names,
152
      num_total: num_total,
153
      num_valid: num_valid
154
    }
155
    next_file = next_grouping.current_submission_used.submission_files.find_by(filename: 'COVER.pdf')
8✔
156
    if next_file.nil?
8✔
157
      flash_message(:warning, I18n.t('exam_templates.assign_scans.no_cover_page'))
6✔
158
    else
159
      @data[:filelink] = download_course_assignment_groups_path(
2✔
160
        current_course, @assignment,
161
        select_file_id: next_grouping.current_submission_used.submission_files.find_by(filename: 'COVER.pdf').id,
162
        show_in_browser: true
163
      )
164
    end
165
  end
166

167
  def get_names
1✔
168
    names = current_course.students
8✔
169
                          .joins(:user)
170
                          .where('(lower(first_name) like ? OR
171
                                   lower(last_name) like ? OR
172
                                   lower(user_name) like ? OR
173
                                   id_number like ?) AND roles.hidden = false AND roles.id NOT IN (?)',
174
                                 "#{ApplicationRecord.sanitize_sql_like(params[:term].downcase)}%",
175
                                 "#{ApplicationRecord.sanitize_sql_like(params[:term].downcase)}%",
176
                                 "#{ApplicationRecord.sanitize_sql_like(params[:term].downcase)}%",
177
                                 "#{ApplicationRecord.sanitize_sql_like(params[:term])}%",
178
                                 Membership.select(:role_id)
179
                                           .joins(:grouping)
180
                                           .where(groupings: { assessment_id: params[:assignment_id] }))
181
                          .pluck_to_hash(:id, 'users.id_number', 'users.user_name',
182
                                         'users.first_name', 'users.last_name')
183
    names = names.map do |h|
8✔
184
      { id: h[:id],
8✔
185
        id_number: h['users.id_number'],
186
        user_name: h['users.user_name'],
187
        value: "#{h['users.first_name']} #{h['users.last_name']}" }
188
    end
189
    render json: names
8✔
190
  end
191

192
  def assign_student_and_next
1✔
193
    # TODO: make this a member route in a new GroupingsController
194
    @grouping = Grouping.joins(:assignment).where('assessments.course_id': current_course.id).find(params[:g_id])
5✔
195
    @assignment = @grouping.assignment
5✔
196
    unless params[:skip]
5✔
197
      # if the user has selected a name from the dropdown, s_id is set
198
      if params[:s_id].present?
5✔
199
        student = current_course.students.find(params[:s_id])
2✔
200
      end
201
      # if the user has typed in the whole name without select, or if they typed a name different from the select s_id
202
      if student.nil? || "#{student.first_name} #{student.last_name}" != params[:names]
5✔
203
        student = current_course.students.joins(:user).where(
3✔
204
          'lower(CONCAT(first_name, \' \', last_name)) like ? OR lower(CONCAT(last_name, \' \', first_name)) like ?',
205
          ApplicationRecord.sanitize_sql_like(params[:names].downcase),
206
          ApplicationRecord.sanitize_sql_like(params[:names].downcase)
207
        ).first
208
      end
209
      if student.nil?
5✔
210
        flash_message(:error, t('exam_templates.assign_scans.student_not_found', name: params[:names]))
2✔
211
        head :not_found
2✔
212
        return
2✔
213
      end
214
      StudentMembership
3✔
215
        .find_or_create_by(role: student, grouping: @grouping, membership_status: StudentMembership::STATUSES[:inviter])
216
    end
217
    next_grouping
3✔
218
  end
219

220
  def next_grouping
1✔
221
    # TODO: is this actually a route that is called from anywhere or just a helper method?
222
    if params[:a_id].present?
3✔
UNCOV
223
      @assignment = Assignment.find(params[:a_id])
×
224
    end
225
    next_grouping = Grouping.get_assign_scans_grouping(@assignment, params[:g_id])
3✔
226
    if next_grouping.nil?
3✔
227
      head :not_found
1✔
228
      return
1✔
229
    end
230
    names = next_grouping.non_rejected_student_memberships.map do |u|
2✔
UNCOV
231
      u.user.display_name
×
232
    end
233
    num_valid = @assignment.get_num_valid
2✔
234
    num_total = @assignment.groupings.size
2✔
235
    if num_valid == num_total
2✔
UNCOV
236
      flash_message(:success, t('exam_templates.assign_scans.done'))
×
237
    end
238
    if !@grouping.nil? && next_grouping.id == @grouping.id
2✔
UNCOV
239
      render json: {
×
240
        grouping_id: next_grouping.id,
241
        students: names,
242
        num_total: num_total,
243
        num_valid: num_valid
244
      }
245
    else
246
      data = {
247
        group_name: next_grouping.group.group_name,
2✔
248
        grouping_id: next_grouping.id,
249
        students: names,
250
        num_total: num_total,
251
        num_valid: num_valid
252
      }
253
      next_file = next_grouping.current_submission_used.submission_files.find_by(filename: 'COVER.pdf')
2✔
254
      unless next_file.nil?
2✔
UNCOV
255
        data[:filelink] = download_course_assignment_groups_path(
×
256
          current_course,
257
          @assignment,
258
          select_file_id: next_grouping.current_submission_used.submission_files.find_by(filename: 'COVER.pdf').id,
259
          show_in_browser: true
260
        )
261
      end
262
      render json: data
2✔
263
    end
264
  end
265

266
  # Allows the user to upload a csv file listing groups. If group_name is equal
267
  # to the only member of a group and the assignment is configured with
268
  # allow_web_subits == false, the student's username will be used as the
269
  # repository name. If MarkUs is not repository admin, the repository name as
270
  # specified by the second field will be used instead.
271
  def upload
1✔
272
    assignment = Assignment.find(params[:assignment_id])
13✔
273
    begin
274
      data = process_file_upload(['.csv'])
13✔
275
    rescue StandardError => e
276
      flash_message(:error, e.message)
3✔
277
    else
278
      group_rows = []
10✔
279
      result = MarkusCsv.parse(data[:contents], encoding: data[:encoding]) do |row|
10✔
280
        next if row.blank?
22✔
281
        raise CsvInvalidLineError if row[0].blank?
22✔
282

283
        group_rows << row.compact_blank
21✔
284
      end
285
      if result[:invalid_lines].empty?
10✔
286
        @current_job = CreateGroupsJob.perform_later assignment, group_rows
3✔
287
        session[:job_id] = @current_job.job_id
3✔
288
      else
289
        flash_message(:error, result[:invalid_lines])
7✔
290
      end
291
    end
292
    redirect_to course_assignment_groups_path(current_course, assignment)
13✔
293
  end
294

295
  def create_groups_when_students_work_alone
1✔
296
    @assignment = Assignment.find(params[:assignment_id])
6✔
297
    if @assignment.group_max == 1
6✔
298
      # data is a list of lists containing: [[group_name, group_member], ...]
299
      data = current_course.students
3✔
300
                           .joins(:user)
301
                           .where(hidden: false)
302
                           .pluck('users.user_name')
303
                           .map { |user_name| [user_name, user_name] }
15✔
304
      @current_job = CreateGroupsJob.perform_later @assignment, data
3✔
305
      session[:job_id] = @current_job.job_id
3✔
306
    end
307

308
    respond_to do |format|
6✔
309
      format.js { render 'shared/_poll_job' }
12✔
310
    end
311
  end
312

313
  def download
1✔
314
    # TODO: make this a member route in a new SubmissionFileController
UNCOV
315
    file = SubmissionFile.find(params[:select_file_id])
×
316
    # TODO: remove this check once this is moved to a new SubmissionFileController
317
    return page_not_found unless file.course == current_course
×
318

319
    file_contents = file.retrieve_file
×
320

321
    filename = file.filename
×
UNCOV
322
    send_data_download file_contents, filename: filename
×
323
  end
324

325
  def download_grouplist
1✔
326
    assignment = Assignment.find(params[:assignment_id])
5✔
327
    groupings = assignment.groupings.includes(:group, student_memberships: :role)
5✔
328

329
    file_out = MarkusCsv.generate(groupings) do |grouping|
5✔
330
      # csv format is group_name, repo_name, user1_name, user2_name, ... etc
331
      [grouping.group.group_name].concat(
5✔
332
        grouping.student_memberships.map do |member|
333
          member.role.user_name
10✔
334
        end
335
      )
336
    end
337

338
    send_data(file_out,
5✔
339
              type: 'text/csv',
340
              filename: "#{assignment.short_identifier}_group_list.csv",
341
              disposition: 'attachment')
342
  end
343

344
  def use_another_assignment_groups
1✔
UNCOV
345
    target_assignment = Assignment.find(params[:assignment_id])
×
UNCOV
346
    source_assignment = Assignment.find(params[:clone_assignment_id])
×
347

348
    return head :unprocessable_entity if target_assignment.course != source_assignment.course
×
349

350
    if source_assignment.nil?
×
UNCOV
351
      flash_message(:warning, t('groups.clone_warning.could_not_find_source'))
×
352
    elsif target_assignment.nil?
×
353
      flash_message(:warning, t('groups.clone_warning.could_not_find_target'))
×
354
    else
355
      # Clone the groupings
UNCOV
356
      clone_warnings = target_assignment.clone_groupings_from(source_assignment.id)
×
UNCOV
357
      unless clone_warnings.empty?
×
358
        clone_warnings.each { |w| flash_message(:warning, w) }
×
359
      end
360
    end
361

UNCOV
362
    redirect_back(fallback_location: root_path)
×
363
  end
364

365
  def accept_invitation
1✔
366
    # TODO: make this a member route in a new GroupingsController
367
    @assignment = Assignment.find(params[:assignment_id])
6✔
368
    @grouping = @assignment.groupings.find(params[:grouping_id])
6✔
369
    begin
370
      current_role.join(@grouping)
6✔
371
    rescue ActiveRecord::RecordInvalid, RuntimeError => e
372
      flash_message(:error, e.message)
3✔
373
      status = :unprocessable_entity
3✔
374
    else
375
      m_logger = MarkusLogger.instance
3✔
376
      m_logger.log("Student '#{current_role.user_name}' joined group " \
3✔
377
                   "'#{@grouping.group.group_name}'(accepted invitation).")
378
      status = :found
3✔
379
    end
380
    redirect_to course_assignment_path(current_course, @assignment), status: status
6✔
381
  end
382

383
  def decline_invitation
1✔
384
    # TODO: make this a member route in a new GroupingsController
385
    @assignment = Assignment.find(params[:assignment_id])
3✔
386
    @grouping = @assignment.groupings.find(params[:grouping_id])
3✔
387
    begin
388
      @grouping.decline_invitation(current_role)
3✔
389
    rescue RuntimeError => e
390
      flash_message(:error, e.message)
2✔
391
      status = :unprocessable_entity
2✔
392
    else
393
      m_logger = MarkusLogger.instance
1✔
394
      m_logger.log("Student '#{current_role.user_name}' declined invitation for group '#{@grouping.group.group_name}'.")
1✔
395
      status = :found
1✔
396
    end
397
    redirect_to course_assignment_path(current_course, @assignment), status: status
3✔
398
  end
399

400
  def create
1✔
401
    @assignment = Assignment.find(params[:assignment_id])
1✔
402
    @student = current_role
1✔
403
    m_logger = MarkusLogger.instance
1✔
404
    begin
405
      return unless flash_allowance(:error, allowance_to(:create_group?, @assignment)).value
1✔
UNCOV
406
      if params[:workalone]
×
UNCOV
407
        return unless flash_allowance(:error, allowance_to(:work_alone?, @assignment)).value
×
408
        @student.create_group_for_working_alone_student(@assignment.id)
×
409
      else
410
        return unless flash_allowance(:error, allowance_to(:autogenerate_group_name?, @assignment)).value
×
UNCOV
411
        @student.create_autogenerated_name_group(@assignment)
×
412
      end
413
      m_logger.log("Student '#{@student.user_name}' created group.", MarkusLogger::INFO)
×
414
    rescue RuntimeError => e
415
      flash_message(:error, e.message)
×
UNCOV
416
      m_logger.log("Failed to create group. User: '#{@student.user_name}', Error: '#{e.message}'.", MarkusLogger::ERROR)
×
417
    end
418
  ensure
419
    redirect_to course_assignment_path(current_course, @assignment)
1✔
420
  end
421

422
  def destroy
1✔
423
    @assignment = Assignment.find(params[:assignment_id])
1✔
424
    @grouping = current_role.accepted_grouping_for(@assignment.id)
1✔
425
    m_logger = MarkusLogger.instance
1✔
426
    if @grouping.nil?
1✔
427
      m_logger.log('Failed to delete group, since no accepted group for this user existed.' \
1✔
428
                   "User: '#{current_role.user_name}'.", MarkusLogger::ERROR)
429
      flash_message(:error, I18n.t('groups.destroy.errors.do_not_have_a_group'))
1✔
430
      redirect_to course_assignment_path(current_course, @assignment)
1✔
431
      return
1✔
432
    end
UNCOV
433
    if flash_allowance(:error, allowance_to(:destroy?, @grouping)).value
×
434
      begin
435
        Repository.get_class.update_permissions_after(only_on_request: true) do
×
UNCOV
436
          @grouping.student_memberships.each do |member|
×
437
            @grouping.remove_member(member.id)
×
438
          end
439
        end
UNCOV
440
        @grouping.destroy
×
UNCOV
441
        flash_message(:success, I18n.t('flash.actions.destroy.success', resource_name: Group.model_name.human))
×
442
        m_logger.log("Student '#{current_role.user_name}' deleted group '" \
×
443
                     "#{@grouping.group.group_name}'.", MarkusLogger::INFO)
444
      rescue RuntimeError => e
UNCOV
445
        m_logger.log("Failed to delete group '#{@grouping.group.group_name}'. User: '" \
×
446
                     "#{current_role.user_name}', Error: '#{e.message}'.", MarkusLogger::ERROR)
447
      end
448
    end
UNCOV
449
    redirect_to course_assignment_path(current_course, @assignment)
×
450
  end
451

452
  def invite_member
1✔
453
    @assignment = Assignment.find(params[:assignment_id])
4✔
454

455
    @grouping = current_role.accepted_grouping_for(@assignment.id)
4✔
456
    if @grouping.nil?
4✔
UNCOV
457
      flash_message(:error,
×
458
                    I18n.t('groups.invite_member.errors.need_to_create_group'))
459
      redirect_to course_assignment_path(@course, @assignment)
×
UNCOV
460
      return
×
461
    end
462
    if flash_allowance(:error, allowance_to(:invite_member?, @grouping)).value
4✔
463
      to_invite = params[:invite_member].split(',')
4✔
464
      errors = @grouping.invite(to_invite)
4✔
465
      if errors.blank?
4✔
466
        to_invite.each do |i|
4✔
467
          i = i.strip
6✔
468
          invited_user = current_course.students.joins(:user).where(hidden: false).find_by('users.user_name': i)
6✔
469
          if invited_user.receives_invite_emails?
6✔
470
            NotificationMailer.with(inviter: current_role,
4✔
471
                                    invited: invited_user,
472
                                    grouping: @grouping).grouping_invite_email.deliver_later
473
          end
474
        end
475
        flash_message(:success, I18n.t('groups.invite_member.success'))
4✔
476
      else
UNCOV
477
        flash_message(:error, errors.join(' '))
×
478
      end
479
    end
480
    redirect_to course_assignment_path(current_course, @assignment.id)
4✔
481
  end
482

483
  # Deletes pending invitations
484
  def disinvite_member
1✔
485
    assignment = Assignment.find(params[:assignment_id])
4✔
486
    membership = assignment.student_memberships.find(params[:membership])
4✔
487
    authorized = flash_allowance(:error, allowance_to(:disinvite_member?,
4✔
488
                                                      membership.grouping,
489
                                                      context: { membership: membership })).value
490
    if authorized
4✔
491
      disinvited_student = membership.role
1✔
492
      membership.destroy
1✔
493
      m_logger = MarkusLogger.instance
1✔
494
      m_logger.log("Student '#{current_role.user_name}' cancelled invitation for '#{disinvited_student.user_name}'.")
1✔
495
      flash_message(:success, I18n.t('groups.members.member_disinvited'))
1✔
496
    end
497
    status = authorized ? :found : :forbidden
4✔
498
    redirect_to course_assignment_path(current_course, assignment.id), status: status
4✔
499
  end
500

501
  # Deletes memberships which have been declined by students
502
  def delete_rejected
1✔
503
    @assignment = Assignment.find(params[:assignment_id])
5✔
504
    membership = @assignment.student_memberships.find(params[:membership])
5✔
505
    grouping = membership.grouping
5✔
506
    authorized = flash_allowance(:error,
5✔
507
                                 allowance_to(:delete_rejected?, grouping, context: { membership: membership })).value
508
    membership.destroy if authorized
5✔
509
    status = authorized ? :found : :forbidden
5✔
510
    redirect_to course_assignment_path(current_course, @assignment), status: status
5✔
511
  end
512

513
  # These actions act on all currently selected students & groups
514
  def global_actions
1✔
515
    assignment = Assignment.includes([{ groupings: [{ student_memberships: :role, ta_memberships: :role }, :group] }])
9✔
516
                           .find(params[:assignment_id])
517
    grouping_ids = params[:groupings]
9✔
518
    student_ids = params[:students]
9✔
519
    students_to_remove = params[:students_to_remove]
9✔
520

521
    # Start exception catching. If an exception is raised,
522
    # return http response code of 400 (bad request) along
523
    # the error string. The front-end should get it and display
524
    # the message in an error div.
525
    begin
526
      groupings = Grouping.where(id: grouping_ids)
9✔
527
      check_for_groupings(groupings)
9✔
528

529
      # Students are only needed for assign/unassign so don't
530
      # need to check.
531
      students = Student.where(id: student_ids)
9✔
532

533
      case params[:global_actions]
9✔
534
      when 'delete'
535
        delete_groupings(groupings)
2✔
536
      when 'invalid'
537
        invalidate_groupings(groupings)
1✔
538
      when 'valid'
539
        validate_groupings(groupings)
1✔
540
      when 'assign'
541
        add_members(students, groupings, assignment)
2✔
542
      when 'unassign'
543
        remove_members(students_to_remove, groupings)
3✔
544
      end
545
      head :ok
8✔
546
    rescue StandardError => e
547
      flash_now(:error, e.message)
1✔
548
      head :bad_request
1✔
549
    end
550
  end
551

552
  def download_starter_file
1✔
553
    assignment = Assignment.find(params[:assignment_id])
6✔
554
    grouping = current_role.accepted_grouping_for(assignment.id)
6✔
555

556
    authorize! grouping, with: GroupingPolicy
6✔
557

558
    grouping.reset_starter_file_entries if grouping.starter_file_changed
4✔
559

560
    zip_name = "#{assignment.short_identifier}-starter-files-#{current_role.user_name}"
4✔
561
    zip_path = File.join('tmp', zip_name + '.zip')
4✔
562
    FileUtils.rm_rf zip_path
4✔
563
    Zip::File.open(zip_path, create: true) do |zip_file|
4✔
564
      grouping.starter_file_entries.reload.each { |entry| entry.add_files_to_zip_file(zip_file) }
12✔
565
    end
566
    send_file zip_path, filename: File.basename(zip_path)
4✔
567
  end
568

569
  def populate_repo_with_starter_files
1✔
570
    assignment = Assignment.find(params[:assignment_id])
19✔
571
    grouping = current_role.accepted_grouping_for(assignment.id)
19✔
572

573
    authorize! grouping, with: GroupingPolicy
19✔
574

575
    grouping.reset_starter_file_entries if grouping.starter_file_changed
16✔
576

577
    grouping.access_repo do |repo|
16✔
578
      txn = repo.get_transaction(current_role.user_name)
16✔
579
      grouping.starter_file_entries.reload.each { |entry| entry.add_files_to_repo(repo, txn) }
48✔
580
      if repo.commit(txn)
16✔
581
        flash_message(:success, I18n.t('assignments.starter_file.populate_repo_success'))
16✔
582
      else
UNCOV
583
        flash_message(:error, I18n.t('assignments.starter_file.populate_repo_error'))
×
584
      end
585
    end
586
    redirect_to course_assignment_path(current_course, assignment)
16✔
587
  end
588

589
  private
1✔
590

591
  # These methods are called through global actions.
592

593
  # Check that there is at least one grouping selected
594
  def check_for_groupings(groupings)
1✔
595
    if groupings.blank?
9✔
UNCOV
596
      raise I18n.t('groups.select_a_group')
×
597
    end
598
  end
599

600
  # Given a list of grouping, sets their group status to invalid if possible
601
  def invalidate_groupings(groupings)
1✔
602
    groupings.each(&:invalidate_grouping)
1✔
603
  end
604

605
  # Given a list of grouping, sets their group status to valid if possible
606
  def validate_groupings(groupings)
1✔
607
    groupings.each(&:validate_grouping)
1✔
608
  end
609

610
  # Deletes the given list of groupings if possible. Removes each member first.
611
  def delete_groupings(groupings)
1✔
612
    # If any groupings have a submission raise an error.
613
    if groupings.any?(&:has_submission?)
2✔
614
      raise I18n.t('groups.could_not_delete') # should add names of grouping we could not delete
1✔
615
    else
616
      # Remove each student from every group.
617
      Repository.get_class.update_permissions_after(only_on_request: true) do
1✔
618
        groupings.each do |grouping|
1✔
619
          grouping.student_memberships.each do |mem|
1✔
620
            grouping.remove_member(mem.id)
1✔
621
          end
622
          grouping.delete_grouping
1✔
623
        end
624
      end
625
    end
626
  end
627

628
  # Adds students to grouping. `groupings` should be an array with
629
  # only one element, which is the grouping that is supposed to be
630
  # added to.
631
  def add_members(students, groupings, assignment)
1✔
632
    if groupings.size != 1
2✔
UNCOV
633
      raise I18n.t('groups.select_only_one_group')
×
634
    end
635
    if students.blank?
2✔
UNCOV
636
      raise I18n.t('groups.select_a_student')
×
637
    end
638

639
    grouping = groupings.first
2✔
640

641
    students.each do |student|
2✔
642
      add_member(student, grouping, assignment)
4✔
643
    end
644

645
    # Generate warning if the number of people assigned to a group exceeds
646
    # the maximum size of a group
647
    students_in_group = grouping.student_membership_number
2✔
648
    group_name = grouping.group.group_name
2✔
649
    if assignment.student_form_groups && (students_in_group > assignment.group_max)
2✔
UNCOV
650
      raise I18n.t('groups.assign_over_limit', group: group_name)
×
651
    end
652
  end
653

654
  # Adds the student given in student_id to the grouping given in grouping
655
  def add_member(student, grouping, assignment)
1✔
656
    set_membership_status = if grouping.student_memberships.empty?
4✔
UNCOV
657
                              StudentMembership::STATUSES[:inviter]
×
658
                            else
659
                              StudentMembership::STATUSES[:accepted]
4✔
660
                            end
661
    @bad_user_names = []
4✔
662

663
    if student.hidden
4✔
UNCOV
664
      raise I18n.t('groups.invite_member.errors.not_found', user_name: student.user_name)
×
665
    end
666
    if student.has_accepted_grouping_for?(assignment.id)
4✔
UNCOV
667
      raise I18n.t('groups.invite_member.errors.already_grouped', user_name: student.user_name)
×
668
    end
669
    errors = grouping.invite(student.user_name, set_membership_status, invoked_by_instructor: true)
4✔
670
    grouping.reload
4✔
671

672
    if errors.present?
4✔
UNCOV
673
      raise errors.join(' ')
×
674
    end
675

676
    # Generate a warning if a member is added to a group and they
677
    # have fewer grace days credits than already used by that group
678
    if student.remaining_grace_credits < grouping.grace_period_deduction_single
4✔
UNCOV
679
      @warning_grace_day = I18n.t('groups.grace_day_over_limit', group: grouping.group.group_name)
×
680
    end
681

682
    grouping.reload
4✔
683
  end
684

685
  # Removes the students with user names in +member_names+ from the
686
  # groupings in +groupings+. This removes any type of student membership
687
  # even pending memberships.
688
  #
689
  # This is meant to be called with the params from global_actions
690
  def remove_members(member_names, groupings)
1✔
691
    members_to_remove = current_course.students.joins(:user).where('users.user_name': member_names)
3✔
692
    Repository.get_class.update_permissions_after(only_on_request: true) do
3✔
693
      members_to_remove.each do |member|
3✔
694
        groupings.each do |grouping|
2✔
695
          membership = grouping.student_memberships.find_by(role_id: member.id)
2✔
696
          remove_member(membership, grouping)
2✔
697
        end
698
      end
699
    end
700
  end
701

702
  # Removes the given student membership from the given grouping
703
  def remove_member(membership, grouping)
1✔
704
    grouping.remove_member(membership.id)
2✔
705
    grouping.reload
2✔
706
  end
707

708
  # This override is necessary because this controller is acting as a controller
709
  # for both groups and groupings.
710
  #
711
  # TODO: move all grouping methods into their own controller and remove this
712
  def record
1✔
713
    @record ||= Grouping.find_by(id: request.path_parameters[:id]) if request.path_parameters[:id]
147✔
714
  end
715
end
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