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

MarkUsProject / Markus / 26613409730

29 May 2026 01:58AM UTC coverage: 90.279% (+0.002%) from 90.277%
26613409730

Pull #7965

github

web-flow
Merge 24b298f37 into 5b70aab6e
Pull Request #7965: fix: missing null and presence checks

971 of 2151 branches covered (45.14%)

Branch coverage included in aggregate %.

61 of 61 new or added lines in 32 files covered. (100.0%)

5 existing lines in 1 file now uncovered.

46059 of 49943 relevant lines covered (92.22%)

121.69 hits per line

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

79.87
/app/controllers/grade_entry_forms_controller.rb
1
# The actions necessary for managing grade entry forms.
2

3
class GradeEntryFormsController < ApplicationController
1✔
4
  include RoutingHelper
1✔
5

6
  before_action { authorize! }
99✔
7
  layout 'assignment_content'
1✔
8

9
  responders :flash
1✔
10

11
  # Create a new grade entry form
12
  def new
1✔
13
    @grade_entry_form = current_course.grade_entry_forms.new
2✔
14
  end
15

16
  def create
1✔
17
    # Edit params before updating model
18
    new_params = update_grade_entry_form_params(params)
2✔
19

20
    @grade_entry_form = current_course.grade_entry_forms.create(new_params)
2✔
21
    respond_with(@grade_entry_form,
2✔
22
                 location: -> { edit_course_grade_entry_form_path(current_course, @grade_entry_form) })
2✔
23
  end
24

25
  # Edit the properties of a grade entry form
26
  def edit
1✔
27
    @grade_entry_form = record
2✔
28
  end
29

30
  def update
1✔
31
    @grade_entry_form = record
8✔
32
    # Process changes to input properties
33
    new_params = update_grade_entry_form_params(params)
8✔
34

35
    @grade_entry_form.update(new_params)
8✔
36
    respond_with(@grade_entry_form,
8✔
37
                 location: -> { edit_course_grade_entry_form_path current_course, @grade_entry_form })
8✔
38
  end
39

40
  # View/modify the grades for this grade entry form
41
  def grades
1✔
42
    @grade_entry_form = record
×
43
  end
44

45
  def view_summary
1✔
46
    @grade_entry_form = record
×
47
  end
48

49
  # Update a grade in the table
50
  def update_grade
1✔
51
    grade_entry_form = record
×
52
    grade_entry_student =
53
      grade_entry_form.grade_entry_students.find(params[:student_id])
×
54
    grade =
55
      grade_entry_student.grades.find_or_create_by(grade_entry_item_id: params[:grade_entry_item_id])
×
56

57
    grade.update(grade: params[:updated_grade])
×
58
    grade_entry_student.save # Refresh total grade
×
59
    grade_entry_student.reload
×
60
    render plain: grade_entry_student.get_total_grade
×
61
  end
62

63
  # For students
64
  def student_interface
1✔
65
    @grade_entry_form = record
2✔
66
    unless allowed_to?(:see_hidden?)
2✔
67
      render 'shared/http_status',
1✔
68
             formats: [:html],
69
             locals: {
70
               code: '404',
71
               message: HttpStatusHelper::ERROR_CODE['message']['404']
72
             },
73
             status: :not_found,
74
             layout: false
75
      return
1✔
76
    end
77

78
    # Getting the student's marks for each grade entry item
79
    @grade_entry_student = @grade_entry_form.grade_entry_students.find_by(role_id: current_role.id)
1✔
80
    @columns = []
1✔
81
    @data = []
1✔
82
    @item_percentages = []
1✔
83
    @labels = []
1✔
84
    @grade_entry_form.grade_entry_items.each do |grade_entry_item|
1✔
85
      @columns << "#{grade_entry_item.name} (#{grade_entry_item.out_of})"
×
86
      @labels << grade_entry_item.name
×
87
      mark = @grade_entry_student.grades.find_by(grade_entry_item_id: grade_entry_item.id)
×
88
      if !mark.nil? && !mark.grade.nil?
×
89
        @data << mark.grade
×
90
        @item_percentages << ((mark.grade * 100) / grade_entry_item.out_of).round(2)
×
91
      else
92
        @data << t(:not_applicable)
×
93
        @item_percentages << nil
×
94
      end
95
    end
96

97
    # Get data for the total marks column
98
    if @grade_entry_form.show_total
1✔
99
      @columns << "#{GradeEntryForm.human_attribute_name(:total)} (#{@grade_entry_form.max_mark})"
×
100
      total = @grade_entry_student.get_total_grade
×
101
      if !total.nil?
×
102
        @data << total
×
103
      else
104
        @data << t(:not_applicable)
×
105
      end
106
    end
107
  end
108

109
  def get_mark_columns
1✔
110
    grade_entry_form = record
×
111
    data = grade_entry_form.grade_entry_items.map do |column|
×
112
      {
113
        accessor: column.id.to_s,
×
114
        Header: "#{column.name} (#{column.out_of})",
115
        data: { bonus: column.bonus }
116
      }
117
    end
118
    render json: data
×
119
  end
120

121
  def populate_grades_table
1✔
122
    grade_entry_form = record
13✔
123
    student_pluck_attrs = [
124
      Arel.sql('grade_entry_students.id as _id'),
13✔
125
      :released_to_student,
126
      Arel.sql('users.user_name as user_name'),
127
      Arel.sql('users.first_name as first_name'),
128
      Arel.sql('users.last_name as last_name'),
129
      Arel.sql('roles.hidden as hidden'),
130
      Arel.sql('roles.section_id as section_id')
131
    ]
132

133
    if current_role.instructor?
13✔
134
      students = grade_entry_form.grade_entry_students
6✔
135
                                 .joins(:user)
136
                                 .pluck_to_hash(*student_pluck_attrs)
137
      grades = grade_entry_form.grade_entry_students
6✔
138
                               .joins(:grades)
139
                               .pluck(:id, 'grades.grade_entry_item_id', 'grades.grade')
140
                               .group_by { |x| x[0] }
6✔
141
    elsif current_role.ta?
7✔
142
      students = current_role.grade_entry_students
7✔
143
                             .where(grade_entry_form: grade_entry_form)
144
                             .joins(:user)
145
                             .pluck_to_hash(*student_pluck_attrs)
146
      grades = current_role.grade_entry_students
7✔
147
                           .where(grade_entry_form: grade_entry_form)
148
                           .joins(:grades)
149
                           .pluck(:id, 'grades.grade_entry_item_id', 'grades.grade')
150
                           .group_by { |x| x[0] }
5✔
151
    end
152

153
    if grade_entry_form.show_total
13✔
154
      total_grades = GradeEntryStudent.get_total_grades(students.pluck(:_id))
2✔
155
    end
156

157
    student_grades = students.map do |s|
13✔
158
      (grades[s[:_id]] || []).each do |_, grade_entry_item_id, grade|
11✔
159
        s[grade_entry_item_id] = grade
11✔
160
      end
161
      if grade_entry_form.show_total
11✔
162
        total_marks = total_grades[s[:_id]]
2✔
163
        s[:total_marks] = total_marks.nil? ? t(:not_applicable) : total_marks
2✔
164
      end
165
      s
11✔
166
    end
167
    render json: { data: student_grades,
13✔
168
                   sections: current_course.sections.pluck(:id, :name).to_h }
169
  end
170

171
  # Release/unrelease the marks for all the students or for a subset of students
172
  def update_grade_entry_students
1✔
173
    if params[:students].blank?
8✔
174
      flash_message(:warning, I18n.t('grade_entry_forms.grades.select_a_student'))
×
175
    else
176
      grade_entry_form = record
8✔
177
      release = params[:release_results] == 'true'
8✔
178
      GradeEntryStudent.transaction do
8✔
179
        data = record.course
8✔
180
                     .students
181
                     .joins(:grade_entry_students)
182
                     .where('grade_entry_students.assessment_id': grade_entry_form.id,
183
                            'grade_entry_students.id': params[:students])
184
                     .pluck('grade_entry_students.id', 'roles.id')
185
                     .map { |ges_id, r_id| { id: ges_id, role_id: r_id, released_to_student: release } }
14✔
186
        GradeEntryStudent.upsert_all(data)
8✔
UNCOV
187
        num_changed = data.length
×
UNCOV
188
        flash_message(:success, I18n.t('grade_entry_forms.grades.successfully_changed',
×
189
                                       numGradeEntryStudentsChanged: num_changed))
UNCOV
190
        action = release ? 'released' : 'unreleased'
×
UNCOV
191
        log_message = "#{action} #{num_changed} for marks spreadsheet '#{grade_entry_form.short_identifier}'."
×
UNCOV
192
        MarkusLogger.instance.log(log_message)
×
193
      rescue StandardError => e
194
        flash_message(:error, e.message)
8✔
195
        raise ActiveRecord::Rollback
8✔
196
      end
197
      GradeEntryStudent.where(id: params[:students]).includes(:role).find_each do |current_student|
8✔
198
        if current_student.role.receives_results_emails?
14✔
199
          NotificationMailer.with(student: current_student, form: grade_entry_form, course: current_course)
8✔
200
                            .release_spreadsheet_email.deliver_later
201
        end
202
      end
203
    end
204
  end
205

206
  # Download the grades for this grade entry form as a CSV file
207
  def download
1✔
208
    grade_entry_form = record
8✔
209
    send_data grade_entry_form.export_as_csv(current_role),
8✔
210
              disposition: 'attachment',
211
              type: 'text/csv',
212
              filename: "#{grade_entry_form.short_identifier}_grades_report.csv"
213
  end
214

215
  # Upload the grades for this grade entry form using a CSV file
216
  def upload
1✔
217
    @grade_entry_form = record
19✔
218
    begin
219
      data = process_file_upload(['.csv'])
19✔
220
    rescue StandardError => e
221
      flash_message(:error, e.message)
3✔
222
    else
223
      overwrite = params[:overwrite]
16✔
224
      grades_file = data[:contents]
16✔
225
      result = @grade_entry_form.from_csv(grades_file, overwrite)
16✔
226
      flash_csv_result(result)
16✔
227
    end
228
    redirect_to action: 'grades', id: @grade_entry_form.id
19✔
229
  end
230

231
  def grade_distribution
1✔
232
    grade_entry_form = record
8✔
233

234
    intervals = 20
8✔
235
    dict_data = grade_entry_form.grade_entry_items.map do |item|
8✔
236
      { label: item.name, data: item.grade_distribution_array(intervals) }
8✔
237
    end
238
    column_distributions = {
239
      labels: (0..(intervals - 1)).map { |i| "#{5 * i}-#{5 * i + 5}" },
176✔
240
      datasets: dict_data
241
    }
242

243
    grade_distribution = {
244
      labels: (0..(intervals - 1)).map { |i| "#{5 * i}-#{5 * i + 5}" },
176✔
245
      datasets: [{ data: grade_entry_form.grade_distribution_array(intervals) }]
246
    }
247

248
    summary = {
249
      name: "#{grade_entry_form.short_identifier}: #{grade_entry_form.description}",
8✔
250
      average: grade_entry_form.results_average(points: true) || 0,
251
      median: grade_entry_form.results_median(points: true) || 0,
252
      standard_deviation: grade_entry_form.results_standard_deviation || 0,
253
      max_mark: grade_entry_form.max_mark,
254
      num_entries: grade_entry_form.count_non_nil,
255
      groupings_size: grade_entry_form.grade_entry_students.joins(:role).where('roles.hidden': false).count,
256
      num_fails: grade_entry_form.results_fails,
257
      num_zeros: grade_entry_form.results_zeros
258
    }
259

260
    column_summary = grade_entry_form.grade_entry_items.map do |item|
8✔
261
      {
262
        name: item.name,
8✔
263
        average: item.average || 0,
264
        median: item.median || 0,
265
        max_mark: item.out_of || 0,
266
        standard_deviation: item.standard_deviation || 0,
267
        position: item.position,
268
        num_zeros: item.grades_array.count(&:zero?)
269
      }
270
    end
271

272
    render json: {
8✔
273
      grade_distribution: grade_distribution,
274
      column_distributions: column_distributions,
275
      summary: summary,
276
      column_summary: column_summary
277
    }
278
  end
279

280
  def switch
1✔
281
    redirect_options = referer_options
17✔
282

283
    if redirect_options[:controller] == 'grade_entry_forms'
17✔
284
      redirect_options[:id] = params[:id]
3✔
285
      redirect_to redirect_options
3✔
286
    elsif redirect_options[:grade_entry_form_id]
14✔
287
      redirect_options[:grade_entry_form_id] = params[:id]
2✔
288
      redirect_to redirect_options
2✔
289
    elsif current_role.instructor?
12✔
290
      redirect_to edit_course_grade_entry_form_path(current_course, params[:id])
4✔
291
    elsif current_role.ta?
8✔
292
      redirect_to grades_course_grade_entry_form_path(current_course, params[:id])
4✔
293
    else # current_role.student?
294
      redirect_to student_interface_course_grade_entry_form_path(current_course, params[:id])
4✔
295
    end
296
  end
297

298
  def destroy
1✔
299
    @grade_entry_form = record
2✔
300
    begin
301
      @grade_entry_form.destroy!
2✔
302
      redirect_to course_assignments_path(@current_course)
1✔
303
      flash_message(:success, t('grade_entry_forms.successfully_deleted'))
1✔
304
    rescue ActiveRecord::RecordNotDestroyed
305
      redirect_to edit_course_grade_entry_form_path(@current_course, params[:id])
1✔
306
      flash_message(:error, t('grade_entry_forms.failed_deletion'))
1✔
307
    end
308
  end
309

310
  # Helper functions
311
  private
1✔
312

313
  # Removes items that have empty names (so they don't get updated)
314
  def update_grade_entry_form_params(attributes)
1✔
315
    grade_entry_items =
316
      params[:grade_entry_form][:grade_entry_items_attributes]
10✔
317

318
    unless grade_entry_items.nil?
10✔
319
      # Delete items with empty name and out_of
320
      grade_entry_items.delete_if { |_, item| item[:name].empty? && item[:out_of].empty? }
18✔
321
      # Update the attributes hash
322
      max_position = 1
4✔
323
      grade_entry_items.each_value do |item|
4✔
324
        # Some items are being deleted so don't update those
325
        unless item[:_destroy] == 1
6✔
326
          item[:position] = max_position
6✔
327
          max_position += 1
6✔
328
        end
329
      end
330
    end
331
    attributes[:grade_entry_items_attributes] = grade_entry_items
10✔
332
    grade_entry_form_params(attributes)
10✔
333
  end
334

335
  def grade_entry_form_params(attributes)
1✔
336
    attributes.require(:grade_entry_form)
10✔
337
              .permit(:description,
338
                      :message,
339
                      :due_date,
340
                      :show_total,
341
                      :short_identifier,
342
                      :is_hidden,
343
                      grade_entry_items_attributes: [:name,
344
                                                     :out_of,
345
                                                     :position,
346
                                                     :bonus,
347
                                                     :_destroy,
348
                                                     :id])
349
  end
350
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