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

Download Wikidata stats for a campaign #4803

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -11,6 +11,20 @@ const CampaignStatsDownloadModal = ({ match }) => {
const editorsLink = `/campaigns/${campaignSlug}/students.csv`;
const editorsByCourseLink = `/campaigns/${campaignSlug}/students.csv?course=true`;
const instructorsLink = `/campaigns/${campaignSlug}/instructors.csv?course=true`;
const wikidataLink = `/campaigns/${campaignSlug}/wikidata.csv`;

let wikidataCsvLink;
if (Features.wikiEd) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to limit this based on Features.wikiEd.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that, because there is this limit in the course version (app/assets/javascripts/components/overview/course_stats_download_modal.jsx
So I implied it would be the same for a campaign. I will remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! We should change that limitation on CourseStatsDownloadModal... now it should check for whether the course object has a course_stats property. @1v4n4's project made the wikidata stats data available without the Wiki Ed restriction.

wikidataCsvLink = (
<>
<hr />
<p>
<a href={wikidataLink} className="button right">{I18n.t('campaign.data_wikidata')}</a>
{I18n.t('campaign.data_wikidata_info')}
</p>
</>
);
}

if (!show) {
return (
Expand Down Expand Up @@ -52,6 +66,7 @@ const CampaignStatsDownloadModal = ({ match }) => {
<a href={instructorsLink} className="button right">{I18n.t('campaign.data_instructors')}</a>
{I18n.t('campaign.data_instructors_info')}
</p>
{wikidataCsvLink}
</div>
);
};
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/campaigns_controller.rb
Expand Up @@ -11,7 +11,8 @@ class CampaignsController < ApplicationController
before_action :set_campaign, only: %i[overview programs articles users edit
update destroy add_organizer remove_organizer
remove_course courses ores_plot articles_csv
revisions_csv alerts students instructors]
revisions_csv alerts students instructors
wikidata]
before_action :require_create_permissions, only: [:create]
before_action :require_write_permissions, only: %i[update destroy add_organizer
remove_organizer remove_course edit]
Expand Down Expand Up @@ -196,6 +197,10 @@ def revisions_csv
csv_of('revisions')
end

def wikidata
csv_of('wikidata')
end

private

def csv_of(type)
Expand Down
30 changes: 18 additions & 12 deletions app/workers/campaign_csv_worker.rb
Expand Up @@ -13,23 +13,29 @@ def self.generate_csv(campaign:, filename:, type:, include_course:)
def perform(campaign_id, filename, type, include_course)
campaign = Campaign.find(campaign_id)
builder = CampaignCsvBuilder.new(campaign)
data = case type
when 'instructors'
campaign.users_to_csv(:instructors, course: include_course)
when 'students'
campaign.users_to_csv(:students, course: include_course)
when 'courses'
builder.courses_to_csv
when 'articles'
builder.articles_to_csv
when 'revisions'
builder.revisions_to_csv
end
data = to_csv(type, campaign, builder, include_course)

write_csv(filename, data)
CsvCleanupWorker.perform_at(1.week.from_now, filename)
end

def to_csv(type, campaign, builder, include_course)
case type
when 'instructors'
campaign.users_to_csv(:instructors, course: include_course)
when 'students'
campaign.users_to_csv(:students, course: include_course)
when 'courses'
builder.courses_to_csv
when 'articles'
builder.articles_to_csv
when 'revisions'
builder.revisions_to_csv
when 'wikidata'
builder.wikidata_to_csv
end
end

private

def write_csv(filename, data)
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Expand Up @@ -361,6 +361,8 @@ en:
data_editors_by_course_info: List of enrolled editors and their courses/programs
data_editor_usernames: Editor usernames
data_editor_usernames_info: List of editor usernames, one per line
data_wikidata: Wikidata stats
data_wikidata_info: Detailed breakdown of Wikidata activity, based on edit summaries
default_course_type: Default Course Type
default_passcode: Default Passcode
default_passcode_explanation: "By default, new programs in this campaign should have:"
Expand Down
2 changes: 2 additions & 0 deletions config/locales/fr.yml
Expand Up @@ -394,6 +394,8 @@ fr:
data_editors_by_course_info: Liste des rédacteurs inscrits avec leurs cours/programmes
data_editor_usernames: Noms d’utilisateur des rédacteurs
data_editor_usernames_info: Liste des noms d'utilisateur des rédacteurs, un par
data_wikidata: Statistiques de Wikidata
data_wikidata_info: Répartition détaillée de l’activité de Wikidata, d’après les résumés de modification
ligne
default_course_type: Type de cours par défaut
default_passcode: Mot de passe par défaut
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Expand Up @@ -244,6 +244,7 @@
get 'articles_csv'
get 'revisions_csv'
get 'alerts'
get 'wikidata'
put 'add_organizer'
put 'remove_organizer'
put 'remove_course'
Expand Down
6 changes: 6 additions & 0 deletions lib/analytics/campaign_csv_builder.rb
Expand Up @@ -5,6 +5,7 @@
require_dependency "#{Rails.root}/lib/analytics/course_articles_csv_builder"
require_dependency "#{Rails.root}/lib/analytics/course_revisions_csv_builder"
require_dependency "#{Rails.root}/app/workers/campaign_csv_worker"
require "#{Rails.root}/lib/analytics/course_wikidata_csv_builder"

class CampaignCsvBuilder
def initialize(campaign)
Expand Down Expand Up @@ -44,6 +45,11 @@ def revisions_to_csv
CSV.generate { |csv| csv_data.each { |line| csv << line } }
end

def wikidata_to_csv
courses = @campaign.courses.joins(:course_stat)
CourseWikidataCsvBuilder.new(courses).generate_csv
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer this to follow the same pattern as the other methods like articles_to_csv, building an aggregate CSV by looping through the courses in the campaign, and adding a row of wikidata stats for each course.

Making CourseWikidataCsvBuilder handle the cases of one as well as multiple courses will make this harder to reason about and change, I think.

The problem with that approach is that, unlike the other CSV builders, the output for the Wikidata one isn't a format that is suitable to that aggregation strategy. I think the best way around that will be to change the output format of the Wikidata CSV builder. Currently, it uses headers of revision_type, count with one row per revision type. The alternative would be to have a column for each revision type, so that the campaign version could have one row per course.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the join so I do not have to loop, but I can change to the articles_to_csv pattern.
I also will make 2 methods: one CourseWikidataCsvBuilder for courses and another one for campaign.

So, if I understand well, your format proposal for campaign is:
claims created, claims changed, claims removed, etc. course // header
4,2,1, xx,x,"This course name" // lines of data for one course of a campaign
2,4,5, xx, xxx "This other course name" // same

Also, do you want me to change CourseWikidataCsvBuilder so that it will be the same format, but with just one line ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct, except that course name should be the first column.

end

class AllCourses
def self.courses
Course.all
Expand Down
20 changes: 13 additions & 7 deletions lib/analytics/course_wikidata_csv_builder.rb
Expand Up @@ -4,22 +4,28 @@

class CourseWikidataCsvBuilder
def initialize(course)
@course = course
@courses = course.is_a?(ActiveRecord::Relation) ? course : [course]
end

def generate_csv
csv_data = [CSV_HEADERS]
stats = if @course.course_stat
@course.course_stat.stats_hash['www.wikidata.org']
else
{ 'total revisions' => 0 }
end
stats.each do |revision_type, count|
aggregate_stats.each do |revision_type, count|
csv_data << [revision_type, count]
end
CSV.generate { |csv| csv_data.each { |line| csv << line } }
end

def aggregate_stats
@courses.each.with_object({}) do |course, stats_aggrted|
stats = if course.course_stat
course.course_stat.stats_hash['www.wikidata.org']
else
{ 'total revisions' => 0 }
end
stats_aggrted.merge!(stats) { |_, old, new| old + new }
end
end

CSV_HEADERS = %w[
revision_type
count
Expand Down
9 changes: 9 additions & 0 deletions spec/controllers/campaigns_controller_spec.rb
Expand Up @@ -437,6 +437,15 @@
expect(csv).to include(article.title)
expect(csv).to include('references_added')
end

it 'returns a csv of wikidata' do
expect(CsvCleanupWorker).to receive(:perform_at)
get "/campaigns/#{campaign.slug}/wikidata.csv"
get "/campaigns/#{campaign.slug}/wikidata.csv"
follow_redirect!
csv = response.body.force_encoding('utf-8')
expect(csv).to include('revision_type,count')
end
end

describe '#overview' do
Expand Down
10 changes: 10 additions & 0 deletions spec/factories/course_stats.rb
@@ -0,0 +1,10 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: course_stats

FactoryBot.define do
factory :course_stats, class: 'CourseStat' do
nil
end
end
51 changes: 51 additions & 0 deletions spec/lib/analytics/course_wikidata_csv_builder_spec.rb
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require 'rails_helper'
require "#{Rails.root}/lib/analytics/course_wikidata_csv_builder.rb"

describe CourseWikidataCsvBuilder do
let(:course) { create(:course) }
let(:another_course) { create(:course, slug: 'myschool/mycourse') }
let(:builder) { described_class.new(course) }
let(:campaign_builder) { described_class.new(ActiveRecord::Relation.new(Course)) }
let(:create_course_stat) do
create(:course_stats,
stats_hash: { 'www.wikidata.org' => { 'claims created' => 2 } }, course_id: course.id)
end
let(:create_another_course_stat) do
create(:course_stats,
stats_hash: { 'www.wikidata.org' => { 'claims created' => 9 } },
course_id: another_course.id)
end

context 'when no course_stat' do
it 'generates only headers' do
expect(builder.generate_csv).to eq "revision_type,count\ntotal revisions,0\n"
end
end

context 'when course_stat exists' do
before do
create_course_stat
end

it 'generates csv data' do
expect(builder.generate_csv).to eq "revision_type,count\nclaims created,2\n"
end
end

# generate_csv also used in campaign context
# see controller campaign and CSV actions
# see also campaigns_controller_spec
context 'when multiple courses in a campaign' do
before do
create_course_stat
create_another_course_stat
end

# Since 2 + 9 = 11
it 'generates csv of aggregated data' do
expect(campaign_builder.generate_csv).to eq "revision_type,count\nclaims created,11\n"
end
end
end