Skip to content
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

Add live course statistics for teachers #4502

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/controllers/courses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ def statistics
@answered = @course.answered_questions.count
end

def live_statistics
time_range = 1.hour.ago..Time.zone.now

@online_users = @course.users.online.count
# number of submissions per minute for the last hour
@submissions_per_minute = @course.submissions.where(created_at: time_range).group('MINUTE(created_at)').count
@activities_being_worked_on = @course.activities_being_worked_on(3, time_range)
end

def scoresheet
@title = I18n.t('courses.scoresheet.scoresheet')
@crumbs = [[@course.name, course_path(@course)], [I18n.t('courses.scoresheet.scoresheet'), '#']]
Expand Down
9 changes: 9 additions & 0 deletions app/models/course.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ def series_being_worked_on(limit = 3, exclude = [])
result
end

def activities_being_worked_on(limit = 3, time_range = 1.hour.ago..Time.zone.now)
exercises.joins('inner join submissions ON submissions.exercise_id = activities.id')
.where(submissions: { created_at: time_range, course_id: id })
.group('activities.id')
.select('activities.*, COUNT(*) as submission_count')
.reorder(Arel.sql('COUNT(*) DESC'))
.first(limit)
end

def homepage_activities(user, limit = 3)
result = []
incomplete_activities = series.visible.joins(:activities) # all activities in visible series
Expand Down
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ class User < ApplicationRecord
.reorder 'correct.count': direction, 'attempted.count': direction
}

scope :online, -> { where(seen_at: 5.minutes.ago..) }

def provider_allows_blank_email
return if institution&.uses_lti? || institution&.uses_oidc? || institution&.uses_smartschool?

Expand Down
4 changes: 4 additions & 0 deletions app/policies/course_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def statistics?
course_admin?
end

def live_statistics?
course_admin?
end

def update_membership?
course_admin?
end
Expand Down
7 changes: 7 additions & 0 deletions app/views/courses/live_statistics.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
json.online_users @online_users
json.submissions_per_minute @submissions_per_minute
json.activities_being_worked_on @activities_being_worked_on&.each do |activity|
json.extract! activity, :id, :name
json.url course_activity_url(@course, activity)
json.submission_count activity.submission_count
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
end
member do
get 'statistics'
get 'live_statistics'
get 'subscribe/:secret', to: 'courses#registration', as: 'registration'
get 'manage_series'
get 'scoresheet'
Expand Down
71 changes: 71 additions & 0 deletions test/controllers/courses_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -854,4 +854,75 @@ def with_users_signed_in(users)
event3 = cal.events.third
assert_nil event3
end

test 'live statistics should contain the number of online users' do
add_admins
sign_in @admins.first
# Visit the course page to register the user as online
get course_path(@course)

get live_statistics_course_path(@course, format: :json)

stats = response.parsed_body
assert_equal 1, stats['online_users']
end

test 'live submissions contain the number of submissions per minute for the last hour' do
add_admins
sign_in @admins.first

time1 = 1.minutes.ago
time2 = 27.minutes.ago
time3 = 45.minutes.ago
time4 = 3.hours.ago

# Create a submissions
create :submission, course: @course, created_at: time1
create :submission, course: @course, created_at: time2
create :submission, course: @course, created_at: time2
create :submission, course: @course, created_at: time3
create :submission, course: @course, created_at: time4

get live_statistics_course_path(@course, format: :json)

stats = response.parsed_body
assert_equal 1, stats['submissions_per_minute'][time1.min.to_s]
assert_equal 2, stats['submissions_per_minute'][time2.min.to_s]
assert_equal 1, stats['submissions_per_minute'][time3.min.to_s]
end

test 'live statistics should contain the three activities with most submissions in the past hour and their submission count' do
add_admins
sign_in @admins.first

exercise1 = create :exercise
exercise2 = create :exercise
exercise3 = create :exercise
exercise4 = create :exercise

create :series, course: @course, exercises: [exercise1, exercise2, exercise3, exercise4]

# Create a submissions
create :submission, course: @course, exercise: exercise1
3.times do
create :submission, course: @course, exercise: exercise2
end
2.times do
create :submission, course: @course, exercise: exercise3
end
4.times do
create :submission, course: @course, exercise: exercise4, created_at: 2.hours.ago
end

get live_statistics_course_path(@course, format: :json)

stats = response.parsed_body
assert_equal 3, stats['activities_being_worked_on'][0]['submission_count']
assert_equal 2, stats['activities_being_worked_on'][1]['submission_count']
assert_equal 1, stats['activities_being_worked_on'][2]['submission_count']

assert_equal exercise2.name, stats['activities_being_worked_on'][0]['name']
assert_equal exercise3.name, stats['activities_being_worked_on'][1]['name']
assert_equal exercise1.name, stats['activities_being_worked_on'][2]['name']
end
end
42 changes: 42 additions & 0 deletions test/models/course_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -560,4 +560,46 @@ class CourseTest < ActiveSupport::TestCase
CourseMembership.create(user: user, course: course, status: :course_admin)
assert_equal 1, course.homepage_series(user).count
end

test 'activities being worked on should return the activities with most recent submissions' do
course = create :course, series_count: 2, exercises_per_series: 2
user = create :student
CourseMembership.create(user: user, course: course, status: :student)

# no activity should return empty array
assert_equal [], course.activities_being_worked_on

# should return activities with most recent submissions
create :submission, user: user, exercise: course.series.first.exercises.first, course: course, created_at: DateTime.now - 30.minutes
# 2 submissions
create :submission, user: user, exercise: course.series.first.exercises.second, course: course
create :submission, user: user, exercise: course.series.first.exercises.second, course: course
# 3 submissions
create :submission, user: user, exercise: course.series.second.exercises.second, course: course
create :submission, user: user, exercise: course.series.second.exercises.second, course: course
create :submission, user: user, exercise: course.series.second.exercises.second, course: course
# should be ignored, more than 1 hour ago
create :submission, user: user, exercise: course.series.second.exercises.first, course: course, created_at: DateTime.now - 2.hours
create :submission, user: user, exercise: course.series.second.exercises.first, course: course, created_at: DateTime.now - 2.hours
create :submission, user: user, exercise: course.series.second.exercises.first, course: course, created_at: DateTime.now - 2.hours
create :submission, user: user, exercise: course.series.second.exercises.first, course: course, created_at: DateTime.now - 2.hours

assert_equal course.series.second.exercises.second, course.activities_being_worked_on.first
assert_equal course.series.first.exercises.second, course.activities_being_worked_on.second
assert_equal course.series.first.exercises.first, course.activities_being_worked_on.third

# should be able to limit number of activities
assert_equal 3, course.activities_being_worked_on.count
assert_equal 2, course.activities_being_worked_on(2).count
assert_equal 3, course.activities_being_worked_on(7).count

# should include submission_count
assert_equal 3, course.activities_being_worked_on.first[:submission_count]
assert_equal 2, course.activities_being_worked_on.second[:submission_count]
assert_equal 1, course.activities_being_worked_on.third[:submission_count]

# should be able to specify time range
assert_equal 4, course.activities_being_worked_on(7, 4.hours.ago..).count
assert_equal 2, course.activities_being_worked_on(7, 3.hours.ago..15.minutes.ago).count
end
end
9 changes: 9 additions & 0 deletions test/models/user_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1070,4 +1070,13 @@ def setup
question.update(question_state: :answered)
assert_equal false, user.reload.open_questions?
end

test 'online scope should return users that have been active in the last 5 minutes' do
user = create :user
assert_equal 0, User.online.count
user.update(seen_at: Time.zone.now)
assert_equal 1, User.online.count
user.update(seen_at: 6.minutes.ago)
assert_equal 0, User.online.count
end
end