Skip to content

Commit 98d2b98

Browse files
authored
Merge pull request #6278 from gabina/data-rearchitecture-rewrite-IndividualStatisticsTimeslicePresenter-without-revisions
[Data rearchitecture] Rewrite individual statistics timeslice presenter without revisions
2 parents 86fcb49 + 2baa396 commit 98d2b98

File tree

6 files changed

+184
-22
lines changed

6 files changed

+184
-22
lines changed

app/assets/javascripts/components/user_profiles/student_stats.jsx

+15-17
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,35 @@ const StudentStats = ({ username, stats, maxProject }) => {
1515
{I18n.t(`${stats.course_string_prefix}.courses_enrolled`)}
1616
</small>
1717
</div>
18-
<div className= "stat-display__stat">
18+
<div className= "stat-display__stat tooltip-trigger">
1919
<div className="stat-display__value">
2020
{stats.individual_word_count}
21+
<img src ="/assets/images/info.svg" alt = "tooltip default logo" />
2122
</div>
2223
<small>
2324
{I18n.t('metrics.word_count')}
2425
</small>
26+
<div className="tooltip dark">
27+
<h4> {stats.individual_word_count} </h4>
28+
<p>
29+
{I18n.t('user_profiles.words_added_disclaimer')}
30+
</p>
31+
</div>
2532
</div>
26-
<div className= "stat-display__stat">
33+
<div className= "stat-display__stat tooltip-trigger">
2734
<div className="stat-display__value">
2835
{stats.individual_references_count}
36+
<img src ="/assets/images/info.svg" alt = "tooltip default logo" />
2937
</div>
3038
<small>
3139
{I18n.t('metrics.references_count')}
3240
</small>
33-
</div>
34-
<div className= "stat-display__stat">
35-
<div className="stat-display__value">
36-
{stats.individual_article_views}
41+
<div className="tooltip dark">
42+
<h4> {stats.individual_references_count} </h4>
43+
<p>
44+
{I18n.t('user_profiles.references_disclaimer')}
45+
</p>
3746
</div>
38-
<small>
39-
{I18n.t(`metrics.${ArticleUtils.projectSuffix(maxProject, 'view_count_description')}`)}
40-
</small>
4147
</div>
4248
<div className= "stat-display__stat">
4349
<div className="stat-display__value">
@@ -47,14 +53,6 @@ const StudentStats = ({ username, stats, maxProject }) => {
4753
{I18n.t(`metrics.${ArticleUtils.projectSuffix(maxProject, 'articles_edited')}`)}
4854
</small>
4955
</div>
50-
<div className= "stat-display__stat">
51-
<div className="stat-display__value">
52-
{stats.individual_articles_created}
53-
</div>
54-
<small>
55-
{I18n.t(`metrics.${ArticleUtils.projectSuffix(maxProject, 'articles_created')}`)}
56-
</small>
57-
</div>
5856
<div className ="stat-display__stat tooltip-trigger">
5957
<div className="stat-display__value">
6058
{stats.individual_upload_count}

app/controllers/user_profiles_controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def update
4040

4141
def stats
4242
@courses_users = @user.courses_users.includes(:course).where(courses: { private: false })
43-
@individual_stats_presenter = IndividualStatisticsPresenter.new(user: @user)
43+
@individual_stats_presenter = IndividualStatisticsTimeslicePresenter.new(user: @user)
4444
@courses_list = public_courses
4545
.where(courses_users: { role: CoursesUsers::Roles::INSTRUCTOR_ROLE })
4646
@courses_presenter = CoursesPresenter.new(current_user:,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# frozen_string_literal: true
2+
3+
require_dependency "#{Rails.root}/lib/word_count"
4+
5+
# Presenter to provide statistics about a user's individual contributions during
6+
# courses in which the user was a student.
7+
class IndividualStatisticsTimeslicePresenter
8+
def initialize(user:)
9+
@user = user
10+
set_data_from_course_user
11+
set_data_from_article_course
12+
set_upload_usage_counts
13+
end
14+
15+
def individual_courses
16+
@user.courses.nonprivate.where(courses_users: { role: CoursesUsers::Roles::STUDENT_ROLE })
17+
end
18+
19+
def course_string_prefix
20+
Features.default_course_string_prefix
21+
end
22+
23+
def individual_word_count
24+
WordCount.from_characters individual_character_count
25+
end
26+
27+
def individual_character_count
28+
@course_user_data[:characters]
29+
end
30+
31+
def individual_references_count
32+
@course_user_data[:references]
33+
end
34+
35+
def individual_upload_count
36+
@upload_usage_counts.length
37+
end
38+
39+
def individual_upload_usage_count
40+
@upload_usage_counts.values.sum
41+
end
42+
43+
def individual_article_count
44+
@article_course_data.length
45+
end
46+
47+
private
48+
49+
def set_data_from_course_user
50+
@course_user_data = {}
51+
@course_user_data[:characters] = 0
52+
@course_user_data[:references] = 0
53+
individual_courses.each do |course|
54+
course_user_records(course).each do |course_user|
55+
@course_user_data[:characters] += course_user.character_sum_ms
56+
@course_user_data[:references] += course_user.references_count
57+
end
58+
end
59+
end
60+
61+
def set_data_from_article_course
62+
@article_course_data = {}
63+
individual_courses.each do |course|
64+
article_course_records(course).each do |article_course|
65+
@article_course_data[article_course.article_id] = 1
66+
end
67+
end
68+
end
69+
70+
def set_upload_usage_counts
71+
@upload_usage_counts = {}
72+
individual_courses.each do |course|
73+
course.uploads.where(user_id: @user.id).each do |upload|
74+
@upload_usage_counts[upload.id] = upload.usage_count || 0
75+
end
76+
end
77+
end
78+
79+
def article_course_records(course)
80+
course.articles_courses
81+
.where('user_ids LIKE ?', "%- #{@user.id}\n%")
82+
.joins(:article)
83+
.includes(:article)
84+
.where(articles: { namespace: Article::Namespaces::MAINSPACE, deleted: false })
85+
end
86+
87+
def course_user_records(course)
88+
course.courses_users.where(user: @user)
89+
end
90+
end

app/views/user_profiles/stats.json.jbuilder

-4
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,8 @@ if @user.course_student?
4242
json.individual_word_count number_to_human @individual_stats_presenter.individual_word_count
4343
json.individual_references_count number_to_human @individual_stats_presenter
4444
.individual_references_count
45-
json.individual_article_views number_to_human @individual_stats_presenter
46-
.individual_article_views
4745
json.individual_article_count number_to_human @individual_stats_presenter
4846
.individual_article_count
49-
json.individual_articles_created number_to_human @individual_stats_presenter
50-
.individual_articles_created
5147
json.individual_upload_count number_to_human @individual_stats_presenter
5248
.individual_upload_count
5349
json.individual_upload_usage_count number_to_human @individual_stats_presenter

config/locales/en.yml

+2
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,8 @@ en:
16391639
location_placeholder: Add your location (optional)
16401640
institution_placeholder: Institution name (optional)
16411641
image_link_placeholder: Add link to image (optional)
1642+
references_disclaimer: References might be overcounted if the same user edit is considered part of multiple courses.
1643+
words_added_disclaimer: Words added might be overcounted if the same user edit is considered part of multiple courses.
16421644

16431645
wiki_edits:
16441646
notify_overdue:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
require_relative '../../app/presenters/individual_statistics_presenter'
5+
6+
describe IndividualStatisticsTimeslicePresenter do
7+
describe 'individual_article_views' do
8+
subject { described_class.new(user:) }
9+
10+
let(:start) { 1.year.ago.beginning_of_day }
11+
let(:course_end) { 1.day.ago.end_of_day }
12+
let(:course1) { create(:course, start:, end: course_end) }
13+
let(:course2) { create(:course, slug: 'foo/2', start:, end: course_end) }
14+
let(:user) { create(:user) }
15+
let(:article) { create(:article, average_views: 10) }
16+
let(:refs_tags_key) { 'feature.wikitext.revision.ref_tags' }
17+
let(:array_revisions) { [] }
18+
19+
context 'when a user is in two courses that overlap' do
20+
before do
21+
create(:commons_upload, user_id: user.id, usage_count: 1,
22+
uploaded_at: start + 1.minute)
23+
array_revisions << create(:revision, views: 100, user_id: user.id, article_id: article.id,
24+
date: start + 1.minute, new_article: true, characters: 100,
25+
features: {
26+
refs_tags_key => 22
27+
}, scoped: true)
28+
create(:courses_user, user_id: user.id, course_id: course1.id)
29+
create(:courses_user, user_id: user.id, course_id: course2.id)
30+
create(:articles_course, article_id: article.id, course_id: course1.id)
31+
create(:articles_course, article_id: article.id, course_id: course2.id)
32+
ArticlesCourses.update_from_course_revisions(course1, array_revisions)
33+
ArticlesCourses.update_from_course_revisions(course2, array_revisions)
34+
TimesliceManager.new(course1).create_timeslices_for_new_course_wiki_records(course1.wikis)
35+
TimesliceManager.new(course2).create_timeslices_for_new_course_wiki_records(course2.wikis)
36+
37+
revision_data = { start:, end: start + 1.day - 1.second,
38+
revisions: array_revisions }
39+
CourseUserWikiTimeslice.update_course_user_wiki_timeslices(course1, user.id,
40+
course1.wikis.first,
41+
revision_data)
42+
CourseUserWikiTimeslice.update_course_user_wiki_timeslices(course2, user.id,
43+
course2.wikis.first,
44+
revision_data)
45+
CoursesUsers.update_all_caches_from_timeslices(CoursesUsers.all)
46+
47+
ArticleCourseTimeslice.update_article_course_timeslices(course1, article.id,
48+
revision_data)
49+
ArticleCourseTimeslice.update_article_course_timeslices(course2, article.id,
50+
revision_data)
51+
ArticlesCourses.update_all_caches_from_timeslices(ArticlesCourses.all)
52+
course1.update_cache_from_timeslices
53+
course2.update_cache_from_timeslices
54+
end
55+
56+
it 'does\'t double count the same articles in multiple courses' do
57+
expect(course1.articles_courses.count).to eq(1)
58+
expect(course2.articles_courses.count).to eq(1)
59+
60+
# double count character count and references
61+
expect(subject.individual_character_count).to eq(200)
62+
expect(subject.individual_references_count).to eq(44)
63+
64+
# does not double count article count
65+
expect(subject.individual_article_count).to eq(1)
66+
end
67+
68+
it 'does not double count upload stats' do
69+
expect(course1.uploads.count).to eq(1)
70+
expect(course2.uploads.count).to eq(1)
71+
expect(subject.individual_upload_count).to eq(1)
72+
expect(subject.individual_upload_usage_count).to eq(1)
73+
end
74+
end
75+
end
76+
end

0 commit comments

Comments
 (0)