diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index b0641c57e2..7cf8612328 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -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'), '#']] diff --git a/app/models/course.rb b/app/models/course.rb index 708e4ca7cc..39943b4f27 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index e530357ad7..455bfae3e8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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? diff --git a/app/policies/course_policy.rb b/app/policies/course_policy.rb index 1dab3e2993..8348c3b42c 100644 --- a/app/policies/course_policy.rb +++ b/app/policies/course_policy.rb @@ -71,6 +71,10 @@ def statistics? course_admin? end + def live_statistics? + course_admin? + end + def update_membership? course_admin? end diff --git a/app/views/courses/live_statistics.json.jbuilder b/app/views/courses/live_statistics.json.jbuilder new file mode 100644 index 0000000000..bcf472a5ff --- /dev/null +++ b/app/views/courses/live_statistics.json.jbuilder @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 5f5bcc391d..6f5a56f35b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/test/controllers/courses_controller_test.rb b/test/controllers/courses_controller_test.rb index a9ea37dcd0..81cd21036e 100644 --- a/test/controllers/courses_controller_test.rb +++ b/test/controllers/courses_controller_test.rb @@ -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 diff --git a/test/models/course_test.rb b/test/models/course_test.rb index 2cc4e84348..7319ae5a45 100644 --- a/test/models/course_test.rb +++ b/test/models/course_test.rb @@ -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 diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 67a0aede50..a2664fda36 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -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