Skip to content

Commit

Permalink
Merge pull request #4803 from cyrillefr/add_download_option_for_wikid…
Browse files Browse the repository at this point in the history
…ata_stats_to_entire_campagin_4345

Download Wikidata stats for a campaign
  • Loading branch information
ragesoss committed Mar 7, 2022
2 parents 2c38f04 + 8a7bc2d commit b20a93a
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 26 deletions.
Expand Up @@ -11,6 +11,8 @@ 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`;


if (!show) {
return (
Expand Down Expand Up @@ -52,6 +54,11 @@ const CampaignStatsDownloadModal = ({ match }) => {
<a href={instructorsLink} className="button right">{I18n.t('campaign.data_instructors')}</a>
{I18n.t('campaign.data_instructors_info')}
</p>
<hr />
<p>
<a href={wikidataLink} className="button right">{I18n.t('campaign.data_wikidata')}</a>
{I18n.t('campaign.data_wikidata_info')}
</p>
</div>
);
};
Expand Down
Expand Up @@ -35,7 +35,7 @@ const CourseStatsDownloadModal = createReactClass({
const wikidataCsvLink = `/course_wikidata_csv?course=${this.props.course.slug}`;

let wikidataLink;
if (Features.wikiEd && this.props.course.home_wiki.project === 'wikidata') {
if (this.props.course.course_stats && this.props.course.home_wiki.project === 'wikidata') {
wikidataLink = (
<>
<hr />
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

This comment has been minimized.

Copy link
@verdy-p

verdy-p Mar 8, 2022

wrong placement of these two lines: this splits the message before in two parts. The last part was on a continuation line, which is then interpreted as no longer be part of the message before, but belonging to the last inserted message.

Beware with continuation lines in YML:

YML is cool for human edit (one file at a time), but not for automated edits (e.g. with sed/bash scripts, processing many files line per line to find the location for inserting/replacing/deleting messages (and ignoring the possibility of continuation lines in this YML format, or similar formats, or even in Python source files, that don't have statement terminators).

JSON is safer than YML if you intend to manage hundreds of locales with hundreds of message files: This bug should have not happened if the JSON format was used, as the syntax error would have been detected early (for mismatched quotation marks). JSON is still readable (ans it is natively supported in all modern languages or libraries). Alternative: the INI format (even simpler), or the XML syntax (but hard to read by humans).

But if you use YML, avoid using any continuation line, even if lines are longer than your convention in your favorite text editor: message files should be comparable side-by-side, allowing matching messages entirely line by line, and in the same order (possibly the same number of lines; only 1 line per message, escaping linebreaks if needed; however for translatability, they should not be arbitrarily long parageraphs or lists, and should avoid breaking sentences: this will ease the maintenance and review).

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
20 changes: 20 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,25 @@ def revisions_to_csv
CSV.generate { |csv| csv_data.each { |line| csv << line } }
end

def wikidata_to_csv
csv_data = [CourseWikidataCsvBuilder::CSV_HEADERS]
courses = @campaign.courses
.joins(:course_stat)
.where(home_wiki_id: Wiki.find_by(project: 'wikidata').id)
courses.find_each do |course|
csv_data << CourseWikidataCsvBuilder.new(course).stat_row
end

csv_data << sum_wiki_columns(csv_data) if courses.any?

CSV.generate { |csv| csv_data.each { |line| csv << line } }
end

def sum_wiki_columns(csv_data)
# Skip 1st header row + 1st column course name
csv_data[1..].transpose[1..].map(&:sum).unshift('Total')
end

class AllCourses
def self.courses
Course.all
Expand Down
48 changes: 37 additions & 11 deletions lib/analytics/course_wikidata_csv_builder.rb
Expand Up @@ -9,19 +9,45 @@ def initialize(course)

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|
csv_data << [revision_type, count]
end
csv_data << stat_row if @course.course_stat && @course.home_wiki.project == 'wikidata'

CSV.generate { |csv| csv_data.each { |line| csv << line } }
end

CSV_HEADERS = %w[
revision_type
count
def stat_row
hash_stats = @course
.course_stat.stats_hash['www.wikidata.org']
.merge({ 'course name' => @course.title })
CSV_HEADERS.map { |elmnt| hash_stats.fetch elmnt, '' }
end

CSV_HEADERS = [
'course name',
'claims created',
'claims changed',
'claims removed',
'items created',
'labels added',
'labels changed',
'labels removed',
'descriptions added',
'descriptions changed',
'descriptions removed',
'aliases added',
'aliases changed',
'aliases removed',
'merged from',
'merged to',
'interwiki links added',
'interwiki links removed',
'redirects created',
'reverts performed',
'restorations performed',
'items cleared',
'qualifiers added',
'other updates',
'unknown',
'no data',
'total revisions'
].freeze
end
14 changes: 13 additions & 1 deletion spec/controllers/campaigns_controller_spec.rb
Expand Up @@ -400,16 +400,19 @@
end

describe 'CSV actions' do
let(:wikidata) { Wiki.get_or_create(language: nil, project: 'wikidata') }
let(:course) { create(:course) }
let(:another_course) { create(:course, home_wiki: wikidata, slug: 'campaign/acourse') }
let(:campaign) { create(:campaign) }
let(:article) { create(:article) }
let(:user) { create(:user) }
let!(:revision) { create(:revision, article: article, user: user, date: course.start + 1.hour) }
let(:request_params) { { slug: campaign.slug, format: :csv } }

before do
stub_wiki_validation
login_as(user)
campaign.courses << course
campaign.courses.push course, another_course
create(:courses_user, course: course, user: user)
end

Expand Down Expand Up @@ -437,6 +440,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('course name,claims created')
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
35 changes: 35 additions & 0 deletions spec/lib/analytics/course_wikidata_csv_builder_spec.rb
@@ -0,0 +1,35 @@
# frozen_string_literal: true

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

describe CourseWikidataCsvBuilder do
let(:wikidata) { Wiki.get_or_create(language: nil, project: 'wikidata') }
let(:course) { create(:course, home_wiki: wikidata) }
let(:builder) { described_class.new(course) }
let(:create_course_stat) do
create(:course_stats,
stats_hash: { 'www.wikidata.org' => { 'claims created' => 2 } }, course_id: course.id)
end

before { stub_wiki_validation }

context 'when no course_stat' do
it 'generates only headers' do
expect(builder.generate_csv).to start_with('course name,')
expect(builder.generate_csv.lines.count).to eq(1)
end
end

context 'when course_stat exists' do
before do
create_course_stat
end

it 'generates csv data' do
expect(builder.generate_csv.lines.first).to start_with('course name,claims created')
expect(builder.generate_csv.lines.last).to start_with(course.title + ',2')
expect(builder.generate_csv.lines.count).to eq 2
end
end
end

0 comments on commit b20a93a

Please sign in to comment.