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

MarkUsProject / Markus / 23815623133

31 Mar 2026 07:30PM UTC coverage: 91.726% (+0.01%) from 91.713%
23815623133

Pull #7886

github

web-flow
Merge e6d1f2cd3 into 166782a29
Pull Request #7886: TICKET-604: Return structured JSON from grade entry forms show endpoint

948 of 1842 branches covered (51.47%)

Branch coverage included in aggregate %.

106 of 108 new or added lines in 2 files covered. (98.15%)

23 existing lines in 3 files now uncovered.

45258 of 48532 relevant lines covered (93.25%)

130.06 hits per line

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

74.42
/app/controllers/api/grade_entry_forms_controller.rb
1
module Api
1✔
2
  class GradeEntryFormsController < MainApiController
1✔
3
    DEFAULT_FIELDS = [:id, :short_identifier, :description, :due_date, :is_hidden, :visible_on, :visible_until,
1✔
4
                      :show_total].freeze
5
    STUDENT_FIELDS = [:user_name, :last_name, :first_name, :id_number, :email].freeze
1✔
6

7
    # Returns the specified grade entry form
8
    # Requires: id
9
    # Default: returns CSV export (backward compatible)
10
    # Optional: .json extension or Accept: application/json header for structured JSON with student grades
11
    #           user_name (filter to a single student, JSON only)
12
    def show
1✔
13
      grade_entry_form = record
7✔
14
      if grade_entry_form.nil?
7✔
NEW
15
        render 'shared/http_status', locals: { code: '404', message:
×
16
          'Grade Entry Form not found' }, status: :not_found
NEW
17
        return
×
18
      end
19

20
      if request.format.json?
7✔
21
        render_json_show(grade_entry_form)
6✔
22
        return
6✔
23
      end
24

25
      send_data grade_entry_form.export_as_csv(current_role),
1✔
26
                type: 'text/csv',
27
                filename: "#{grade_entry_form.short_identifier}_grades_report.csv",
28
                disposition: 'inline'
29
    end
30

31
    def index
1✔
32
      grade_entry_forms = get_collection(current_course.grade_entry_forms) || return
18✔
33

34
      include_args = { only: DEFAULT_FIELDS, include: { grade_entry_items: { only: [:id, :name, :out_of] } } }
18✔
35

36
      respond_to do |format|
18✔
37
        format.xml do
18✔
38
          xml = grade_entry_forms.to_xml(**include_args, root: 'grade_entry_forms', skip_types: 'true')
9✔
39
          render xml: xml
9✔
40
        end
41
        format.json { render json: grade_entry_forms.to_json(include_args) }
27✔
42
      end
43
    end
44

45
    # create a new grade entry form
46
    # Requires:
47
    #   :short_identifier, :description
48
    # Optional:
49
    #   :description, :due_date, :is_hidden
50
    #   grade_entry_items:
51
    #     :name, :out_of, :bonus
52
    def create
1✔
53
      if has_missing_params?([:short_identifier])
10✔
54
        # incomplete/invalid HTTP params
55
        render 'shared/http_status', locals: { code: '422', message:
2✔
56
          HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content
57
        return
2✔
58
      end
59

60
      # check if there is an existing assignment
61
      form = current_course.grade_entry_forms.find_by(short_identifier: params[:short_identifier])
8✔
62
      unless form.nil?
8✔
63
        render 'shared/http_status', locals: { code: '409', message:
1✔
64
          'Grade Entry Form already exists' }, status: :conflict
65
        return
1✔
66
      end
67

68
      ApplicationRecord.transaction do
7✔
69
        create_params = params.permit(*DEFAULT_FIELDS)
7✔
70
        create_params[:is_hidden] ||= false
7✔
71
        create_params[:description] ||= ''
7✔
72
        create_params[:course_id] = params[:course_id]
7✔
73
        new_form = GradeEntryForm.new(create_params)
7✔
74
        unless new_form.save
7✔
75
          render 'shared/http_status', locals: { code: '422', message:
3✔
76
            new_form.errors.full_messages.first }, status: :unprocessable_content
77
          raise ActiveRecord::Rollback
3✔
78
        end
79

80
        params[:grade_entry_items]&.each&.with_index do |column_params, i|
4✔
UNCOV
81
          column_params = column_params.permit(:name, :out_of, :bonus).to_h.symbolize_keys
×
UNCOV
82
          grade_item = new_form.grade_entry_items.build(**column_params, position: i + 1)
×
UNCOV
83
          unless grade_item.save
×
84
            render 'shared/http_status', locals: { code: '422', message:
×
85
              grade_item.errors.full_messages.first }, status: :unprocessable_content
86
            raise ActiveRecord::Rollback
×
87
          end
88
        end
89
        render 'shared/http_status', locals: { code: '201', message:
4✔
90
          HttpStatusHelper::ERROR_CODE['message']['201'] }, status: :created
91
      end
92
    end
93

94
    # create a new grade entry form
95
    # params:
96
    #   :short_identifier, :description, :date, :is_hidden
97
    #   grade_entry_items:
98
    #     :id, :name, :out_of, :bonus
99
    #
100
    # if the grade_entry_items id param is set, an existing item will be
101
    # updated, otherwise a new grade_entry_item will be created
102
    def update
1✔
103
      # check if there is an existing assignment
104
      form = record
2✔
105
      if form.nil?
2✔
UNCOV
106
        render 'shared/http_status', locals: { code: '404', message:
×
107
          'Grade Entry Form not found' }, status: :not_found
UNCOV
108
        return
×
109
      end
110

111
      ApplicationRecord.transaction do
2✔
112
        update_params = params.permit(*DEFAULT_FIELDS)
2✔
113
        unless form.update(update_params)
2✔
114
          render 'shared/http_status', locals: { code: '500', message:
1✔
115
            form.errors.full_messages.first }, status: :internal_server_error
116
          raise ActiveRecord::Rollback
1✔
117
        end
118

119
        position = form.grade_entry_items.count
1✔
120
        params[:grade_entry_items]&.each do |column_params|
1✔
UNCOV
121
          if column_params[:id].nil?
×
UNCOV
122
            column_params = column_params.permit(:name, :out_of, :bonus).to_h.symbolize_keys
×
UNCOV
123
            grade_item = form.grade_entry_items.build(**column_params, position: position += 1)
×
124
            unless grade_item.save
×
125
              render 'shared/http_status', locals: { code: '500', message:
×
126
                grade_item.errors.full_messages.first }, status: :internal_server_error
127
              raise ActiveRecord::Rollback
×
128
            end
129
          else
130
            column_params = column_params.permit(:id, :name, :out_of, :bonus).to_h.symbolize_keys
×
UNCOV
131
            grade_item = form.grade_entry_items.where(id: column_params[:id]).first
×
UNCOV
132
            if grade_item.nil?
×
133
              render 'shared/http_status', locals: { code: '404', message:
×
134
                "Grade Entry Item with id=#{column_params[:id]} not found" }, status: :not_found
135
              raise ActiveRecord::Rollback
×
136
            end
UNCOV
137
            unless grade_item.update(column_params)
×
138
              render 'shared/http_status', locals: { code: '500', message:
×
139
                grade_item.errors.full_messages.first }, status: :internal_server_error
140
              raise ActiveRecord::Rollback
×
141
            end
142
          end
143
        end
144
        render 'shared/http_status', locals: { code: '200', message:
1✔
145
          HttpStatusHelper::ERROR_CODE['message']['200'] }, status: :ok
146
      end
147
    end
148

149
    def update_grades
1✔
150
      if has_missing_params?([:user_name, :grade_entry_items])
3✔
151
        # incomplete/invalid HTTP params
UNCOV
152
        render 'shared/http_status', locals: { code: '422', message:
×
153
          HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content
UNCOV
154
        return
×
155
      end
156

157
      grade_entry_form = record
3✔
158

159
      grade_entry_student = grade_entry_form.grade_entry_students
3✔
160
                                            .joins(:user)
161
                                            .where('users.user_name': params[:user_name])
162
                                            .first
163

164
      if grade_entry_student.nil?
3✔
165
        # There is no student with that user_name
UNCOV
166
        render 'shared/http_status', locals: { code: '422', message:
×
167
          'There is no student with that user_name' }, status: :unprocessable_content
UNCOV
168
        return
×
169
      end
170

171
      Grade.transaction do
3✔
172
        params[:grade_entry_items].each do |item, score|
3✔
173
          grade_entry_item = GradeEntryItem.find_by(name: item, assessment_id: params[:id])
3✔
174

175
          if grade_entry_item.nil?
3✔
176
            # There is no such grade entry item
UNCOV
177
            render 'shared/http_status', locals: { code: '422', message:
×
178
              "There is no grade entry item named #{item}" }, status: :unprocessable_content
UNCOV
179
            raise ActiveRecord::Rollback
×
180
          end
181
          grade = grade_entry_student.grades.find_or_create_by(grade_entry_item_id: grade_entry_item.id)
3✔
182
          grade.update(grade: score)
3✔
183
        end
184
        if grade_entry_student.save
3✔
185
          render 'shared/http_status', locals: { code: '200', message:
3✔
186
            HttpStatusHelper::ERROR_CODE['message']['200'] }, status: :ok
187
        else
188
          # Some error occurred (including invalid mark)
UNCOV
189
          render 'shared/http_status', locals: { code: '500', message:
×
190
            grade_entry_student.errors.full_messages.first }, status: :internal_server_error
UNCOV
191
          raise ActiveRecord::Rollback
×
192
        end
193
      end
194
    end
195

196
    def destroy
1✔
197
      # check if the grade entry form exists
198
      grade_entry_form = record
2✔
199
      if grade_entry_form.nil?
2✔
UNCOV
200
        render 'shared/http_status', locals: { code: '404', message:
×
201
          'Grade Entry Form not found' }, status: :not_found
UNCOV
202
        return
×
203
      end
204
      # delete the grade entry form
205
      begin
206
        grade_entry_form.destroy!
2✔
207
        render 'shared/http_status',
1✔
208
               locals: { code: '200',
209
                         message: 'Grade Entry Form successfully deleted' }, status: :ok
210
      rescue ActiveRecord::RecordNotDestroyed
211
        render 'shared/http_status',
1✔
212
               locals: { code: :conflict,
213
                         message: 'Grade Entry Form contains non-nil grades' }, status: :conflict
214
      end
215
    end
216

217
    private
1✔
218

219
    def render_json_show(grade_entry_form)
1✔
220
      filter_user = params[:user_name].presence
6✔
221

222
      students = fetch_students(grade_entry_form, filter_user)
6✔
223
      if filter_user && students.empty?
6✔
224
        render 'shared/http_status', locals: { code: '422', message:
1✔
225
          'No student with that user_name' }, status: :unprocessable_content
226
        return
1✔
227
      end
228

229
      grades_by_user = fetch_grades_by_user(grade_entry_form, filter_user)
5✔
230
      total_grades = grade_entry_form.show_total ? GradeEntryStudent.get_total_grades(students.pluck(:ges_id)) : {}
5✔
231

232
      form_data = grade_entry_form.as_json(only: DEFAULT_FIELDS)
5✔
233
      form_data['grade_entry_items'] = grade_entry_form.grade_entry_items.order(:position)
5✔
234
                                                       .as_json(only: [:id, :name, :out_of, :bonus])
235
      form_data['students'] = build_student_data(students, grades_by_user, total_grades,
5✔
236
                                                 include_total: grade_entry_form.show_total)
237

238
      render json: form_data
5✔
239
    end
240

241
    def fetch_students(grade_entry_form, filter_user)
1✔
242
      query = Student.left_outer_joins(:grade_entry_students, :user, :section)
6✔
243
                     .where(hidden: false, 'grade_entry_students.assessment_id': grade_entry_form.id)
244
      query = query.where('users.user_name': filter_user) if filter_user
6✔
245
      query.order(:user_name)
6✔
246
           .pluck_to_hash(:user_name, :last_name, :first_name, 'sections.name as section_name',
247
                          :id_number, :email, 'grade_entry_students.id as ges_id')
248
    end
249

250
    def fetch_grades_by_user(grade_entry_form, filter_user)
1✔
251
      query = grade_entry_form.grades.joins(:grade_entry_item, grade_entry_student: :user)
5✔
252
      query = query.where('users.user_name': filter_user) if filter_user
5✔
253
      query.pluck('users.user_name', 'grade_entry_items.name', :grade)
5✔
254
           .group_by { |user_name, _item, _grade| user_name }
5✔
255
           .transform_values { |rows| rows.to_h { |_user, item, grade| [item, grade] } }
9✔
256
    end
257

258
    def build_student_data(students, grades_by_user, total_grades, include_total:)
1✔
259
      students.map do |student|
5✔
260
        entry = student.slice(*STUDENT_FIELDS).merge(
5✔
261
          section_name: student[:section_name],
262
          grades: grades_by_user.fetch(student[:user_name], {})
263
        )
264
        entry[:total_grade] = total_grades[student[:ges_id]] if include_total
5✔
265
        entry
5✔
266
      end
267
    end
268
  end
269
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