-
Notifications
You must be signed in to change notification settings - Fork 2
feat(2629): Add stat cards to group view #2645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
15f293e
bfeaa4f
59200c7
de63e5d
79a38a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| # rubocop:disable Metrics/ClassLength | ||
| class GroupsController < HtmlController | ||
| include Pagy::Method | ||
|
|
||
|
|
@@ -22,6 +23,9 @@ def show | |
| lesson_url: lesson_path(Lesson.find_by(id: summary.lesson_id)) | ||
| } | ||
| end | ||
| @nr_of_active_students = active_student_count | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So...this is something that's in reality missing from the group. What we can do here is introduce a method for lazy loading active students in the actual def active_students(as_of: Time.zone.today)
Student
.joins(:enrollments)
.merge(enrollments.active(as_of))
.where(enrollments: { group_id: id }, students: { deleted_at: nil })
.distinct
endThen just remove this line and use that in the group view. Also we should add a spec for this so we know it calculates correctly. |
||
| @current_average_score = @group_summaries.last&.dig(:average_mark) | ||
| populate_skill_growth | ||
| end | ||
|
|
||
| def new | ||
|
|
@@ -107,6 +111,43 @@ def confirm_enrollments | |
|
|
||
| private | ||
|
|
||
| def active_student_count | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this whole method after doing the above |
||
| @group.enrollments | ||
| .active | ||
| .joins(:student) | ||
| .where(students: { deleted_at: nil }) | ||
| .distinct | ||
| .count(:student_id) | ||
| end | ||
|
|
||
| def populate_skill_growth | ||
| growths = average_growth_per_skill | ||
| @most_improved_skill = growths.min_by { |g| [-g[:growth], g[:skill_name], g[:skill_id]] } | ||
| @least_improved_skill = growths.min_by { |g| [g[:growth], g[:skill_name], g[:skill_id]] } | ||
| end | ||
|
|
||
| def average_growth_per_skill | ||
| deltas_by_skill = Hash.new { |hash, key| hash[key] = [] } | ||
| marks_by_student_and_skill.each do |(_student_id, skill_id, skill_name), marks| | ||
| next if marks.size < 2 | ||
|
|
||
| deltas_by_skill[[skill_id, skill_name]] << (marks.last - marks.first) | ||
| end | ||
|
|
||
| deltas_by_skill.map { |(skill_id, skill_name), deltas| { skill_id:, skill_name:, growth: deltas.sum.to_f / deltas.size } } | ||
| end | ||
|
|
||
| def marks_by_student_and_skill | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole calculation seeks for all grades connected to a group, which is incorrect. We want the non-deleted grades for the current active students within lessons of this group, since that's what we're showing. So here you would need to again add some checks to see if the grades were valid, for active students only, and are not deleted. Instead of trying to do this whole thing through ruby, maybe create a separate db view called like 'group_statistics' or something, and see if it's faster that way. I'm not sure if it will be, but try and let me know. |
||
| marks = Hash.new { |hash, key| hash[key] = [] } | ||
| Grade.joins(:lesson, :skill) | ||
| .where(deleted_at: nil) | ||
| .where(lessons: { group_id: @group.id, deleted_at: nil }) | ||
| .order('lessons.date ASC') | ||
| .pluck(:student_id, 'skills.id', 'skills.skill_name', :mark) | ||
| .each { |student_id, skill_id, skill_name, mark| marks[[student_id, skill_id, skill_name]] << mark } | ||
| marks | ||
| end | ||
|
|
||
| def group_params | ||
| params.require(:group).permit :group_name, :mlid, :chapter_id | ||
| end | ||
|
|
@@ -121,3 +162,4 @@ def new_params | |
| params.permit :chapter_id | ||
| end | ||
| end | ||
| # rubocop:enable Metrics/ClassLength | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,9 @@ class Enrollment < ApplicationRecord | |
|
|
||
| scope :by_student, ->(student_id) { where student_id: } | ||
| scope :by_group, ->(group_id) { where group_id: } | ||
| # Enrollments that are open at `as_of`: started on/before it and not yet ended (inactive_since is | ||
| # exclusive, matching Student#active_enrollment? and Student.unenrolled_for_organization). | ||
| scope :active, ->(as_of = Time.zone.now) { where('active_since <= ? AND (inactive_since IS NULL OR inactive_since > ?)', as_of, as_of) } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine just remove the comments, the scope is already self-explanatory (Also, these comments seem AI generated, although I'm all for using tools to get the job done, try to refrain from leaving everything up to an agent) |
||
|
|
||
| validates :active_since, presence: true | ||
| validates :inactive_since, comparison: { greater_than: :active_since, message: I18n.t(:enrollment_end_before_start) }, allow_nil: true | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,13 +26,29 @@ end %> | |
| <% end %> | ||
|
|
||
| <section> | ||
| <div class="w-1/2"> | ||
| <%= render CommonComponents::Card.new(title: t(:average_performance_for_last_30_lessons).capitalize) do |card| %> | ||
| <% card.with_card_content do %> | ||
| <div id="group-chart" class="ct-chart ct-octave bg-white full-width"></div> | ||
| <% end %> | ||
| <% end %> | ||
| <div class="flex gap-4"> | ||
| <div class="w-1/2"> | ||
| <%= render CommonComponents::Card.new(title: t(:average_performance_for_last_30_lessons).capitalize) do |card| %> | ||
| <% card.with_card_content do %> | ||
| <div id="group-chart" class="ct-chart ct-octave bg-white full-width"></div> | ||
| <% end %> | ||
| <% end %> | ||
| </div> | ||
| <div class="w-1/2"> | ||
| <div class="mt-6"> | ||
| <%= render CommonComponents::StatCards.new( | ||
| label: t(:overview).capitalize, | ||
| columns: 2, | ||
| stats: [ | ||
| { title: t(:nr_of_active_students), value: @nr_of_active_students }, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change to |
||
| { title: t(:current_average_score_for_group), value: @current_average_score&.round(2) || t(:student_not_graded) }, | ||
| { title: t(:most_improved_skill), value: @most_improved_skill&.dig(:skill_name) || t(:student_not_graded) }, | ||
| { title: t(:least_improved_skill), value: @least_improved_skill&.dig(:skill_name) || t(:student_not_graded) } | ||
| ] | ||
| ) %> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </section> | ||
| <turbo-frame id="students-table"> | ||
| <%= render GroupEnrolledStudentsComponent.new(students: @group.students, group: @group, students_with_invalid_grades: @students_with_invalid_grades) %> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also here, switch to |
||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. student_not_graded reads as 'Not Graded'. this should be then just |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole section can be under one context called something like 'group statistics'. Also, just have one happy path and one unhappy path spec here testing that they're present, and that they're not if the group hasn't been graded. This controller spec class should test specific controller actions, it can get bloated if we test every single variable and its permutations. You can add one feature spec to test that an average and most/least improved skill are calculated correctly on screen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capitalize first letter