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

DTL (Test > Levelbuilder): 91ebb935 #35078

Merged
merged 31 commits into from
Jun 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ec13687
add a helper class for interacting with the Crowdin API, and a script…
Hamms May 15, 2020
4777a77
save etags non-temporarily
Hamms May 15, 2020
66293f1
s/parallel_sync/fetch_changes
Hamms May 15, 2020
aa293a4
save etags
Hamms May 15, 2020
78f30d0
add a script for downloading the changes identified by the 'fetch cha…
Hamms May 18, 2020
818f480
update etags
Hamms May 18, 2020
bcbfa28
rearrange crowdin code into a project api class and a utils class
Hamms May 27, 2020
49e7337
update sync down to use new crowdin API
Hamms May 27, 2020
c7d3db4
use logger and multiple log levels
Hamms May 27, 2020
0e4abf3
round output to nearest 10
Hamms May 27, 2020
f983d4b
commit etags updates in down&out PR
Hamms May 27, 2020
56354ce
move new crowdin classes to libs directory, make the sync-down script…
Hamms May 27, 2020
3211a97
docs!
Hamms May 27, 2020
6c9dfee
add a test
Hamms May 27, 2020
3ca8ab0
Update lib/cdo/crowdin/utils.rb
Hamms May 28, 2020
48e9799
initialize logger outside of the loop
Hamms May 28, 2020
ac36ba9
improve comments and variable names
Hamms May 28, 2020
5d0101a
add an optimization that avoids making unnecessary body requests. Als…
Hamms May 28, 2020
65a56bc
Disable creating new prospects in Pardot since we went over the Pardo…
hacodeorg May 29, 2020
8e350cc
Recruitment: don't show banner on non-en teacher homepage
breville May 29, 2020
c6e4134
staging content changes (-)
deploy-code-org May 29, 2020
5f1c6ae
Merge pull request #35040 from code-dot-org/ha/cr-stop-adding
hacodeorg May 29, 2020
fa40db7
Merge pull request #35050 from code-dot-org/restart-recruitment-banne…
breville May 29, 2020
24afedf
Merge pull request #35054 from code-dot-org/staging
deploy-code-org May 29, 2020
147a0e0
Update facilitator redirects
breville May 29, 2020
6bf5eca
Only change one redirect, not three
breville May 29, 2020
8b51a20
run extra write to reset unused changes _before_ the regular loop, ra…
Hamms May 29, 2020
9b81468
add explicit error for missing changes.json
Hamms May 29, 2020
10aa347
Merge pull request #34846 from code-dot-org/i18n-sync-export_file
Hamms May 30, 2020
f2209ee
Merge pull request #35056 from code-dot-org/update-facilitator-redirects
breville May 30, 2020
91ebb93
Merge pull request #35062 from code-dot-org/staging
deploy-code-org Jun 1, 2020
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
2 changes: 1 addition & 1 deletion apps/src/templates/studioHomepages/TeacherHomepage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default class TeacherHomepage extends Component {
<HeaderBanner headingText={i18n.homepageHeading()} short={true} />
<ProtectedStatefulDiv ref="flashes" />
<ProtectedStatefulDiv ref="teacherReminders" />
{specialAnnouncement && (
{isEnglish && specialAnnouncement && (
<SpecialAnnouncementActionBlock announcement={specialAnnouncement} />
)}
{announcement && showAnnouncement && (
Expand Down
3 changes: 2 additions & 1 deletion bin/cron/build_contact_rollups_v2
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ require 'cdo/only_one'

def main
contact_rollups = ContactRollupsV2.new
contact_rollups.build_and_sync
contact_rollups.collect_and_process_contacts
contact_rollups.sync_updated_contacts_with_pardot
contact_rollups.report_results
end

Expand Down
650 changes: 650 additions & 0 deletions bin/i18n/crowdin/codeorg-markdown_etags.json

Large diffs are not rendered by default.

10,466 changes: 10,466 additions & 0 deletions bin/i18n/crowdin/codeorg_etags.json

Large diffs are not rendered by default.

2,522 changes: 2,522 additions & 0 deletions bin/i18n/crowdin/hour-of-code_etags.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions bin/i18n/sync-all.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,13 @@ def create_down_out_pr
return unless should_i "create the down & out PR"
`git checkout -B #{DOWN_OUT_BRANCH}`

I18nScriptUtils.git_add_and_commit(
[
"bin/i18n/crowdin/*etags.json"
],
"etags updates"
)

I18nScriptUtils.git_add_and_commit(
[
"pegasus/cache",
Expand Down
52 changes: 26 additions & 26 deletions bin/i18n/sync-down.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,39 @@
# https://crowdin.com/project/codeorg

require_relative 'i18n_script_utils'
require 'open3'

require 'cdo/crowdin/utils'
require 'cdo/crowdin/project'

def sync_down
I18nScriptUtils.with_synchronous_stdout do
puts "Beginning sync down"

logger = Logger.new(STDOUT)
logger.level = Logger::INFO

CROWDIN_PROJECTS.each do |name, options|
puts "Downloading translations from #{name} project"
command = "crowdin --config #{options[:config_file]} --identity #{options[:identity_file]} download translations"

# Filter the output because the crowdin translation download is _super_
# verbose; it includes not only a progress spinner, but also information
# about each individual file downloaded in each individual language.
#
# We really only care about general progress monitoring, so we remove or
# ignore any things we identify as "noise" in the output.
Open3.popen2(command) do |_stdin, stdout, status_thread|
while line = stdout.gets
# strip out the progress spinner, which is implemented as the sequence
# \-/| followed by a backspace character
line.gsub!(/[\|\/\-\\][\b]/, '')

# skip lines detailing individual file extraction
next if line.start_with?("Extracting: ")

# skip warning that happens if the sync is run multiple times in succession
next if line == "Warning: Export was skipped. Please note that this method can be invoked only once per 30 minutes.\n"

puts line
end

raise "Sync down failed" unless status_thread.value.success?
end
api_key = YAML.load_file(options[:identity_file])["api_key"]
project_id = YAML.load_file(options[:config_file])["project_identifier"]
project = Crowdin::Project.new(project_id, api_key)
options = {
etags_json: File.join(File.dirname(__FILE__), "crowdin", "#{project_id}_etags.json"),
locales_dir: File.join(I18N_SOURCE_DIR, '..'),
logger: logger
}
utils = Crowdin::Utils.new(project, options)

puts "Fetching list of changed files"
prefetch = Time.now
utils.fetch_changes
postfetch = Time.now
puts "Changes fetched in #{Time.at(postfetch - prefetch).utc.strftime('%H:%M:%S')}"
puts "Downloading changed files"
predownload = Time.now
utils.download_changed_files
postdownload = Time.now
puts "Files downloaded in #{Time.at(postdownload - predownload).utc.strftime('%H:%M:%S')}"
end

puts "Sync down complete"
Expand Down
1 change: 0 additions & 1 deletion dashboard/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,6 @@
t.boolean "autoplay_enabled", default: false, null: false
t.index ["code"], name: "index_sections_on_code", unique: true, using: :btree
t.index ["course_id"], name: "fk_rails_20b1e5de46", using: :btree
t.index ["script_id"], name: "fk_rails_5c2401d1cb", using: :btree
t.index ["user_id"], name: "index_sections_on_user_id", using: :btree
end

Expand Down
2 changes: 1 addition & 1 deletion dashboard/lib/contact_rollups_v2.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'cdo/log_collector'

class ContactRollupsV2
def initialize(is_dry_run: true)
def initialize(is_dry_run: false)
@is_dry_run = is_dry_run
@log_collector = LogCollector.new('Contact Rollups')
end
Expand Down
4 changes: 2 additions & 2 deletions dashboard/test/lib/contact_rollups_v2_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ContactRollupsV2Test < ActiveSupport::TestCase
PardotV2.stubs(:submit_batch_request).once.returns([])

# Execute the pipeline
ContactRollupsV2.new(is_dry_run: false).build_and_sync
ContactRollupsV2.new.build_and_sync

# Verify email preference
pardot_memory_record = ContactRollupsPardotMemory.find_by(email: email_preference.email, pardot_id: 1)
Expand Down Expand Up @@ -67,7 +67,7 @@ class ContactRollupsV2Test < ActiveSupport::TestCase
PardotV2.stubs(:submit_batch_request).once.returns([])

# Execute the pipeline
ContactRollupsV2.new(is_dry_run: false).build_and_sync
ContactRollupsV2.new.build_and_sync

# Verify results
pardot_memory_record = ContactRollupsPardotMemory.find_by(email: email, pardot_id: pardot_id)
Expand Down
113 changes: 113 additions & 0 deletions lib/cdo/crowdin/project.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require 'httparty'

module Crowdin
# This class represents a single project hosted on Crowdin, and provides
# access to data on that project via Crowdin's API
class Project
include HTTParty

attr_reader :id

# @param project_id [String]
# @param api_key [String]
# @see https://crowdin.com/project/codeorg/settings#api for an example of
# how to retrieve these values for the "code.org" project
def initialize(project_id, api_key)
@id = project_id
self.class.base_uri("https://api.crowdin.com/api/project/#{project_id}")
self.class.default_params key: api_key
end

# @see https://support.crowdin.com/api/info/
def project_info
self.class.post("/info")
end

# @param file [String] name of file (within crowdin) to be downloaded
# @param language [String] crowdin language code
# @param etag [String, nil] the last file version tag returned by crowdin
# for this file. If no changes have occurred since the provided etag was
# generated, crowdin will return a 304 (Not Modified) status instead of
# downloading the file. See the export-file Crowdin documentation for
# details
# @param attempts [Number, nil] how many times we should retry the download
# if it fails
# @param only_head [Boolean, nil] whether to make a HEAD request rather
# than a full GET request. Defaults to false.
# @see https://support.crowdin.com/api/export-file/
def export_file(file, language, etag: nil, attempts: 3, only_head: false)
options = {
query: {
file: file,
language: language
}
}

unless etag.nil?
options[:headers] = {
"If-None-Match" => etag
}
end

only_head ? self.class.head("/export-file", options) : self.class.get("/export-file", options)
rescue Net::ReadTimeout => error
# Handle a timeout by simply retrying. We default to three attempts before
# giving up; if this doesn't work out, other things we could consider:
#
# - increasing the default number of attempts
# - increasing the number of attempts for certain high-failure-rate calls
# - increasing the timeout, either globally or for this specific call
STDERR.puts "Crowdin.export_file(#{file}) timed out: #{error}"
raise if attempts <= 1
export_file(file, language, etag: etag, attempts: attempts - 1, only_head: only_head)
end

# Retrieve all languages currently enabled in the crowdin project. Each
# language is a hash containing the language name and code, as well as
# other internal crowdin values.
# @example [{"name"=>"Norwegian", "code"=>"no", "can_translate"=>"1", "can_approve"=>"1"}, ...]
# @return [Array<Hash>]
def languages
project_info["info"]["languages"]["item"]
end

# Retrieve all files currently uploaded to the crowdin project.
# @example ["/dashboard/base.yml", "/dashboard/data.yml", ...]
# @return [Array<String>]
def list_files
files = project_info["info"]["files"]["item"]
results = []
each_file(files) do |file, path|
results << File.join(path, file["name"])
end
results
end

private

# Iterate through files as returned by crowdin. Crowdin returns files in a
# nested format, where each file is a "node", and directories are nodes
# that can contain other nodes. This helper simply knows how to traverse
# that simulated directory structure, and will yield each file in turn
# along with its directory.
# @param files [Array<Hash>]
# @param path [String, nil]
# @yield [name, path] the name of a file and the full path to the directory
# in which it can be found.
def each_file(files, path="")
files = [files] unless files.is_a? Array
files.each do |file|
case file["node_type"]
when "directory"
subfiles = file["files"]["item"]
subpath = File.join(path, file["name"])
each_file(subfiles, subpath) {|f, p| yield f, p}
when "file"
yield file, path
else
raise "Cannot process file of type #{file['node_type']}"
end
end
end
end
end
88 changes: 88 additions & 0 deletions lib/cdo/crowdin/utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'json'
require 'parallel'

module Crowdin
class Utils
# @param project [Crowdin::Project]
# @param options [Hash, nil]
# @param options.changes_json [String, nil] path to file where files with
# changes will be written out in JSON format
# @param options.etags_json [String, nil] path to file where etags will be
# written out in JSON format
# @param options.locales_dir [String, nil] path to directory where changed
# files should be downloaded
# @param options.logger [Logger, nil]
def initialize(project, options={})
@project = project
@changes_json = options.fetch(:changes_json, "/tmp/#{project.id}_changes.json")
@etags_json = options.fetch(:etags_json, "/tmp/#{project.id}_etags.json")
@locales_dir = options.fetch(:locales_dir, "/tmp/locales")
@logger = options.fetch(:logger, Logger.new(STDOUT))
end

# Fetch from Crowdin a list of files changed since the last sync. Uses
# etags sourced from the @etags_json file to define what we mean by "since
# the last sync," and writes the results out to @changes_json.
def fetch_changes
etags = File.exist?(@etags_json) ? JSON.parse(File.read(@etags_json)) : {}

# Clear out existing changes json if it exists
File.write(@changes_json, '{}')
changes = {}

languages = @project.languages
num_languages = languages.length
languages.each_with_index do |language, i|
language_code = language["code"]
@logger.debug("#{language['name']} (#{language_code}): #{i}/#{num_languages}")
@logger.info("~#{(i * 100 / num_languages).round(-1)}% complete (#{i}/#{num_languages})") if i > 0 && i % (num_languages / 5) == 0

etags[language_code] ||= {}
files = @project.list_files

changed_files = Parallel.map(files) do |file|
etag = etags[language_code].fetch(file, nil)
response = @project.export_file(file, language_code, etag: etag, only_head: true)
case response.code
when 200
[file, response.headers["etag"]]
when 304
nil
else
raise "cannot handle response code #{response.code}"
end
end.compact

next if changed_files.empty?

changes[language_code] = changed_files.to_h
etags[language_code].merge!(changes[language_code])
File.write(@etags_json, JSON.pretty_generate(etags))
File.write(@changes_json, JSON.pretty_generate(changes))
end
end

# Downloads all files referenced in @changes_json to @locales_dir
def download_changed_files
raise "No existing changes json at #{@changes_json}; please run fetch_changes first" unless File.exist?(@changes_json)
changes = JSON.parse(File.read(@changes_json))
@logger.info("#{changes.keys.length} languages have changes")
@project.languages.each do |language|
code = language["code"]
name = language["name"]
files = changes.fetch(code, nil)
next unless files.present?
filenames = files.keys
locale_dir = File.join(@locales_dir, name)

@logger.debug("#{name} (#{code}): #{filenames.length} files have changes")
Parallel.each(filenames) do |file|
response = @project.export_file(file, code)
dest = File.join(locale_dir, file)
FileUtils.mkdir_p(File.dirname(dest))
File.write(dest, response.body)
end
end
end
end
end