diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..81e00690
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: bundler
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
diff --git a/.gitignore b/.gitignore
index ce1e7efa..3799e7bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,6 @@
tmp/**
public/system/**
.idea/**
+.byebug_history
+pg_data/
+.vscode/
diff --git a/.ruby-version b/.ruby-version
index c1026d29..0b4a2023 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-ruby-2.3.1
+ruby-2.4.6
diff --git a/.travis.yml b/.travis.yml
index 597b83e6..e46178f8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,20 +9,20 @@ addons:
services:
- postgresql
- redis
+ - elasticsearch
language: ruby
rvm:
- - 2.3.1
+ - 2.4.6
cache: bundler
env:
- RAILS_ENV=test
+ - SIDEKIQ_SOURCE=https://gems.contribsys.com
before_install:
- - curl -O https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-1.2.1.deb && sudo dpkg -i --force-confnew elasticsearch-1.2.1.deb && sudo service elasticsearch restart
- psql -c 'create database travis_ci_test;' -U postgres
- cp config/database.travis.yml config/database.yml
- git --git-dir=spec/fixtures/repository.git init --bare
+ - bundle config gems.contribsys.com $BUNDLE_GEMS__CONTRIBSYS__COM
script:
- bundle exec rake db:test:prepare
- - bundle exec rake environment elasticsearch:import:model FORCE=y CLASS=Commit
- - bundle exec rake environment elasticsearch:import:model FORCE=y CLASS=Key
- - bundle exec rake environment elasticsearch:import:model FORCE=y CLASS=Translation
+ - bundle exec rake chewy:reset
- bundle exec rspec
diff --git a/Brewfile b/Brewfile
index fb2f44b7..cb1d310f 100644
--- a/Brewfile
+++ b/Brewfile
@@ -1,6 +1,7 @@
brew 'cmake'
brew 'libarchive'
brew 'libgit2'
-brew 'postgresql'
+brew 'postgresql@9.4'
brew 'qt'
brew 'redis'
+brew 'elasticsearch@5.6'
diff --git a/Dockerfile b/Dockerfile
index 6dc217e2..e2318847 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,15 @@
-FROM ruby:2.3.1
+FROM ruby:2.4.6
ARG BUNDLE_GEMS__CONTRIBSYS__COM
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
+COPY square_primary_certificate_authority_g2.crt /usr/local/share/ca-certificates/
+COPY square_service_authority_g2.crt /usr/local/share/ca-certificates/
+RUN update-ca-certificates
+
+# RUN printf "deb http://archive.debian.org/debian/ jessie main\ndeb-src http://archive.debian.org/debian/ jessie main\ndeb http://security.debian.org jessie/updates main\ndeb-src http://security.debian.org jessie/updates main" > /etc/apt/sources.list
RUN apt-get update -qq \
&& apt-get install -y build-essential nodejs libarchive-dev libpq-dev \
postgresql-client cmake tidy git \
diff --git a/Gemfile b/Gemfile
index e9bdc31f..1a187d84 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,8 @@
-source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-ruby '2.3.1'
+source 'https://rubygems.org'
+
+ruby '2.4.6'
# FRAMEWORK
gem 'rails', '4.2.10'
@@ -15,6 +16,7 @@ gem 'rubyzip'
# AUTHENTICATION
gem 'devise'
+gem 'devise_security_extension'
# MODELS
gem 'pg', '< 1.0'
@@ -29,11 +31,8 @@ gem 'after-commit-on-action'
gem 'postgres_ext'
gem 'active_record_union'
-# ElASTICSEARCH
-gem 'elasticsearch', '< 6.0'
-gem 'elasticsearch-rails'
-gem 'elasticsearch-model'
-gem 'elasticsearch-dsl'
+# ELASTICSEARCH
+gem 'chewy'
# VIEWS
gem 'jquery-rails'
@@ -46,8 +45,9 @@ gem 'slim-rails'
gem 'autoprefixer-rails'
# UTILITIES
+gem 'bundler'
gem 'json'
-gem 'rugged', github: 'brandonweeks/rugged', tag: 'v0.24.0-square0', submodules: true
+gem 'rugged', github: 'squarit/rugged', tag: 'v0.27.2-square0', submodules: true
gem 'coffee-script'
gem 'unicode_scanner'
gem 'httparty'
@@ -59,6 +59,7 @@ gem 'aws-sdk', '< 3'
gem 'execjs'
gem 'safemode'
gem 'pivot_table'
+gem 'sentry-raven'
# IMPORTING
gem 'nokogiri'
@@ -80,9 +81,10 @@ gem 'uea-stemmer'
gem 'faker'
# BACKGROUND JOBS
-source "https://gems.contribsys.com/" do
+source 'https://gems.contribsys.com' do
gem 'sidekiq-pro', '= 3.4.5'
end
+
gem 'sidekiq-failures', github: 'mhfs/sidekiq-failures'
gem 'sinatra', require: nil
gem 'whenever', require: nil
@@ -97,6 +99,14 @@ gem 'sass-rails'
gem 'coffee-rails'
gem 'uglifier'
gem 'hogan_assets', github: 'rubenrails/hogan_assets', branch: 'fix_for_sprockets' # sprockets 3 compatibility
+gem 'awesome_print'
+
+source 'https://rails-assets.org' do
+ gem 'rails-assets-raven-js'
+end
+
+# METRICS
+gem 'newrelic_rpm'
group :development do
gem 'capistrano'
@@ -113,6 +123,13 @@ group :development do
gem 'binding_of_caller'
end
+group :dummy do
+ # needed for deployment
+ gem 'rbnacl', '< 5.0'
+ gem 'rbnacl-libsodium'
+ gem 'bcrypt_pbkdf', '< 2.0'
+end
+
group :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
@@ -121,3 +138,5 @@ group :test do
gem 'database_cleaner'
gem 'capybara'
end
+
+gem "message_format", "~> 0.0.5"
diff --git a/Gemfile.lock b/Gemfile.lock
index d04cbda9..77830f77 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,11 +1,3 @@
-GIT
- remote: https://github.com/brandonweeks/rugged.git
- revision: cdabe1e11ab85428c380df21cbd3117ca232a865
- tag: v0.24.0-square0
- submodules: true
- specs:
- rugged (0.24.0)
-
GIT
remote: https://github.com/mhfs/sidekiq-failures.git
revision: ade3c0035e8b7df0821a71328fe31dc706226aeb
@@ -23,6 +15,14 @@ GIT
sprockets (>= 2.0.3)
tilt (>= 1.3.3)
+GIT
+ remote: https://github.com/squarit/rugged.git
+ revision: e521d4903a624f119e77c7e66cf24a34687f9b33
+ tag: v0.27.2-square0
+ submodules: true
+ specs:
+ rugged (0.27.2)
+
GIT
remote: https://github.com/thoughtbot/paperclip.git
revision: 523bd46c768226893f23889079a7aa9c73b57d68
@@ -54,6 +54,7 @@ GIT
GEM
remote: https://rubygems.org/
remote: https://gems.contribsys.com/
+ remote: https://rails-assets.org/
specs:
CFPropertyList (2.3.6)
actionmailer (4.2.10)
@@ -101,6 +102,7 @@ GEM
arel (6.0.4)
autoprefixer-rails (7.2.4)
execjs
+ awesome_print (1.8.0)
aws-sdk (2.10.115)
aws-sdk-resources (= 2.10.115)
aws-sdk-core (2.10.115)
@@ -110,6 +112,7 @@ GEM
aws-sdk-core (= 2.10.115)
aws-sigv4 (1.0.2)
bcrypt (3.1.11)
+ bcrypt_pbkdf (1.0.1)
better_errors (2.4.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
@@ -123,6 +126,7 @@ GEM
sass (>= 3.5.2)
builder (3.2.3)
byebug (10.0.1)
+ camertron-eprun (1.1.1)
capistrano (3.10.1)
airbrussh (>= 1.0.0)
i18n
@@ -147,7 +151,12 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (>= 2.0, < 4.0)
+ chewy (5.0.0)
+ activesupport (>= 4.0)
+ elasticsearch (>= 2.0.0)
+ elasticsearch-dsl
chronic (0.10.2)
+ cldr-plurals-runtime-rb (1.0.1)
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
@@ -172,21 +181,19 @@ GEM
railties (>= 4.1.0, < 5.2)
responders
warden (~> 1.2.3)
+ devise_security_extension (0.9.2)
+ devise (>= 2.0.0)
+ rails (>= 3.1.1)
diff-lcs (1.3)
dropzonejs-rails (0.8.2)
rails (> 3.1)
- elasticsearch (5.0.4)
- elasticsearch-api (= 5.0.4)
- elasticsearch-transport (= 5.0.4)
- elasticsearch-api (5.0.4)
+ elasticsearch (6.1.0)
+ elasticsearch-api (= 6.1.0)
+ elasticsearch-transport (= 6.1.0)
+ elasticsearch-api (6.1.0)
multi_json
- elasticsearch-dsl (0.1.5)
- elasticsearch-model (5.0.2)
- activesupport (> 3)
- elasticsearch (~> 5)
- hashie
- elasticsearch-rails (5.0.2)
- elasticsearch-transport (5.0.4)
+ elasticsearch-dsl (0.1.6)
+ elasticsearch-transport (6.1.0)
faraday
multi_json
erubi (1.7.0)
@@ -223,7 +230,6 @@ GEM
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
- hashie (3.5.7)
html_validation (1.1.5)
httparty (0.15.6)
multi_xml (>= 0.5.2)
@@ -258,6 +264,8 @@ GEM
lumberjack (1.0.13)
mail (2.7.0)
mini_mime (>= 0.1.1)
+ message_format (0.0.5)
+ twitter_cldr (~> 4.0)
method_source (0.9.0)
mime-types (3.1)
mime-types-data (~> 3.2015)
@@ -274,6 +282,7 @@ GEM
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.2.0)
+ newrelic_rpm (6.3.0.355)
nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
notiffany (0.1.1)
@@ -316,6 +325,7 @@ GEM
bundler (>= 1.3.0, < 2.0)
railties (= 4.2.10)
sprockets-rails
+ rails-assets-raven-js (3.20.1)
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.9)
@@ -331,10 +341,14 @@ GEM
activesupport (= 4.2.10)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
- rake (12.3.0)
+ rake (12.3.1)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
+ rbnacl (4.0.2)
+ ffi
+ rbnacl-libsodium (1.0.16)
+ rbnacl (>= 3.0.1)
redcarpet (3.4.0)
redis (3.3.5)
redis-actionpack (5.0.2)
@@ -408,6 +422,8 @@ GEM
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
+ sentry-raven (2.7.1)
+ faraday (>= 0.7.6, < 1.0)
sexp_processor (4.10.0)
shellany (0.0.1)
sidekiq (4.2.10)
@@ -457,6 +473,10 @@ GEM
actionpack (>= 3.1)
jquery-rails
railties (>= 3.1)
+ twitter_cldr (4.4.4)
+ camertron-eprun
+ cldr-plurals-runtime-rb (~> 1.0)
+ tzinfo
tzinfo (1.2.4)
thread_safe (~> 0.1)
uea-stemmer (0.10.3)
@@ -481,29 +501,30 @@ DEPENDENCIES
active_record_union
after-commit-on-action
autoprefixer-rails
+ awesome_print
aws-sdk (< 3)
+ bcrypt_pbkdf (< 2.0)
better_errors
binding_of_caller
boolean
bootstrap
+ bundler
capistrano
capistrano-bundler
capistrano-rails
capistrano-rvm
capistrano-sidekiq
capybara
+ chewy
coffee-rails
coffee-script
configoro
database_cleaner
devise
+ devise_security_extension
diff-lcs
docx (= 0.3.1)!
dropzonejs-rails
- elasticsearch (< 6.0)
- elasticsearch-dsl
- elasticsearch-model
- elasticsearch-rails
execjs
factory_bot_rails
faker
@@ -518,7 +539,9 @@ DEPENDENCIES
json
kaminari
libarchive
+ message_format (~> 0.0.5)
mustache
+ newrelic_rpm
nokogiri
paperclip!
parslet
@@ -529,7 +552,10 @@ DEPENDENCIES
rack-attack
rack-cache
rails (= 4.2.10)
+ rails-assets-raven-js!
rails-observers
+ rbnacl (< 5.0)
+ rbnacl-libsodium
redcarpet
redis-mutex
redis-namespace
@@ -541,6 +567,7 @@ DEPENDENCIES
rugged!
safemode
sass-rails
+ sentry-raven
sidekiq-failures!
sidekiq-pro (= 3.4.5)!
similar_text
@@ -561,7 +588,7 @@ DEPENDENCIES
yard
RUBY VERSION
- ruby 2.3.1p112
+ ruby 2.4.6p354
BUNDLED WITH
- 1.16.1
+ 1.17.3
diff --git a/README.md b/README.md
index e0d6880b..b42fe345 100644
--- a/README.md
+++ b/README.md
@@ -83,35 +83,23 @@ Create and import your first project!
### Setting up your development environment without Docker
-Developing for Shuttle requires Ruby 2.3.1, PostgreSQL, Redis, Tidy, Sidekiq Pro
-ElasticSearch 1.7, and a modern version of libarchive. To run Shuttle for the
-first time:
+Developing for Shuttle requires Ruby 2.4.6, PostgreSQL 9.4, Redis, Tidy, Sidekiq
+Pro ElasticSearch 5.6, and a modern version of libarchive. To run Shuttle for
+the first time:
1. Clone this project.
-2. Install Ruby 2.3.1. If you are using RVM, you can do so using the
+2. Install Ruby 2.4.6. If you are using RVM, you can do so using the
`.ruby-version` file.
3. Run `brew bundle` to install all dependencies available via Homebrew, which
are specified in the `Brewfile`, or install them manually referencing the
Brewfile.
-4. Since ElasticSearch 1.7 is required, you will have to download and install it
- manually at https://www.elastic.co/downloads/past-releases/elasticsearch-1-7-6.
-
- If you are already running a more modern version of ElasticSearch, you can
- run this older version simultaneously on a different port (e.g. 9201) by
- altering the `config/elasticsearch.yml` file in the ElasticSearch install
- directory. If you do this, make sure you override the default ElasticSearch
- URL when running Shuttle by either creating a
- `config/environments/development/elasticsearch.yml` file (to override the
- file under `config/environments/common`), or setting the `ELASTICSEARCH_URL`
- instance variable.
-
-5. Buy Sidekiq Pro and place your private source URL in Gemfile as specified by
+4. Buy Sidekiq Pro and place your private source URL in Gemfile as specified by
the Sidekiq Pro documentation.
-6. Create a PostgreSQL user called `shuttle`, and make it the owner of two
+5. Create a PostgreSQL user called `shuttle`, and make it the owner of two
PostgreSQL databases, `shuttle_development` and `shuttle_test`:
``` sh
@@ -120,51 +108,45 @@ first time:
createdb -O shuttle shuttle_test
```
-7. You will need to tell Bundler where the libarchive install directory is. If
+6. You will need to tell Bundler where the libarchive install directory is. If
you installed libarchive using Homebrew, you can do this by running
``` sh
bundle config build.libarchive --with-opt-dir=$(brew --prefix libarchive)
```
-8. Likewise, for Rugged, you will need to tell Bundler where the libgit2 install
+7. Likewise, for Rugged, you will need to tell Bundler where the libgit2 install
directory is. If you installed libgit2 using Homebrew:
``` sh
bundle config build.rugged --with-opt-dir=$(brew --prefix libgit2)
```
-9. Make sure that PostgreSQL, Redis, and ElasticSearch are running. If you
- installed them via Homebrew, running `brew info postgresql` and
- `brew info redis` will tell you how to run them. For ElasticSearch, read the
- README in your install directory.
+8. Make sure that PostgreSQL, Redis, and ElasticSearch are running. If you
+ installed them via Homebrew, running `brew info postgresql@9.6`,
+ `brew info redis`, and `brew info elasticsearch` will tell you how to run
+ them.
-10. Install the `mailcatcher` gem, which is used to receive emails sent in
+9. Install the `mailcatcher` gem, which is used to receive emails sent in
development. (This gem is not a part of the Gemfile because it's typically
installed as part of a global or system-wide gemset to be used with
all projects.)
-11. Optionally, install the `foreman` gem, which runs all the processes
+
+10. Optionally, install the `foreman` gem, which runs all the processes
necessary for development.
-12. Install all required gems by running `bundle install`.
-13. Run `rake db:migrate db:seed` to migrate and seed the database.
-14. Run `RAILS_ENV=test rake db:migrate` to setup the test database.
-15. Initialize the ElasticSearch index for development by running
- ``` sh
- rake environment elasticsearch:import:model FORCE=y CLASS=Commit
- rake environment elasticsearch:import:model FORCE=y CLASS=Key
- rake environment elasticsearch:import:model FORCE=y CLASS=Translation
- ```
+11. Install all required gems by running `bundle install`.
+
+12. Run `rake db:migrate db:seed` to migrate and seed the database.
+
+13. Run `RAILS_ENV=test rake db:migrate` to setup the test database.
-16. Do the same for the test indexes:
+14. Initialize the ElasticSearch index for development by running
+ `rake chewy:reset`.
- ``` sh
- RAILS_ENV=test rake environment elasticsearch:import:model FORCE=y CLASS=Commit
- RAILS_ENV=test rake environment elasticsearch:import:model FORCE=y CLASS=Key
- RAILS_ENV=test rake environment elasticsearch:import:model FORCE=y CLASS=Translation
- ```
+15. Do the same for the test indexes: `RAILS_ENV=test rake chewy:reset`
-17. Verify that all specs pass with `rspec spec`.
+16. Verify that all specs pass with `rspec spec`.
#### Starting the server
@@ -457,7 +439,7 @@ RSpec specs under the `spec` directory. Views and JavaScript files are not
specced. Almost all unit tests use factories rather than mocks, putting them
somewhat closer to integration tests.
-If you are using Docker, first run `docker-compose -f docker-compose.test.yml`
+If you are using Docker, first run `docker-compose -f docker-compose.test.yml build`
to build the images. This only has to be done once, and each time you make a
change to the source code. Run `docker-compose -f docker-compose.test.yml up` to
start the environment, and
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index b6155c2e..d4a9b38e 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -68,6 +68,8 @@
#= require popper
#= require bootstrap
#=
+#= require raven-js
+#
#= require_tree ../templates
#= require_tree ./application
diff --git a/app/assets/javascripts/application/translation_workbench.js.coffee.erb b/app/assets/javascripts/application/translation_workbench.js.coffee.erb
index c308a6b3..efe2f719 100644
--- a/app/assets/javascripts/application/translation_workbench.js.coffee.erb
+++ b/app/assets/javascripts/application/translation_workbench.js.coffee.erb
@@ -336,8 +336,10 @@ class TranslationItem
fenced_p.append htmlEscape(copy[copy_index...range[0]])
copy_index = range[0]
# consume the range and enclose it in a span
- $('').addClass('fenced').text(copy[range[0]..range[1]]).appendTo fenced_p
- copy_index = range[1] + 1
+ copy_trimmed = copy[range[0]..range[1]]
+ if copy_trimmed.length > 0
+ $('').addClass('fenced').text(copy_trimmed).appendTo fenced_p
+ copy_index = range[1] + 1
# consume from the end of the last range to the end of the string
if copy_index < copy.length
diff --git a/app/assets/stylesheets/base/_base.scss b/app/assets/stylesheets/base/_base.scss
index 1754855e..4fc20776 100644
--- a/app/assets/stylesheets/base/_base.scss
+++ b/app/assets/stylesheets/base/_base.scss
@@ -1,15 +1,15 @@
@import "base/vars";
@import "css3-mixins";
-// ----- RESPONSIVE
-
-// @include ipad-and-larger {
-// .compact-only { display: none; }
-// }
-
-// @include iphone-landscape-and-smaller {
-// .full-size-only { display: none; }
-// }
+@import url("https://rsms.me/inter/inter.css");
+*, html {
+ font-family: "Inter", sans-serif;
+}
+@supports (font-variation-settings: normal) {
+ html {
+ font-family: "Inter var", sans-serif;
+ }
+}
// ----- LAYOUT
@@ -21,9 +21,8 @@ html {
body {
height: 100%;
background-color: white;
- font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: $base-font-size;
- line-height: 21px;
+ line-height: $base-line-height;
}
.content-container {
@@ -35,7 +34,7 @@ body {
.header {
h1 {
display: inline-block;
- color: $gray3;
+ color: $gray2;
margin-top: 0px;
}
@@ -51,7 +50,6 @@ body {
h6 {
color: $gray4;
- text-transform: uppercase;
margin-left: 30px;
line-height: 10px;
margin-top: 0px;
@@ -60,15 +58,13 @@ body {
.border {
border: 1px solid $gray5;
- overflow-y: scroll;
}
hr.divider {
border: 0;
height: 1px;
background-color: $gray5;
- margin-top: 5px;
- margin-bottom: 40px;
+ margin: 5px 0 10px;
}
.hide {
diff --git a/app/assets/stylesheets/base/_controls.scss b/app/assets/stylesheets/base/_controls.scss
index 09942f8d..2a5afe9b 100644
--- a/app/assets/stylesheets/base/_controls.scss
+++ b/app/assets/stylesheets/base/_controls.scss
@@ -15,7 +15,6 @@
font-weight: normal;
letter-spacing: 1px;
height: $input-size;
- text-transform: uppercase;
text-decoration: none !important;
padding: 5px 20px;
@@ -25,7 +24,6 @@
&:hover {
background: darken($bg-color, 5%);
- color: $color !important;
cursor: pointer;
text-decoration: none !important;
}
@@ -53,6 +51,15 @@
padding: 5px;
text-align: center;
}
+
+ &.button--secondary {
+ background-color: white;
+ color: $dark-blue;
+ border-color: $dark-blue;
+ &:hover {
+ background: rgba(41, 150, 204, 0.1);
+ }
+ }
}
@mixin field-options {
@@ -128,7 +135,6 @@ textarea {
select {
@include field-options;
@include appearance(none);
- font-family: "Helvetica Neue", Helvetica, sans-serif;
text-indent: 0.01px;
text-overflow: '';
diff --git a/app/assets/stylesheets/base/_forms.scss b/app/assets/stylesheets/base/_forms.scss
index 68dbeac6..53804799 100644
--- a/app/assets/stylesheets/base/_forms.scss
+++ b/app/assets/stylesheets/base/_forms.scss
@@ -37,7 +37,6 @@ fieldset {
legend {
font-size: $legend-size;
line-height: $legend-line-height;
- text-transform: uppercase;
font-weight: 600;
letter-spacing: 1px;
display: block;
diff --git a/app/assets/stylesheets/base/_grid.scss b/app/assets/stylesheets/base/_grid.scss
index 6faa5f8b..562a74c3 100644
--- a/app/assets/stylesheets/base/_grid.scss
+++ b/app/assets/stylesheets/base/_grid.scss
@@ -43,7 +43,7 @@
}
@include widescreen {
- .container { width: $widescreen-size; }
+ .container { width: 95%; max-width: $widescreen-size; }
.row { @include columns($widescreen-column-width); }
}
diff --git a/app/assets/stylesheets/base/_tables.scss b/app/assets/stylesheets/base/_tables.scss
index 313e15d6..23ae7a99 100644
--- a/app/assets/stylesheets/base/_tables.scss
+++ b/app/assets/stylesheets/base/_tables.scss
@@ -12,10 +12,6 @@
.checkbox + .checkbox, input + .checkbox { margin-left: 15px; }
input + input, input + select, select + input, select + select { margin-left: 15px; }
- .main {
-
- }
-
.collapse {
margin-top: 30px;
margin-left: 40px;
@@ -60,9 +56,8 @@ table.table {
tr {
text-align: left;
font-weight: bold;
- text-transform: uppercase;
th {
- padding: 15px 10px;
+ padding: 14px 8px;
}
}
}
@@ -75,7 +70,15 @@ table.table {
td {
padding: 10px;
+ vertical-align: middle;
}
}
}
}
+
+.header {
+ margin-bottom: 6px;
+}
+.header-buttons {
+ padding-bottom: 10px;
+}
diff --git a/app/assets/stylesheets/base/_typography.scss b/app/assets/stylesheets/base/_typography.scss
index ccb78107..9f8c1496 100644
--- a/app/assets/stylesheets/base/_typography.scss
+++ b/app/assets/stylesheets/base/_typography.scss
@@ -13,14 +13,10 @@ h6 { font-size: $h6-size; line-height: $h6-line-height; }
h2 { font-size: $h4-size; }
h3 { font-size: $h5-size; }
h4 { font-size: $h6-size; }
- h5 { font-size: 11px; text-transform: uppercase; }
- h6 { font-size: 11px; text-transform: uppercase; }
}
h1, h2, h3, h4, h5, h6 {
margin-top: 20px;
- margin-bottom: 10px;
- font-weight: 100;
&:first-child { margin-top: 0; }
}
@@ -42,14 +38,14 @@ a {
strong { font-weight: 500; }
em { font-style: italic; }
-p.small, small { font-size: 80%; }
+p.small, small, .small { font-size: 80%; }
dl {
dt { font-weight: bold; }
dd { margin: 5px 0 10px 10px; }
}
-tt, kbd, code, samp, pre { font-family: Menlo, monospace; }
+tt, kbd, code, samp, pre { font-family: 'SF Mono', 'Roboto Mono', Monaco, Menlo, monospace; }
// tt is used for code atoms such as variable or class names
tt { font-weight: bold; }
// kbd is used for keyboard input strings
@@ -61,7 +57,7 @@ code { border-bottom: 1px solid $gray5; }
code.short { border-bottom: none; }
// pre is used for large code blocks
pre {
- background-color: #f9f9f9;
+ background-color: $gray6;
@include border-radius($notification-radius-size);
padding: 20px;
@@ -89,4 +85,14 @@ pre {
.lowercase {
text-transform: lowercase;
-}
\ No newline at end of file
+}
+
+.monospace,
+.revision-prefix,
+.revision {
+ font-family: $monospace-font-family;
+}
+
+.subtitle {
+ color: $gray3;
+}
diff --git a/app/assets/stylesheets/base/_vars.scss b/app/assets/stylesheets/base/_vars.scss
index e65ddcb6..fe192f1b 100644
--- a/app/assets/stylesheets/base/_vars.scss
+++ b/app/assets/stylesheets/base/_vars.scss
@@ -1,3 +1,6 @@
+$font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+$monospace-font-family: 'SF Mono', 'Roboto Mono', Monaco, monospace;
$base-font-size: 14px;
$nav-font-size: 12px;
$control-font-size: 14px;
@@ -11,7 +14,6 @@ $h4-size: 18px;
$h5-size: 14px;
$h6-size: 12px;
-
$base-line-height: 21px;
$legend-line-height: 28px;
@@ -23,7 +25,7 @@ $h4-line-height: 28px;
$h5-line-height: 24px;
$h6-line-height: 20px;
-$button-radius-size: 1px;
+$button-radius-size: 4px;
$input-radius-size: 1px;
$notification-radius-size: 4px;
@@ -32,6 +34,8 @@ $subnav-height: 40px;
$input-size: 32px;
$button-size: 32px;
+$medium-letter-spacing: 0.0365em;
+
// ----- COLORS
$gray1: #212121;
@@ -41,8 +45,9 @@ $gray4: #b3b3b3;
$gray5: #dfdfdf;
$gray6: #f9f9f9;
+
$off-blue: #def1f7;
-$dark-blue: #2884b4;
+$dark-blue: #2996cc;
$navbar-background: #2b2b2b;
$subnav-background: #373737;
@@ -68,7 +73,7 @@ $input-white: #fbfcfc;
$input-white-text: #6e797f;
$input-white-border: #d3d9de;
-$input-blue: #4296c2;
+$input-blue: $dark-blue;
$input-blue-text: white;
$input-blue-border: #3688b3;
@@ -92,10 +97,10 @@ $button-flag: #f5d68d;
$columns: 16;
-$widescreen-size: 1200px;
+$widescreen-size: 1440px;
$widescreen-column-width: $widescreen-size/$columns;
-$desktop-size: 960px;
+$desktop-size: 1200px;
$desktop-column-width: $desktop-size/$columns;
$ipad-size: 768px;
diff --git a/app/assets/stylesheets/pages/glossary.css.scss b/app/assets/stylesheets/pages/glossary.css.scss
index 66733a20..6e03c397 100644
--- a/app/assets/stylesheets/pages/glossary.css.scss
+++ b/app/assets/stylesheets/pages/glossary.css.scss
@@ -118,7 +118,6 @@ body.glossary {
body.source_glossary_entries, body.locale_glossary_entries {
.eight.columns {
- margin-top: -30px;
padding: 30px;
@include box-sizing;
position: relative;
diff --git a/app/assets/stylesheets/pages/groups.css.scss b/app/assets/stylesheets/pages/groups.css.scss
new file mode 100644
index 00000000..20422ee8
--- /dev/null
+++ b/app/assets/stylesheets/pages/groups.css.scss
@@ -0,0 +1,35 @@
+@import "base/vars";
+@import "css3-mixins";
+
+body.groups {
+ .control-group label {
+ padding-top: 0;
+ width: 170px;
+ }
+
+ groups-show, groups-issues {
+ .download-button {
+ margin-right: 10px;
+ }
+ }
+
+ groups-manifest {
+ .locale-manifest {
+ .locale {
+ text-transform: uppercase;
+ font-weight: bold;
+ font-size: 1.5em;
+ margin-bottom: 20px;
+ }
+
+ hr {
+ margin: 20px 0 30px 0;
+ }
+ }
+ }
+
+ .hide-btn, .show-btn {
+ width: 100px;
+ margin-left: 10px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/home.css.scss b/app/assets/stylesheets/pages/home.css.scss
index 598d0c13..3ce0662d 100644
--- a/app/assets/stylesheets/pages/home.css.scss
+++ b/app/assets/stylesheets/pages/home.css.scss
@@ -20,15 +20,33 @@ body.home {
}
.filter-bar {
- input[type="submit"] { width: 125px; margin-left: 15px; }
- select[name="filter__rfc5646_locales"] { width: 200px; }
- select[name="filter__status"] { width: 200px; }
- input[name="commits_filter__sha"], input[name="articles_filter__name"], input[name="groups_filter__name"] { width: 350px; }
- select[name="commits_filter__project_id"], select[name="articles_filter__project_id"], select[name="groups_filter__project_id"] { width: 160px; }
+ input[type="submit"] {
+ width: 125px;
+ margin-left: 15px;
+ }
+ select[name="filter__rfc5646_locales"] {
+ width: 200px;
+ }
+ select[name="filter__status"] {
+ min-width: 200px;
+ }
+ input[name="commits_filter__sha"],
+ input[name="articles_filter__name"],
+ input[name="groups_filter__name"] {
+ width: 230px;
+ }
+ select[name="commits_filter__project_id"],
+ select[name="articles_filter__project_id"],
+ select[name="groups_filter__project_id"] {
+ min-width: 160px;
+ max-width: 300px;
+ }
.twitter-typeahead {
margin-left: 15px;
- input + input { margin: 0px; }
+ input + input {
+ margin: 0px;
+ }
}
.advanced-filters-label {
@@ -45,7 +63,10 @@ body.home {
tbody {
tr {
border-top: 3px white solid;
- &:hover {cursor: pointer;}
+ transition: 0.3s background-color;
+ &:hover {
+ cursor: pointer;
+ }
td {
&.status-translating,
@@ -54,45 +75,95 @@ body.home {
padding: 0;
width: 5px;
}
- &.status-translating { background-color: $commit-red; }
- &.status-loading { background-color: $commit-blue; }
- &.status-ready { background-color: $commit-green; }
+ &.status-translating {
+ background-color: $commit-red;
+ }
+ &.status-loading {
+ background-color: $commit-blue;
+ }
+ &.status-ready {
+ background-color: $commit-green;
+ }
- &.due-date { width: 110px;}
- &.centered { text-align: center; }
+ &.due-date {
+ width: 110px;
+ }
+ &.centered {
+ text-align: center;
+ }
- select { width: 100%; }
- input[type="text"] { width: 110px; }
- a { text-decoration: underline; }
- .description { width: 280px; }
- .translate-link { padding: 5px 10px; }
+ select {
+ width: 100%;
+ }
+ input[type="text"] {
+ width: 110px;
+ }
+ a {
+ text-decoration: underline;
+ }
+ .description {
+ text-align: center;
+ font-size: 10px;
+ line-height: 12px;
+ vertical-align: top;
+ }
+ .translate-link {
+ padding: 5px 10px;
+ }
}
}
}
}
- .priority-0 { color: #E94239; font-weight: bold; }
- .priority-1 { color: #E98C39; font-weight: bold; }
- .priority-2 { color: #E9B039; font-weight: bold; }
- .priority-3 { color: $input-green; font-weight: bold; }
+ .priority-0 {
+ color: #e94239;
+ font-weight: bold;
+ }
+ .priority-1 {
+ color: #e98c39;
+ font-weight: bold;
+ }
+ .priority-2 {
+ color: #e9b039;
+ font-weight: bold;
+ }
+ .priority-3 {
+ color: $input-green;
+ font-weight: bold;
+ }
.pagination-links {
width: 100%;
font-size: 20px;
margin: 10px 0px;
.pagination {
- display:-moz-box; /* Firefox */
- display:-webkit-box; /* Safari and Chrome */
- display:-ms-flexbox; /* Internet Explorer 10 */
- display:box;
+ display: -moz-box; /* Firefox */
+ display: -webkit-box; /* Safari and Chrome */
+ display: -ms-flexbox; /* Internet Explorer 10 */
+ display: box;
+ }
+ .switch-page {
+ width: 198px;
}
- .switch-page { width: 198px; }
.pages {
@include flex;
text-align: center;
- .page + .page { margin-left: 8px; }
+ .page + .page {
+ margin-left: 8px;
+ }
}
}
- .pagination-info { width: 100%; text-align: center; }
+ .pagination-info {
+ width: 100%;
+ text-align: center;
+ }
+
+ .csv-button {
+ margin-right: 10px;
+ }
+}
+.description-container {
+ padding: 0 !important; // yes, I'm doing this so I don't have to chase this down
+ width: 120px;
}
diff --git a/app/assets/stylesheets/pages/projects.css.scss b/app/assets/stylesheets/pages/projects.css.scss
index 1f76248b..24859b46 100644
--- a/app/assets/stylesheets/pages/projects.css.scss
+++ b/app/assets/stylesheets/pages/projects.css.scss
@@ -35,10 +35,9 @@ body.projects {
tr {
text-align: left;
font-weight: bold;
- text-transform: uppercase;
-
th {
padding: 15px;
+ min-width: 100px;
}
}
}
@@ -87,7 +86,6 @@ body.projects {
projects-new,
projects-update {
.eight.columns {
- margin-top: -30px;
padding: 30px;
box-sizing: border-box;
}
diff --git a/app/assets/stylesheets/pages/reports.css.scss b/app/assets/stylesheets/pages/reports.css.scss
new file mode 100644
index 00000000..f3ed8da9
--- /dev/null
+++ b/app/assets/stylesheets/pages/reports.css.scss
@@ -0,0 +1,36 @@
+.day {
+ border: 1px solid rgba(128, 128, 128, 0.212);
+ border-radius: 4px;
+ padding: 10px;
+ margin: 10px 0;
+ h2 {
+ text-align: center;
+ margin-bottom: 20px;
+ }
+ .report-buttons {
+ display: flex;
+ display: flex;
+ justify-content: space-around;
+ a {
+ border: 1px solid;
+ border-radius: 3px;
+ padding: 5px 13px;
+ box-shadow: 3px 3px 0px rebeccapurple;
+ transition: 0.3s box-shadow;
+ &:hover {
+ box-shadow: 1px 1px 0px rebeccapurple;
+ text-decoration: none;
+ }
+ &:active {
+ box-shadow: inset 2px 2px 0 rebeccapurple;
+ }
+ }
+ }
+ .date-picker {
+ width: 380px;
+ margin: 10px auto 30px;
+ label {
+ margin-right: 10px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/translations.scss b/app/assets/stylesheets/pages/translations.scss
index da70c4ba..9a0a0283 100644
--- a/app/assets/stylesheets/pages/translations.scss
+++ b/app/assets/stylesheets/pages/translations.scss
@@ -4,7 +4,6 @@
body.translations {
.eight, .sixteen {
&.columns {
- margin-top: -30px;
padding: 30px;
@include box-sizing;
position: relative;
@@ -164,4 +163,4 @@ body.translations {
margin-left: 10px;
}
}
-}
\ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/unified_authentication.scss b/app/assets/stylesheets/pages/unified_authentication.scss
index 1b3ca7ce..679a4697 100644
--- a/app/assets/stylesheets/pages/unified_authentication.scss
+++ b/app/assets/stylesheets/pages/unified_authentication.scss
@@ -5,10 +5,14 @@
$form-radius-size: 4px;
body.unified-authentication, body.passwords {
- background-image: image-url('login-background.jpg');
- background-color: $gray3;
- background-repeat: repeat-y;
- background-size: cover;
+ // background-image: image-url('login-background.jpg');
+ // background-color: $gray3;
+ // background-repeat: repeat-y;
+ // background-size: cover;
+ background: #00F260; /* fallback for old browsers */
+ background: -webkit-linear-gradient(to left, #0575E6, #00F260); /* Chrome 10-25, Safari 5.1-6 */
+ background: linear-gradient(to left, #0575E6, #00F260); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
+
#navbar-container {
background-color: transparent;
@@ -31,7 +35,6 @@ body.unified-authentication, body.passwords {
&>.ten.columns {
background-color: $gray1;
- @include opacity(0.9);
color: white;
font-weight: 300;
text-align: center;
@@ -40,7 +43,7 @@ body.unified-authentication, body.passwords {
h1 {
font-size: 50px;
- font-weight: 100;
+ font-weight: 200;
line-height: 58px;
}
diff --git a/app/assets/stylesheets/pages/users.css.scss b/app/assets/stylesheets/pages/users.css.scss
index 1750cf9d..ed7924b2 100644
--- a/app/assets/stylesheets/pages/users.css.scss
+++ b/app/assets/stylesheets/pages/users.css.scss
@@ -9,8 +9,6 @@ body.users {
tr {
text-align: left;
font-weight: bold;
- text-transform: uppercase;
-
th {
padding: 15px;
}
@@ -96,4 +94,4 @@ body.users {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/assets/stylesheets/partials/_footer.scss b/app/assets/stylesheets/partials/_footer.scss
index e71ca197..9c8878bf 100644
--- a/app/assets/stylesheets/partials/_footer.scss
+++ b/app/assets/stylesheets/partials/_footer.scss
@@ -15,25 +15,32 @@ footer {
margin-top: -9em;
height: 9em;
overflow: hidden;
- background-color: $navbar-background;
+ background: #DA4453; /* fallback for old browsers */
+ background: -webkit-linear-gradient(to top, #89216B, #DA4453); /* Chrome 10-25, Safari 5.1-6 */
+ background: linear-gradient(to top, #89216B, #DA4453); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
+
+
p {
font-size: 12px;
font-weight: 500;
letter-spacing: 1px;
text-align: center;
text-transform: uppercase;
- color: #555555;
+ color: #fff;
margin-top: 20px;
a {
- color: #555555;
+ color: #fff;
text-decoration: underline;
+ &:hover {
+ color: #eee;
+ }
}
i {
- color: darkred;
+ color: #fff;
}
}
img {
- opacity:0.2;
+ opacity: 0.2;
}
}
diff --git a/app/assets/stylesheets/partials/_navbar.scss b/app/assets/stylesheets/partials/_navbar.scss
index 3d131e4f..a6e81aa6 100644
--- a/app/assets/stylesheets/partials/_navbar.scss
+++ b/app/assets/stylesheets/partials/_navbar.scss
@@ -7,10 +7,8 @@ $navbar-min-width: 910px;
@include clearfix;
padding: 1.3em 1em;
- color: rgba(255,255,255,.5);
- text-transform: uppercase;
- font-weight: bold;
- font-size: 12px;
+ font-size: $base-font-size;
+ letter-spacing: $medium-letter-spacing;
&.navbar-dark {
background-color: $navbar-background;
@@ -18,7 +16,6 @@ $navbar-min-width: 910px;
.navbar-brand {
float: left;
- text-transform: uppercase;
margin-right: 1.5em;
img {
@@ -28,7 +25,6 @@ $navbar-min-width: 910px;
}
button.navbar-toggler {
- color: rgba(255,255,255,.5);
border: solid 1px rgba(255,255,255,.1);
padding: .25rem .75rem;
background: 0 0;
@@ -55,7 +51,7 @@ $navbar-min-width: 910px;
.nav-link {
display: block;
padding: .5em 1em;
- font-size: 12px;
+ font-size: $base-font-size;
&.active {
a {
@@ -64,7 +60,7 @@ $navbar-min-width: 910px;
}
a {
- color: $navbar-gray-text;
+ color: $gray4;
&:hover {
text-decoration: none;
diff --git a/app/assets/stylesheets/partials/_sidebar.scss b/app/assets/stylesheets/partials/_sidebar.scss
index ebc0d986..dbc43ce4 100644
--- a/app/assets/stylesheets/partials/_sidebar.scss
+++ b/app/assets/stylesheets/partials/_sidebar.scss
@@ -34,7 +34,7 @@
&.progress-bar {
dl {
@include box-sizing(border-box);
- margin: 30px;
+ margin: 20px 10px;
dt {
text-transform: uppercase;
strong {
@@ -62,6 +62,11 @@
margin: 0;
}
}
+ a {
+ margin-right: 0;
+ padding: 0;
+ text-transform: none;
+ }
}
}
}
diff --git a/app/assets/stylesheets/partials/_worker_status.scss b/app/assets/stylesheets/partials/_worker_status.scss
index 84b436f6..5735b945 100644
--- a/app/assets/stylesheets/partials/_worker_status.scss
+++ b/app/assets/stylesheets/partials/_worker_status.scss
@@ -6,19 +6,55 @@
box-shadow: inset 0 4px 5px darken($navbar-background, 10%);
background-color: darken($navbar-background, 5%);
- color: $gray3;
+ color: $gray4;
+ .fa {
+ display: none;
+ }
&.clickable:hover {
box-shadow: inset 0 4px 5px darken($navbar-background, 15%);
background-color: darken($navbar-background, 10%);
cursor: pointer;
- &.worker-status-idle { i { color: $input-green; } }
- &.worker-status-busy { i { color: darken($input-orange, 20%) !important; } }
- &.worker-status-swamped { i { color: $input-red !important; } }
+ &.worker-status-idle {
+ i {
+ color: $input-green;
+ }
+ }
+ &.worker-status-busy {
+ i {
+ color: darken($input-orange, 20%) !important;
+ }
+ }
+ &.worker-status-swamped {
+ i {
+ color: $input-red !important;
+ }
+ }
}
- &.worker-status-idle { i { color: darken($input-green, 10%); } }
- &.worker-status-busy { i { color: darken($input-orange, 30%) !important; } }
- &.worker-status-swamped { i { color: darken($input-red, 10%) !important; } }
+ &.worker-status-idle {
+ i {
+ color: darken($input-green, 10%);
+ &.fa-check {
+ display: inline-block;
+ }
+ }
+ }
+ &.worker-status-busy {
+ i {
+ color: darken($input-orange, 30%) !important;
+ &.fa-refresh {
+ display: inline-block;
+ }
+ }
+ }
+ &.worker-status-swamped {
+ i {
+ color: darken($input-red, 10%) !important;
+ &.fa-cloud {
+ display: inline-block;
+ }
+ }
+ }
}
diff --git a/app/chewy/commits_index.rb b/app/chewy/commits_index.rb
new file mode 100644
index 00000000..ec150921
--- /dev/null
+++ b/app/chewy/commits_index.rb
@@ -0,0 +1,25 @@
+class CommitsIndex < Chewy::Index
+ settings analysis: {
+ analyzer: {
+ sha: {
+ tokenizer: 'keyword'
+ }
+ }
+ }
+
+ define_type Commit.includes(:commits_keys) do
+ field :id, type: 'integer'
+ field :project_id, type: 'integer'
+ field :user_id, type: 'integer'
+ field :priority, type: 'integer'
+ field :due_date, type: 'date'
+ field :created_at, type: 'date'
+ field :revision, analyzer: 'sha'
+ field :loading, type: 'boolean'
+ field :ready, type: 'boolean'
+ field :exported, type: 'boolean'
+ field :fingerprint, analyzer: 'sha'
+ field :duplicate, type: 'boolean'
+ field :key_ids, value: ->(c) { c.commits_keys.pluck(:key_id) }
+ end
+end
diff --git a/app/chewy/keys_index.rb b/app/chewy/keys_index.rb
new file mode 100644
index 00000000..03175bc6
--- /dev/null
+++ b/app/chewy/keys_index.rb
@@ -0,0 +1,19 @@
+class KeysIndex < Chewy::Index
+ settings analysis: {
+ tokenizer: {
+ key_tokenizer: {type: 'pattern', pattern: '[^A-Za-z0-9]'}
+ },
+ analyzer: {
+ key_analyzer: {type: 'custom', tokenizer: 'key_tokenizer', filter: 'lowercase'}
+ }
+ }
+
+ define_type Key do
+ field :id, type: 'integer'
+ field :original_key, type: 'text', analyzer: 'key_analyzer'
+ field :original_key_exact, type: 'keyword', value: ->(t) { t.original_key }
+ field :project_id, type: 'integer'
+ field :ready, type: 'boolean'
+ field :hidden_in_search, type: 'boolean'
+ end
+end
diff --git a/app/chewy/translations_index.rb b/app/chewy/translations_index.rb
new file mode 100644
index 00000000..c4625fe9
--- /dev/null
+++ b/app/chewy/translations_index.rb
@@ -0,0 +1,40 @@
+class TranslationsIndex < Chewy::Index
+ TRANSLATION_STATE_NEW = 0
+ TRANSLATION_STATE_TRANSLATED = 1
+ TRANSLATION_STATE_APPROVED = 2
+ TRANSLATION_STATE_REJECTED = 3
+
+ define_type Translation.includes(key: :section) do
+ field :id, type: 'integer'
+ field :copy, analyzer: 'snowball', type: 'text', similarity: 'classic'
+ field :source_copy, analyzer: 'snowball', type: 'text', similarity: 'classic'
+ field :project_id, type: 'integer', value: ->(t) { t.key.project_id }
+ field :article_id, type: 'integer', value: ->(t) { t.key.section&.article_id }
+ field :section_id, type: 'integer', value: ->(t) { t.key.section_id }
+ field :is_block_tag, type: 'boolean', value: ->(t) { t.key.is_block_tag }
+ field :section_active, type: 'boolean', value: ->(t) { t.key.section&.active }
+ field :index_in_section, type: 'integer', value: ->(t) { t.key.index_in_section }
+ field :translator_id, type: 'integer'
+ field :reviewer_id, type: 'integer'
+ field :rfc5646_locale, type: 'keyword'
+ field :created_at, type: 'date'
+ field :updated_at, type: 'date'
+ field :translation_state, 'integer', value: -> (t) do
+ if t.approved
+ TRANSLATION_STATE_APPROVED
+ elsif t.translated
+ if t.approved.nil?
+ TRANSLATION_STATE_TRANSLATED
+ else
+ TRANSLATION_STATE_REJECTED
+ end
+
+ else
+ TRANSLATION_STATE_NEW
+ end
+ end
+ field :translated, type: 'boolean', value: -> (t) { !!t.translated } # converts nil to false
+ field :approved, type: 'boolean', value: ->(t) { !!t.approved } # converts nil to false
+ field :hidden_in_search, type: 'boolean', value: ->(t) { t.key.hidden_in_search }
+ end
+end
diff --git a/app/controllers/api/v1/groups_controller.rb b/app/controllers/api/v1/groups_controller.rb
index 58918a63..c3a04e10 100644
--- a/app/controllers/api/v1/groups_controller.rb
+++ b/app/controllers/api/v1/groups_controller.rb
@@ -23,6 +23,7 @@ module API
module V1
class GroupsController < ApplicationController
respond_to :json, only: [:index, :create, :update, :destroy]
+ respond_to :html, only: [:show]
skip_before_filter :authenticate_user!, if: :api_request?
skip_before_action :verify_authenticity_token, if: :api_request?
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 77772fe0..1eb74430 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -124,6 +124,7 @@ class ApplicationController < ActionController::Base
self.responder = JsonDetailResponder
before_filter :authenticate_user!
+ before_filter :set_raven_context
rescue_from(ActiveRecord::RecordNotFound) do
respond_to do |format|
@@ -252,4 +253,12 @@ def fix_empty_hashes(*fields)
end
end
end
+
+ private
+
+ def set_raven_context
+ return unless user_signed_in?
+ Raven.user_context user_id: current_user.id, email: current_user.email
+ end
+
end
diff --git a/app/controllers/commits_controller.rb b/app/controllers/commits_controller.rb
index 63d5ae2e..e6ffef18 100644
--- a/app/controllers/commits_controller.rb
+++ b/app/controllers/commits_controller.rb
@@ -126,14 +126,16 @@ def issues
# | `id` | The SHA of a Commit. |
def search
+ pending_locales = @commit.translations.where('approved IS NOT TRUE').group(:rfc5646_locale).count.keys
@locales = @project.locale_requirements.inject({}) do |hsh, (locale, required)|
hsh[locale.rfc5646] = {
required: required,
targeted: true,
- finished: @commit.translations.where('approved IS NOT TRUE AND rfc5646_locale = ?', locale.rfc5646).first.nil?
+ finished: !pending_locales.include?(locale.rfc5646)
}
hsh
end
+
@keys = CommitsSearchKeysFinder.new(params, @commit).find_keys
@keys_presenter = CommitsSearchPresenter.new(params[:locales], current_user.translator?, @project, @commit)
end
@@ -177,6 +179,7 @@ def create
flash[:alert] = t('controllers.commits.create.project_not_linked_error', revision: params[:commit][:revision].strip)
redirect_to root_url
rescue Timeout::Error
+ Raven.capture_exception err, extra: { project_id: project_id, sha: sha }
flash[:alert] = t('controllers.commits.create.timeout', revision: revision)
redirect_to root_url
end
@@ -300,7 +303,8 @@ def ping_stash
end
def reindex
- Translation.batch_refresh_elastic_search @commit
+ CommitsIndex.import! @commit
+ TranslationsIndex.import! @commit.translations
flash[:success] = t('controllers.commits.reindex.success')
respond_with @commit, location: project_commit_url(@project, @commit)
end
@@ -345,7 +349,8 @@ def manifest
compiler = Compiler.new(@commit)
file = compiler.manifest(request.format,
locale: params[:locale].presence,
- partial: params[:partial].parse_bool)
+ partial: params[:partial].parse_bool,
+ encoding: params[:encoding])
response.charset = file.encoding
response.headers['X-Git-Revision'] = @commit.revision
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 617e24c1..5d4dce03 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -14,8 +14,10 @@
# Contains landing pages appropriate for each of the User roles.
-class HomeController < ApplicationController
+require 'csv'
+require 'date'
+class HomeController < ApplicationController
# Typical number of commits to show per page.
PER_PAGE = 15
@@ -53,4 +55,100 @@ def index
@assets = items_finder.find_assets
@presenter = HomeIndexPresenter.new(@commits, @articles, @groups, @assets, @form[:filter__locales])
end
+
+ def csv
+ # manually setting how many rows to download
+ params[:limit] = 1000
+ @form = HomeIndexForm.new(params, cookies)
+ type = params[:type]
+ items_finder = HomeIndexItemsFinder.new(current_user, @form)
+ @commits = items_finder.find_commits
+ @articles = items_finder.find_articles
+ @groups = items_finder.find_groups
+ @assets = items_finder.find_assets
+ @items = items_finder.public_send("find_#{type}s")
+ @presenter = HomeIndexPresenter.new(@commits, @articles, @groups, @assets, @form[:filter__locales])
+ csv_file = CSV.generate do |csv|
+ identifier = type == 'commit' ? 'sha' : 'name'
+ if identifier == 'sha'
+ csv << ['Project',
+ 'SHA',
+ 'Created',
+ 'Due Date',
+ 'Priority',
+ 'New Strings',
+ 'New Words',
+ 'Review Strings',
+ 'Review Words',
+ 'Translate Link',
+ 'Requester Email']
+ @items.each do |item|
+ project = Project.find item.project_id
+ display_created_date = "#{item.created_at.month}/#{item.created_at.day}/#{item.created_at.year}"
+ unless item.due_date.nil?
+ display_due_date = "#{item.due_date.month}/#{item.due_date.day}/#{item.due_date.year}"
+ end
+ strings_to_translate = @presenter.item_stat(item, :translations, :new)
+ strings_to_review = @presenter.item_stat(item, :translations, :pending)
+ words_to_translate = @presenter.item_stat(item, :words, :new)
+ words_to_review = @presenter.item_stat(item, :words, :pending)
+ commit_url = project_commit_url(item.project, item)
+ requester_email = item.user.email if item.user
+ csv << [project.name,
+ commit_url,
+ display_created_date,
+ display_due_date,
+ item.priority,
+ strings_to_translate,
+ words_to_review,
+ strings_to_review,
+ words_to_translate,
+ project_commit_url(project, item),
+ requester_email]
+ end
+ else
+ csv << ['Project',
+ 'Name',
+ 'Created',
+ 'Due Date',
+ 'Priority',
+ 'Groups',
+ 'Review Strings',
+ 'Review Words',
+ 'New Strings',
+ 'New Words',
+ 'Translate Link',
+ 'Requester Email']
+ @items.each do |item|
+ project = Project.find item.project_id
+ display_created_date = "#{item.created_at.month}/#{item.created_at.day}/#{item.created_at.year}"
+ unless item.due_date.nil?
+ display_due_date = "#{item.due_date.month}/#{item.due_date.day}/#{item.due_date.year}"
+ end
+ groups = item.groups.count
+ strings_to_translate = @presenter.item_stat(item, :translations, :new)
+ strings_to_review = @presenter.item_stat(item, :translations, :pending)
+ words_to_translate = @presenter.item_stat(item, :words, :new)
+ words_to_review = @presenter.item_stat(item, :words, :pending)
+ article_link = api_v1_project_article_url(item.project.id, item.name)
+ requester_email = item.email
+ csv << [project.name,
+ article_link,
+ display_created_date,
+ display_due_date,
+ item.priority,
+ groups,
+ strings_to_translate,
+ words_to_translate,
+ strings_to_review,
+ words_to_review,
+ project_commit_url(project, item),
+ requester_email]
+ end
+ end
+ end
+ send_data csv_file, type: 'text/plain',
+ filename: "#{type}s.csv",
+ disposition: 'attachment'
+ end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 70c83aec..aaab7edf 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -405,4 +405,26 @@ def project_params
project_params["targeted_rfc5646_locales"] = targeted_rfc5646_locales
project_params
end
+
+ def project_path_changed?(params)
+ # checks if only_paths have been modified
+ if @project.only_paths.sort != params['only_paths'].sort
+ return true
+ end
+
+ # checks if locales have been modified in key_locale_inclusions
+ if @project.key_locale_inclusions.keys.sort != params['key_locale_inclusions'].keys.sort
+ return true
+ end
+
+ # checks if filters in each locale have been modified
+ @project.key_locale_inclusions.each do |locale, filters|
+ if filters.sort != params['key_locale_inclusions'][locale].sort
+ return true
+ end
+ end
+
+ # neither only_paths, nor key_locale_inclusions has been modified.
+ return false;
+ end
end
diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb
new file mode 100644
index 00000000..7db313ce
--- /dev/null
+++ b/app/controllers/reports_controller.rb
@@ -0,0 +1,44 @@
+require 'csv'
+
+class ReportsController < ApplicationController
+ def index
+ @today = Date.today
+ @yesterday = Date.yesterday
+ @range = Date.today...Date.today - 30
+ end
+
+ def incoming
+ parsed_date = Date.parse(params[:date])
+ range = parsed_date.beginning_of_day...parsed_date.end_of_day
+ @report = Report.where(date: range).where(report_type: 'incoming')
+ send_data convert_json_to_csv(@report), type: 'text/plain',
+ filename: "incoming-#{params[:date]}.csv",
+ disposition: :attachment
+ end
+
+ def pending
+ range = Date.parse(params[:date])...Date.tomorrow
+ @report = Report.where(date: range).where(report_type: 'pending')
+ send_data convert_json_to_csv(@report), type: 'text/plain',
+ filename: "pending-#{params[:date]}.csv",
+ disposition: :attachment
+ end
+
+ def completed
+ range = Date.parse(params[:date])...Date.tomorrow
+ @report = Report.where(date: range).where(report_type: 'completed')
+ send_data convert_json_to_csv(@report), type: 'text/plain',
+ filename: "completed-#{params[:date]}.csv",
+ disposition: :attachment
+ end
+
+ def convert_json_to_csv(jobs)
+ csv_file = CSV.generate do |csv|
+ csv << ['project_name', 'targeted_locale', 'strings', 'words', 'date']
+ jobs.each do |job|
+ csv << [job[:project], job[:locale], job[:strings], job[:words], job[:date]]
+ end
+ end
+ csv_file
+ end
+end
diff --git a/app/controllers/translations_controller.rb b/app/controllers/translations_controller.rb
index 81faeb01..54af4974 100644
--- a/app/controllers/translations_controller.rb
+++ b/app/controllers/translations_controller.rb
@@ -325,7 +325,6 @@ def translation_params
def change_hidden_in_search_to(state)
@key.update!(hidden_in_search: state)
- @translation.update_elasticsearch_index
end
def decorate_fuzzy_match(translations, source_copy)
@@ -337,7 +336,7 @@ def decorate_fuzzy_match(translations, source_copy)
rfc5646_locale: translation.rfc5646_locale,
project_name: translation.key.project.name.truncate(30)
}
- end.reject { |t| t[:match_percentage] < FuzzyMatchTranslationsFinder::MINIMUM_FUZZY_MATCH }
+ end.reject { |t| t[:match_percentage] < FuzzyMatchTranslationsFinder::FUZZY_MATCH_MIN_SCORE }
translations.sort! { |a, b| b[:match_percentage] <=> a[:match_percentage] }
end
end
diff --git a/app/helpers/custom_metric_helper.rb b/app/helpers/custom_metric_helper.rb
new file mode 100644
index 00000000..1f118fdd
--- /dev/null
+++ b/app/helpers/custom_metric_helper.rb
@@ -0,0 +1,73 @@
+module CustomMetricHelper
+ extend self
+
+ SIDEKIQ_WORKER_LONGEVITY = 'SidekiqWorker/longevity'
+ SIDEKIQ_WORKER_JOBS_BUSY = 'SidekiqWorker/jobs/busy'
+ SIDEKIQ_WORKER_JOBS_ENQUEUED = 'SidekiqWorker/jobs/enqueued'
+
+ PROJECT_PROCESSING_LOADING_TIME = 'project/processing/loading'
+ PROJECT_PROCESSING_TRANSLATING_TIME = 'project/processing/translating'
+ PROJECT_PROCESSING_REVIEWING_TIME = 'project/processing/translating'
+ PROJECT_PROCESSING_READY_TIME = 'project/processing/ready'
+ PROJECT_PROESSSING_STASH_TIME = 'project/processing/stash'
+
+ PROJECT_STATISTICS_FILES = 'project/statistics/files'
+ PROJECT_STATISTICS_STRINGS = 'project/statistics/strings'
+ PROJECT_STATISTICS_WORDS = 'project/statistics/words'
+
+ def record_sidekiq_longevity(host_to_longevities)
+ host_to_longevities.each do |hostname, longevity|
+ record_metric(longevity, SIDEKIQ_WORKER_LONGEVITY, hostname)
+ end
+ end
+
+ def record_sidekiq_jobs(busy_jobs, enqueued_jobs)
+ record_metric(busy_jobs, SIDEKIQ_WORKER_JOBS_BUSY)
+ record_metric(enqueued_jobs, SIDEKIQ_WORKER_JOBS_ENQUEUED)
+ end
+
+ # time from project created to loaded
+ def record_project_loading_time(project_name, loading_time)
+ record_metric(loading_time, PROJECT_PROCESSING_LOADING_TIME, project_name)
+ end
+
+ # time from project loaded to fully translated
+ def record_project_translating_time(project_name, locale_name, translating_time)
+ record_metric(translating_time, PROJECT_PROCESSING_TRANSLATING_TIME, project_name, locale_name)
+ end
+
+ # time from project fully translated to fully reviewed
+ def record_project_reviewing_time(project_name, locale_name, reviewing_time)
+ record_metric(reviewing_time, PROJECT_PROCESSING_REVIEWING_TIME, project_name, locale_name)
+ end
+
+ # time from project created to ready
+ def record_project_ready_time(project_name, ready_time)
+ record_metric(ready_time, PROJECT_PROCESSING_READY_TIME, project_name)
+ end
+
+ # time from project approved to ping done
+ def record_project_ping_stash_time(project_name, ping_stash_time)
+ record_metric(ping_stash_time, PROJECT_PROESSSING_STASH_TIME, project_name)
+ end
+
+ # counts for project files, keys per locale and words per locale
+ def record_project_statistics(project_name, blobs, locales_to_keys, locales_to_words)
+ record_metric(blobs, PROJECT_STATISTICS_FILES, project_name)
+
+ locales_to_keys.each do |locale_name, keys|
+ record_metric(keys, PROJECT_STATISTICS_STRINGS, project_name, locale_name)
+ end
+
+ locales_to_words.each do |locale_name, words|
+ record_metric(words, PROJECT_STATISTICS_WORDS, project_name, locale_name)
+ end
+ end
+
+ private
+
+ def record_metric(metric, metric_name, *sub_metric_names)
+ full_metric_name = (['Custom', metric_name] + sub_metric_names).join('/')
+ ::NewRelic::Agent.record_metric(full_metric_name, metric)
+ end
+end
diff --git a/app/mailers/fencer_validation_mailer.rb b/app/mailers/fencer_validation_mailer.rb
new file mode 100644
index 00000000..f060f939
--- /dev/null
+++ b/app/mailers/fencer_validation_mailer.rb
@@ -0,0 +1,91 @@
+# Copyright 2014 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Sends emails related to Comments
+
+class FencerValidationMailer < ActionMailer::Base
+ include ActionView::Helpers::TextHelper
+
+ default from: Shuttle::Configuration.mailer.from
+
+ # Notifies shuttle team and localization team about suspicious source strings.
+ #
+ # @return [Mail::Message] The email to be delivered.
+
+ def suspicious_source_found(job, suspicious_keys_errors)
+ @job = job
+ @suspicious_keys_errors = suspicious_keys_errors
+
+ @author_name = author_name
+ @author_email = author_email
+ @job_url = job_url
+ @formatted_keys_errors = formatted_keys_errors
+
+ mail_addresses = [Shuttle::Configuration.mailer.from, Shuttle::Configuration.mailer.localization_list]
+ if Rails.env.production?
+ subject = t('mailer.fencer_validation.suspicious_translation_found.subject.production')
+ else
+ subject = t('mailer.fencer_validation.suspicious_translation_found.subject.staging')
+ end
+ mail to: mail_addresses, subject: subject
+ end
+
+ def author_name
+ case @job.project.job_type
+ when 'commit'
+ @job.author || author_email
+ when 'article'
+ author_email
+ when 'asset'
+ author_email
+ else
+ raise ArgumentError, "job type not supported: #{job.project.job_type}"
+ end
+ end
+
+ def author_email
+ case @job.project.job_type
+ when 'commit'
+ @job.author_email
+ when 'article'
+ @job.email
+ when 'asset'
+ @job.email
+ else
+ raise ArgumentError, "job type not supported: #{job.project.job_type}"
+ end
+ end
+
+ def job_url
+ case @job.project.job_type
+ when 'commit'
+ project_commit_url(@job.project, @job)
+ when 'article'
+ api_v1_project_article_url(@job.project, @job.name)
+ when 'asset'
+ project_asset_url(@job.project, @job)
+ else
+ raise ArgumentError, "job type not supported: #{job.project.job_type}"
+ end
+ end
+
+ def formatted_keys_errors
+ @suspicious_keys_errors.map do |key, reason|
+ [
+ project_key_translation_url(key.project, key, key.translations.first),
+ reason
+ ]
+ end
+ end
+end
diff --git a/app/mediators/translation_update_mediator.rb b/app/mediators/translation_update_mediator.rb
index 45069d6e..ec797b75 100644
--- a/app/mediators/translation_update_mediator.rb
+++ b/app/mediators/translation_update_mediator.rb
@@ -45,13 +45,11 @@ def update!
copy_to_translations = translations_that_should_be_multi_updated
return if failure?
- ActiveRecord::Base.observers.disable :translation_observer do
- Translation.transaction do
- copy_to_translations.each do |translation|
- update_single_translation!(translation)
- end
- update_single_translation!(@primary_translation)
+ Translation.transaction do
+ copy_to_translations.each do |translation|
+ update_single_translation!(translation)
end
+ update_single_translation!(@primary_translation)
end
check_and_invoke_article_pinger
@@ -141,7 +139,11 @@ def update_single_translation!(translation)
if (translation.copy || "").empty? && !@params[:blank_string].parse_bool
untranslate(translation)
else
- translation.translator = @user if translation.copy != translation.copy_was
+ if translation.copy != translation.copy_was
+ if translation.translator.nil? || @user.translator_only?
+ translation.translator = @user
+ end
+ end
if @user.reviewer?
translation.reviewer = @user
translation.review_date = Time.now
@@ -150,8 +152,8 @@ def update_single_translation!(translation)
end
end
+ translation.updating_params = @params.merge(is_edit: is_edit) # for creating TranslationChange
translation.save!
- TranslationChange.create_from_params!(translation, @params, is_edit)
end
# Untranslates a translation, but doesn't call `save`.
diff --git a/app/middleware/chewy_atomic.rb b/app/middleware/chewy_atomic.rb
new file mode 100644
index 00000000..0f91ac4d
--- /dev/null
+++ b/app/middleware/chewy_atomic.rb
@@ -0,0 +1,11 @@
+# Wraps Sidekiq jobs with a `Chewy.strategy(:atomic)` call.
+
+class ChewyAtomic
+ def initialize(options=nil)
+
+ end
+
+ def call(_worker, _msg, _queue)
+ Chewy.strategy(:atomic) { yield }
+ end
+end
diff --git a/app/middleware/health_check.rb b/app/middleware/health_check.rb
index d0ed559c..d18140a9 100644
--- a/app/middleware/health_check.rb
+++ b/app/middleware/health_check.rb
@@ -12,7 +12,7 @@ def call(env)
if env['ORIGINAL_FULLPATH'] == '/_status'
db = Project.connection.select_all('SELECT CURRENT_TIME') rescue nil
redis = (Shuttle::Redis.get('s') || true) rescue nil
- elasticsearch = Elasticsearch::Model.search({size: 1}, Translation).results.first rescue nil
+ elasticsearch = TranslationsIndex.limit(1).first rescue nil
status = (db && redis && elasticsearch) ? 'OK' : 'error'
json = {
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 2db7f0dc..c5a0d064 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -92,33 +92,8 @@ class Commit < ActiveRecord::Base
alias_method :active_keys, :keys # called in ArticleOrCommitStats
alias_method :active_issues, :issues # called in ArticleOrCommitIssuesPresenter
- include Elasticsearch::Model
- include Elasticsearch::Model::Callbacks
- include IndexHelper
- Commit.index_name "shuttle_#{Rails.env}_commits"
-
- mapping do
- indexes :project_id, type: 'integer'
- indexes :user_id, type: 'integer'
- indexes :priority, type: 'integer'
- indexes :due_date, type: 'date'
- indexes :created_at, type: 'date'
- indexes :revision, index: :not_analyzed, type: 'string'
- indexes :loading, type: 'boolean'
- indexes :ready, type: 'boolean'
- indexes :exported, type: 'boolean'
- indexes :fingerprint, type: 'string'
- indexes :duplicate, type: 'boolean'
- end
-
- def regular_index_fields
- %w(project_id user_id priority due_date created_at revision ready exported loading fingerprint duplicate)
- end
-
- def special_index_fields
- {
- key_ids: commits_keys.pluck(:key_id)
- }
+ update_index('commits#commit') do
+ self unless previous_changes.blank? && persisted?
end
validates :project,
@@ -200,10 +175,16 @@ def revision_prefix
# approved_at to be the current time.
def recalculate_ready!
+ already_ready = self.ready
self.ready = successfully_loaded? && keys_are_ready? && !errored_during_import?
self.approved_at = Time.current if self.ready && self.approved_at.nil?
- index_elasticsearch_document
save!
+
+ # records metric only when becoming ready
+ if !already_ready and self.ready and self.approved_at and self.created_at
+ ready_time = self.approved_at - self.created_at
+ CustomMetricHelper.record_project_ready_time(self.project.slug, ready_time)
+ end
end
# Returns `true` if all Translations applying to this commit have been
diff --git a/app/models/concerns/index_helper.rb b/app/models/concerns/index_helper.rb
deleted file mode 100644
index 1722b3c6..00000000
--- a/app/models/concerns/index_helper.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Copyright 2016 Square Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-# Include this module in model after including `Elasticsearch::Model`.
-# Define INDEX_FIELD array including regular fields that are able to reuse
-# rails active record methods to retrieve value from database. Create method
-# `special_index_fields` to return a hash including all special fileds
-
-module IndexHelper
-
- def self.included(base)
- base.extend ClassMethods
- end
-
- def regular_index_fields
- []
- end
-
- # override Elasticsearch::Model::Serializing.as_indexed_json method to only include mapped fields
- def as_indexed_json(_options={})
- Hash[regular_index_fields.map { |field| [field, self.send(field)] }].merge(special_index_fields.stringify_keys)
- end
-
- def update_elasticsearch_index
- __elasticsearch__.update_document
- end
-
- def index_elasticsearch_document
- __elasticsearch__.index_document
- end
-
- module ClassMethods
- def refresh_elasticsearch_index!
- __elasticsearch__.refresh_index!
- end
- end
-end
\ No newline at end of file
diff --git a/app/models/group.rb b/app/models/group.rb
index 34e121db..7b2bb6a6 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -78,4 +78,17 @@ def recalculate_ready!
self.save!
end
+ # Inherited from ArticleOrCommitStats
+ include ArticleOrCommitStats
+ def translations_not_done(*locales)
+ articles.map { |article| article.translations_not_done(*locales) }.sum
+ end
+
+ def translations_done(*locales)
+ articles.map { |article| article.translations_done(*locales) }.sum
+ end
+
+ def translations_total(*locales)
+ articles.map { |article| article.translations_total(*locales) }.sum
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index c7887c8a..a0640633 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -137,32 +137,11 @@ def apply_readiness_hooks?() !skip_readiness_hooks end
digest_field :key, scope: :for_key
digest_field :source_copy, scope: :source_copy_matches
- include Elasticsearch::Model
- include Elasticsearch::Model::Callbacks
- include IndexHelper
- Key.index_name "shuttle_#{Rails.env}_keys"
-
- settings analysis: {tokenizer: {key_tokenizer: {type: 'pattern', pattern: '[^A-Za-z0-9]'}},
- analyzer: {key_analyzer: {type: 'custom', tokenizer: 'key_tokenizer', filter: 'lowercase'}}}
-
- mapping do
- indexes :original_key, type: 'multi_field', fields: {
- original_key: {type: 'string', analyzer: 'key_analyzer'},
- original_key_exact: {type: 'string', index: :not_analyzed}
- }
- indexes :project_id, type: 'integer'
- indexes :ready, type: 'boolean'
- indexes :hidden_in_search, type: 'boolean'
+ update_index('keys#key') do
+ self unless previous_changes.blank? && persisted?
end
-
- def regular_index_fields
- %w(original_key project_id ready)
- end
-
- def special_index_fields
- {
- hidden_in_search: formatted_hidden_in_search
- }
+ update_index('translations#translation') do
+ translations.reload unless previous_changes.blank?
end
validates :project,
@@ -220,39 +199,47 @@ def importer_name() importer_class.try(:human_name) end
# to create pending Translation requests for each string in the new locale.
def add_pending_translations(model = nil)
- locales = model && model.targeted_locales ? model.targeted_locales : targeted_locales
- base = model && model.base_locale ? model.base_locale : base_locale
-
- translations.in_locale(base).find_or_create!(
- source_copy: source_copy,
- copy: source_copy,
- source_locale: base,
- locale: base,
- approved: true,
- preserve_reviewed_status: true,
- )
-
- locales.each do |locale|
- next if skip_key?(locale)
- t = translations.in_locale(locale).find_or_create!(
- source_copy: source_copy,
- source_locale: base_locale,
- locale: locale,
+ Chewy.strategy(:atomic) do
+ locales = model && model.targeted_locales ? model.targeted_locales : targeted_locales
+ base = model && model.base_locale ? model.base_locale : base_locale
+
+ translations.in_locale(base).find_or_create!(
+ source_copy: source_copy,
+ copy: source_copy,
+ source_locale: base,
+ locale: base,
+ approved: true,
+ preserve_reviewed_status: true,
)
- if is_auto_approved_string?(source_copy) || (article.present? && is_block_tag)
- t.update!(
- copy: source_copy,
- approved: true,
- translated: true,
- preserve_reviewed_status: true,
+ locales.each do |locale|
+ next if skip_key?(locale)
+
+ translation_updated = false
+ t = translations.in_locale(locale).find_or_create!(
+ source_copy: source_copy,
+ source_locale: base_locale,
+ locale: locale,
)
+ translation_updated ||= t.previous_changes.present?
+
+ if is_auto_approved_string?(source_copy) || (article.present? && is_block_tag)
+ t.update!(
+ copy: source_copy,
+ approved: true,
+ translated: true,
+ preserve_reviewed_status: true,
+ )
+ translation_updated ||= t.previous_changes.present?
+ end
+
+ if translation_updated && !t.approved?
+ finder = FuzzyMatchTranslationsFinder.new(source_copy, t)
+ t.update(
+ tm_match: finder.top_fuzzy_match_percentage
+ )
+ end
end
-
- finder = FuzzyMatchTranslationsFinder.new(source_copy, t)
- t.update(
- tm_match: finder.top_fuzzy_match_percentage
- )
end
end
@@ -263,9 +250,11 @@ def add_pending_translations(model = nil)
# only pending Translations.
def remove_excluded_pending_translations
- translations.not_base.not_translated.where(approved: nil).find_each do |translation|
- if skip_key?(translation.locale) || !targeted_locales.include?(translation.locale)
- translation.destroy
+ Chewy.strategy(:atomic) do
+ translations.not_base.not_translated.where(approved: nil).find_each do |translation|
+ if skip_key?(translation.locale) || !targeted_locales.include?(translation.locale)
+ translation.destroy
+ end
end
end
end
@@ -287,18 +276,12 @@ def recalculate_ready!
# @param [Commit, Project, Article] obj The object whose keys should be batch recalculated
def self.batch_recalculate_ready!(obj)
- not_ready_key_ids = obj.translations.in_locale(*obj.required_locales).not_approved.not_block_tag.select(:key_id).uniq.pluck(:key_id)
- ready_key_ids = obj.keys.pluck(:id) - not_ready_key_ids
- ready_key_ids.in_groups_of(500, false) { |group| Key.where(id: group).update_all(ready: true) }
- not_ready_key_ids.in_groups_of(500, false) { |group| Key.where(id: group).update_all(ready: false) }
-
- # Since update_all bypasses all callbacks, `ready` field for some Keys should be out of sync in ElasticSearch at this point.
- # We need to update ElasticSearch with the new ready fields.
- obj.keys.each { |key| key.update_elasticsearch_index }
- end
-
- def formatted_hidden_in_search
- !!hidden_in_search
+ Chewy.strategy(:atomic) do
+ not_ready_key_ids = obj.translations.in_locale(*obj.required_locales).not_approved.not_block_tag.select(:key_id).uniq.pluck(:key_id)
+ ready_key_ids = obj.keys.pluck(:id) - not_ready_key_ids
+ ready_key_ids.in_groups_of(500, false) { |group| Key.where(id: group).update_all(ready: true) }
+ not_ready_key_ids.in_groups_of(500, false) { |group| Key.where(id: group).update_all(ready: false) }
+ end
end
# checks if a string should be auto approved.
diff --git a/app/models/report.rb b/app/models/report.rb
new file mode 100644
index 00000000..22168966
--- /dev/null
+++ b/app/models/report.rb
@@ -0,0 +1,2 @@
+class Report < ActiveRecord::Base
+end
diff --git a/app/models/section.rb b/app/models/section.rb
index df88e119..90a8c4a5 100644
--- a/app/models/section.rb
+++ b/app/models/section.rb
@@ -67,6 +67,8 @@ class Section < ActiveRecord::Base
digest_field :name, scope: :for_name
digest_field :source_copy, scope: :source_copy_matches
+ update_index('translations#translation') { translations.reload }
+
validates :name, presence: true, uniqueness: { scope: :article_id }
validates :source_copy, presence: true
validates :article, presence: true, strict: true
diff --git a/app/models/translation.rb b/app/models/translation.rb
index d2edf55c..8b43c45b 100644
--- a/app/models/translation.rb
+++ b/app/models/translation.rb
@@ -49,7 +49,7 @@ class Translation < ActiveRecord::Base
belongs_to :key, inverse_of: :translations
belongs_to :translator, class_name: 'User', foreign_key: 'translator_id', inverse_of: :authored_translations
belongs_to :reviewer, class_name: 'User', foreign_key: 'reviewer_id', inverse_of: :reviewed_translations
- has_many :translation_changes, inverse_of: :translation, dependent: :delete_all
+ has_many :translation_changes, inverse_of: :translation, dependent: :destroy
has_many :issues, inverse_of: :translation, dependent: :destroy
has_many :commits_keys, primary_key: :key_id, foreign_key: :key_id
has_many :assets_keys, primary_key: :key_id, foreign_key: :key_id
@@ -61,44 +61,8 @@ class Translation < ActiveRecord::Base
locale_field :source_locale, from: :source_rfc5646_locale
locale_field :locale
- include Elasticsearch::Model
- include Elasticsearch::Model::Callbacks
- include IndexHelper
- Translation.index_name "shuttle_#{Rails.env}_translations"
-
- def regular_index_fields
- %w(copy source_copy id translator_id rfc5646_locale created_at updated_at translated)
- end
- mapping do
- indexes :copy, analyzer: 'snowball', type: 'string'
- indexes :source_copy, analyzer: 'snowball', type: 'string'
- indexes :id, type: 'integer', index: :not_analyzed
- indexes :project_id, type: 'integer'
- indexes :article_id, type: 'integer'
- indexes :section_id, type: 'integer'
- indexes :is_block_tag, type: 'boolean'
- indexes :section_active, type: 'boolean'
- indexes :index_in_section, type: 'integer'
- indexes :translator_id, type: 'integer'
- indexes :rfc5646_locale, type: 'string', index: :not_analyzed
- indexes :created_at, type: 'date'
- indexes :updated_at, type: 'date'
- indexes :translated, type: 'boolean'
- indexes :approved, type: 'integer'
- indexes :hidden_in_search, type: 'boolean'
- end
-
- def special_index_fields
- {
- project_id: self.key.project_id,
- article_id: self.key.section.try(:article_id),
- section_id: self.key.section_id,
- is_block_tag: self.key.is_block_tag,
- section_active: self.key.section.try(:active),
- index_in_section: self.key.index_in_section,
- approved: if approved==true then 1 elsif approved==false then 0 else nil end,
- hidden_in_search: self.key.formatted_hidden_in_search
- }
+ update_index('translations#translation') do
+ self unless previous_changes.blank? && persisted?
end
validates :key,
@@ -130,6 +94,8 @@ def special_index_fields
# @private
# @return [User] The person who changed this Translation
attr_accessor :modifier
+ # passing updating params to create TranslationChange
+ attr_accessor :updating_params
# TODO: Fold this into DailyMetric?
def self.total_words_per_project
@@ -271,19 +237,4 @@ def fences_must_match
# ` "べ"[1..27].hash == "".hash ` returns false
# ` "べ"[1..27] == "" ` returns true
end
-
- # TODO elasticsearch-rails doesn't have batch import, we should update this method once we find better solution
- # Batch updates `obj`s Translations in elastic search.
- # Expects `obj` to have a `keys` association.
- # This is currently only run in ArticleImporter::Finisher.
- #
- # @param [Commit, Project, Article, Asset] obj The object whose translations should be batch refreshed in ElasticSearch
-
- def self.batch_refresh_elastic_search(obj)
- obj.keys.includes(:translations, :section).find_in_batches do |keys|
- keys.map(&:translations).flatten.each do |translation|
- translation.update_elasticsearch_index
- end
- end
- end
end
diff --git a/app/models/translation_change.rb b/app/models/translation_change.rb
index ca2d9024..e3baf98c 100644
--- a/app/models/translation_change.rb
+++ b/app/models/translation_change.rb
@@ -35,7 +35,7 @@ class TranslationChange < ActiveRecord::Base
belongs_to :project
belongs_to :article
belongs_to :asset
- has_many :edit_reasons
+ has_many :edit_reasons, dependent: :delete_all
has_many :reasons, through: :edit_reasons
serialize :diff, Hash
@@ -57,11 +57,10 @@ class TranslationChange < ActiveRecord::Base
}
def self.create_from_translation!(translation)
- diff = translation.previous_changes.slice(*TRACKED_ATTRIBUTES)
- TranslationChange.create(translation: translation, user: translation.modifier, diff: diff) if diff.present?
+ create_from_params!(translation, translation.updating_params || {})
end
- def self.create_from_params!(translation, params, is_edit)
+ def self.create_from_params!(translation, params)
diff = translation.previous_changes.slice(*TRACKED_ATTRIBUTES)
if diff.present?
project_id = Project.joins(:slugs).where('slugs.slug = ?', params[:project_id]).pluck('projects.id').first
@@ -73,8 +72,8 @@ def self.create_from_params!(translation, params, is_edit)
translation: translation,
user: translation.modifier,
diff: diff,
- role: translation.modifier.role,
- is_edit: is_edit,
+ role: translation.modifier&.role,
+ is_edit: params[:is_edit],
tm_match: translation.tm_match,
sha: commit,
article_id: article_id,
diff --git a/app/models/user.rb b/app/models/user.rb
index 9bb8f7f4..c7f32410 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -60,7 +60,7 @@ class User < ActiveRecord::Base
ROLES = %w(monitor translator reviewer admin)
devise :database_authenticatable, :registerable, :confirmable,
- :recoverable, :rememberable, :trackable, :validatable, :lockable
+ :recoverable, :rememberable, :trackable, :validatable, :lockable, :expirable
has_many :authored_translations, class_name: 'Translation', foreign_key: 'translator_id', inverse_of: :translator, dependent: :nullify
has_many :reviewed_translations, class_name: 'Translation', foreign_key: 'reviewer_id', inverse_of: :reviewer, dependent: :nullify
@@ -107,6 +107,15 @@ def after_confirmation
# @private Used by Devise.
def active_for_authentication?() super && role? end
+ def self.reset_password_by_token(attributes={})
+ recoverable = super(attributes)
+
+ # re-enable expired account caused by inactivity
+ recoverable.update_last_activity! if recoverable.persisted?
+
+ recoverable
+ end
+
# @return [String] The User's full name.
def name() I18n.t 'models.user.name', first: first_name, last: last_name end
# @return [String] An abbreviated name for the user.
@@ -115,6 +124,7 @@ def abbreviated_name() I18n.t('models.user.name', first: first_name, last: last_
ROLES.each do |role|
define_method(:"#{role}?") { self.role == role || self.role == 'admin' }
+ define_method(:"#{role}_only?") { self.role == role }
end
# @private
diff --git a/app/presenters/locale_projects_show_presenter.rb b/app/presenters/locale_projects_show_presenter.rb
index 5a90f929..6ae50c16 100644
--- a/app/presenters/locale_projects_show_presenter.rb
+++ b/app/presenters/locale_projects_show_presenter.rb
@@ -53,6 +53,12 @@ def selected_asset
@_selected_asset ||= @project.assets.find_by_id(form[:asset_id])
end
+ # @return [Group, nil] selected Group if there is one
+
+ def selected_group
+ @_selected_group ||= @project.groups.where(display_name: form[:group]).first
+ end
+
# @return [Array>] an array of selectable options for Sections
def selectable_sections
diff --git a/app/services/commits_search_keys_finder.rb b/app/services/commits_search_keys_finder.rb
index 0391defa..28b45f71 100644
--- a/app/services/commits_search_keys_finder.rb
+++ b/app/services/commits_search_keys_finder.rb
@@ -16,7 +16,6 @@
class CommitsSearchKeysFinder
attr_reader :commit, :form
- include Elasticsearch::DSL
# The number of records to return by default.
PER_PAGE = 50
@@ -27,39 +26,18 @@ def initialize(form, commit)
end
def search_query
- query_filter = form[:filter]
- status = form[:status]
- key_ids = commit.keys.pluck(:id)
- current_page = page
+ query_params = []
+ query_params << { match: { original_key: { query: form[:filter], operator: 'and'} } } if form[:filter].present?
- search {
- query do
- filtered do
- if query_filter.present?
- query do
- match 'original_key' do
- query query_filter
- operator 'and'
- end
- end
- end
- filter do
- bool do
- must { ids values: key_ids }
- case status
- when 'approved'
- must { term ready: 1 }
- when 'pending'
- must { term ready: 0 }
- end
- end
- end
- end
- end
- sort { by :original_key_exact, order: 'asc', ignore_unmapped: true } unless query_filter
- from (current_page - 1) * PER_PAGE
- size PER_PAGE
- }.to_hash
+ filter_params = []
+ filter_params << { ids: { values: commit.keys.pluck(:id) } }
+ filter_params << { term: { ready: true } } if form[:status] == 'approved'
+ filter_params << { term: { ready: false } } if form[:status] == 'pending'
+
+ query = KeysIndex.query(query_params).filter(filter_params).offset((page - 1) * PER_PAGE).limit(PER_PAGE)
+ query = query.order(original_key_exact: { order: :asc }) unless form[:filter]
+
+ return query
end
def page
@@ -67,14 +45,7 @@ def page
end
def find_keys
- keys_in_es = Elasticsearch::Model.search(search_query, Key).results
-
- keys = Key.where(id: keys_in_es.map(&:id))
- .where('commits_keys.commit_id': commit.id)
- .includes(:translations, :commits_keys)
- .order('commits_keys.created_at asc, original_key asc')
-
- # Don't sort the keys since they are sorted in the line above
- PaginatableObjects.new(keys, keys_in_es, page, PER_PAGE, false)
+ keys = search_query.load(scope: -> { includes(:translations, :commits_keys) })
+ return PaginatableObjects.new(keys, page, PER_PAGE)
end
end
diff --git a/app/services/fuzzy_match_translations_finder.rb b/app/services/fuzzy_match_translations_finder.rb
index b61e7c33..132d1d39 100644
--- a/app/services/fuzzy_match_translations_finder.rb
+++ b/app/services/fuzzy_match_translations_finder.rb
@@ -14,9 +14,10 @@
class FuzzyMatchTranslationsFinder
attr_reader :translation
- include Elasticsearch::DSL
- MINIMUM_FUZZY_MATCH = 60
+ FUZZY_MATCH_MIN_SCORE = 60
+ FUZZY_MATCH_RESULT_SIZE = 5
+ ES_SEARCH_BATCH_SIZE = 5
def initialize(query_filter, translation)
@query_filter = query_filter
@@ -24,36 +25,25 @@ def initialize(query_filter, translation)
end
def search_query
- limit = 5
- query_filter = @query_filter
- target_locales = translation.locale.fallbacks.map(&:rfc5646)
- search {
- query do
- filtered do
- query do
- match 'source_copy' do
- query query_filter
- operator 'or'
- end
- end
- filter do
- bool do
- must { term approved: 1 }
- must { terms rfc5646_locale: target_locales }
- must { term hidden_in_search: false }
- end
- end
- end
- end
+ query_params = []
+ query_params << { match: { source_copy: { query: @query_filter, operator: 'or' } } }
- size limit
- }.to_hash
+ filter_params = []
+ filter_params << { term: { approved: true } }
+ filter_params << { terms: { rfc5646_locale: translation.locale.fallbacks.map(&:rfc5646) } }
+ filter_params << { term: { hidden_in_search: false } }
+
+ query = TranslationsIndex.query(query_params).filter(filter_params)
+ query = query.limit(ES_SEARCH_BATCH_SIZE)
+
+ return query
end
def find_fuzzy_match
- translations_in_es = Elasticsearch::Model.search(search_query, Translation).results
- translations = Translation.where(id: translations_in_es.map(&:id)).includes(key: :project)
- SortingHelper.order_by_elasticsearch_result_order(translations, translations_in_es)
+ translations = search_query.load(scope: -> { includes(key: :project) }).objects
+ translations = translations.reject { |t| @query_filter.similar(t.source_copy) < FUZZY_MATCH_MIN_SCORE }
+ translations = translations.sort { |a, b| @query_filter.similar(b.source_copy) <=> @query_filter.similar(a.source_copy) }
+ translations.first(FUZZY_MATCH_RESULT_SIZE).map { |t| Translation.find(t.id) }
end
def top_fuzzy_match_percentage
@@ -62,7 +52,7 @@ def top_fuzzy_match_percentage
{
match_percentage: @translation.source_copy.similar(tran.source_copy),
}
- end.reject { |t| t[:match_percentage] < MINIMUM_FUZZY_MATCH }
+ end.reject { |t| t[:match_percentage] < FUZZY_MATCH_MIN_SCORE }
translations.sort! { |a, b| b[:match_percentage] <=> a[:match_percentage] }
translations.any? ? translations.first[:match_percentage] : 0.0
end
diff --git a/app/services/home_index_items_finder.rb b/app/services/home_index_items_finder.rb
index 0d97ec33..026e5bd0 100644
--- a/app/services/home_index_items_finder.rb
+++ b/app/services/home_index_items_finder.rb
@@ -16,7 +16,6 @@
class HomeIndexItemsFinder
attr_reader :user, :form
- include Elasticsearch::DSL
def initialize(user, form)
@user = user
@@ -24,91 +23,58 @@ def initialize(user, form)
end
def search_query
- # FILTERS AND SORTING
- status = form[:filter__status]
- locales = form[:filter__locales]
- sort_field = form[:sort__field]
- sort_direction = form[:sort__direction]
- sha = form[:commits_filter__sha]
- project_id = form[:commits_filter__project_id]
- hide_exported = form[:commits_filter__hide_exported]
- hide_autoimported = form[:commits_filter__hide_autoimported]
- show_only_mine = form[:commits_filter__show_only_mine]
- hide_duplicates = form[:commits_filter__hide_duplicates]
-
- # PAGINATION
- offset = form[:offset]
- limit = form[:limit]
-
- # UNCOMPLETED IN SPECIFIC LOCALES
- if locales.present? && (status == 'uncompleted')
- uncompleted_key_ids_in_locales = uncompleted_key_ids_in_locales()
- end
-
- search {
- query do
- filtered do
- filter do
- bool do
- must { prefix revision: sha } if sha
- must { term project_id: project_id } unless project_id == 'all'
- must { term exported: false } if hide_exported
- must { exists field: :user_id } if hide_autoimported
- must { term user_id: 1 } if show_only_mine
- must { term loading: false }
- must { term duplicate: false } if hide_duplicates
-
- case status
- when 'uncompleted'
- locales.present? ? must { terms key_ids: uncompleted_key_ids_in_locales } : must { term ready: false }
- when 'completed'
- must { term ready: true }
- when 'hidden'
- # Do nothing as Commit has no such state. We have this to line up with Article since
- # Commit and Article share the same frontend template.
- must { match_all }
- when 'all'
- must { match_all }
+ commits = CommitsIndex.filter(bool: {must: [
+ form[:commits_filter__sha] ? {prefix: {revision: form[:commits_filter__sha]}} : nil,
+ form[:commits_filter__project_id] == 'all' ? nil : {term: {project_id: form[:commits_filter__project_id]}},
+ form[:commits_filter__hide_exported] ? {term: {exported: false}} : nil,
+ form[:commits_filter__hide_autoimported] ? {exists: {field: :user_id}} : nil,
+ form[:commits_filter__show_only_mine] ? {term: {user_id: 1}} : nil,
+ {term: {loading: false}},
+ form[:commits_filter__hide_duplicates] ? {term: {duplicate: :false}} : nil,
+ (case form[:filter__status]
+ when 'uncompleted'
+ if form[:filter__locales].present?
+ {terms: {key_ids: uncompleted_key_ids_in_locales}}
+ else
+ {term: {ready: false}}
+ end
+ when 'completed'
+ {term: {ready: true}}
+ when 'hidden'
+ # Do nothing as Commit has no such state. We have this to line up with Article since
+ # Commit and Article share the same frontend template.
+ nil
+ when 'all'
+ nil
+ end)
+ ].compact}).
+ offset(form[:offset]).limit(form[:limit])
+
+ commits = case form[:sort__field]
+ when 'due'
+ commits.order(due_date: (form[:sort__direction].nil? ? 'asc' : form[:sort__direction]),
+ priority: 'asc',
+ created_at: 'desc')
+ when 'create'
+ commits.order(created_at: (form[:sort__direction].nil? ? 'desc' : form[:sort__direction]),
+ priority: 'asc',
+ due_date: 'asc')
+ else
+ commits.order(priority: (form[:sort__direction].nil? ? 'asc' : form[:sort__direction]),
+ due_date: 'asc',
+ created_at: 'desc')
end
- end
- end
- end
- end
- from offset
- size limit
- sort do
- case sort_field
- when 'due'
- by :due_date, order: sort_direction.nil? ? 'asc' : sort_direction
- by :priority, order: 'asc'
- by :created_at, order: 'desc'
- when 'create'
- by :created_at, order: sort_direction.nil? ? 'desc' : sort_direction
- by :priority, order: 'asc'
- by :due_date, order: 'asc'
- else
- by :priority, order: sort_direction.nil? ? 'asc' : sort_direction
- by :due_date, order: 'asc'
- by :created_at, order: 'desc'
- end
- end
- }.to_hash
+ return commits
end
def find_commits
- #Search
- commits_in_es = Elasticsearch::Model.search(search_query, Commit).results
-
- # LOAD
- commits = Commit
- .where(id: commits_in_es.map(&:id))
- .includes(:user, project: :slugs)
- PaginatableObjects.new(commits, commits_in_es, form[:page], form[:limit])
+ commits = search_query.load(scope: -> { includes(:user, project: :slugs) })
+ PaginatableObjects.new(commits, form[:page], form[:limit])
end
def find_articles
- articles = Article.includes(:project).showing
+ articles = Article.includes(:project, :groups).showing
# filter by name
articles = articles.for_name(form[:articles_filter__name]) if form[:articles_filter__name]
@@ -136,7 +102,7 @@ def find_articles
when 'due'
"due_date #{direction || 'asc'}"
when 'create'
- "created_at #{direction || 'desc'}"
+ "last_import_requested_at #{direction || 'desc'}"
when 'priority'
"priority #{direction || 'asc'}"
end
@@ -188,7 +154,7 @@ def find_assets
end
def find_groups
- groups = Group.includes(:project).showing.joins(:articles)
+ groups = Group.includes(:project, :articles).showing.joins(:articles)
# filter by name
groups = groups.where("groups.display_name like '%#{form[:groups_filter__name]}%'") if form[:groups_filter__name]
@@ -214,11 +180,11 @@ def find_groups
direction = %w(asc desc).include?(form[:sort__direction]) ? form[:sort__direction] : nil
order_by = case form[:sort__field]
when 'due'
- "due_date #{direction || 'asc'}"
+ "groups.due_date #{direction || 'asc'}"
when 'create'
- "created_at #{direction || 'desc'}"
+ "groups.created_at #{direction || 'desc'}"
when 'priority'
- "priority #{direction || 'asc'}"
+ "groups.priority #{direction || 'asc'}"
end
groups = groups.order(order_by) if order_by
diff --git a/app/services/locale_projects_show_finder.rb b/app/services/locale_projects_show_finder.rb
index 2a3a5fd4..becd6eaf 100644
--- a/app/services/locale_projects_show_finder.rb
+++ b/app/services/locale_projects_show_finder.rb
@@ -15,7 +15,6 @@
class LocaleProjectsShowFinder
attr_reader :form
- include Elasticsearch::DSL
PER_PAGE = 50
@@ -24,92 +23,39 @@ def initialize(form)
end
def search_query
- include_translated = form[:include_translated]
- include_approved = form[:include_approved]
- include_new = form[:include_new]
- include_block_tags = form[:include_block_tags]
-
- current_page = page
- query_filter = form[:query_filter]
- translation_ids_in_commit = form[:translation_ids_in_commit]
- article_id = form[:article_id]
- section_id = form[:section_id]
- translation_ids_in_assest = form[:translation_ids_in_assest]
- locale = form[:locale]
- project_id = form[:project_id]
- project = form[:project]
- filter_source = form[:filter_source]
+ query_params = []
+ text_to_search = form[:query_filter]
+ if text_to_search.present?
+ if form[:filter_source] == 'source'
+ query_params << { match: { source_copy: {query: text_to_search, operator: 'and' } } }
+ elsif form[:filter_source] == 'translated'
+ query_params << { match: { copy: {query: text_to_search, operator: 'and'} } }
+ end
+ end
- search {
- query do
- filtered do
- if query_filter.present?
- if filter_source == 'source'
- query do
- match 'source_copy' do
- query query_filter
- operator 'and'
- end
- end
- elsif filter_source == 'translated'
- query do
- match 'copy' do
- query query_filter
- operator 'and'
- end
- end
- end
- end
+ filter_params = []
+ filter_params << { term: { project_id: form[:project_id] } }
+ filter_params << { term: { rfc5646_locale: form[:locale].rfc5646 } } if form[:locale]
+ filter_params << { ids: { values: form[:translation_ids_in_commit] } } if form[:translation_ids_in_commit]
+ filter_params << { term: { article_id: form[:article_id] } } if form[:article_id].present?
+ filter_params << { ids: { values: form[:translation_ids_in_assest] } } if form[:translation_ids_in_assest]
+ filter_params << { term: { section_id: form[:section_id] } } if form[:section_id].present?
+ filter_params << { term: { section_active: true } } if form[:project].article?
+ filter_params << { exists: { field: :index_in_section } } if form[:project].article?
+ filter_params << { bool: { must_not: { term: { is_block_tag: true } } } } if form[:project].article? && !form[:include_block_tags]
- filter do
- bool do
- must { term project_id: project_id }
- must { term rfc5646_locale: locale.rfc5646 } if locale
- must { ids values: translation_ids_in_commit } if translation_ids_in_commit
- must { term article_id: article_id } if article_id.present?
- must { ids values: translation_ids_in_assest } if translation_ids_in_assest
- must { term section_id: section_id } if section_id.present?
- must { term section_active: true } if project.article? # active sections
- must { exists field: :index_in_section } if project.article? # active keys in sections
- must_not { term is_block_tag: true } if project.article? && !include_block_tags
+ state_params = []
+ state_params << TranslationsIndex::TRANSLATION_STATE_APPROVED if form[:include_approved]
+ state_params << TranslationsIndex::TRANSLATION_STATE_TRANSLATED if form[:include_translated]
+ state_params << TranslationsIndex::TRANSLATION_STATE_NEW if form[:include_new]
+ state_params << TranslationsIndex::TRANSLATION_STATE_REJECTED if form[:include_new]
+ filter_params << { terms: { translation_state: state_params } } unless state_params.empty?
- if include_translated && include_approved && include_new
- #include everything
- elsif include_translated && include_approved
- must { term translated: 1 }
- elsif include_translated && include_new
- should { missing field: 'approved', existence: true, null_value: true }
- should { term approved: 0 }
- elsif include_approved && include_new
- should { term approved: 1 }
- should { term translated: 0 }
- elsif include_approved
- must { term approved: 1 }
- elsif include_new
- should { term translated: 0 }
- should { term approved: 0 }
- elsif include_translated
- must { missing field: 'approved', existence: true, null_value: true }
- must { term translated: 1 }
- else
- # include nothing
- throw :include_nothing
- end
- end
- end
- end
- end
+ query = TranslationsIndex.query(query_params).filter(filter_params)
+ query = query.order(section_id: :asc, index_in_section: :asc) if form[:project].article?
+ query = query.offset((page-1) * PER_PAGE).limit(PER_PAGE)
- if project.article?
- sort do
- by :section_id, order: 'asc'
- by :index_in_section, order: 'asc'
- end
- end
-
- from (current_page - 1) * PER_PAGE
- size PER_PAGE
- }.to_hash
+ return query
end
def page
@@ -117,23 +63,18 @@ def page
end
def find_translations
- translations_in_es = Elasticsearch::Model.search(search_query, Translation).results
- translations = Translation
- .where(id: translations_in_es.map(&:id))
- .where('commits.revision': form[:commit])
- .includes({key: [:project, :commits, :assets, :translations, :section, {article: :project}]}, :locale_associations, :translation_changes)
+ include_tables = [{ key: [:project, :assets, :translations, :section, { article: [:project, article_groups: :group] }] }, :locale_associations, :translation_changes]
+
+ scope = -> { includes(include_tables) }
if form[:article_id]
- translations = translations.order('keys.section_id, keys.index_in_section')
+ scope = -> { includes(include_tables).order('keys.section_id, keys.index_in_section') }
elsif form[:commit]
- translations = translations.order('commits_keys.created_at, keys.original_key')
- elsif form[:asset_id]
- translations = translations.order('assets_keys.created_at, keys.original_key')
+ scope = -> { includes(include_tables).order('translations.created_at') }
elsif form[:group]
- translations = translations.joins({article: {article_groups: :group}}).where("groups.display_name = ?", form[:group])
- translations = translations.order('article_groups.index_in_group, keys.section_id, keys.index_in_section')
+ scope = -> { includes(include_tables).order('article_groups.index_in_group, keys.section_id, keys.index_in_section') }
end
- # Don't sort the keys since they are sorted in the line above
- PaginatableObjects.new(translations, translations_in_es, page, PER_PAGE, false)
+ translations = search_query.load(scope: scope)
+ return PaginatableObjects.new(translations, page, PER_PAGE)
end
end
diff --git a/app/services/match_translations_finder.rb b/app/services/match_translations_finder.rb
index 753ecdfc..823b8819 100644
--- a/app/services/match_translations_finder.rb
+++ b/app/services/match_translations_finder.rb
@@ -14,37 +14,30 @@
class MatchTranslationsFinder
attr_reader :translation
- include Elasticsearch::DSL
def initialize(translation)
@translation = translation
end
def search_query(rfc5646, source_copy)
- search {
- query do
- filtered do
- filter do
- bool do
- must { term approved: 1 }
- must { term rfc5646_locale: rfc5646 }
- must { term source_copy: source_copy }
- end
- end
- end
- end
- sort { by :created_at, order: 'desc' }
- size 1
- }.to_hash
+ filter_params = []
+ filter_params << { term: { approved: true } }
+ filter_params << { term: { rfc5646_locale: rfc5646 } }
+ filter_params << { term: { source_copy: source_copy } }
+
+ query = TranslationsIndex.filter(filter_params)
+ query = query.order(created_at: :desc)
+ query = query.limit(1)
+
+ return query
end
def find_first_match_translation
source_copy = translation.source_copy
translation.locale.fallbacks.each do |fallback|
- query = search_query(fallback.rfc5646, source_copy)
- first_matched_translation = Elasticsearch::Model.search(query, Translation).results.first
- return first_matched_translation.to_hash["_source"] if first_matched_translation
+ first_matched_translation = search_query(fallback.rfc5646, source_copy).load.objects.first
+ return first_matched_translation if first_matched_translation
end
- nil
+ return nil
end
-end
\ No newline at end of file
+end
diff --git a/app/services/search_commits_finder.rb b/app/services/search_commits_finder.rb
index b59c4746..c5400624 100644
--- a/app/services/search_commits_finder.rb
+++ b/app/services/search_commits_finder.rb
@@ -14,38 +14,26 @@
class SearchCommitsFinder
attr_reader :params
- include Elasticsearch::DSL
def initialize(params)
@params = params
end
def search_query
- sha = params[:sha]
- project_id = params[:project_id].to_i
- limit = params.fetch(:limit, 50)
+ query_params = []
- search {
- query do
- filtered do
- filter do
- bool do
- must { prefix revision: sha } if sha
- must { term project_id: project_id } if project_id > 0
- must { match_all }
- end
- end
- end
- end
+ query_params << { prefix: { revision: params[:sha] } } if params[:sha]
+ query_params << { term: { project_id: params[:project_id].to_i } } if params[:project_id].present?
+ query_params << { bool: { must: { match_all: {} } } }
- size limit
- sort { by :created_at, order: 'desc' }
- }.to_hash
+ query = CommitsIndex.filter(query_params)
+ query = query.order(created_at: :desc)
+ query = query.limit(params.fetch(:limit, 50))
+
+ return query
end
def find_commits
- commits_in_es = Elasticsearch::Model.search(search_query, Commit).results
- commits = Commit.where(id: commits_in_es.map(&:id)).includes(:project)
- SortingHelper.order_by_elasticsearch_result_order(commits, commits_in_es)
+ search_query.load(scope: -> { includes(:project) }).objects
end
-end
\ No newline at end of file
+end
diff --git a/app/services/search_keys_finder.rb b/app/services/search_keys_finder.rb
index 89be55ee..c757cd46 100644
--- a/app/services/search_keys_finder.rb
+++ b/app/services/search_keys_finder.rb
@@ -23,44 +23,29 @@ def initialize(user, params)
end
def search_query
- query_filter = @params[:filter]
- status = @params[:status]
- offset = @params[:offset].to_i
- project_id = @params[:project_id]
- limit = @params.fetch(:limit, PER_PAGE)
- not_elastic = @params[:not_elastic_search]
- hidden_keys = @params[:hidden_in_search]
-
- search {
- query do
- filtered do
- if query_filter.present? && !not_elastic
- query do
- match 'original_key' do
- query query_filter
- operator 'and'
- end
- end
- end
- filter do
- bool do
- must { term original_key_exact: query_filter } if query_filter && not_elastic
- must { term project_id: project_id }
- must { term ready: status } unless status.blank?
- hidden_keys ? must { term hidden_in_search: true } : must { term hidden_in_search: false }
- end
- end
- end
+ query_params = []
+ text_to_search = @params[:filter]
+ if text_to_search.present?
+ if !@params[:not_elastic_search]
+ query_params << { match: { original_key: { query: text_to_search, operator: 'and' } } }
+ else
+ query_params << { term: { original_key_exact: text_to_search } }
end
- sort { by :original_key, order: 'asc' } unless query_filter.present?
- from offset
- size limit
- }.to_hash
+ end
+
+ filter_params = []
+ filter_params << { term: { project_id: @params[:project_id].to_i } } if @params[:project_id].present?
+ filter_params << { term: { ready: @params[:status].to_b } } if @params[:status].present?
+ filter_params << { term: { hidden_in_search: @params[:hidden_in_search].to_b } }
+
+ query = KeysIndex.query(query_params).filter(filter_params)
+ query = query.order(original_key_exact: :asc) if @params[:filter].blank?
+ query = query.offset(@params[:offset].to_i).limit(@params.fetch(:limit, PER_PAGE))
+
+ return query
end
def find_keys
- keys_in_es = Elasticsearch::Model.search(search_query, Key).results
- keys = Key.where(id: keys_in_es.map(&:id)).includes(:translations, :project)
- SortingHelper.order_by_elasticsearch_result_order(keys, keys_in_es)
+ search_query.load(scope: -> { includes(:translations, :project) }).objects
end
-end
\ No newline at end of file
+end
diff --git a/app/services/search_translations_finder.rb b/app/services/search_translations_finder.rb
index 4587b3e7..ea90821f 100644
--- a/app/services/search_translations_finder.rb
+++ b/app/services/search_translations_finder.rb
@@ -17,77 +17,35 @@ class SearchTranslationsFinder
PER_PAGE = 50
attr_reader :form
- include Elasticsearch::DSL
def initialize(form)
@form = form
end
def search_query
- project_id = form[:project_id]
- query_filter = form[:query]
- field = form[:field]
- translator_id = form[:translator_id]
-
- start_date = form[:start_date]
- end_date = form[:end_date]
- if form[:target_locales].present? && form[:target_locales].size > 0
- target_locales = form[:target_locales].first.rfc5646
- end
- hidden_keys = form[:hidden_keys] if form[:hidden_keys].present?
-
- offset = (page - 1) * PER_PAGE
- limit = PER_PAGE
-
- search {
- query do
- filtered do
- if query_filter.present?
- query do
- if field == 'searchable_source_copy'
- match 'source_copy' do
- query query_filter
- operator 'or'
- end
- else
- match 'copy' do
- query query_filter
- operator 'or'
- end
- end
- end
- end
-
- filter do
- bool do
- must { term rfc5646_locale: target_locales } if target_locales
- must { term project_id: project_id } if project_id && project_id > 0
- must { term translator_id: translator_id } if translator_id && translator_id > 0
- if start_date
- must {
- range 'updated_at' do
- gte start_date
- end
- }
- end
- if end_date
- must {
- range 'updated_at' do
- lte end_date
- end
- }
- end
-
- hidden_keys ? must { term hidden_in_search: true } : must { term hidden_in_search: false }
- end
- end
- end
+ query_params = []
+ text_to_search = form[:query]
+ if text_to_search.present?
+ if form[:field] == 'searchable_source_copy'
+ query_params << { match: { source_copy: { query: text_to_search, operator: 'or' } } }
+ else
+ query_params << { match: { copy: { query: text_to_search, operator: 'or' } } }
end
+ end
- sort { by :id, order: 'desc' } unless query_filter.present?
- size limit
- from offset
- }.to_hash
+ filter_params = []
+ filter_params << { term: { rfc5646_locale: form[:target_locales].first.rfc5646} } if form[:target_locales].present? && form[:target_locales].size > 0
+ filter_params << { term: { project_id: form[:project_id].to_i } } if form[:project_id].present?
+ filter_params << { term: { translator_id: form[:translator_id].to_i } } if form[:translator_id].present?
+ filter_params << { term: { reviewer_id: form[:reviewer_id].to_i } } if form[:reviewer_id].present?
+ filter_params << { range: { updated_at: { gte: form[:start_date] } } } if form[:start_date].present?
+ filter_params << { range: { updated_at: { lte: form[:end_date] } } } if form[:end_date].present?
+ filter_params << { term: { hidden_in_search: form[:hidden_keys] || false } }
+
+ query = TranslationsIndex.query(query_params).filter(filter_params)
+ query = query.order(id: :desc) if text_to_search.blank?
+ query = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE)
+ return query
end
def page
@@ -95,9 +53,7 @@ def page
end
def find_translations
- limit = PER_PAGE
- translations_in_es = Elasticsearch::Model.search(search_query, Translation).results
- translations = Translation.where(id: translations_in_es.map(&:id)).includes(key: :project)
- PaginatableObjects.new(translations, translations_in_es, page, limit)
+ translations = search_query.load(scope: -> { includes(key: :project) })
+ return PaginatableObjects.new(translations, page, PER_PAGE)
end
end
diff --git a/app/support/article_and_commit_not_approved_translation_stats.rb b/app/support/article_and_commit_not_approved_translation_stats.rb
index a77e1c59..7dd38d59 100644
--- a/app/support/article_and_commit_not_approved_translation_stats.rb
+++ b/app/support/article_and_commit_not_approved_translation_stats.rb
@@ -63,7 +63,7 @@ def item_stat(item, type, state)
def commit_translation_groups_with_stats
query = Translation.not_base.not_approved.joins(:commits_keys)
- query = query.where(commits_keys: { commit_id: @commits.map(&:id) } )
+ query = query.where(commits_keys: { commit_id: @commits.map(&:id) })
query = query.where(translations: { rfc5646_locale: @locales.map(&:rfc5646) }) if @locales.present?
query = query.group("commit_id, translated, rfc5646_locale")
query.select("commit_id as item_id, translated, rfc5646_locale, COUNT(*) AS translations_count, SUM(words_count) AS words_count")
diff --git a/app/support/home_index_form.rb b/app/support/home_index_form.rb
index 3106900f..c290801d 100644
--- a/app/support/home_index_form.rb
+++ b/app/support/home_index_form.rb
@@ -70,7 +70,7 @@ def [](key)
def set_pagination_variables
vars[:page] = Integer(params[:page]) rescue 1
vars[:offset] = (vars[:page] - 1) * HomeController::PER_PAGE
- vars[:limit] = HomeController::PER_PAGE
+ vars[:limit] = @params[:limit] || HomeController::PER_PAGE
end
def set_filter__status
@@ -169,12 +169,12 @@ def set_articles_filter__project_id
def set_groups_filter__name
vars[:groups_filter__name] = params[:groups_filter__name].presence
end
-
+
def set_groups_filter__project_id
vars[:groups_filter__project_id] = cookies[:home_index__groups_filter__project_id] =
params[:groups_filter__project_id].to_s.presence || cookies[:home_index__groups_filter__project_id].to_s.presence || 'all'
end
-
+
# ASSET SPECIFIC
def set_assets_filter__name
diff --git a/app/support/search_translations_form.rb b/app/support/search_translations_form.rb
index 446c4209..223ff33c 100644
--- a/app/support/search_translations_form.rb
+++ b/app/support/search_translations_form.rb
@@ -10,8 +10,9 @@ def initialize(form)
# @param form hash of parameters to process
def extract_translations_search_params(form)
processed_params = form.deep_dup
- processed_params[:project_id] = processed_params[:project_id].to_i
- processed_params[:translator_id] = processed_params[:translator_id].to_i
+ processed_params[:project_id] = processed_params[:project_id].to_i if processed_params[:project_id].present?
+ processed_params[:translator_id] = processed_params[:translator_id].to_i if processed_params[:translator_id].present?
+ processed_params[:reviewer_id] = processed_params[:reviewer_id].to_i if processed_params[:reviewer_id].present?
start_date = Date.strptime(processed_params[:start_date], "%m/%d/%Y") rescue nil
end_date = Date.strptime(processed_params[:end_date], "%m/%d/%Y") rescue nil
@@ -37,4 +38,4 @@ def extract_translations_search_params(form)
def [](key)
form[key]
end
-end
\ No newline at end of file
+end
diff --git a/app/views/api/v1/articles/_form.html.slim b/app/views/api/v1/articles/_form.html.slim
index f530f8e7..aad48612 100644
--- a/app/views/api/v1/articles/_form.html.slim
+++ b/app/views/api/v1/articles/_form.html.slim
@@ -31,7 +31,13 @@
.control-group
= f.label :priority, class: 'control-label'
.controls
- = @article.priority || '-'
+ - if current_user.admin?
+ = f.select :priority, t("models.article.priority").to_a.map(&:reverse).unshift(['-', nil]), {}, class: 'styled'
+ - else
+ - if @article.priority
+ = t("models.article.priority")[@article.priority]
+ - else
+ | -
.control-group
= f.label :due_date, class: 'control-label'
@@ -74,5 +80,5 @@
= text_area_tag "article[sections_hash[#{section_name}]]", section_source_copy, rows: 6
.form-actions
- = f.submit class: 'primary', value: 'save', data: { confirm: 'Are you sure? Changes are irreversible.'}
+ = f.submit class: 'primary', value: 'Save', data: { confirm: 'Are you sure? Changes are irreversible.'}
button.default href=(@article.persisted? ? api_v1_project_article_path(@project.id, @article.name_was) : root_path) Cancel
diff --git a/app/views/api/v1/articles/_layout.slim b/app/views/api/v1/articles/_layout.slim
index 4c2967a2..812a1e8e 100644
--- a/app/views/api/v1/articles/_layout.slim
+++ b/app/views/api/v1/articles/_layout.slim
@@ -23,7 +23,10 @@
h1
| #{@article.project.name}
strong >
- | Article #{@article.id}
+ | Article
+ strong >
+ | #{@article.id}
+
hr.divider
.row
diff --git a/app/views/api/v1/groups/_layout.slim b/app/views/api/v1/groups/_layout.slim
new file mode 100644
index 00000000..b779aa90
--- /dev/null
+++ b/app/views/api/v1/groups/_layout.slim
@@ -0,0 +1,37 @@
+/ Copyright 2014 Square Inc.
+/
+/ Licensed under the Apache License, Version 2.0 (the "License");
+/ you may not use this file except in compliance with the License.
+/ You may obtain a copy of the License at
+/
+/ http://www.apache.org/licenses/LICENSE-2.0
+/
+/ Unless required by applicable law or agreed to in writing, software
+/ distributed under the License is distributed on an "AS IS" BASIS,
+/ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/ See the License for the specific language governing permissions and
+/ limitations under the License.
+
+.header
+ h1
+ | #{@group.project.name}
+ strong >
+ | Group
+ strong >
+ | #{@group.id}
+
+hr.divider
+
+.row
+ .three.columns.sidebar
+ ul
+ li class=('active' if action_name == 'show')
+ a General
+ / li class=('active' if action_name == 'issues')
+
+ li.divider
+
+ = render partial: 'common/progress_tracker', locals: { item: @group }
+
+ .thirteen.columns.sidebar-main
+ = yield
diff --git a/app/views/api/v1/groups/show.html.slim b/app/views/api/v1/groups/show.html.slim
new file mode 100644
index 00000000..eafa6872
--- /dev/null
+++ b/app/views/api/v1/groups/show.html.slim
@@ -0,0 +1,99 @@
+/ Copyright 2014 Square Inc.
+/
+/ Licensed under the Apache License, Version 2.0 (the "License");
+/ you may not use this file except in compliance with the License.
+/ You may obtain a copy of the License at
+/
+/ http://www.apache.org/licenses/LICENSE-2.0
+/
+/ Unless required by applicable law or agreed to in writing, software
+/ distributed under the License is distributed on an "AS IS" BASIS,
+/ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/ See the License for the specific language governing permissions and
+/ limitations under the License.
+
+- content_for :shuttle_title do
+ = "group Group #{@group.id}"
+- content_for :file_name do
+ = 'views/api/v1/groups/show'
+
+= render layout: 'api/v1/groups/layout' do
+ .row
+ .seven.columns
+ fieldset
+ .control-group
+ = label_tag :name, nil, class: 'control-label'
+ .controls
+ = @group.name
+
+ .control-group
+ = label_tag :display_name, nil, class: 'control-label'
+ .controls
+ = @group.display_name
+
+ .control-group
+ = label_tag :priority, nil, class: 'control-label'
+ .controls
+ = @group.priority || '-'
+
+ .control-group
+ = label_tag :due_date, nil, class: 'control-label'
+ .controls
+ = @group.due_date || '-'
+
+ .control-group
+ = label_tag :description, nil, class: 'control-label'
+ .controls
+ - if @group.description.present?
+ = sanitize @group.description, tags: %w(strong em a br), attributes: %w(href)
+ - else
+ = '-'
+
+ .six.columns
+ fieldset
+ .control-group
+ = label_tag :readiness_status, nil, class: 'control-label'
+ .controls
+ = @group.ready? ? 'Ready' : 'Not Ready'
+
+ .control-group
+ = label_tag :loading_status, nil, class: 'control-label'
+ .controls
+ = @group.loading? ? 'Loading' : 'Loaded'
+
+ .control-group
+ = label_tag :created_at, nil, class: 'control-label'
+ .controls
+ = @group.created_at.try(:to_s, :long) || '-'
+
+ .control-group
+ = label_tag :updated_at, nil, class: 'control-label'
+ .controls
+ = @group.updated_at.try(:to_s, :long) || '-'
+
+ .control-group
+ = label_tag :creator, nil, class: 'control-label'
+ .controls
+ = @group.creator.try(:name) || '-'
+
+ .control-group
+ = label_tag :updater, nil, class: 'control-label'
+ .controls
+ = @group.updater.try(:name) || '-'
+
+ .control-group
+ = label_tag 'Creation method', nil, class: 'control-label'
+ .controls
+ = @group.created_via_api ? 'Via API' : 'Via Website'
+
+ .row
+ .thirteen.columns
+ fieldset
+ legend Linked Articles
+ - articles = @group.article_groups.order(article_id: :asc).map(&:article)
+ - articles.reject { |article| article.ready }.each do |article|
+ .control-group
+ = link_to '⏳ ' + truncate(article.name), api_v1_project_article_url(article.project.id, article.name)
+ - articles.select {|article| article.ready}.each do |article|
+ .control-group
+ = link_to '✅ ' + truncate(article.name), api_v1_project_article_url(article.project.id, article.name)
diff --git a/app/views/assets/_form.html.slim b/app/views/assets/_form.html.slim
index 155ef38f..daf39f28 100644
--- a/app/views/assets/_form.html.slim
+++ b/app/views/assets/_form.html.slim
@@ -44,7 +44,13 @@
.control-group
= f.label :priority, class: 'control-label'
.controls
- = @asset.priority || '-'
+ - if current_user.admin?
+ = f.select :priority, t("models.asset.priority").to_a.map(&:reverse).unshift(['-', nil]), {}, class: 'styled'
+ - else
+ - if @asset.priority
+ = t("models.asset.priority")[@asset.priority]
+ - else
+ | -
.control-group
= f.label :due_date, class: 'control-label'
@@ -94,7 +100,7 @@
.qtip-tooltip#asset-description So that translators understand the context!
.form-actions
- = f.submit class: 'primary', value: 'save', data: { confirm: 'Are you sure? Changes are irreversible.'}
+ = f.submit class: 'primary', value: 'Save', data: { confirm: 'Are you sure? Changes are irreversible.'}
button.default href=(@asset.persisted? ? project_asset_path(@project, @asset) : root_path) Cancel
diff --git a/app/views/assets/_layout.slim b/app/views/assets/_layout.slim
index 291f0874..5afc7df2 100644
--- a/app/views/assets/_layout.slim
+++ b/app/views/assets/_layout.slim
@@ -23,7 +23,10 @@
h1
| #{@asset.project.name}
strong >
- | Asset #{@asset.id}
+ | Asset
+ strong >
+ | #{@asset.id}
+
hr.divider
.row
diff --git a/app/views/commits/_layout.slim b/app/views/commits/_layout.slim
index 315f7fa8..884460c3 100644
--- a/app/views/commits/_layout.slim
+++ b/app/views/commits/_layout.slim
@@ -33,9 +33,11 @@
h1
| #{@commit.project.name}
strong >
- | Commit #{@commit.revision_prefix}
+ | Commit
+ strong >
+ | #{@commit.id}
- h6
+ .subtitle
strong
- if @commit.loading?
- if @commit.import_batch_status
@@ -43,11 +45,11 @@
- else
- current_status = "Hung (Loading, No Import Batch)"
- elsif @commit.ready?
- - current_status = 'Ready'
+ - current_status = 'Ready ☑️'
- elsif @commit.import_errors.present?
- current_status = 'Errored - Import Aborted'
-else
- - current_status = 'Translating'
+ - current_status = 'Translating ⏳'
= "Currently #{current_status}"
span.separator /
= "Found #{@commit.keys.count} Keys"
diff --git a/app/views/commits/show.slim b/app/views/commits/show.slim
index c8bffec4..1f426508 100644
--- a/app/views/commits/show.slim
+++ b/app/views/commits/show.slim
@@ -24,7 +24,7 @@
.nine.columns
.control-group
- = f.label 'SHA', class: 'control-label info'
+ = f.label 'SHA', class: 'control-label info revision'
.controls
= link_to @commit.revision, @commit.git_url
.control-group
@@ -43,6 +43,16 @@
br
+ .control-group
+ = f.label :priority, class: 'control-label'
+ .controls
+ - if current_user.admin?
+ = f.select :priority, t("models.commit.priority").to_a.map(&:reverse).unshift(['-', nil]), {}, class: 'styled'
+ - else
+ - if @commit.priority
+ = t("models.commit.priority")[@commit.priority]
+ - else
+ | -
.control-group
= f.label :due_date, class: 'control-label'
.controls
diff --git a/app/views/common/_progress_tracker.slim b/app/views/common/_progress_tracker.slim
index ca0436ca..95c5b22e 100644
--- a/app/views/common/_progress_tracker.slim
+++ b/app/views/common/_progress_tracker.slim
@@ -22,11 +22,22 @@ li.progress-bar
- finished = (item.translations_not_done(locale) == 0)
li.progress-bar
dl
- dt class=(required ? (finished ? 'text-success' : 'text-error') : nil)
- = locale.rfc5646
- small.lowercase = " (#{required ? 'required' : 'optional'})"
- strong = (finished ? '100%' : "#{(item.fraction_done(locale) * 100).round(2)}%")
- dd.bar-status
- span class=(finished ? 'finished' : 'inprogress') style="width: #{(item.fraction_done(locale) * 100).round(2)}%;"
- dd.text-status class=(required ? (finished ? 'text-success' : 'text-error') : nil)
- = "#{item.translations_not_done(locale)} left (#{item.translations_total(locale)} total)"
+ - if current_user.translator?
+ a href=HomeIndexPresenter.new([],[],[],[],[locale]).translate_link_path(current_user, item)
+ dt class=(required ? (finished ? 'text-success' : 'text-error') : nil)
+ = locale.rfc5646
+ small.lowercase = " (#{required ? 'required' : 'optional'})"
+ strong = (finished ? '100%' : "#{(item.fraction_done(locale) * 100).round(2)}%")
+ dd.bar-status
+ span class=(finished ? 'finished' : 'inprogress') style="width: #{(item.fraction_done(locale) * 100).round(2)}%;"
+ dd.text-status class=(required ? (finished ? 'text-success' : 'text-error') : nil)
+ = "#{item.translations_not_done(locale)} left (#{item.translations_total(locale)} total)"
+ - else
+ dt class=(required ? (finished ? 'text-success' : 'text-error') : nil)
+ = locale.rfc5646
+ small.lowercase = " (#{required ? 'required' : 'optional'})"
+ strong = (finished ? '100%' : "#{(item.fraction_done(locale) * 100).round(2)}%")
+ dd.bar-status
+ span class=(finished ? 'finished' : 'inprogress') style="width: #{(item.fraction_done(locale) * 100).round(2)}%;"
+ dd.text-status class=(required ? (finished ? 'text-success' : 'text-error') : nil)
+ = "#{item.translations_not_done(locale)} left (#{item.translations_total(locale)} total)"
diff --git a/app/views/fencer_validation_mailer/suspicious_source_found.text.erb b/app/views/fencer_validation_mailer/suspicious_source_found.text.erb
new file mode 100644
index 00000000..e5a6a11e
--- /dev/null
+++ b/app/views/fencer_validation_mailer/suspicious_source_found.text.erb
@@ -0,0 +1,12 @@
+Dear localization team,
+
+Shuttle found the following source strings are suspicious in translation request <%= @job_url %>.
+
+<% for @item in @formatted_keys_errors -%>
+ <%= @item.first %> Reason: <%= @item.second %>
+<% end -%>
+
+Please double check these source strings are correct. Contact requester <%= @author_name %> at <%= @author_email %> for correcting source strings if needed.
+
+Sincerely,
+The Shuttle Automated Mailer
diff --git a/app/views/home/_headers.slim b/app/views/home/_headers.slim
index e718b552..146d995b 100644
--- a/app/views/home/_headers.slim
+++ b/app/views/home/_headers.slim
@@ -63,6 +63,14 @@ tr
th Description
+ / group count in articles & article count in groups
+ - if item_type == 'article'
+ th
+ = t("controllers.home.articles.groups")
+ - elsif item_type == 'group'
+ th
+ = t("controllers.home.groups.articles")
+
- if current_user.translator?
th Translate
th Review
diff --git a/app/views/home/_item.slim b/app/views/home/_item.slim
index 472a0032..6abce3b0 100644
--- a/app/views/home/_item.slim
+++ b/app/views/home/_item.slim
@@ -26,7 +26,7 @@ tr.row_class
td = item.project.name
/ Commit SHA or Article Name
- td
+ td class=('revision-prefix' if item.is_a?(Commit))
- if item.is_a?(Commit)
= link_to item.revision_prefix, project_commit_url(item.project, item)
- elsif item.is_a?(Article)
@@ -34,9 +34,9 @@ tr.row_class
- elsif item.is_a?(Asset)
= link_to truncate(item.name), project_asset_url(item.project, item)
- elsif item.is_a?(Group)
- = truncate(item.display_name)
+ = link_to truncate(item.display_name), api_v1_project_group_url(item.project.id, item.name)
/ Create Date
- td.centered
+ td
- if item.is_a?(Commit)
= item.created_at.strftime('%m/%d/%Y')
- elsif item.is_a?(Article)
@@ -47,7 +47,7 @@ tr.row_class
= item.created_at.strftime('%m/%d/%Y')
/ Due Date
- td.due-date.centered
+ td.due-date
- if current_user.admin? or current_user.monitor?
= form_for item, url: @presenter.update_item_path(item) do |f|
= f.text_field :due_date, value: (f.object.due_date.strftime('%m/%d/%Y') if !f.object.due_date.nil?), class: 'datepicker'
@@ -55,7 +55,7 @@ tr.row_class
span = l(item.due_date, format: :mon_day_year)
/ Priority
- td.centered
+ td
- if current_user.admin?
= form_for item, url: @presenter.update_item_path(item) do |f|
= f.select :priority, t("models.#{item_type}.priority").to_a.map(&:reverse).unshift(['-', nil]), {}, class: 'styled'
@@ -67,12 +67,20 @@ tr.row_class
| -
/ Description
- td.centered
+ td.description-container
div.description[data-full-description=@presenter.full_description(item)
data-short-description=@presenter.short_description(item)
data-sub-description=@presenter.sub_description(item)]
= @presenter.short_description(item)
+ / group count in articles & article count in groups
+ - if item.is_a?(Article)
+ td
+ = t("controllers.home.articles.groups_cell", pending_count: item&.groups&.reject(&:ready)&.count, linked_count: item&.groups&.count)
+ - elsif item.is_a?(Group)
+ td
+ = t("controllers.home.groups.articles_cell", pending_count: item&.articles&.reject(&:ready)&.count, linked_count: item&.articles&.count)
+
/ Stats
- if current_user.translator?
/ Word Translation Count
diff --git a/app/views/home/_items.slim b/app/views/home/_items.slim
index d9ddb9f4..c6552979 100644
--- a/app/views/home/_items.slim
+++ b/app/views/home/_items.slim
@@ -34,13 +34,14 @@ ruby:
div id="#{item_type}s"
.header
- - if showRequestTranslationButton
+ .header-buttons
+ - if showRequestTranslationButton
+ .pull-right
+ button.primary href="#add-#{item_type}-translation" rel='modal' disabled=(item_type == 'commit' ? Project.git : Project.not_git).count.zero? Request Translation
.pull-right
- button.primary href="#add-#{item_type}-translation" rel='modal' disabled=(item_type == 'commit' ? Project.git : Project.not_git).count.zero? Request Translation
+ button.button--secondary.csv-button href="/csv?type=#{item_type}" ⇩ Download CSV
h1 = itemsHeader
- hr.divider
-
.border
= render partial: "home/#{item_type}s/filter_bar"
@@ -57,6 +58,9 @@ div id="#{item_type}s"
.row
.pagination-info
= page_entries_info(items, entry_name: item_type)
+ - if current_user.admin?
+ br
+ = "Items shown on page: #{items.size}"
- if item_type != 'group'
= render partial: "home/#{item_type}s/add_translation_modal"
diff --git a/app/views/layouts/_navbar.slim b/app/views/layouts/_navbar.slim
index df2c49c6..2099dbc4 100644
--- a/app/views/layouts/_navbar.slim
+++ b/app/views/layouts/_navbar.slim
@@ -28,6 +28,8 @@ nav.navbar.navbar-dark
- if current_user.translator?
= nav_link "Locale Associations", "locale_associations", locale_associations_url
= nav_link "Glossary", "glossary", glossary_url
+
+ = nav_link "Reports", "reports", reports_url
li.nav-link.dropdown
a.dropdown-toggle href="#" data-toggle="dropdown"
@@ -38,7 +40,7 @@ nav.navbar.navbar-dark
= nav_dropdown_link "Project Translation Report", "stats", "project_translation_report", stats_project_translation_report_url
= nav_dropdown_link "Incoming New Words Report", "stats", "incoming_new_words_report", stats_incoming_new_words_report_url
= nav_dropdown_link "Translator Report", "stats", "translator_report", stats_translator_report_url
- = nav_dropdown_link "Backlog Report", "stats", "backlog_report", stats_backlog_report_url
+ /= nav_dropdown_link "Backlog Report", "stats", "backlog_report", stats_backlog_report_url
= nav_dropdown_link "Quality Report", "stats", "quality_report", stats_quality_report_url
- if current_user.admin?
@@ -57,5 +59,7 @@ nav.navbar.navbar-dark
- if current_user.monitor?
li.nav-link.worker-status class=("clickable" if current_user.admin?)
- = fa_icon "circle"
+ = fa_icon "check"
+ = fa_icon "refresh"
+ = fa_icon "cloud"
| Workers
diff --git a/app/views/layouts/application.slim b/app/views/layouts/application.slim
index 19c1c8c1..49331b30 100644
--- a/app/views/layouts/application.slim
+++ b/app/views/layouts/application.slim
@@ -49,4 +49,7 @@ html lang="en"
= raw(ERB.new(File.read(js_erb_file)).result(binding)) if File.exist?(js_erb_file)
= raw(CoffeeScript.compile(ERB.new(File.read(js_coffee_erb_file)).result(binding))) if File.exist?(js_coffee_erb_file)
+ - if ENV['SENTRY_PUBLIC_DSN']
+ script type='text/javascript' Raven.config("#{ENV['SENTRY_PUBLIC_DSN']}").install()
+
= render partial: 'layouts/flashes'
diff --git a/app/views/locale/projects/show.slim b/app/views/locale/projects/show.slim
index 7b3333cb..de19367a 100644
--- a/app/views/locale/projects/show.slim
+++ b/app/views/locale/projects/show.slim
@@ -29,21 +29,36 @@
= button_tag 'Translate', id: 'translate-link', class: 'submit', type: 'button'
h1
=> @project.name
- - if @presenter.form[:article_id]
+ - if @presenter.form[:commit]&.present?
+ strong >
+ | Commit
+ strong >
+ small.monospace
+ = link_to @presenter.selected_commit.revision_prefix, project_commit_url(@project, @presenter.selected_commit)
+ - elsif @presenter.form[:article_id]&.present?
+ strong >
+ | Article
+ strong >
small
- = link_to @presenter.selected_article.name, api_v1_project_article_path(@project.id, @presenter.selected_article.name)
- - elsif @presenter.form[:group]
+ = link_to truncate(@presenter.selected_article.name), api_v1_project_article_path(@project.id, @presenter.selected_article.name)
+ - elsif @presenter.form[:asset_id]&.present?
+ strong >
+ | Asset
+ strong >
small
- = @presenter.form[:group]
+ = link_to truncate(@presenter.selected_asset.name), project_asset_url(@project, @presenter.selected_asset)
+ - elsif @presenter.form[:group]&.present?
+ strong >
+ | Group
+ strong >
+ small
+ = link_to truncate(@presenter.selected_group.display_name), api_v1_project_group_url(@project.id, @presenter.selected_group.name)
- h6
+ .subtitle
strong
- - last_commit = @project.commits.order('committed_at DESC').first
- - if last_commit
+ - if @presenter.form[:commit]&.present?
| Last imported
- = "#{time_ago_in_words(last_commit.committed_at)} ago"
- | /
- = "#{last_commit.revision_prefix}"
+ = "#{time_ago_in_words(@presenter.selected_commit.committed_at)} ago"
- else
| Never imported before
@@ -61,10 +76,10 @@ div
- if @project.commit?
= select_tag 'commit', options_for_select(@presenter.selectable_commits, @presenter.selected_commit.try!(:revision))
- - elsif @presenter.form[:article_id]
+ - elsif @presenter.form[:article_id]&.present?
= hidden_field_tag 'article_id', @presenter.selected_article.id
= select_tag 'section_id', options_for_select(@presenter.selectable_sections, @presenter.selected_section.try!(:id))
- - elsif @presenter.form[:group]
+ - elsif @presenter.form[:group]&.present?
= hidden_field_tag 'group', @presenter.form[:group]
- elsif @project.asset?
= hidden_field_tag 'asset_id', @presenter.selected_asset.id
diff --git a/app/views/locale_associations/_form.html.slim b/app/views/locale_associations/_form.html.slim
index 0c4029d1..4752f357 100644
--- a/app/views/locale_associations/_form.html.slim
+++ b/app/views/locale_associations/_form.html.slim
@@ -44,5 +44,5 @@ hr.divider
= f.check_box :uncheck_disabled
span.help-block Do you want to disable unchecking the checkbox if it's checked?
.form-actions
- = f.submit class: 'primary', value: 'save', data: { confirm: 'Are you sure? Changes are irreversible.'}
+ = f.submit class: 'primary', value: 'Save', data: { confirm: 'Are you sure? Changes are irreversible.'}
button.default href=locale_associations_url Cancel
diff --git a/app/views/projects/_form.slim b/app/views/projects/_form.slim
index 753d95e1..f3192fec 100644
--- a/app/views/projects/_form.slim
+++ b/app/views/projects/_form.slim
@@ -299,7 +299,7 @@
= @project.api_token
.form-actions
- = f.submit class: 'primary', value: 'save', data: { confirm: 'Are you sure? Changes are irreversible.'}
+ = f.submit class: 'primary', value: 'Save', data: { confirm: 'Are you sure? Changes are irreversible.'}
button.default href=projects_url Cancel
- content_for :javascript do
diff --git a/app/views/projects/index.slim b/app/views/projects/index.slim
index f4c39eb7..2c971d69 100644
--- a/app/views/projects/index.slim
+++ b/app/views/projects/index.slim
@@ -37,6 +37,6 @@ hr.divider
tr onclick="document.location = '#{edit_project_url(project)}'"
td = project.name
td = project.job_type.titlecase
- td = project.repository_url
+ td.small.monospace = project.repository_url
td = project.required_rfc5646_locales.join(" / ").upcase
td = project.other_rfc5646_locales.join(" / ").upcase
diff --git a/app/views/reports/index.slim b/app/views/reports/index.slim
new file mode 100644
index 00000000..12c264e6
--- /dev/null
+++ b/app/views/reports/index.slim
@@ -0,0 +1,57 @@
+/ Copyright 2014 Square Inc.
+/
+/ Licensed under the Apache License, Version 2.0 (the "License");
+/ you may not use this file except in compliance with the License.
+/ You may obtain a copy of the License at
+/
+/ http://www.apache.org/licenses/LICENSE-2.0
+/
+/ Unless required by applicable law or agreed to in writing, software
+/ distributed under the License is distributed on an "AS IS" BASIS,
+/ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/ See the License for the specific language governing permissions and
+/ limitations under the License.
+
+- content_for :shuttle_title do
+ = "Reports - Shuttle"
+- content_for :file_name do
+ = 'views/reports/index'
+
+
+.header
+ h1 Reports based on job status
+
+
+.day
+ h2 =@today
+ div.report-buttons
+ a href="/reports/download/incoming/#{@today}" ↙️ Incoming jobs report
+ a href="/reports/download/pending/#{@today}" ⏳ Pending jobs report
+ a href="/reports/download/completed/#{@today}" ✓ Completed jobs report
+
+.day
+ h2 =@yesterday
+ div.report-buttons
+ a href="/reports/download/incoming/#{@yesterday}" ↙️ Incoming jobs report
+ a href="/reports/download/pending/#{@yesterday}" ⏳ Pending jobs report
+ a href="/reports/download/completed/#{@yesterday}" ✓ Completed jobs report
+
+.day
+ h2 Custom date in the past 30 days
+ .date-picker.center
+ label for="pickedDate" Pick a date and then a report type
+ input type="date" id="pickedDate" name="pickedDate" value="#{@today}" min="#{@today - 30}" max="#{@today}"
+ div.report-buttons.custom-dates
+ a href="#" data-type="incoming" ↙️ Incoming jobs report
+ a href="#" data-type="pending" ⏳ Pending jobs report
+ a href="#" data-type="completed" ✓ Completed jobs report
+
+javascript:
+ var buttons = document.querySelectorAll('.custom-dates a')
+ for (var i = 0; i < buttons.length; i++) {
+ buttons[i].addEventListener('click', function(event) {
+ var date = document.getElementById('pickedDate').value;
+ var reportType = event.target.dataset.type;
+ window.location = '/reports/download/' + reportType + '/' + date;
+ });
+ }
diff --git a/app/views/search/translations.slim b/app/views/search/translations.slim
index 43d923a2..798bcf38 100644
--- a/app/views/search/translations.slim
+++ b/app/views/search/translations.slim
@@ -51,6 +51,11 @@ div
.controls
= select_tag 'translator_id', options_for_select(User.order('email ASC').map { |u| [u.name, u.id] }.unshift(['Anyone', nil]))
+ .control-group
+ = label_tag 'reviewer_id', 'Reviewer', class: 'control-label'
+ .controls
+ = select_tag 'reviewer_id', options_for_select(User.order('email ASC').map {|u| [u.name, u.id]}.unshift(['Anyone', nil]))
+
.control-group
= label_tag 'start_date', 'Start Date', class: 'control-label'
.controls
@@ -60,6 +65,7 @@ div
= label_tag 'end_date', 'End Date', class: 'control-label'
.controls
= text_field_tag 'end_date', '', class: 'datepicker'
+
.control-group
= label_tag 'hidden_keys', 'Hidden Translations', class: 'control-label'
.controls
diff --git a/app/views/users/index.slim b/app/views/users/index.slim
index 29a4f9d7..f6f659d7 100644
--- a/app/views/users/index.slim
+++ b/app/views/users/index.slim
@@ -20,8 +20,6 @@
.header
h1 Users
-hr.divider
-
.border
table.table.hover-rows
thead
diff --git a/app/views/users/show.slim b/app/views/users/show.slim
index a8fbd791..e0847aa5 100644
--- a/app/views/users/show.slim
+++ b/app/views/users/show.slim
@@ -106,5 +106,5 @@ hr.divider
.form-actions
- = f.submit class: 'primary', value: 'save'
+ = f.submit class: 'primary', value: 'Save'
button.danger href=user_url(@user) data-method='DELETE' data-confirm='Are you sure you want to delete this account?' Delete
diff --git a/app/workers/article_importer.rb b/app/workers/article_importer.rb
index 58d0bae4..3998fca1 100644
--- a/app/workers/article_importer.rb
+++ b/app/workers/article_importer.rb
@@ -77,7 +77,9 @@ def on_success(_status, options)
# Keys are refreshed as part of `Key.batch_recalculate_ready!`.
# Translations need to be refreshed in case section data (such as `activeness`) changed in
# the last re-import. CommitKeyCreator takes care of refreshing Translations in a Commit during a Commit import.
- Translation.batch_refresh_elastic_search(article)
+ TranslationsIndex.import! article.reload.translations
+
+ PostLoadingChecker.launch(article)
end
end
end
diff --git a/app/workers/asset_importer.rb b/app/workers/asset_importer.rb
index 7252971d..82479e1b 100644
--- a/app/workers/asset_importer.rb
+++ b/app/workers/asset_importer.rb
@@ -59,7 +59,9 @@ def on_success(_status, options)
Key.batch_recalculate_ready!(asset)
AssetRecalculator.new.perform(asset.id)
- Translation.batch_refresh_elastic_search(asset)
+ TranslationsIndex.import! asset.reload.translations
+
+ PostLoadingChecker.launch(asset)
end
end
end
diff --git a/app/workers/auto_importer.rb b/app/workers/auto_importer.rb
index 3b9dd07b..1f991401 100644
--- a/app/workers/auto_importer.rb
+++ b/app/workers/auto_importer.rb
@@ -54,13 +54,16 @@ def perform(project_id)
begin
project.commit! branch, other_fields: {description: "Automatically imported from the #{branch} branch"}
rescue Git::CommitNotFoundError => err
- branches_to_delete << branch # branch doesn't actually exist; remove from watched branches and ignore
+ # TODO: Disable removing watched branch. SHUTTLE-913
+ Rails.logger.warn("[AutoImporter] Watched branch #{branch} not found in project #{project.name}")
+ # branches_to_delete << branch # branch doesn't actually exist; remove from watched branches and ignore
end
end
project.watched_branches = project.watched_branches - branches_to_delete
project.save!
rescue Timeout::Error => err
+ Raven.capture_exception err, extra: { project_id: project_id }
self.class.perform_in 2.minutes, project_id
end
diff --git a/app/workers/commit_creator.rb b/app/workers/commit_creator.rb
index 56b1bb70..b00c30c4 100644
--- a/app/workers/commit_creator.rb
+++ b/app/workers/commit_creator.rb
@@ -34,6 +34,7 @@ def perform(project_id, sha, options={})
rescue Git::CommitNotFoundError, Project::NotLinkedToAGitRepositoryError => err
CommitMailer.notify_import_errors_in_commit_creator(options[:other_fields].try!(:symbolize_keys).try!(:[], :user_id), project_id, sha, err).deliver_now
rescue Timeout::Error => err
+ Raven.capture_exception err, extra: { project_id: project_id, sha: sha }
self.class.perform_in 2.minutes, project_id, sha
end
diff --git a/app/workers/commit_importer.rb b/app/workers/commit_importer.rb
index ca529c5d..ddf14c71 100644
--- a/app/workers/commit_importer.rb
+++ b/app/workers/commit_importer.rb
@@ -41,6 +41,7 @@ class Finisher
def on_success(_status, options)
commit = Commit.find(options['commit_id'])
+ already_loaded = commit.loaded_at.present?
# mark related blobs as parsed so that we don't parse them again
mark_not_errored_blobs_as_parsed(commit)
@@ -59,8 +60,21 @@ def on_success(_status, options)
# finish loading
commit.update!(loading: false, import_batch_id: nil)
+ # records metric only when never loaded before
+ if !already_loaded and commit.loaded_at and commit.created_at
+ loading_time = commit.loaded_at - commit.created_at
+ CustomMetricHelper.record_project_loading_time(commit.project.slug, loading_time)
+
+ active_translations = commit.active_translations.group_by { |t| t.rfc5646_locale }
+ locale_to_keys = active_translations.map { |locale, ts| [locale, ts.count] }.to_h
+ locale_to_words = active_translations.map { |locale, ts| [locale, ts.map(&:words_count).sum] }.to_h
+ CustomMetricHelper.record_project_statistics(commit.project.slug, commit.blobs.count, locale_to_keys, locale_to_words)
+ end
+
# the readiness hooks were all disabled, so now we need to go through and calculate commit readiness and stats.
CommitRecalculator.new.perform commit.id
+
+ PostLoadingChecker.launch(commit)
end
private
diff --git a/app/workers/commit_key_creator.rb b/app/workers/commit_key_creator.rb
index ef0ece2f..8d57a779 100644
--- a/app/workers/commit_key_creator.rb
+++ b/app/workers/commit_key_creator.rb
@@ -55,6 +55,7 @@ def perform(blob_id, commit_id, importer, keys)
def self.update_key_associations(keys, commit)
keys.reject! { |key| skip_key?(key, commit) }
keys.map(&:id).uniq.each { |k| commit.commits_keys.where(key_id: k).find_or_create! }
+ CommitsIndex.import!(commit.id)
end
include SidekiqLocking
diff --git a/app/workers/commits_cleaner.rb b/app/workers/commits_cleaner.rb
index eda28628..438e29d1 100644
--- a/app/workers/commits_cleaner.rb
+++ b/app/workers/commits_cleaner.rb
@@ -14,7 +14,7 @@
class CommitsCleaner
include Sidekiq::Worker
- sidekiq_options queue: :low, retry: 5
+ sidekiq_options queue: :low, retry: false
def perform
log("Cleaning old commits for #{Date.today}")
diff --git a/app/workers/inactive_user_decommissioner.rb b/app/workers/inactive_user_decommissioner.rb
new file mode 100644
index 00000000..380e4485
--- /dev/null
+++ b/app/workers/inactive_user_decommissioner.rb
@@ -0,0 +1,68 @@
+# Copyright 2016 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This workder puts ex-Squarer into expired state to prevent them from accessing Shuttle.
+# The expired users can use Forgot Password to re-activate their accounts.
+class InactiveUserDecommissioner
+ include Sidekiq::Worker
+ sidekiq_options queue: :high, retry: false
+
+ def perform
+ report_message('started')
+
+ # Retrieves domain users
+ decomission_inactive_user_url = Shuttle::Configuration.features[:decomission_inactive_user_url]
+ decomission_inactive_user_domain = Shuttle::Configuration.features[:decomission_inactive_user_domain]
+
+ ldap_user_response = HTTParty.get(Shuttle::Configuration.features[:decomission_inactive_user_url])
+ unless ldap_user_response.code == 200
+ report_message("Failed to retrieve LDAP users. error code: #{ldap_user_response.code}")
+ return
+ end
+
+ ldap_user_details = ldap_user_response.parsed_response
+ active_user_details = ldap_user_details.reject { |u| u['state'] == 'disabled' }
+ if active_user_details.count < 3000
+ # Stop processing in case LDAP returns empty or partial accounts back.
+ # This will avoid putting all Square users into expired state.
+ report_message("Found #{active_user_details.count} active LDAP users. Skip processing because of too few.")
+ return
+ end
+ active_user_names = active_user_details.map { |detail| detail['username'] }
+
+ # Finds non-expired non-domain accounts
+ shuttle_users = User.where("email like '%#{decomission_inactive_user_domain}'")
+ inactive_shuttle_users = shuttle_users.reject do |user|
+ email = user.email.downcase
+
+ email_domains = email.split('@')
+ raise "Not expected domain user #{user.id}" unless email_domains.count == 2 and email_domains[1] == decomission_inactive_user_domain
+
+ email_users = email_domains[0].split('+')
+ user.expired? || active_user_names.include?(email_users[0])
+ end
+
+ # Puts the inactive accounts as expired.
+ inactive_shuttle_users.each do |inactive_user|
+ report_message("Inactivate user #{inactive_user.email}")
+ inactive_user.update(last_activity_at: User.expire_after.ago)
+ end
+ end
+
+ def report_message(message)
+ Rails.logger.info("InactiveUserDecommissioner - #{message}")
+ end
+
+ include SidekiqLocking
+end
diff --git a/app/workers/post_loading_checker.rb b/app/workers/post_loading_checker.rb
new file mode 100644
index 00000000..8ee177b6
--- /dev/null
+++ b/app/workers/post_loading_checker.rb
@@ -0,0 +1,58 @@
+# Copyright 2019 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Processing the Commit, Article or Asset after loading them into Shuttle.
+
+class PostLoadingChecker
+ include Sidekiq::Worker
+ sidekiq_options queue: :high
+
+ VALIDATORS = [
+ TranslationValidator::SourceFencerValidator,
+ TranslationValidator::TranslationAutoMigration,
+ ]
+
+ def perform(job_type, job_id)
+ job = find_job(job_type, job_id)
+ return if job.nil?
+
+ VALIDATORS.each do |validator|
+ begin
+ validator.new(job).run
+ rescue => e
+ Rails.logger.error("#{PostLoadingChecker} - Failed to run #{validator} on (#{job_type}, #{job_id})")
+ Rails.logger.info("#{PostLoadingChecker} - due to exception: #{e.inspect}")
+ end
+ end
+ end
+
+ def find_job(job_type, job_id)
+ case job_type
+ when 'commit'
+ Commit.where(id: job_id).first
+ when 'article'
+ Article.where(id: job_id).first
+ when 'asset'
+ Asset.where(id: job_id).first
+ else
+ raise ArgumentError, "invalid model type: #{job_type}, #{job_id}"
+ end
+ end
+
+ def self.launch(job)
+ PostLoadingChecker.perform_once(job.project.job_type, job.id)
+ end
+
+ include SidekiqLocking
+end
diff --git a/app/workers/sidekiq_worker_restarter.rb b/app/workers/sidekiq_worker_restarter.rb
new file mode 100644
index 00000000..f221b091
--- /dev/null
+++ b/app/workers/sidekiq_worker_restarter.rb
@@ -0,0 +1,76 @@
+# Copyright 2016 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This worker stops sidekiq worker gracefully after draining existing running jobs.
+class SidekiqWorkerRestarter
+ include Sidekiq::Worker
+ sidekiq_options queue: :high, retry: false
+
+ MIN_RUNNING_PROCESSES = 4
+ MAX_RUNNING_DURATION = 3.hours
+
+ def perform
+ Rails.logger.info("Sidekiq Worker Restarter - started")
+ processes = Sidekiq::ProcessSet.new.sort { |p1, p2| p1['started_at'] - p2['started_at'] }
+ record_sidekiq_metrics(processes)
+ restart_oldest_process(processes)
+ end
+
+ def restart_oldest_process(processes)
+ # finds process in quiet state
+ process = processes.select { |p| p['quiet'] == 'true' }.first
+ if process.present?
+ if process['busy'] == 0
+ Rails.logger.info("Sidekiq Worker Restarter - signal quiet worker #{process['pid']} to stop")
+ process.stop!
+ else
+ Rails.logger.info("Sidekiq Worker Restarter - quiet worker #{process['pid']} is busy (#{process['busy']} jobs)")
+ end
+ return
+ end
+
+ # Sidekiq::ProcessSet misses some workers sometimes for unknown reason.
+ # checks if there are enough available running processes
+ if processes.count <= MIN_RUNNING_PROCESSES
+ Rails.logger.info("Sidekiq Worker Restarter - no enough running workers (#{processes.count} workers)")
+ return
+ end
+
+ # finds process running longer enough to restart
+ expiration_time = MAX_RUNNING_DURATION.ago
+ process = processes.select { |p| p['started_at'] < expiration_time.to_i }.first
+ if process.present?
+ Rails.logger.info("Sidekiq Worker Restarter - signal worker #{process['pid']} to quiet")
+ process.quiet!
+ else
+ Rails.logger.info("Sidekiq Worker Restarter - no expired worker found")
+ end
+ end
+
+ def record_sidekiq_metrics(processes)
+ host_processes = processes.group_by { |p| p['hostname'] }
+ host_to_longevities = {}
+ host_processes.each do |hostname, ps|
+ min_started_at = ps.map { |p| p['started_at'] }.min
+ host_to_longevities[hostname] = Time.now.to_i - min_started_at
+ end
+ CustomMetricHelper.record_sidekiq_longevity(host_to_longevities)
+
+ busy_jobs = processes.map { |x| x['busy'] }.sum
+ enqueued_jobs = Sidekiq::Stats.new.enqueued
+ CustomMetricHelper.record_sidekiq_jobs(busy_jobs, enqueued_jobs)
+ end
+
+ include SidekiqLocking
+end
diff --git a/app/workers/stash_webhook_pinger.rb b/app/workers/stash_webhook_pinger.rb
index 6db2589f..1897743a 100644
--- a/app/workers/stash_webhook_pinger.rb
+++ b/app/workers/stash_webhook_pinger.rb
@@ -20,7 +20,7 @@ class StashWebhookPinger
include Sidekiq::Worker
include Rails.application.routes.url_helpers
- sidekiq_options queue: :high, retry: 5
+ sidekiq_options queue: :high, retry: 10
# Executes this worker.
#
diff --git a/bin/docker-setup b/bin/docker-setup
index 74d6ed80..cbd2a45c 100755
--- a/bin/docker-setup
+++ b/bin/docker-setup
@@ -5,9 +5,5 @@ require 'pathname'
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
Dir.chdir APP_ROOT do
- system 'bin/rake', 'db:migrate', 'db:seed'
-
- system({'FORCE' => 'y', 'CLASS' => 'Commit'}, 'bin/rake', 'environment', 'elasticsearch:import:model')
- system({'FORCE' => 'y', 'CLASS' => 'Key'}, 'bin/rake', 'environment', 'elasticsearch:import:model')
- system({'FORCE' => 'y', 'CLASS' => 'Translation'}, 'bin/rake', 'environment', 'elasticsearch:import:model')
+ system 'bin/rake', 'db:migrate', 'db:seed', 'chewy:reset'
end
diff --git a/bin/docker-tests b/bin/docker-tests
index 4121c5c9..e42221cc 100755
--- a/bin/docker-tests
+++ b/bin/docker-tests
@@ -7,9 +7,9 @@ APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
Dir.chdir APP_ROOT do
system 'bin/rake', 'db:migrate'
- system({'FORCE' => 'y', 'CLASS' => 'Commit'}, 'bin/rake', 'environment', 'elasticsearch:import:model')
- system({'FORCE' => 'y', 'CLASS' => 'Key'}, 'bin/rake', 'environment', 'elasticsearch:import:model')
- system({'FORCE' => 'y', 'CLASS' => 'Translation'}, 'bin/rake', 'environment', 'elasticsearch:import:model')
+ # TODO: update with better way of waiting for ElasticSearch ready
+ system 'echo', 'wait 10 seconds for elasticsearch readiness......'
+ system 'sleep', '10'
exec 'bundle', 'exec', 'rspec', 'spec'
end
diff --git a/bin/setup b/bin/setup
index 53f5757f..baa71c2b 100755
--- a/bin/setup
+++ b/bin/setup
@@ -21,9 +21,7 @@ Dir.chdir APP_ROOT do
system "bin/rake db:setup"
puts "\n== Preparing ElasticSearch =="
- system 'bin/rake', 'elasticsearch:import:model', 'FORCE=y', 'CLASS=Commit'
- system 'bin/rake', 'elasticsearch:import:model', 'FORCE=y', 'CLASS=Key'
- system 'bin/rake', 'elasticsearch:import:model', 'FORCE=y', 'CLASS=Translation'
+ system 'bin/rake', 'chewy:reset'
puts "\n== Removing old logs and tempfiles =="
system "rm -f log/*"
diff --git a/config/application.rb b/config/application.rb
index c0c96aa2..f665ad27 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -22,6 +22,9 @@
module Shuttle
class Application < Rails::Application
+ # Do not swallow errors in after_commit/after_rollback callbacks.
+ config.active_record.raise_in_transactional_callbacks = true
+
# Load configoro settings here so that the settings can be used in application.rb, development.rb, production.rb, etc...
config.before_configuration do
Configoro.initialize
@@ -51,9 +54,6 @@ class Application < Rails::Application
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de
- # Do not swallow errors in after_commit/after_rollback callbacks.
- config.active_record.raise_in_transactional_callbacks = true
-
# Use SQL instead of Active Record's schema dumper when creating the database.
# This is necessary if your schema can't be completely dumped by the schema dumper,
# like if you have constraints or database-specific column types
diff --git a/config/chewy.yml b/config/chewy.yml
new file mode 100644
index 00000000..3ee20d76
--- /dev/null
+++ b/config/chewy.yml
@@ -0,0 +1,7 @@
+---
+development:
+ host: <%= ENV.fetch('SHUTTLE_ES_URL', 'localhost:9200') %>
+ prefix: shuttle_development
+test:
+ host: <%= ENV.fetch('SHUTTLE_ES_URL', 'localhost:9200') %>
+ prefix: shuttle_test
diff --git a/config/deploy.rb b/config/deploy.rb
index 80458bb0..3718661c 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -14,12 +14,13 @@
set :application, 'shuttle'
-set :repo_url, 'https://github.com/Square/shuttle.git'
+set :repo_url, 'https://git.sqcorp.co/scm/intl/shuttle.git'
ask(:branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp })
-set :deploy_to, "/var/www/#{fetch :application}"
+set :deploy_to, "/app/#{fetch :application}"
append :linked_files,
+ 'config/chewy.yml',
'config/database.yml',
'config/secrets.yml'
append :linked_dirs,
@@ -29,15 +30,49 @@
'vendor/bundle'
set :rvm_type, :system
-set :rvm_ruby_version, "2.3.1@#{fetch :application}"
+set :rvm_ruby_version, "2.4.6@#{fetch :application}"
+set :runit_timeout, 60
set :whenever_roles, [:app, :primary_cron]
namespace :deploy do
desc 'Restart application'
task :restart do
- on roles(:app), in: :sequence, wait: 5 do
+ on roles(:web), in: :sequence, wait: 5 do
execute :touch, release_path.join('tmp/restart.txt')
end
end
end
+
+namespace :sidekiq do
+ Rake::Task["sidekiq:quiet"].clear_actions
+ task :quiet do
+ on roles fetch(:sidekiq_roles) do
+ within release_path do
+ execute :sidekiqctl, 'quiet', release_path.join('tmp/pids/sidekiq-0.pid')
+ execute :sidekiqctl, 'quiet', release_path.join('tmp/pids/sidekiq-1.pid')
+ execute :sidekiqctl, 'quiet', release_path.join('tmp/pids/sidekiq-2.pid')
+ end
+ end
+ end
+
+ Rake::Task["sidekiq:stop"].clear_actions
+ task :stop do
+ on roles fetch(:sidekiq_roles) do
+ sudo "sv -w #{fetch(:runit_timeout)} stop sidekiq0"
+ sudo "sv -w #{fetch(:runit_timeout)} stop sidekiq1"
+ sudo "sv -w #{fetch(:runit_timeout)} stop sidekiq2"
+ end
+ end
+
+ Rake::Task["sidekiq:start"].clear_actions
+ task :start do
+ on roles fetch(:sidekiq_roles) do
+ sudo "sv start sidekiq0"
+ sudo "sv start sidekiq1"
+ sudo "sv start sidekiq2"
+ end
+ end
+end
+
+after 'deploy:publishing', 'deploy:restart'
diff --git a/config/deploy/production.rb b/config/deploy/production.rb
index 65254332..320f176e 100644
--- a/config/deploy/production.rb
+++ b/config/deploy/production.rb
@@ -1,12 +1,14 @@
set :stage, :production
-WEB_BOXES = %w[user@shuttle.example.com]
-WORKER_BOXES = %w[user@shuttle.example.com]
+
+WEB_BOXES = (1..2).map { |i| "square@shuttle-web-b-#{i.to_s.rjust(2, '0')}.sqcorp.co" }
+WORKER_BOXES = (1..2).map { |i| "square@shuttle-worker-b-#{i.to_s.rjust(2, '0')}.sqcorp.co" }
role :app, WEB_BOXES + WORKER_BOXES
role :web, WEB_BOXES
role :db, WEB_BOXES.first
role :sidekiq, WORKER_BOXES
+set :sidekiq_roles, [:sidekiq]
role :primary_cron, WORKER_BOXES.first
set :branch, 'deployable'
diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb
index fcb60b4c..2fc28ced 100644
--- a/config/deploy/staging.rb
+++ b/config/deploy/staging.rb
@@ -1,13 +1,15 @@
set :stage, :staging
set :rails_env, :staging
-STAGING_BOXES = %w[user@shuttle-staging.example.com]
+DEV_WEB_BOXES = (1..1).map { |i| "square@shuttle-web-dev-a-#{i.to_s.rjust(2, '0')}.sqcorp.co" }
+DEV_WORKER_BOXES = (1..1).map { |i| "square@shuttle-worker-dev-a-#{i.to_s.rjust(2, '0')}.sqcorp.co" }
-role :app, STAGING_BOXES
-role :web, STAGING_BOXES
-role :db, STAGING_BOXES.first
-role :sidekiq, STAGING_BOXES
-role :primary_cron, STAGING_BOXES.first
+role :app, DEV_WEB_BOXES + DEV_WORKER_BOXES
+role :web, DEV_WEB_BOXES
+role :db, DEV_WEB_BOXES.first
+role :sidekiq, DEV_WORKER_BOXES
+set :sidekiq_roles, [:sidekiq]
+role :primary_cron, DEV_WORKER_BOXES.first
append :linked_files,
'config/environments/staging/credentials.yml',
diff --git a/config/environments/common/automatic_user_privileges.yml b/config/environments/common/automatic_user_privileges.yml
index 9569e0ce..a00cc273 100644
--- a/config/environments/common/automatic_user_privileges.yml
+++ b/config/environments/common/automatic_user_privileges.yml
@@ -1,7 +1,7 @@
# After a user signs up and confirms their email address, he will automatically get 'monitor' access
# if his email is from one of the domains listed here. We recommend putting in your company email
# domain here if you trust them and don't want to have to manually activate users from within the company.
-domains_to_get_monitor_role_after_email_confirmation: ['example.com']
+domains_to_get_monitor_role_after_email_confirmation: ['squareup.com']
# Will be used to determine if the current user can use the autofill feature
-domains_who_can_search_users: ['example.com']
+domains_who_can_search_users: ['squareup.com']
diff --git a/config/environments/common/features.yml b/config/environments/common/features.yml
index a681585e..0b47af85 100644
--- a/config/environments/common/features.yml
+++ b/config/environments/common/features.yml
@@ -1 +1,6 @@
enable_blank_string_auto_approval: true
+
+decomission_inactive_user_url: https://registry-office-accessible.global.square/api/v1/users/basic_info
+decomission_inactive_user_domain: squareup.com
+
+skip_levenshtein_distance_diff: true
diff --git a/config/environments/common/git.yml b/config/environments/common/git.yml
index 8d1e9036..1b4e12cb 100644
--- a/config/environments/common/git.yml
+++ b/config/environments/common/git.yml
@@ -1,4 +1,4 @@
# The author info that will be used when Shuttle is updating the touchdown branch, i.e. when pushing manifest file
author:
name: Shuttle
- email: shuttle-noreply@example.com
+ email: shuttle-noreply@squareup.com
diff --git a/config/environments/common/globalsight.yml b/config/environments/common/globalsight.yml
new file mode 100644
index 00000000..2fbf0ffd
--- /dev/null
+++ b/config/environments/common/globalsight.yml
@@ -0,0 +1 @@
+--- {}
diff --git a/config/environments/common/mailer.yml b/config/environments/common/mailer.yml
index d561edb5..f52f6146 100644
--- a/config/environments/common/mailer.yml
+++ b/config/environments/common/mailer.yml
@@ -1,2 +1,3 @@
-from: shuttle-engineers@example.com
-translators_list: translators@example.com
+from: shuttle-engineering@squareup.com
+localization_list: g11n@squareup.com
+translators_list: l10n@squareup.com
diff --git a/config/environments/common/reports.yml b/config/environments/common/reports.yml
index 5da714b6..63d0fc95 100644
--- a/config/environments/common/reports.yml
+++ b/config/environments/common/reports.yml
@@ -1 +1 @@
-internal_domains: ['@example.com']
+internal_domains: ['@squareup.com']
diff --git a/config/environments/common/services.yml b/config/environments/common/services.yml
index 86c77bb0..f104a6f1 100644
--- a/config/environments/common/services.yml
+++ b/config/environments/common/services.yml
@@ -1,8 +1,8 @@
# Will be used to determine the url of a commit
-github_enterprise_domain: "git.example.com"
+github_enterprise_domain: "git.squareup.com"
# Will be used to determine the url of a commit
-stash_domain: "stash.example.com"
+stash_domain: "git.sqcorp.co"
# Will be linked to in the footer to give users a way to provide feedback to the engineers running Shuttle
-feedback_link: "https://jira.example.com"
+feedback_link: "https://jira.corp.squareup.com/secure/CreateIssueDetails!init.jspa?pid=12827&issuetype=6"
diff --git a/config/environments/common/webhook_pinger.yml b/config/environments/common/webhook_pinger.yml
index eddc8a2d..71f0408d 100644
--- a/config/environments/common/webhook_pinger.yml
+++ b/config/environments/common/webhook_pinger.yml
@@ -1,2 +1,2 @@
---
-host: bitbucket.example.com
+host: git.sqcorp.co
diff --git a/config/environments/development/elasticsearch.yml b/config/environments/development/elasticsearch.yml
new file mode 100644
index 00000000..46387bb5
--- /dev/null
+++ b/config/environments/development/elasticsearch.yml
@@ -0,0 +1,2 @@
+---
+xpack.security.enabled: false
diff --git a/config/environments/development/sentry.yml b/config/environments/development/sentry.yml
new file mode 100644
index 00000000..8f5f0277
--- /dev/null
+++ b/config/environments/development/sentry.yml
@@ -0,0 +1,4 @@
+---
+# disabling Sentry Raven logging by not setting DSN and secret key
+dsn:
+secret_key:
diff --git a/config/environments/production.rb b/config/environments/production.rb
index a9a7035a..e0af4ccb 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -60,7 +60,7 @@
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
- config.log_level = :debug
+ config.log_level = :info
# Prepend all log lines with the following tags.
# config.log_tags = [ :subdomain, :uuid ]
@@ -97,7 +97,7 @@
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
- address: 'localhost',
+ address: '127.0.0.1',
port: 25
}
end
diff --git a/config/environments/production/default_url_options.yml b/config/environments/production/default_url_options.yml
index 72714587..69a6f1f1 100644
--- a/config/environments/production/default_url_options.yml
+++ b/config/environments/production/default_url_options.yml
@@ -1,2 +1,2 @@
-host: shuttle.example.com
+host: shuttle.squareup.com
protocol: https
diff --git a/config/environments/production/elasticsearch.yml b/config/environments/production/elasticsearch.yml
index 3645f7c7..3c672dc7 100644
--- a/config/environments/production/elasticsearch.yml
+++ b/config/environments/production/elasticsearch.yml
@@ -1,2 +1,2 @@
---
-url: "http://shuttle-es-production.example.com:9200"
+url: "http://es-a.corp.squareup.com:9206"
diff --git a/config/environments/production/redis.yml b/config/environments/production/redis.yml
deleted file mode 100644
index 0c2a6300..00000000
--- a/config/environments/production/redis.yml
+++ /dev/null
@@ -1,2 +0,0 @@
----
-host: shuttle-redis.example.com
diff --git a/config/environments/production/workbench.yml b/config/environments/production/workbench.yml
index 9cc3a987..cfe42df9 100644
--- a/config/environments/production/workbench.yml
+++ b/config/environments/production/workbench.yml
@@ -1,2 +1,2 @@
enable_reasons: true
-show_article_groups: false
+show_article_groups: true
diff --git a/config/environments/staging.rb b/config/environments/staging.rb
index e1a5b23c..26cf7e86 100644
--- a/config/environments/staging.rb
+++ b/config/environments/staging.rb
@@ -90,4 +90,10 @@
config.log_formatter = ::Logger::Formatter.new
config.use_ssl = true
+
+ config.action_mailer.delivery_method = :smtp
+ config.action_mailer.smtp_settings = {
+ address: '127.0.0.1',
+ port: 25
+ }
end
diff --git a/config/environments/staging/default_url_options.yml b/config/environments/staging/default_url_options.yml
index 1c7fcd28..bfe3b369 100644
--- a/config/environments/staging/default_url_options.yml
+++ b/config/environments/staging/default_url_options.yml
@@ -1,2 +1,2 @@
-host: shuttle-stage.example.com
+host: shuttle-dev.sqcorp.co
protocol: https
diff --git a/config/environments/staging/elasticsearch.yml b/config/environments/staging/elasticsearch.yml
index 8592736a..8fe66f47 100644
--- a/config/environments/staging/elasticsearch.yml
+++ b/config/environments/staging/elasticsearch.yml
@@ -1,2 +1,2 @@
---
-url: "http://shuttle-es-staging.example.com:9200"
+url: "https://shuttle-es-dev.sqcorp.co"
diff --git a/config/environments/staging/services.yml b/config/environments/staging/services.yml
new file mode 100644
index 00000000..603fe4b8
--- /dev/null
+++ b/config/environments/staging/services.yml
@@ -0,0 +1,5 @@
+# Will be used to determine the url of a commit
+github_enterprise_domain: "bitbucket-staging.sqcorp.co"
+
+# Will be used to determine the url of a commit
+stash_domain: "bitbucket-staging.sqcorp.co"
diff --git a/config/environments/staging/webhook_pinger.yml b/config/environments/staging/webhook_pinger.yml
new file mode 100644
index 00000000..9935b08e
--- /dev/null
+++ b/config/environments/staging/webhook_pinger.yml
@@ -0,0 +1,2 @@
+---
+host: bitbucket-staging.sqcorp.co
diff --git a/config/environments/staging/workbench.yml b/config/environments/staging/workbench.yml
index af96205e..cfe42df9 100644
--- a/config/environments/staging/workbench.yml
+++ b/config/environments/staging/workbench.yml
@@ -1,2 +1,2 @@
-enable_reasons: false
+enable_reasons: true
show_article_groups: true
diff --git a/config/environments/test/sentry.yml b/config/environments/test/sentry.yml
new file mode 100644
index 00000000..3eeec656
--- /dev/null
+++ b/config/environments/test/sentry.yml
@@ -0,0 +1,3 @@
+---
+# disabling Sentry Raven logging by not setting a DSN
+dsn:
diff --git a/config/environments/test/sidekiq.yml b/config/environments/test/sidekiq.yml
new file mode 100644
index 00000000..e20e3426
--- /dev/null
+++ b/config/environments/test/sidekiq.yml
@@ -0,0 +1,3 @@
+---
+server_pool_size: 1
+client_pool_size: 1
diff --git a/config/initializers/chewy.rb b/config/initializers/chewy.rb
new file mode 100644
index 00000000..27a4b4b6
--- /dev/null
+++ b/config/initializers/chewy.rb
@@ -0,0 +1,23 @@
+# Instrument ElasticSearch code in NewRelic
+
+ActiveSupport::Notifications.subscribe('import_objects.chewy') do |_name, start, finish|
+ metric_name = 'Database/ElasticSearch/import'
+ duration = (finish - start).to_f
+
+ self.class.trace_execution_scoped([metric_name]) do
+ # NewRelic::Agent.instance.transaction_sampler.notice_sql(logged, nil, duration)
+ # NewRelic::Agent.instance.sql_sampler.notice_sql(logged, metric_name, nil, duration)
+ NewRelic::Agent.record_metric(metric_name, duration)
+ end
+end
+
+ActiveSupport::Notifications.subscribe('search_query.chewy') do |_name, start, finish|
+ metric_name = 'Database/ElasticSearch/search'
+ duration = (finish - start).to_f
+
+ self.class.trace_execution_scoped([metric_name]) do
+ # NewRelic::Agent.instance.transaction_sampler.notice_sql(logged, nil, duration)
+ # NewRelic::Agent.instance.sql_sampler.notice_sql(logged, metric_name, nil, duration)
+ NewRelic::Agent.record_metric(metric_name, duration)
+ end
+end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 37b7fe88..42b7bcee 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -137,7 +137,7 @@
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
- # config.confirm_within = 3.days
+ config.confirm_within = 3.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
@@ -281,4 +281,42 @@
# When using OmniAuth, Devise cannot automatically set OmniAuth path,
# so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = '/my_engine/users/auth'
+
+ # ==> Security Extension
+ # Configure security extension for devise
+
+ # Should the password expire (e.g 3.months)
+ # config.expire_password_after = false
+
+ # Need 1 char of A-Z, a-z and 0-9
+ # config.password_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
+
+ # How many passwords to keep in archive
+ # config.password_archiving_count = 5
+
+ # Deny old password (true, false, count)
+ # config.deny_old_passwords = true
+
+ # enable email validation for :secure_validatable. (true, false, validation_options)
+ # dependency: need an email validator like rails_email_validator
+ # config.email_validation = true
+
+ # captcha integration for recover form
+ # config.captcha_for_recover = true
+
+ # captcha integration for sign up form
+ # config.captcha_for_sign_up = true
+
+ # captcha integration for sign in form
+ # config.captcha_for_sign_in = true
+
+ # captcha integration for unlock form
+ # config.captcha_for_unlock = true
+
+ # captcha integration for confirmation form
+ # config.captcha_for_confirmation = true
+
+ # Time period for account expiry from last_activity_at
+ config.expire_after = 90.days
+
end
diff --git a/config/initializers/no_proxy.rb b/config/initializers/no_proxy.rb
new file mode 100644
index 00000000..ca0fc9fb
--- /dev/null
+++ b/config/initializers/no_proxy.rb
@@ -0,0 +1 @@
+ENV['no_proxy']='square,sqcorp.co,corp.squareup.com'
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
new file mode 100644
index 00000000..e37d57d9
--- /dev/null
+++ b/config/initializers/sentry.rb
@@ -0,0 +1,10 @@
+require 'raven'
+
+if !Rails.env.test?
+ Raven.configure do |config|
+ config.dsn = Shuttle::Configuration.sentry.dsn if Shuttle::Configuration.sentry.dsn
+ config.secret_key = Shuttle::Configuration.sentry.secret_key if Shuttle::Configuration.sentry.secret_key
+ config.tags = { environment: Rails.env }
+ config.excluded_exceptions = ['ActionController::RoutingError', 'Sidekiq::Shutdown']
+ end
+end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 74869991..75586f93 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -25,6 +25,7 @@ def process_id() Process.pid end
Sidekiq.configure_client do |config|
config.redis = redis
end
+
Sidekiq.configure_server do |config|
begin
require 'sidekiq/pro/reliable_fetch'
@@ -32,6 +33,11 @@ def process_id() Process.pid end
# no sidekiq pro
end
config.redis = redis
+
+ require 'chewy_atomic'
+ config.server_middleware do |chain|
+ chain.add ChewyAtomic
+ end
end
end
diff --git a/config/locales/devise.security_extension.en.yml b/config/locales/devise.security_extension.en.yml
new file mode 100644
index 00000000..e15bf5cb
--- /dev/null
+++ b/config/locales/devise.security_extension.en.yml
@@ -0,0 +1,14 @@
+en:
+ errors:
+ messages:
+ taken_in_past: "was already taken in the past!"
+ equal_to_current_password: "must be different to the current password!"
+ password_format: "must contain big, small letters and digits"
+ devise:
+ invalid_captcha: "The captcha input is not valid!"
+ password_expired:
+ updated: "Your new password is saved."
+ change_required: "Your password is expired. Please renew your password!"
+ failure:
+ session_limited: 'Your login credentials were used in another browser. Please sign in again to continue in this browser.'
+ expired: 'Your account has expired due to inactivity. Please use Forgot Password to re-activate your account.'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index b211ceff..21c8fcbf 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -166,10 +166,14 @@ en:
header: Commits
articles:
header: Articles
+ groups: Groups
+ groups_cell: "%{pending_count} (%{linked_count})"
assets:
header: Assets
groups:
header: Article Groups
+ articles: Articles
+ articles_cell: "%{pending_count} (%{linked_count})"
assets:
project_not_found: Invalid project
asset_not_found: Asset doesn't exist
@@ -302,6 +306,7 @@ en:
Android: "{android}"
Erb: "<%= ERb %>"
Html: ""
+ IntlMessageFormat: "{ICU Message}"
MessageFormat: "Java MessageFormat [{0}]"
Mustache: "{{Mustache}}"
Printf: "printf() [%s]"
@@ -408,3 +413,8 @@ en:
request_screenshot:
subject: "[Shuttle] Requesting screenshots for commit %{sha}"
success: "Successfully requested screenshots for commit %{sha}"
+ fencer_validation:
+ suspicious_translation_found:
+ subject:
+ production: "[ACTION REQUIRED] [Shuttle] Found suspicious source strings"
+ staging: "[NO ACTION REQUIRED] [Shuttle Staging] Found suspicious source strings"
diff --git a/config/locales/zzz_manifest.yml b/config/locales/zzz_manifest.yml
new file mode 100644
index 00000000..28583753
--- /dev/null
+++ b/config/locales/zzz_manifest.yml
@@ -0,0 +1,49 @@
+---
+ja:
+ workers:
+ translations_mass_copier:
+ from:
+ not_a_targeted_or_base_locale: ソースロケールは基本ロケールでも、プロジェクトのターゲットロケールのいずれもありません
+ from_and_to_cannot_be_equal: ソースおよびターゲットロケールが互いに等しくすることができない
+ invalid_rfc5646_locale: 無効な %{kind} ロケール
+ iso639s_doesnt_match: ソースとターゲットのロケールが同じ言語ファミリに含まれていない(彼らのISO639sが一致しない)
+ project_translations_adder_and_remover_batch_still_running: プロジェクト翻訳加算バッチがまだ実行されている。それが終了した後にしてみてください。
+ to:
+ cannot_copy_to_projects_base_locale: 基本ロケールを投影するコピーすることはできません
+ not_a_targeted_locale: ターゲットロケールは、プロジェクトのターゲットロケールのものではありません
+en-AU:
+ workers:
+ translations_mass_copier:
+ from:
+ not_a_targeted_or_base_locale: Source locale is neither the base locale nor one of the project target locales
+ from_and_to_cannot_be_equal: Source and target locales cannot be equal to each other
+ invalid_rfc5646_locale: Invalid %{kind} locale
+ iso639s_doesnt_match: Source and target locales are not in the same language family (their ISO639s do not match)
+ project_translations_adder_and_remover_batch_still_running: Project Translations Adder and Remover batch is still running. Try after it finishes.
+ to:
+ cannot_copy_to_projects_base_locale: Cannot copy to project base locale
+ not_a_targeted_locale: Target locale is not one of the project target locales
+en-GB:
+ workers:
+ translations_mass_copier:
+ from:
+ not_a_targeted_or_base_locale: Source locale is neither the base locale nor one of the project target locales
+ from_and_to_cannot_be_equal: Source and target locales cannot be equal to each other
+ invalid_rfc5646_locale: Invalid %{kind} locale
+ iso639s_doesnt_match: Source and target locales are not in the same language family (their ISO639s do not match)
+ project_translations_adder_and_remover_batch_still_running: Project Translations Adder And Remover batch is still running. Try after it finishes.
+ to:
+ cannot_copy_to_projects_base_locale: Cannot copy to project base locale
+ not_a_targeted_locale: Target locale is not one of the project target locales
+en-IN:
+ workers:
+ translations_mass_copier:
+ from:
+ not_a_targeted_or_base_locale: Source locale is neither the base locale nor one of the project target locales
+ from_and_to_cannot_be_equal: Source and target locales cannot be equal to each other
+ invalid_rfc5646_locale: Invalid %{kind} locale
+ iso639s_doesnt_match: Source and target locales are not in the same language family (their ISO639s do not match)
+ project_translations_adder_and_remover_batch_still_running: Project Translations Adder And Remover batch is still running. Try after it finishes.
+ to:
+ cannot_copy_to_projects_base_locale: Cannot copy to project base locale
+ not_a_targeted_locale: Target locale is not one of the project target locales
diff --git a/config/routes.rb b/config/routes.rb
index 907b7352..7fa6095a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -153,12 +153,19 @@
end
end
+ get 'reports' => 'reports#index', as: :reports
+ get 'reports/download/incoming/:date' => 'reports#incoming'
+ get 'reports/download/pending/:date' => 'reports#pending'
+ get 'reports/download/completed/:date' => 'reports#completed'
+
# HOME PAGES
get 'administrators' => 'home#administrators', as: :administrators
get 'translators' => 'home#translators', as: :translators
get 'reviewers' => 'home#reviewers', as: :reviewers
root to: 'home#index'
+ get 'csv' => 'home#csv'
+
require 'sidekiq/web'
begin
require 'sidekiq/pro/web'
diff --git a/config/schedule.rb b/config/schedule.rb
index 79a92332..c0bb73d0 100644
--- a/config/schedule.rb
+++ b/config/schedule.rb
@@ -18,11 +18,19 @@
runner 'AutoImporter.perform_once'
end
+every 5.minutes, roles: [:primary_cron] do
+ runner 'SidekiqWorkerRestarter.perform_once'
+end
+
+every :day, at: '12:00 am', roles: [:primary_cron] do
+ runner 'InactiveUserDecommissioner.perform_once'
+end
+
every :day, at: '12:00 am', roles: [:primary_cron] do
rake 'metrics:update'
end
-every 1.minute, roles: [:primary_cron] do
+every 5.minute, roles: [:primary_cron] do
rake 'touchdown:update'
end
@@ -30,10 +38,22 @@
rake 'maintenance:cleanup_commits'
end
-every :saturday, at: '1am', roles: [:app] do
+every :day, at: '1am', roles: [:app] do
rake 'maintenance:cleanup_repos'
end
every :day, roles: [:primary_cron] do
rake 'maintenance:reap_deleted_commits'
end
+
+every :day, at: '2am', roles: [:app] do
+ rake 'reports:generate:incoming'
+end
+
+every :day, at: '3am', roles: [:app] do
+ rake 'reports:generate:pending'
+end
+
+every :day, at: '4am', roles: [:app] do
+ rake 'reports:generate:completed'
+end
diff --git a/db/migrate/20190517232627_add_devise_extenstion.rb b/db/migrate/20190517232627_add_devise_extenstion.rb
new file mode 100644
index 00000000..77904e30
--- /dev/null
+++ b/db/migrate/20190517232627_add_devise_extenstion.rb
@@ -0,0 +1,11 @@
+class AddDeviseExtenstion < ActiveRecord::Migration
+ def change
+ # https://github.com/phatworx/devise_security_extension
+
+ # Expirable on inactivity
+ add_column :users, :last_activity_at, :datetime
+ add_column :users, :expired_at, :datetime
+ add_index :users, :last_activity_at
+ add_index :users, :expired_at
+ end
+end
diff --git a/db/migrate/20190620214800_create_reports.rb b/db/migrate/20190620214800_create_reports.rb
new file mode 100644
index 00000000..8ec34187
--- /dev/null
+++ b/db/migrate/20190620214800_create_reports.rb
@@ -0,0 +1,15 @@
+class CreateReports < ActiveRecord::Migration
+ def change
+ create_table :reports do |t|
+ t.datetime :date
+ t.string :project
+ t.string :locale
+ t.integer :strings
+ t.integer :words
+ t.string :report_type
+
+ t.timestamps null: false
+ end
+ add_index :reports, :date
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index e732d683..fe617511 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -2,8 +2,8 @@
-- PostgreSQL database dump
--
--- Dumped from database version 9.6.10
--- Dumped by pg_dump version 9.6.10
+-- Dumped from database version 9.4.21
+-- Dumped by pg_dump version 9.6.13
SET statement_timeout = 0;
SET lock_timeout = 0;
@@ -12,6 +12,7 @@ SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
+SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
@@ -354,10 +355,10 @@ CREATE TABLE public.commits (
exported boolean DEFAULT false NOT NULL,
loaded_at timestamp without time zone,
description text,
- author character varying,
- author_email character varying,
+ author character varying(255),
+ author_email character varying(255),
pull_request_url text,
- import_batch_id character varying,
+ import_batch_id character varying(255),
import_errors text,
revision character varying(40) NOT NULL,
fingerprint character varying,
@@ -468,6 +469,39 @@ CREATE SEQUENCE public.edit_reasons_id_seq
ALTER SEQUENCE public.edit_reasons_id_seq OWNED BY public.edit_reasons.id;
+--
+-- Name: globalsight_api_records; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.globalsight_api_records (
+ id integer NOT NULL,
+ job_id integer NOT NULL,
+ status character varying(255) NOT NULL,
+ article_id integer,
+ created_at timestamp without time zone,
+ updated_at timestamp without time zone
+);
+
+
+--
+-- Name: globalsight_api_records_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.globalsight_api_records_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: globalsight_api_records_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.globalsight_api_records_id_seq OWNED BY public.globalsight_api_records.id;
+
+
--
-- Name: groups; Type: TABLE; Schema: public; Owner: -
--
@@ -565,7 +599,7 @@ CREATE TABLE public.keys (
original_key text NOT NULL,
source_copy text,
context text,
- importer character varying,
+ importer character varying(255),
source text,
fencers text,
other_data text,
@@ -604,8 +638,8 @@ ALTER SEQUENCE public.keys_id_seq OWNED BY public.keys.id;
CREATE TABLE public.locale_associations (
id integer NOT NULL,
- source_rfc5646_locale character varying NOT NULL,
- target_rfc5646_locale character varying NOT NULL,
+ source_rfc5646_locale character varying(255) NOT NULL,
+ target_rfc5646_locale character varying(255) NOT NULL,
checked boolean DEFAULT false NOT NULL,
uncheck_disabled boolean DEFAULT false NOT NULL,
created_at timestamp without time zone,
@@ -679,9 +713,9 @@ CREATE TABLE public.projects (
name character varying(256) NOT NULL,
repository_url character varying(256),
created_at timestamp without time zone,
- translations_adder_and_remover_batch_id character varying,
+ translations_adder_and_remover_batch_id character varying(255),
disable_locale_association_checkbox_settings boolean DEFAULT false NOT NULL,
- base_rfc5646_locale character varying DEFAULT 'en'::character varying NOT NULL,
+ base_rfc5646_locale character varying(255) DEFAULT 'en'::character varying NOT NULL,
targeted_rfc5646_locales text,
skip_imports text,
key_exclusions text,
@@ -692,11 +726,11 @@ CREATE TABLE public.projects (
only_paths text,
skip_importer_paths text,
only_importer_paths text,
- default_manifest_format character varying,
+ default_manifest_format character varying(255),
watched_branches text,
- touchdown_branch character varying,
+ touchdown_branch character varying(255),
manifest_directory text,
- manifest_filename character varying,
+ manifest_filename character varying(255),
github_webhook_url text,
stash_webhook_url text,
api_token character(240) NOT NULL,
@@ -758,12 +792,48 @@ CREATE SEQUENCE public.reasons_id_seq
ALTER SEQUENCE public.reasons_id_seq OWNED BY public.reasons.id;
+--
+-- Name: reports; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.reports (
+ id integer NOT NULL,
+ date timestamp without time zone,
+ project character varying,
+ locale character varying,
+ strings integer,
+ words integer,
+ report_type character varying,
+ created_at timestamp without time zone NOT NULL,
+ updated_at timestamp without time zone NOT NULL
+);
+
+
+--
+-- Name: reports_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.reports_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: reports_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.reports_id_seq OWNED BY public.reports.id;
+
+
--
-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.schema_migrations (
- version character varying NOT NULL
+ version character varying(255) NOT NULL
);
@@ -775,8 +845,8 @@ CREATE TABLE public.screenshots (
commit_id integer NOT NULL,
created_at timestamp without time zone,
updated_at timestamp without time zone,
- image_file_name character varying,
- image_content_type character varying,
+ image_file_name character varying(255),
+ image_content_type character varying(255),
image_file_size integer,
image_updated_at timestamp without time zone,
id integer NOT NULL
@@ -1007,20 +1077,22 @@ CREATE TABLE public.users (
role character varying(50) DEFAULT NULL::character varying,
created_at timestamp without time zone,
updated_at timestamp without time zone,
- confirmation_token character varying,
- first_name character varying NOT NULL,
- last_name character varying NOT NULL,
- encrypted_password character varying NOT NULL,
+ confirmation_token character varying(255),
+ first_name character varying(255) NOT NULL,
+ last_name character varying(255) NOT NULL,
+ encrypted_password character varying(255) NOT NULL,
remember_created_at timestamp without time zone,
current_sign_in_at timestamp without time zone,
last_sign_in_at timestamp without time zone,
- current_sign_in_ip character varying,
- last_sign_in_ip character varying,
+ current_sign_in_ip character varying(255),
+ last_sign_in_ip character varying(255),
confirmed_at timestamp without time zone,
confirmation_sent_at timestamp without time zone,
locked_at timestamp without time zone,
reset_password_sent_at timestamp without time zone,
approved_rfc5646_locales text,
+ last_activity_at timestamp without time zone,
+ expired_at timestamp without time zone,
CONSTRAINT encrypted_password_exists CHECK ((char_length((encrypted_password)::text) > 20)),
CONSTRAINT users_email_check CHECK ((char_length((email)::text) > 0)),
CONSTRAINT users_failed_attempts_check CHECK ((failed_attempts >= 0)),
@@ -1124,6 +1196,13 @@ ALTER TABLE ONLY public.daily_metrics ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.edit_reasons ALTER COLUMN id SET DEFAULT nextval('public.edit_reasons_id_seq'::regclass);
+--
+-- Name: globalsight_api_records id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.globalsight_api_records ALTER COLUMN id SET DEFAULT nextval('public.globalsight_api_records_id_seq'::regclass);
+
+
--
-- Name: groups id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -1173,6 +1252,13 @@ ALTER TABLE ONLY public.projects ALTER COLUMN id SET DEFAULT nextval('public.pro
ALTER TABLE ONLY public.reasons ALTER COLUMN id SET DEFAULT nextval('public.reasons_id_seq'::regclass);
+--
+-- Name: reports id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.reports ALTER COLUMN id SET DEFAULT nextval('public.reports_id_seq'::regclass);
+
+
--
-- Name: screenshots id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -1287,11 +1373,11 @@ ALTER TABLE ONLY public.comments
--
--- Name: commits commits_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+-- Name: commits commits_new_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.commits
- ADD CONSTRAINT commits_pkey PRIMARY KEY (id);
+ ADD CONSTRAINT commits_new_pkey PRIMARY KEY (id);
--
@@ -1310,6 +1396,14 @@ ALTER TABLE ONLY public.edit_reasons
ADD CONSTRAINT edit_reasons_pkey PRIMARY KEY (id);
+--
+-- Name: globalsight_api_records globalsight_api_records_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.globalsight_api_records
+ ADD CONSTRAINT globalsight_api_records_pkey PRIMARY KEY (id);
+
+
--
-- Name: groups groups_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1366,6 +1460,14 @@ ALTER TABLE ONLY public.reasons
ADD CONSTRAINT reasons_pkey PRIMARY KEY (id);
+--
+-- Name: reports reports_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.reports
+ ADD CONSTRAINT reports_pkey PRIMARY KEY (id);
+
+
--
-- Name: screenshots screenshots_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1437,10 +1539,10 @@ CREATE INDEX comments_user ON public.comments USING btree (user_id);
--
--- Name: commits_date; Type: INDEX; Schema: public; Owner: -
+-- Name: commits_date_new; Type: INDEX; Schema: public; Owner: -
--
-CREATE INDEX commits_date ON public.commits USING btree (project_id, committed_at);
+CREATE INDEX commits_date_new ON public.commits USING btree (project_id, committed_at);
--
@@ -1458,10 +1560,10 @@ CREATE INDEX commits_priority ON public.commits USING btree (priority, due_date)
--
--- Name: commits_ready_date; Type: INDEX; Schema: public; Owner: -
+-- Name: commits_ready_date_new; Type: INDEX; Schema: public; Owner: -
--
-CREATE INDEX commits_ready_date ON public.commits USING btree (project_id, ready, committed_at);
+CREATE INDEX commits_ready_date_new ON public.commits USING btree (project_id, ready, committed_at);
--
@@ -1667,6 +1769,13 @@ CREATE UNIQUE INDEX index_locale_associations_on_source_and_target_rfc5646_local
CREATE UNIQUE INDEX index_projects_on_api_token ON public.projects USING btree (api_token);
+--
+-- Name: index_reports_on_date; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_reports_on_date ON public.reports USING btree (date);
+
+
--
-- Name: index_sections_on_article_id; Type: INDEX; Schema: public; Owner: -
--
@@ -1730,6 +1839,20 @@ CREATE INDEX index_translations_on_rfc5646_locale ON public.translations USING b
CREATE UNIQUE INDEX index_users_on_confirmation_token ON public.users USING btree (confirmation_token);
+--
+-- Name: index_users_on_expired_at; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_users_on_expired_at ON public.users USING btree (expired_at);
+
+
+--
+-- Name: index_users_on_last_activity_at; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_users_on_last_activity_at ON public.users USING btree (last_activity_at);
+
+
--
-- Name: issues_translation; Type: INDEX; Schema: public; Owner: -
--
@@ -1923,6 +2046,14 @@ ALTER TABLE ONLY public.commits_keys
ADD CONSTRAINT commits_keys_commit_id_fkey FOREIGN KEY (commit_id) REFERENCES public.commits(id) ON DELETE CASCADE;
+--
+-- Name: commits_keys commits_keys_commit_id_fkey1; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.commits_keys
+ ADD CONSTRAINT commits_keys_commit_id_fkey1 FOREIGN KEY (commit_id) REFERENCES public.commits(id) ON DELETE CASCADE;
+
+
--
-- Name: commits_keys commits_keys_key_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1932,11 +2063,11 @@ ALTER TABLE ONLY public.commits_keys
--
--- Name: commits commits_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+-- Name: commits commits_new_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.commits
- ADD CONSTRAINT commits_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ ADD CONSTRAINT commits_new_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
--
@@ -2127,7 +2258,7 @@ ALTER TABLE ONLY public.translations
-- PostgreSQL database dump complete
--
-SET search_path TO "$user", public;
+SET search_path TO "$user",public;
INSERT INTO schema_migrations (version) VALUES ('20130605211557');
@@ -2161,6 +2292,8 @@ INSERT INTO schema_migrations (version) VALUES ('20130821011614');
INSERT INTO schema_migrations (version) VALUES ('20131008220117');
+INSERT INTO schema_migrations (version) VALUES ('20131031034100');
+
INSERT INTO schema_migrations (version) VALUES ('20131111213136');
INSERT INTO schema_migrations (version) VALUES ('20131116042827');
@@ -2301,6 +2434,8 @@ INSERT INTO schema_migrations (version) VALUES ('20160404235737');
INSERT INTO schema_migrations (version) VALUES ('20160516051607');
+INSERT INTO schema_migrations (version) VALUES ('20170126001545');
+
INSERT INTO schema_migrations (version) VALUES ('20170508202319');
INSERT INTO schema_migrations (version) VALUES ('20171024225818');
@@ -2339,3 +2474,7 @@ INSERT INTO schema_migrations (version) VALUES ('20181028020548');
INSERT INTO schema_migrations (version) VALUES ('20181029221959');
+INSERT INTO schema_migrations (version) VALUES ('20190517232627');
+
+INSERT INTO schema_migrations (version) VALUES ('20190620214800');
+
diff --git a/doc/DEVELOPER_GUIDE.md b/doc/DEVELOPER_GUIDE.md
index 39786747..57a328ce 100644
--- a/doc/DEVELOPER_GUIDE.md
+++ b/doc/DEVELOPER_GUIDE.md
@@ -140,6 +140,9 @@ use one or both of them.
### Downloading manifest files
+If you're using the Shuttle Gem, it adds the `rake shuttle:complete[git_sha]`
+and `rake shuttle:preview[git_sha]` commands.
+
For localization libraries that store their translated strings in one file (this
is most libraries, including Rails i18n, Ember.js, and others), you can download
a manifest file. This file will contain solely translated strings for one or
@@ -171,7 +174,6 @@ The value of `:format` will depend on the localization library you are using:
| Library | `:format` | Notes |
|:------------------------------------|:-------------|:--------------------------------------------------------------------------------------------------------|
-| Android | `android` | Will be a gzipped tarball that can be extracted into your project root. |
| Rails i18n (YAML) | `yaml` | All locales will be merged into one file unless the `locale` query parameter is specified. |
| Ember.js | `js` | All locales will be merged into one file unless the `locale` query parameter is specified. |
| Ember.js (dependency-injected) | `jsm` | Similar to Ember.js, but places translations under a `module.exports` object. Locale must be specified. |
@@ -193,6 +195,9 @@ A normal response status is 200 OK. If the commit is not fully localized and
`:format` is not recognized, the response status will be 406 Not Acceptable. If
`locale` is required and not provided, the response status is 400 Bad Request.
+For Android developers, follow the **Downloading localized files**
+section and copy the localized `res` folders into your repo's `res` folder.
+
### Downloading localized files
For localization libraries that expect translated strings to be reintegrated
diff --git a/doc/README_FOR_SQUARES.md b/doc/README_FOR_SQUARES.md
new file mode 100644
index 00000000..1bffcf7a
--- /dev/null
+++ b/doc/README_FOR_SQUARES.md
@@ -0,0 +1,54 @@
+# Workflow for Square Engineers contributing to Shuttle
+
+Shuttle's codebase is opensouced on Github but also present on Bitbucket.
+Nearly all contributions to Shuttle should be made against the Github
+repository. There is a small amount of Square specific code that only exists in
+the Bitbucket repository. The list of Square specific code includes:
+
+* Sentry bug reporting
+* Square specific deploy configuration
+* Square specific Redis and ElasticSearch IPs
+* Square SSL certificates in Docker
+* Square gem mirror
+* Set `no_proxy` to avoid proxying corp requests
+* Kochiku CI script
+
+### Example steps to setup Shuttle for development with Github and Bitbucket as remotes
+
+Setup Repository:
+
+ mkdir ~/Development/shuttle
+ cd ~/Development/shuttle
+
+ git init
+ git remote add bitbucket ssh://git@git.corp.squareup.com/intl/kochiku.git
+ git remote add github git@github.com:square/shuttle.git
+ git fetch bitbucket
+ git fetch github
+
+ git checkout -b github-master github/master
+ git checkout -b bitbucket-master bitbucket/master
+
+Create and push a new Github branch:
+
+ git checkout github-master
+ git pull
+ git checkout -b new-github-branch-name
+ # ... make changes and commit them
+ git push -u github HEAD
+
+Create and push a new Bitbucket branch:
+
+ git checkout bitbucket-master
+ git pull
+ git checkout -b new-bitbucket-branch-name
+ # ... make changes and commit them
+ git push -u bitbucket HEAD
+
+Merge changes on Github into Bitbucket:
+
+ git checkout bitbucket-master
+ git pull
+ git fetch github
+ git merge --log --no-ff github/master
+ git push bitbucket master
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
index 3d400738..c41e6b25 100644
--- a/docker-compose.test.yml
+++ b/docker-compose.test.yml
@@ -12,7 +12,11 @@ services:
image: redis
elasticsearch:
- image: quay.io/trackmaven/elasticsearch:1.7
+ image: docker.elastic.co/elasticsearch/elasticsearch:6.5.1
+ ports:
+ - 9200:9200
+ environment:
+ - xpack.security.enabled=false
tests:
build:
@@ -21,18 +25,14 @@ services:
- BUNDLE_GEMS__CONTRIBSYS__COM
links: &links
- postgresql
- - postgresql:postgresql.shuttle.local
- redis
- - redis:redis.shuttle.local
- elasticsearch
- - elasticsearch:elasticsearch.shuttle.local
environment: &environment
- RAILS_ENV=test
- RACK_ENV=test
- - SHUTTLE_DB_HOST=postgresql.shuttle.local
- - SHUTTLE_REDIS_HOST=redis.shuttle.local
- - SHUTTLE_ES_URL=elasticsearch.shuttle.local:9200
- - SHUTTLE_MAIL_HOST=mail.shuttle.local
+ - SHUTTLE_DB_HOST=postgresql
+ - SHUTTLE_REDIS_HOST=redis
+ - SHUTTLE_ES_URL=elasticsearch:9200
# worker:
# build: .
diff --git a/docker-compose.yml b/docker-compose.yml
index c945de6f..af828c02 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,19 +4,25 @@ services:
postgresql:
image: postgres:9.4
volumes:
- - postgres_data:/var/lib/postgresql/data
+ - ./../pg_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=shuttle
- POSTGRES_PASSWORD=
- POSTGRES_DB=shuttle_development
+ ports:
+ - '5432:5432'
redis:
image: redis
elasticsearch:
- image: quay.io/trackmaven/elasticsearch:1.7
+ image: docker.elastic.co/elasticsearch/elasticsearch:6.5.1
volumes:
- es_data:/usr/share/elasticsearch/data
+ ports:
+ - '9200:9200'
+ environment:
+ - xpack.security.enabled=false
web:
build: .
@@ -24,11 +30,8 @@ services:
ports:
- '3000:3000'
links: &links
- - postgresql
- postgresql:postgresql.shuttle.local
- - redis
- redis:redis.shuttle.local
- - elasticsearch
- elasticsearch:elasticsearch.shuttle.local
- mailcatcher:mail.shuttle.local
environment: &environment
@@ -38,6 +41,8 @@ services:
- SHUTTLE_REDIS_HOST=redis.shuttle.local
- 'SHUTTLE_ES_URL=elasticsearch.shuttle.local:9200'
- SHUTTLE_MAIL_HOST=mail.shuttle.local
+ volumes:
+ - .:/app
worker:
build: .
diff --git a/kochiku.yml b/kochiku.yml
new file mode 100644
index 00000000..7757f86d
--- /dev/null
+++ b/kochiku.yml
@@ -0,0 +1,8 @@
+language: ruby
+ruby:
+ - ruby-2.4.6
+test_command: 'script/ci'
+targets:
+- type: specs
+ glob: spec/**/*_spec.rb
+ workers: 10
diff --git a/lib/assets/javascripts/fencer.js.coffee b/lib/assets/javascripts/fencer.js.coffee
index 90eb7546..46e9574e 100644
--- a/lib/assets/javascripts/fencer.js.coffee
+++ b/lib/assets/javascripts/fencer.js.coffee
@@ -34,7 +34,7 @@ class root.Fencer
when 'MessageFormat'
fence_index = fence.match(/^\{(\d+)[,}]/)[1]
new RegExp("\\{#{fence_index}(,[^}]+)?\\}")
- when 'Erb', 'Html', 'Strftime'
+ when 'Erb', 'Html', 'Strftime', 'IntlMessageFormat'
# these are all optional fences
null
else
diff --git a/lib/compiler.rb b/lib/compiler.rb
index 1703fb35..73f005d9 100644
--- a/lib/compiler.rb
+++ b/lib/compiler.rb
@@ -60,15 +60,16 @@ def manifest(format, options={})
io = StringIO.new
exporter = exporter.new(@commit)
+ exporter.override_encoding = options[:encoding]
exporter.export io, *locales_for_export(*locales, options[:partial])
io.rewind
filename = locales.size == 1 ? locales.first.rfc5646 : 'manifest'
return File.new(
io,
- exporter.class.character_encoding,
+ exporter.active_encoding,
"#{filename}.#{exporter.class.file_extension}",
- exporter.class.mime_type
+ exporter.mime_type
)
end
diff --git a/lib/exporter/base.rb b/lib/exporter/base.rb
index 08cd9ca8..aa41ae63 100644
--- a/lib/exporter/base.rb
+++ b/lib/exporter/base.rb
@@ -30,6 +30,8 @@ module Exporter
class Base
extend AbstractClass
+ attr_accessor :override_encoding
+
# Prepares an exporter for use with a Commit.
#
# @param [Commit] commit A Commit whose Translations will be exported.
@@ -75,9 +77,9 @@ def self.request_mime
# @return [String] The MIME type of this exporter's output format.
- def self.mime_type
- mime = request_mime.to_s
- mime << "; charset=#{character_encoding.downcase}" unless character_encoding == 'UTF-8'
+ def mime_type
+ mime = self.class.request_mime.to_s
+ mime += "; charset=#{active_encoding.downcase}" unless active_encoding == 'UTF-8'
mime
end
@@ -89,12 +91,20 @@ def self.human_name() I18n.t "exporter.#{ident}.name" end
def self.ident() to_s.demodulize.underscore end
# @return [String] The character encoding the output is in.
- def self.character_encoding() 'UTF-8' end
+ def self.default_encoding() 'UTF-8' end
# @return [true, false] Whether this exporter is capable of exporting
# multiple locales in a single file (default true).
def self.multilingual?() true end
+ def active_encoding
+ if override_encoding&.upcase == 'UTF-8'
+ override_encoding
+ else
+ self.class.default_encoding
+ end
+ end
+
# Locates an exporter subclass from its unique identifier.
#
# @param [String] ident An identifier.
@@ -215,6 +225,7 @@ def translation_hash(locale, deduplicate=[])
# or an array index (previous if did run)
this_object[last] = translation.copy
rescue => err
+ Raven.capture_exception err, extra: { translation_id: translation.id }
raise if Rails.env.test?
end
end
diff --git a/lib/exporter/ios.rb b/lib/exporter/ios.rb
index 04787c7c..43f662cc 100644
--- a/lib/exporter/ios.rb
+++ b/lib/exporter/ios.rb
@@ -23,8 +23,11 @@ module Exporter
class Ios < Base
include Multifile
+ def self.default_encoding() 'UTF-16LE' end
+
def export_files(receiver, *locales)
exporter = Exporter::Strings.new(@commit)
+ exporter.override_encoding = override_encoding
locales.each do |locale|
Translation.in_commit(@commit).includes(:key).
@@ -39,9 +42,11 @@ def export_files(receiver, *locales)
next unless source.end_with?('.strings')
stream = StringIO.new
- # write the BOM
- stream.putc 0xFF
- stream.putc 0xFE
+ if active_encoding == 'UTF-16LE'
+ # write the BOM
+ stream.putc 0xFF
+ stream.putc 0xFE
+ end
translations.sort_by { |t| t.key.key }.each do |translation|
exporter.export_translation stream, translation
end
diff --git a/lib/exporter/strings.rb b/lib/exporter/strings.rb
index 541d4241..16d29f3f 100644
--- a/lib/exporter/strings.rb
+++ b/lib/exporter/strings.rb
@@ -36,9 +36,11 @@ def export(io, *locales)
raise NoLocaleProvidedError, ".strings files can only be for a single locale" unless locales.size == 1
locale = locales.first
- # write the BOM
- io.putc 0xFF
- io.putc 0xFE
+ if active_encoding == 'UTF-16LE'
+ # write the BOM
+ io.putc 0xFF
+ io.putc 0xFE
+ end
translations = Translation.in_commit(@commit).where(rfc5646_locale: locale.rfc5646).
sort_by { |t| t.key.key }
@@ -56,11 +58,15 @@ def export_translation(io, translation)
part << %("#{escape translation.key.original_key}" = "#{escape translation.copy}";\n)
part << "\n"
- io.write part.encode('UTF-16LE').force_encoding('BINARY')
+ if active_encoding == 'UTF-16LE'
+ io.write part.encode(active_encoding).force_encoding('BINARY')
+ else
+ io.write part.encode(active_encoding)
+ end
end
def self.file_extension() 'strings' end
- def self.character_encoding() 'UTF-16LE' end
+ def self.default_encoding() 'UTF-16LE' end
def self.request_format() :strings end
def self.multilingual?() false end
diff --git a/lib/fencer/intl_message_format.rb b/lib/fencer/intl_message_format.rb
index bd1bfb00..4c27f0f6 100644
--- a/lib/fencer/intl_message_format.rb
+++ b/lib/fencer/intl_message_format.rb
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+require 'message_format'
+
module Fencer
# Fences out Message Format tags using the intl-messageformat syntax.
@@ -21,52 +23,66 @@ module Fencer
module IntlMessageFormat
extend self
- UNESCAPED_LEFT_BRACE = /(^.?|[^\\][^\\]){/
- UNESCAPED_RIGHT_BRACE = /(^.?|[^\\][^\\])}/
+ ESCAPED_BRACE = /\\([{}])/
def fence(string)
- scanner = UnicodeScanner.new(string)
-
- tokens = Hash.new { |hsh, k| hsh[k] = [] }
- until scanner.eos?
- match = scanner.scan_until(UNESCAPED_LEFT_BRACE)
- break unless match
-
- start = scanner.pos - 1 # less the brace
- token = scanner.scan_until(UNESCAPED_RIGHT_BRACE)
- next unless token
-
- stop = scanner.pos - 1 # ranges are inclusive
- tokens['{' + token] << (start..stop)
+ begin
+ tokenize(::MessageFormat::Parser.parse(sanitize(string)))
+ rescue
+ {}
end
-
- return tokens
end
- # Scan string to ensure that left braces are paired with right braces
- # and that left braces are closed before encountering another left brace.
def valid?(string)
- scanner = UnicodeScanner.new(string)
-
- first_left_brace_match = scanner.scan_until(UNESCAPED_LEFT_BRACE)
- return true unless first_left_brace_match
+ begin
+ ::MessageFormat::Parser.parse(sanitize(string))
+ true
+ rescue
+ false
+ end
+ end
- until scanner.eos?
- # Make sure there's a right brace to match with the left one.
- right_brace_match = scanner.scan_until(UNESCAPED_RIGHT_BRACE)
- return false unless right_brace_match
+ private
- right_brace_index = scanner.pos
- scanner.unscan # Reset to last time we saw a left brace.
+ def tokenize(segments, prefix: nil)
+ # takes in the parsed structure from message_format and tokenize recursively all parameters in the string
+ # {name} => {":name" => [-1..0]}
+ # {num, plural , =0 {nothing!} one {one!}} => "{":number|plural|=0" => [-1..0], ":number|plural|one" => [-1..0]}"
+ tokens = {}
- # If there are no more left braces, we're good.
- left_brace_match = scanner.scan_until(UNESCAPED_LEFT_BRACE)
- return true unless left_brace_match
+ segments.each do |segment|
+ next unless segment.instance_of?(Array)
+ if segment.last.instance_of?(Hash)
+ # select, plural, selectordinal
+ *data, format = segment
+ format.each do |k, v|
+ key = generate_key(prefix, [*data, k])
- # Make sure the next right brace happens before the next left brace.
- left_brace_index = scanner.pos
- return false if left_brace_index < right_brace_index
+ if v.length == 1 && v[0].instance_of?(String)
+ # option is plain string
+ tokens[key] = (tokens[key] || []).push(-1..0)
+ else
+ tokens.merge!(tokenize(v, prefix: key))
+ end
+ end
+ else
+ # simple argument, number, date, time type
+ key = generate_key(prefix, segment)
+ tokens[key] = (tokens[key] || []).push(-1..0)
+ end
end
+
+ tokens
+ end
+
+ def generate_key(prefix, args)
+ "#{prefix}:#{args.join('|')}"
+ end
+
+ def sanitize(string)
+ # intl-message-format uses blackslash for escaping which is not consitent with ICU standard
+ # replace blackslash with single quote so that message_format lib can parse it properly
+ string.gsub(ESCAPED_BRACE, '\'\1\'')
end
end
end
diff --git a/lib/importer/android.rb b/lib/importer/android.rb
index 954745e0..c4d4ae4a 100644
--- a/lib/importer/android.rb
+++ b/lib/importer/android.rb
@@ -53,8 +53,9 @@ def import_strings
end
context = find_comment(tag).try!(:content)
+ content = has_cdata?(tag) ? tag.children[0].to_s : tag.content
add_string "#{file.path}:#{tag['name']}",
- unescape(strip(tag.content)),
+ unescape(strip(content)),
context: clean_comment(context),
original_key: tag['name']
end
@@ -143,6 +144,14 @@ def unescape(string)
return result
end
+ def has_cdata?(tag)
+ tag.children.each do |child|
+ if child.to_s.include?('CDATA')
+ return true
+ end
+ end
+ end
+
def find_comment(tag)
tag = tag.previous
tag = tag.previous while tag.try!(:text?)
diff --git a/lib/importer/strings.rb b/lib/importer/strings.rb
index f9f206a8..614d748d 100644
--- a/lib/importer/strings.rb
+++ b/lib/importer/strings.rb
@@ -68,6 +68,8 @@ def unescape(str)
result << "\r"
elsif scanner.scan(/t/)
result << "\t"
+ elsif scanner.scan(/\n/)
+ # join lines
elsif (match = scanner.scan(/[0-9a-f]{4}/))
result << Integer("0x#{match}").chr('utf-16')
elsif (match = scanner.scan(/[0-7]{3}/))
diff --git a/lib/paginatable_objects.rb b/lib/paginatable_objects.rb
index 9596eb8a..18accfe5 100644
--- a/lib/paginatable_objects.rb
+++ b/lib/paginatable_objects.rb
@@ -17,11 +17,9 @@ class PaginatableObjects
attr_reader :objects, :total_count, :current_page, :limit_value
delegate :map, :each, :first, :length, :size, :sort_by, to: :objects
- def initialize(objects, objects_in_es, current_page, limit_value, sort = true)
- @objects = sort ?
- SortingHelper.order_by_elasticsearch_result_order(objects, objects_in_es)
- : objects
- @total_count = objects_in_es.total
+ def initialize(objects, current_page, limit_value)
+ @objects = objects.objects
+ @total_count = objects.total
@current_page = current_page
@limit_value = limit_value
end
diff --git a/lib/sorting_helper.rb b/lib/sorting_helper.rb
deleted file mode 100644
index bcca8b61..00000000
--- a/lib/sorting_helper.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright 2016 Square Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-module SortingHelper
- def self.order_by_elasticsearch_result_order(objects, objects_in_es)
- ordered_ids = objects_in_es.map(&:id).map(&:to_i)
- hash = objects.index_by(&:id)
- ordered_objects = []
- ordered_ids.each { |id| ordered_objects << hash[id] if hash[id] }
- ordered_objects
- end
-end
diff --git a/lib/stash_webhook_helper.rb b/lib/stash_webhook_helper.rb
index aa00c674..2323b6d3 100644
--- a/lib/stash_webhook_helper.rb
+++ b/lib/stash_webhook_helper.rb
@@ -23,15 +23,31 @@ def ping(commit, opts = {})
num_pings = opts[:num_times] || DEFAULT_NUM_TIMES
if ping_stash_webhook?(commit)
- # Pretty awful but there's no way we can verify that Stash decided to ignore us
- # Other projects do it this way as well
num_pings.times do
- HTTParty.post(webhook_url(commit), {timeout: 5,
- body: webhook_post_parameters(commit, opts),
- headers: webhook_header_parameters,
- basic_auth: webhook_auth_parameters})
+ params = {
+ timeout: 5,
+ body: webhook_post_parameters(commit, opts),
+ headers: webhook_header_parameters,
+ basic_auth: webhook_auth_parameters
+ }
+ response = HTTParty.post(webhook_url(commit), params)
+
+ # fails with retry on failure.
+ unless response.code.between? 200, 299
+ message = "[StashWebhookHelper] Failed to ping stash for commit #{commit.id}, revision: #{commit.revision}, code: #{response.code}"
+ details = "#{current_commit_state(commit)} - #{response.inspect}"
+ Rails.logger.warn("#{message} - #{details}")
+ raise message
+ end
+
Kernel.sleep(DEFAULT_WAIT_TIME)
end
+
+ # record metric only when the commit is ready
+ if commit.ready and commit.approved_at
+ ping_stash_time = Time.current - commit.approved_at
+ CustomMetricHelper.record_project_ping_stash_time(commit.project.slug, ping_stash_time)
+ end
end
end
diff --git a/lib/tasks/elasticsearch.rake b/lib/tasks/elasticsearch.rake
deleted file mode 100644
index 001eeaea..00000000
--- a/lib/tasks/elasticsearch.rake
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2016 Square Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# A collection of Rake tasks to facilitate importing data from your models into Elasticsearch.
-#
-# To import all elasticsearch indexed models, run:
-#
-# $ bundle exec rake RAILS_ENV=environment elasticsearch:import:model FORCE=y
-#
-# To import the records from your `Article` model, run:
-#
-# $ bundle exec rake RAILS_ENV=environment elasticsearch:import:model CLASS='MyModel'
-#
-# Run this command to display usage instructions:
-#
-# $ bundle exec rake -D elasticsearch
-#
-
-require 'elasticsearch/rails/tasks/import'
-
-Rake::Task['elasticsearch:import:all'].clear
-namespace :elasticsearch do
- namespace :import do
- task :all do
- dir = ENV['DIR'].presence || Rails.root.join('app', 'models')
- puts "[IMPORT] Loading models from: #{dir}"
- Pathname.glob(dir.join('**', '*.rb')).each do |path|
- next if path.each_filename.include?('concerns')
- next if path.each_filename.include?('observers')
- require path.relative_path_from(dir)
- end
-
- ActiveRecord::Base.subclasses.each do |klass|
- next unless klass.respond_to?(:__elasticsearch__)
- puts "[IMPORT] Processing model: #{klass}..."
-
- ENV['CLASS'] = klass.to_s
- Rake::Task['elasticsearch:import:model'].invoke
- Rake::Task['elasticsearch:import:model'].reenable
- end
- end
- end
-end
diff --git a/lib/tasks/maintenance/cleanup_commits.rake b/lib/tasks/maintenance/cleanup_commits.rake
index 1fa6c81f..87b3c065 100644
--- a/lib/tasks/maintenance/cleanup_commits.rake
+++ b/lib/tasks/maintenance/cleanup_commits.rake
@@ -20,13 +20,13 @@ namespace :maintenance do
desc "Cleans up commits that have been deleted from the repository"
task reap_deleted_commits: :environment do
- Commit.includes(:project).find_each do |c|
- begin
- c.commit!
- rescue Git::CommitNotFoundError
- c.destroy
- rescue Elasticsearch::Transport::Transport::Errors::NotFound
- next
+ Chewy.strategy(:atomic) do
+ Commit.includes(:project).find_each do |c|
+ begin
+ c.commit!
+ rescue Git::CommitNotFoundError
+ c.destroy
+ end
end
end
end
diff --git a/lib/tasks/reports.rake b/lib/tasks/reports.rake
new file mode 100644
index 00000000..16844e00
--- /dev/null
+++ b/lib/tasks/reports.rake
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+require 'csv'
+
+# This namespace is used to generate reports for incoming, pending, and
+# completed jobs over a given time
+
+namespace :reports do
+ desc 'Get reports for projects in Shuttle'
+ namespace :generate do
+ task incoming: :environment do
+ system('clear') || system('cls')
+ puts "[reports:generate:incoming] Creating a new report for #{Date.today}"
+ generate_incoming_report
+ end
+
+ task pending: :environment do
+ system('clear') || system('cls')
+ puts "[reports:generate:pending] Creating a new report for #{Date.today}"
+ generate_pending_report
+ end
+
+ task completed: :environment do
+ system('clear') || system('cls')
+ puts "[reports:generate:completed] Creating a new report for #{Date.today}"
+ generate_completed_report
+ end
+ end
+
+ def generate_incoming_report
+ filename = "incoming-#{get_display_date}.csv"
+ date_start = Date.yesterday.beginning_of_day
+ date_range = date_start...Date.yesterday.end_of_day
+
+ commits_loaded = get_commits(date_range)
+ articles_loaded = get_articles(date_range)
+ untranslated_commit_keys = get_untranslated_keys(commits_loaded)
+ untranslated_article_keys = get_untranslated_keys(articles_loaded)
+
+ puts "Found #{untranslated_commit_keys.count} commits and #{untranslated_article_keys.count} articles from #{date_start} to #{Date.today}"
+
+ combined_jobs = commits_loaded.concat(articles_loaded)
+ jobs = create_hash_for_export(combined_jobs)
+ save_incoming_translations jobs
+ end
+
+ def generate_pending_report
+ filename = "pending-#{get_display_date}.csv"
+ yesterday = Date.yesterday
+ date_range = yesterday...Date.today
+
+ commits_loaded = get_commits(date_range)
+ articles_loaded = get_articles(date_range)
+ untranslated_commit_keys = get_untranslated_keys(commits_loaded)
+ untranslated_article_keys = get_untranslated_keys(articles_loaded)
+
+ puts "Found #{untranslated_commit_keys.count} commits and #{untranslated_article_keys.count} articles from #{yesterday} to #{Date.today}"
+ combined_jobs = commits_loaded.concat(articles_loaded)
+ jobs = create_hash_for_export(combined_jobs)
+ save_pending_translations jobs
+ end
+
+ def generate_completed_report
+ filename = "completed-#{get_display_date}.csv"
+ yesterday = Date.yesterday
+ date_range = yesterday...Date.today
+
+ completed_translations = get_completed_translations(date_range)
+ parsed_translations = get_report_from completed_translations
+ save_completed_translations parsed_translations
+ end
+
+ private
+
+ def get_commits(range)
+ Commit.where(loaded_at: range)
+ end
+
+ def get_articles(range)
+ Article.where(created_at: range)
+ end
+
+ def get_untranslated_keys(list_of_jobs)
+ list_of_untranslated_keys = []
+ list_of_jobs.where(ready: false).each do |job|
+ list_of_untranslated_keys << job.keys.where(ready: false)
+ end
+ end
+
+ def get_completed_translations(range)
+ commits = Commit.where(approved_at: range)
+ articles = Article.where(last_completed_at: range)
+ jobs = commits.concat(articles)
+ completed_translations = []
+ puts "jobs in tranlsated: #{jobs.count}"
+ jobs.each do |job|
+ translations = job.translations.where updated_at: range
+ completed_translations << translations.flatten
+ end
+ completed_translations.flatten
+ end
+
+ def get_report_from(translations)
+ export_hash = {}
+ translations.each do |translation|
+ project_name = translation.key.project.name
+ locale = translation.rfc5646_locale
+ identifier = :"#{project_name}_#{locale}"
+ unless export_hash[identifier]
+ export_hash[identifier] = {
+ project: project_name,
+ locale: locale,
+ strings: 0,
+ words: 0
+ }
+ end
+ export_hash[identifier][:strings] = export_hash[identifier][:strings] + 1
+ export_hash[identifier][:words] = export_hash[identifier][:words] + translation.words_count
+ end
+ export_hash
+ end
+
+ def create_hash_for_export(list_of_jobs)
+ jobs = {}
+ list_of_jobs.each do |job|
+ job.keys.where(ready: false).each do |key|
+ project = Project.find(key.project_id)
+ project_name = project.name
+ job.targeted_rfc5646_locales.each do |locale, _exists|
+ identifier = :"#{project_name}_#{locale}"
+ unless jobs[identifier]
+ jobs[identifier] = {
+ locale: locale,
+ project: project,
+ strings: 0,
+ words: 0
+ }
+ end
+ jobs[identifier][:strings] = jobs[identifier][:strings] + 1
+ key.translations.where(approved: [false, nil]).each do |translation|
+ jobs[identifier][:words] = jobs[identifier][:words] + translation.words_count
+ end
+ end
+ end
+ end
+ jobs
+ end
+
+ def save_completed_translations(translations)
+ translations.each do |_identifier, stats|
+ r = Report.new
+ r.project = stats[:project]
+ r.locale = stats[:locale]
+ r.strings = stats[:strings]
+ r.words = stats[:words]
+ r.date = Date.today
+ r.report_type = :completed
+ r.save
+ end
+ end
+
+ def save_incoming_translations(translations)
+ translations.each do |_identifier, stats|
+ r = Report.new
+ r.project = stats[:project].name
+ r.locale = stats[:locale]
+ r.strings = stats[:strings]
+ r.words = stats[:words]
+ r.date = Date.today
+ r.report_type = :incoming
+ r.save
+ end
+ end
+
+ def save_pending_translations(translations)
+ translations.each do |_identifier, stats|
+ r = Report.new
+ r.project = stats[:project].name
+ r.locale = stats[:locale]
+ r.strings = stats[:strings]
+ r.words = stats[:words]
+ r.date = Date.today
+ r.report_type = :pending
+ r.save
+ end
+ end
+
+ def get_display_date
+ Date.today.strftime("%Y-%m-%d")
+ end
+end
diff --git a/lib/translation_diff.rb b/lib/translation_diff.rb
index d93eb93c..78a27bff 100644
--- a/lib/translation_diff.rb
+++ b/lib/translation_diff.rb
@@ -54,6 +54,14 @@ def chunks
# @return [Array] The two strings being compared, formatted to
# highlight the differences.
def diff(joiner="...")
+ if Shuttle::Configuration.features[:skip_levenshtein_distance_diff]
+ # see https://jira.sqcorp.co/browse/SHUTTLE-1138
+ return [
+ (@str1 && @str1.length > 100) ? (@str1[0...100] + '...') : @str1,
+ (@str2 && @str2.length > 100) ? (@str2[0...100] + '...') : @str2,
+ ]
+ end
+
# Base cases
return [@str1, @str2] unless @str1.present? || @str2.present?
return @diff if @diff
@@ -83,6 +91,11 @@ def diff(joiner="...")
# visually align words that are matched according to minimal Levenshtein
# distance
def aligned
+ if Shuttle::Configuration.features[:skip_levenshtein_distance_diff]
+ # see https://jira.sqcorp.co/browse/SHUTTLE-1138
+ return [@str1, @str2]
+ end
+
@aligned ||= [chunks.map { |m| m.one }.join(" "), chunks.map { |m| m.two }.join(" ")]
end
diff --git a/config/initializers/elasticsearch_rails.rb b/lib/translation_validator/base.rb
similarity index 65%
rename from config/initializers/elasticsearch_rails.rb
rename to lib/translation_validator/base.rb
index c22a0ddc..b54e4a14 100644
--- a/config/initializers/elasticsearch_rails.rb
+++ b/lib/translation_validator/base.rb
@@ -1,4 +1,4 @@
-# Copyright 2016 Square Inc.
+# Copyright 2019 Square Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,4 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-Elasticsearch::Model.client = Elasticsearch::Client.new host: Shuttle::Configuration.elasticsearch.url
\ No newline at end of file
+require 'abstract_class'
+
+# Container module for {Validator::Base} and its subclasses.
+
+module TranslationValidator
+ class Base
+ extended AbstractClass
+
+ def initialize(job)
+ @job = job
+ end
+
+ def run
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/lib/translation_validator/source_fencer_validator.rb b/lib/translation_validator/source_fencer_validator.rb
new file mode 100644
index 00000000..600398b0
--- /dev/null
+++ b/lib/translation_validator/source_fencer_validator.rb
@@ -0,0 +1,58 @@
+# Copyright 2019 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module TranslationValidator
+
+ # Checks if the source strings violates their own fencers.
+ class SourceFencerValidator < Base
+ IGNORE_FENCERS = %w(Html)
+
+ # implementations for Validator::Base
+ def run
+ Rails.logger.info("#{SourceFencerValidator} - starting source string validation: #{@job.project.job_type} with id #{@job.id}")
+
+ suspicious_keys_errors = find_suspicious_keys_errors(find_pending_keys)
+ Rails.logger.info("#{SourceFencerValidator} - found #{suspicious_keys_errors.count} suspicious source strings")
+ return if suspicious_keys_errors.blank?
+
+ FencerValidationMailer.suspicious_source_found(@job, suspicious_keys_errors).deliver_now
+ Rails.logger.info("#{SourceFencerValidator} - email sent for these suspicious source strings")
+ end
+
+ # find new translations, which are created after the job.
+ def find_pending_keys
+ @job.translations.includes(key: :project).where(translated: [nil, false]).where("translations.created_at >= ?", @job.created_at).map(&:key).uniq
+ end
+
+ # returns keys failed to pass their fencers
+ def find_suspicious_keys_errors(keys)
+ suspicious_keys_errors = []
+ keys.each do |key|
+ if key.fencers.present?
+ offensive_fencers = key.fencers.reject do |fencer|
+ begin
+ IGNORE_FENCERS.include?(fencer) || Fencer.const_get(fencer).valid?(key.source_copy)
+ rescue
+ false
+ end
+ end
+ if offensive_fencers.present?
+ suspicious_keys_errors << [key, "Violate fencers: #{offensive_fencers.join(', ')}"]
+ end
+ end
+ end
+ suspicious_keys_errors
+ end
+ end
+end
diff --git a/lib/translation_validator/translation_auto_migration.rb b/lib/translation_validator/translation_auto_migration.rb
new file mode 100644
index 00000000..e9528381
--- /dev/null
+++ b/lib/translation_validator/translation_auto_migration.rb
@@ -0,0 +1,89 @@
+# Copyright 2019 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module TranslationValidator
+
+ # This translation_validator walks through the pending translations and automatically migrates the translation from existing TMs.
+ # The order to match the translation:
+ # - from same project
+ # - from same job type
+ # - from other job type
+ class TranslationAutoMigration < Base
+
+ EN_BASE_LOCALES = %w(en en-US)
+ AUTO_TRANSLATION_MIGRATION_NOTE = 'AutoTM'
+
+ # implementations for Validator::Base
+ def run
+ Rails.logger.info("#{TranslationAutoMigration} - starting auto translation migration: #{@job.project.job_type} with id #{@job.id}")
+
+ pending_translations = find_pending_translations
+ Rails.logger.info("#{TranslationAutoMigration} - found #{pending_translations.count} pending translations")
+ return if pending_translations.blank?
+
+ migrated_translations = []
+ pending_translations.each do |translation|
+ approved_translations = find_approved_translations(translation)
+ matched_transaltion = find_approved_translation_from_project(translation, approved_translations) ||
+ find_approved_translation_from_job_type(translation, approved_translations) ||
+ find_approved_translation_from_other(translation, approved_translations)
+ if matched_transaltion
+ migrate_translation(translation, matched_transaltion)
+ migrated_translations << translation
+ end
+ end
+ Rails.logger.info("#{TranslationAutoMigration} - migrated #{migrated_translations.count} translations")
+ return if migrated_translations.blank?
+
+ keys = migrated_translations.map(&:key).uniq
+ keys.map(&:recalculate_ready!)
+ Rails.logger.info("#{TranslationAutoMigration} - recalculated #{keys.count} keys for translations")
+ end
+
+ def find_pending_translations
+ @job.translations.includes(key: :project).where(translated: [nil, false]).where("translations.created_at >= ?", @job.created_at)
+ end
+
+ # find existing approved translations, ordered by :updated_at descending
+ def find_approved_translations(translation)
+ base_locales = EN_BASE_LOCALES.include?(translation.source_rfc5646_locale) ? EN_BASE_LOCALES : translation.source_rfc5646_locale
+ Translation.includes(key: :project).where(source_copy: translation.source_copy, source_rfc5646_locale: base_locales, rfc5646_locale: translation.rfc5646_locale, approved: true).order(updated_at: :desc)
+ end
+
+ # find existing translation from the same project
+ def find_approved_translation_from_project(translation, approved_translations)
+ project_id = translation.key.project.id
+ approved_translations.detect { |t| t.key.project.id == project_id }
+ end
+
+ # find existing translation from same job_type
+ def find_approved_translation_from_job_type(translation, approved_translations)
+ job_type = translation.key.project.job_type
+ approved_translations.detect { |t| t.key.project.job_type == job_type }
+ end
+
+ # find existing translation from other job_types
+ def find_approved_translation_from_other(_translation, approved_translations)
+ approved_translations.first
+ end
+
+ def migrate_translation(translation, approved_translation)
+ translation.copy = approved_translation.copy
+ translation.translated = true
+ translation.notes = "#{AUTO_TRANSLATION_MIGRATION_NOTE}:#{approved_translation.id} #{translation.notes}"
+ translation.translation_date = Time.now
+ translation.save!
+ end
+ end
+end
diff --git a/script/ci b/script/ci
new file mode 100755
index 00000000..b37144c5
--- /dev/null
+++ b/script/ci
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+
+export LC_CTYPE=en_US.UTF-8
+export LANG=en_US.UTF-8
+RUN_LIST=$(echo ${RUN_LIST} | tr "," " ")
+PIDFILE="tmp/elasticsearch.pid"
+
+function install_cmake() {
+ sudo yum install -y cmake
+}
+
+function install_tidy() {
+ sudo yum install -y tidy
+}
+
+function install_libarchive() {
+ sudo yum install -y libarchive-devel
+}
+
+function install_elasticsearch() {
+ ./script/install.d/elasticsearch
+}
+
+function start_elasticsearch() {
+ ./script/elasticsearch &
+
+ curl 'localhost:9200'
+ err=$?
+ while [ $err -ne 0 ]
+ do
+ echo 'Waiting for ElasticSearch to start'
+ sleep 1
+ curl 'localhost:9200'
+ err=$?
+ done
+}
+
+function stop_elasticsearch() {
+ if [ -f $PIDFILE ]; then
+ kill `cat $PIDFILE`
+ sleep 5
+ fi
+
+ ./script/elasticsearch-cleanup
+}
+
+function install_bundler_if_needed() {
+ echo "Checking for Bundler ..."
+ gem install bundler --conservative
+}
+
+function update_gems_if_needed() {
+ set -e # exit if bundle install fails
+
+ echo "Installing gems..."
+ if [[ -n $KOCHIKU_ENV ]]; then
+ bundle_install_from_shared_cache shuttle
+ else
+ bundle check || bundle install
+ fi
+
+ set +e
+}
+
+function prepare_database() {
+ dropdb -h 127.0.0.1 shuttle_test || true
+ dropuser -h 127.0.0.1 shuttle || true
+ createuser -D -R -S -h 127.0.0.1 shuttle || true
+ createdb -h 127.0.0.1 -O shuttle shuttle_test
+# psql -h 127.0.0.1 -U shuttle -f db/structure.sql shuttle_test
+ RAILS_ENV=test bundle exec rake db:migrate
+}
+
+function run_specs() {
+ bundle exec rspec ${RUN_LIST}
+}
+
+function run_jasmine() {
+ bundle exec rake guard:jasmine -t
+}
+
+function prepare() {
+ install_cmake
+ install_tidy
+ install_elasticsearch
+ install_libarchive
+ install_bundler_if_needed &&
+ update_gems_if_needed &&
+ prepare_database
+}
+
+prepare
+start_elasticsearch
+
+set -x
+case "${TEST_RUNNER}" in
+ jasmine)
+ run_jasmine
+ ;;
+
+ specs)
+ run_specs
+ ;;
+ *)
+ echo "unknown test runner: ${TEST_RUNNER}"
+ exit 127
+ ;;
+esac
+
+exit_status=$?
+stop_elasticsearch
+exit $exit_status
diff --git a/script/elasticsearch b/script/elasticsearch
index 912ccd61..5b34bb21 100755
--- a/script/elasticsearch
+++ b/script/elasticsearch
@@ -7,4 +7,4 @@ PIDFILE="tmp/elasticsearch.pid"
./script/elasticsearch-cleanup
export HOSTNAME=localhost
-./vendor/shuttle-es/elasticsearch-1.2.1/bin/elasticsearch -p $PIDFILE
\ No newline at end of file
+./vendor/shuttle-es/elasticsearch-5.6.16/bin/elasticsearch -p $PIDFILE
\ No newline at end of file
diff --git a/script/find_strings_to_replace.rb b/script/find_strings_to_replace.rb
new file mode 100644
index 00000000..f39a45d9
--- /dev/null
+++ b/script/find_strings_to_replace.rb
@@ -0,0 +1,207 @@
+#!/usr/local/bin/env ruby
+
+require "csv"
+require "byebug"
+
+# ABOUT
+#---------
+# this script can be used to replace words in en-US with variant words
+# in other locales, e.g. a word like color -> colour
+
+# clear the output buffer first
+system "clear" or system "cls"
+
+CSV_FILE_PATH = 'replacement_results.csv'
+# get variant words from Google Drive: https://docs.google.com/spreadsheets/d/1NVaPqEz-R5UiAYHfNPYOlOyIVCwA7dAz5xtuvjXbVAg/edit?usp=sharing
+variant_words=
+[
+ # [source, en-GB source, en-GB replacement, en-CA source, en-CA replacement, en-AU source, en-AU replacement]
+ ["Authorization", "Authorization", "Authorisation", "Authorisation", "Authorization", "Authorization", "Authorisation"],
+ ["Authorize", "Authorize", "Authorise", "Authorise", "Authorize", "Authorize", "Authorise"],
+ ["Authorized", "authorised", "authorised", "authorized", "authorized", "authorised", "authorised"],
+ ["authorizing", "authorising", "authorising", "authorizing", "authorizing", "authorising", "authorising"],
+ ["Canceled", "Cancelled", "Cancelled", "Cancelled", "Cancelled", "Cancelled", "Cancelled"],
+ ["canceling", "cancelling", "cancelling", "cancelling", "cancelling", "cancelling", "cancelling"],
+ ["cancellation", "cancellation", "cancellation", "cancellation", "cancellation", "cancellation", "cancellation"],
+ ["Customize", "Customise", "Customise", "Customize", "Customize", "Customise", "Customise"],
+ ["customized", "customised", "customised", "customized", "customized", "customised", "customised"],
+ ["Enroll", "Enrol", "Enrol", "Enrol", "Enrol", "Enrol", "Enrol"],
+ ["enrolled", "enrolled", "enrolled", "enrolled", "enrolled", "enrolled", "enrolled"],
+ ["enrollment", "enrolment", "enrolment", "enrolment", "enrolment", "enrolment", "enrolment"],
+ ["enrolling", "enrolling", "enrolling", "enrolling", "enrolling", "enrolling", "enrolling"],
+ ["initialize", "initialise", "initialise", "initialize", "initialize", "initialise", "initialise"],
+ ["initialized", "--", "initialised", "--", "initialized", "--", "initialised"],
+ ["initializing", "--", "initialising", "--", "initializing", "--", "initialising"],
+ ["initialization", "--", "initialisation", "--", "initialization", "--", "initialisation"],
+ ["Personalize", "Personalise", "Personalise", "Personalize", "Personalize", "Personalise", "Personalise"],
+ ["Personalization", "Personalisation", "Personalisation", "Personalization", "Personalization", "Personalisation", "Personalisation"],
+ ["Uncategorized", "Uncategorised", "Uncategorised", "Uncategorized", "Uncategorized", "Uncategorised", "Uncategorised"],
+ ["Wifi", "Wi-fi", "Wi-fi", "Wi-fi", "Wi-fi", "Wi-fi", "Wi-fi"],
+ ["fulfillment", "fulfillment", "fulfilment", "fulfillment", "fulfillment", "fulfillment", "fulfilment"],
+]
+
+# helper method to print things while debugging
+def pp(input, linesAfter=true, linesBefore=true)
+ if linesBefore == true
+ puts "-" * [(input.size + 2), 15].min
+ end
+ puts input
+ if linesAfter == true
+ puts "-" * [(input.size + 2), 15].min
+ end
+end
+
+# finds replaceable word pairs in a given locale
+# locale: string
+# variant_words: hash object containing all variants for given locale
+# returns Hash (of arrays for each locale)
+def find_replacement_translations(locale, variant_words, translations)
+ pp "Processing #{locale} strings"
+ unchanged = []
+ to_be_replaced = []
+ variant_words.each do |dict|
+ current = dict[:source]
+ origin = dict[:origin]
+ replacement = dict[:target]
+ # keeping a tally of how many will not change due to both current
+ # and replacement being the same
+ if current == replacement
+ unchanged << { current: current, replacement: replacement }
+ next
+ end
+ if current == '--'
+ t = translations.where('copy LIKE ?', "%#{origin}%")
+ puts "#{t.count} strings found in #{locale} for #{origin}"
+ else
+ t = translations.where('copy LIKE ?', "%#{current}%")
+ puts "#{t.count} strings found in #{locale} for #{current}"
+ end
+ # t = translations.where(source_copy: source)
+ # count = t.count
+ # t = t.concat(fuzzy_match)
+ unless (t.nil? or t.empty?) && current[0] != replacement[0]
+ # pp "#{current[0]} matched #{replacement[0]}"
+ t.each do |row|
+ # exact match with word boundaries around the word
+ # this will prevent words being part of ids/classes
+ # and it will also prevent words like "Unenroll"
+ # it's looking for "enroll"
+ unless row.copy.match(/#{current}\b/)
+ next
+ end
+ if current[0] == replacement[0]
+ pp "#{current} will be replaced with #{replacement}"
+ end
+ rep = {
+ locale: locale,
+ source: row.source_copy,
+ current: row.copy,
+ replacement: row.copy && row.copy.gsub(current, replacement),
+ id: row.id,
+ word: replacement,
+ }
+ if rep[:current] != rep[:replacement]
+ puts "Current and replacmeent match: #{rep[:current]} == #{rep[:replacement]}"
+ begin
+ if rep[:replacement].strip_html_tags == rep[:replacement]
+ to_be_replaced << rep
+ else
+ pp "Stripped #{rep[:replacement]} and didn't add to list"
+ end
+ end
+ end
+ end
+ end
+ end
+ puts "Ignoring: #{unchanged.size} strings"
+ puts "Changing: #{to_be_replaced.size} strings"
+ to_be_replaced
+end
+
+# this method builds a locale specific hash of all words
+# returns Hash (locale based arrays of words)
+def build_variant_replacements(variant_words)
+ # first check if the number of words in a given set is not 7
+ # (meaning doesn't include all source/target for each locale + source)
+ invalid_variant_words = variant_words.select { |words| words.count != 7 }
+ unless invalid_variant_words.empty?
+ pp "Found Invalid Variants: #{invalid_variant_words}"
+ raise Exception.new("Found Invalid Variants: #{invalid_variant_words.count}")
+ end
+ locale_words = { 'en-GB' => [], 'en-CA' => [], 'en-AU' => [], }
+ variant_words.each do |source, gb_source, gb_target, ca_source, ca_target, au_source, au_target|
+ puts "A single row below:"
+ puts "#{source}, #{gb_source}, #{gb_target}, #{ca_source}, #{ca_target}, #{au_source}, #{au_target}"
+ locale_words['en-GB'] << { origin: source, source: gb_source, target: gb_target }
+ # puts locale_words['en-GB']
+ locale_words['en-CA'] << { origin: source, source: ca_source, target: ca_target }
+ pp locale_words
+ # puts locale_words['en-CA']
+ locale_words['en-AU'] << { origin: source, source: au_source, target: au_target }
+ # puts locale_words['en-AU']
+ end
+ locale_words
+end
+
+# this method will case a given array into uppercase, lowercase, and sentence case
+# returns Array (all case converted words and original words)
+def add_casing_types(original_array)
+ lowercase_array = []
+ uppercase_array = []
+ capitalized_array = []
+ # go over original_array once to to create temporary arrays
+ original_array.each do |array|
+ temp_lowercase_array = []
+ temp_uppercase_array = []
+ temp_capitalized_array = []
+ array.each do |word|
+ temp_lowercase_array << word.downcase
+ temp_uppercase_array << word.upcase
+ # split the first letter, capitalize it
+ temp_capitalized_array << word.split.map(&:capitalize)[0]
+ end
+ lowercase_array << temp_lowercase_array
+ uppercase_array << temp_uppercase_array
+ capitalized_array << temp_capitalized_array
+ end
+ # appending because at this point, we'll want to append a cased array
+ # just like any other list of words
+ final_array = [].concat(lowercase_array)
+ final_array = final_array.concat(uppercase_array)
+ final_array = final_array.concat(capitalized_array)
+end
+
+# 1st:
+# we add all the different cases to each row
+# an example this will look like this:
+# original row: ['Enroll', 'Enrol', 'Enrol'...etc]
+# uppercase row: ['ENROLL', 'ENROL', 'ENROL'...etc]
+# lowercase row: ['enroll', 'enrol', 'enrol'...etc]
+# capitalized case: ['Enroll', 'Enrol', 'Enrol'...etc]
+cased_variant_words = add_casing_types(variant_words)
+
+# 2nd:
+# we build individual hashes of each locale
+# e.g.
+# { 'en-GB: {origin: 'Enroll', source: 'Enrol', replacement: 'Enrol'} }
+# note: origin is the en-US source origin
+variant_words_dict = build_variant_replacements(cased_variant_words)
+
+# 3rd:
+# Write headers to CSV file
+CSV.open(CSV_FILE_PATH, 'w') do |csv|
+ csv << ['locale', 'word', 'current', 'replacement', 'translation_id']
+end
+
+# 4th:
+# find replacements for each locale and add to CSV file
+variant_words_dict.each do |locale, words_for_locale|
+ translations = Translation.where(rfc5646_locale: locale)
+ locale_replacements = find_replacement_translations(locale, words_for_locale, translations)
+ locale_replacements.each do |rep|
+ CSV.open(CSV_FILE_PATH, 'a+') do |csv|
+ csv << [rep[:locale], rep[:word], rep[:current], rep[:replacement], rep[:id]]
+ end
+ end
+end
+
diff --git a/script/install.d/elasticsearch b/script/install.d/elasticsearch
index f3647ede..aa306c25 100755
--- a/script/install.d/elasticsearch
+++ b/script/install.d/elasticsearch
@@ -6,3 +6,6 @@ mkdir -p vendor
cd vendor
git clone --quiet --shared /mnt/nfs/git/sq/shuttle-es.git
+
+# ES 5.6.16 expects there is folder plugins.
+mkdir -p ./shuttle-es/elasticsearch-5.6.16/plugins
diff --git a/script/replace_strings.rb b/script/replace_strings.rb
new file mode 100644
index 00000000..433cfa8d
--- /dev/null
+++ b/script/replace_strings.rb
@@ -0,0 +1,10 @@
+# Using an imported list of strings from a CSV, this script can replace strings.
+
+arguments = ARGV
+
+file_name = arguments[0]
+dry_run = arguments[1] || true
+
+puts "Filename: #{file_name}"
+puts "Dry run: #{dry_run}"
+
diff --git a/script/reset-deployable.sh b/script/reset-deployable.sh
new file mode 100755
index 00000000..0f35dcc5
--- /dev/null
+++ b/script/reset-deployable.sh
@@ -0,0 +1,38 @@
+echo "Checking out master branch"
+# first we'll checkout master branch
+git checkout master
+
+# if master branch checkout fails due to reasons like
+# existing changes locally that haven't been merged
+# ongoing merge/rebase/bisect
+# we'll fail and exit
+# otherwise, we'll delete deployable branch after checking out master
+if [ $? -eq 0 ]; then
+ echo "\n\nDeleting deployable branch"
+ git branch -D deployable
+else
+ echo "\n\nLooks like something went wrong while checking out master"
+ exit;
+fi
+
+# now let's create a new branch off of master called deployable
+if [ $? -eq 0 ]; then
+ echo "\n\nCreating new version of deployable from master"
+ git checkout -b deployable
+else
+ echo "\n\nCouldn't delete deployable, might not exist"
+ echo "\nDo you still want to create a 'deployable' branch?"
+ select yn in "Yes" "No"; do
+ case $yn in
+ Yes ) git checkout -b deployable; break;;
+ No ) exit;
+ esac
+ done
+fi
+
+# if creating a branch succeeded, we can push it up to origin
+if [ $? -eq 0 ]; then
+ echo "\n\nPush latest version of deployable branch to bitbucket"
+ git push -f origin deployable
+fi
+
diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/commits_controller_spec.rb
index aec63545..d8bf95d3 100644
--- a/spec/controllers/commits_controller_spec.rb
+++ b/spec/controllers/commits_controller_spec.rb
@@ -202,6 +202,24 @@
C
end
+ it "should export a UTF-8 Strings file in one locale" do
+ get :manifest, project_id: @project.to_param, id: @commit.to_param, locale: 'fr', format: 'strings', encoding: 'utf-8'
+ expect(response.status).to eql(200)
+ expect(response.headers['Content-Disposition']).to eql('attachment; filename="fr.strings"')
+ expect(response.headers['Content-Type']).to eql('text/plain; charset=utf-8')
+ expect(response.body.encoding.to_s).to eql('UTF-8')
+
+ body = response.body
+ expect(body).to include(<<-C)
+/* Universal Greeting */
+"key1" = "Bonjour {name}! Avec anninas fromage {count} la bouches.";
+ C
+ expect(body).to include(<<-C)
+/* Shopping cart contents */
+"key2" = "Tu avec carté {count} itém has";
+ C
+ end
+
it "should export a Java properties file in one locale" do
get :manifest, project_id: @project.to_param, id: @commit.to_param, locale: 'fr', format: 'properties'
expect(response.status).to eql(200)
@@ -215,11 +233,65 @@
C
end
- it "should export an iOS tarball manifest" do
+ it "should export an iOS tarball manifest in UTF-16LE" do
+ Commit.find(@commit.id).keys.update_all(source: 'foo/bar.strings')
+
get :manifest, project_id: @project.to_param, id: @commit.to_param, format: 'ios'
expect(response.status).to eql(200)
expect(response.headers['Content-Disposition']).to eql('attachment; filename="manifest.tar.gz"')
- # check body?
+ expect(response.headers['Content-Type']).to eql('application/x-gzip; charset=utf-16le')
+ expect(response.body.encoding.to_s).to eql('UTF-16LE')
+
+ entries = Hash.new
+ Archive.read_open_memory(response.body, Archive::COMPRESSION_GZIP, Archive::FORMAT_TAR_GNUTAR) do |archive|
+ while (entry = archive.next_header)
+ contents = archive.read_data
+ expect(contents.bytes.to_a[0]).to eq(0xFF)
+ expect(contents.bytes.to_a[1]).to eq(0xFE)
+ entries[entry.pathname] = contents.force_encoding('UTF-16LE')
+ end
+ end
+ expect(entries.count).to eq(1)
+
+ body = entries['foo/bar.strings'].encode('UTF-8')
+ expect(body).to include(<<-C)
+/* Universal Greeting */
+"key1" = "Bonjour {name}! Avec anninas fromage {count} la bouches.";
+ C
+ expect(body).to include(<<-C)
+/* Shopping cart contents */
+"key2" = "Tu avec carté {count} itém has";
+ C
+ end
+
+ it "should export an iOS tarball manifest in UTF-8" do
+ Commit.find(@commit.id).keys.update_all(source: 'foo/bar.strings')
+
+ get :manifest, project_id: @project.to_param, id: @commit.to_param, format: 'ios', encoding: 'utf-8'
+ expect(response.status).to eql(200)
+ expect(response.headers['Content-Disposition']).to eql('attachment; filename="manifest.tar.gz"')
+ expect(response.headers['Content-Type']).to eql('application/x-gzip; charset=utf-8')
+ expect(response.body.encoding.to_s).to eql('UTF-8')
+
+ entries = Hash.new
+ Archive.read_open_memory(response.body, Archive::COMPRESSION_GZIP, Archive::FORMAT_TAR_GNUTAR) do |archive|
+ while (entry = archive.next_header)
+ contents = archive.read_data
+ expect(contents.force_encoding('UTF-8')).to eq(contents)
+ entries[entry.pathname] = contents
+ end
+ end
+ expect(entries.count).to eq(1)
+
+ body = entries['foo/bar.strings']
+ expect(body).to include(<<-C)
+/* Universal Greeting */
+"key1" = "Bonjour {name}! Avec anninas fromage {count} la bouches.";
+ C
+ expect(body).to include(<<-C)
+/* Shopping cart contents */
+"key2" = "Tu avec carté {count} itém has";
+ C
end
it "should export a Ruby file in one locale" do
@@ -529,8 +601,6 @@
describe '#search' do
before :each do
- reset_elastic_search
-
@project = FactoryBot.create(:project,
base_rfc5646_locale: 'en',
targeted_rfc5646_locales: { 'en' => true, 'fr' => true, 'es' => false },
@@ -538,17 +608,15 @@
@commit = @project.commit!('HEAD', skip_import: true)
other_commit = FactoryBot.create(:commit, project: @project)
- other_commit.keys = [FactoryBot.create(:key, project: @project).tap(&:add_pending_translations)]
+ other_commit.update(keys: [FactoryBot.create(:key, project: @project).tap(&:add_pending_translations)])
@keys = FactoryBot.create_list(:key, 51, project: @project, ready: false).sort_by(&:key)
@keys.each &:add_pending_translations
- @commit.keys = @keys
+ @commit.update(keys: @keys)
@user = FactoryBot.create(:user, :activated)
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in @user
-
- regenerate_elastic_search_indexes
end
it 'should return the first page of keys if page not specified' do
@@ -579,7 +647,9 @@
it 'filters by requested status' do
approved_key = FactoryBot.create(:key, project: @project, key: 'approved_key', ready: true)
@commit.keys = @keys << approved_key
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
+ KeysIndex.reset!
+ TranslationsIndex.reset!
get :search, project_id: @project.to_param, id: @commit.to_param, status: 'approved'
expect(response.status).to eql 200
@@ -666,7 +736,7 @@
@request.env['devise.mapping'] = Devise.mappings[:user]
@user = FactoryBot.create(:user, :confirmed, role: 'admin')
sign_in @user
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
end
it "should require a monitor" do
@@ -677,7 +747,6 @@
end
it "should delete a commit" do
- skip 'ElasticSearch flaky - deletion throws NotFound exception'
delete :destroy, project_id: @commit.project.to_param, id: @commit.to_param, format: 'json'
expect(response.status).to eql(204)
expect { @commit.reload }.to raise_error(ActiveRecord::RecordNotFound)
diff --git a/spec/controllers/glossary_controller_spec.rb b/spec/controllers/glossary_controller_spec.rb
index 2634f130..3828108f 100644
--- a/spec/controllers/glossary_controller_spec.rb
+++ b/spec/controllers/glossary_controller_spec.rb
@@ -17,8 +17,6 @@
RSpec.describe GlossaryController do
describe '#index' do
before :each do
- reset_elastic_search
-
update_date = DateTime.new(2014, 1, 1)
@user = FactoryBot.create(:user, :confirmed, role: 'translator')
@start_date = (update_date - 1.day).strftime('%m/%d/%Y')
@@ -49,7 +47,7 @@
end
end
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in @user
diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
index b881cc55..9a0649cd 100644
--- a/spec/controllers/home_controller_spec.rb
+++ b/spec/controllers/home_controller_spec.rb
@@ -18,7 +18,6 @@
before :each do
allow_any_instance_of(Article).to receive(:import!) # prevent auto import
- reset_elastic_search
@request.env['devise.mapping'] = Devise.mappings[:user]
@user = FactoryBot.create(:user, :confirmed, role: 'monitor')
sign_in @user
@@ -38,7 +37,8 @@
# red herring: loading commit
FactoryBot.create(:commit, project: @project, loading: true)
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
+ KeysIndex.reset!
end
context "[when 'remove duplicates' filter is selected]" do
@@ -56,7 +56,8 @@
commit1.keys << key2
commit2.keys << key2
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
+ KeysIndex.reset!
get :index, { commits_filter__hide_duplicates: 'true' }
expect(assigns(:commits).map(&:id)).to eq [commit3.id, commit2.id, @commit.id]
@@ -98,7 +99,7 @@
hidden_article = FactoryBot.create(:article, project: @project, hidden: true)
hidden_group = FactoryBot.create(:group, name: 'hidden-group', project: @project, hidden: true)
- get :index, { filter__status: 'hidden' }
+ get :index, { filter__status: 'hidden', sort__field: 'create' }
expect(assigns(:articles).map(&:id)).to eq([hidden_article.id])
expect(assigns(:groups).map(&:id)).to eq([hidden_group.id])
end
@@ -108,7 +109,7 @@
it 'should return all translations in all projects' do
commit1 = FactoryBot.create(:commit, project: @project, ready: false)
commit2 = FactoryBot.create(:commit, project: @project)
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
get :index, { filter__status: 'all', commits_filter__project_id: 'all' }
expect(assigns(:commits).map(&:id)).to eq([commit2.id, commit1.id, @commit.id])
diff --git a/spec/controllers/locales/projects_controller_spec.rb b/spec/controllers/locales/projects_controller_spec.rb
index d852ea2a..6320e0be 100644
--- a/spec/controllers/locales/projects_controller_spec.rb
+++ b/spec/controllers/locales/projects_controller_spec.rb
@@ -18,7 +18,6 @@
describe "#show" do
context "[status filtering]" do
before :each do
- reset_elastic_search
@user = FactoryBot.create(:user, :confirmed, role: 'translator', approved_rfc5646_locales: ['fr-CA'])
@project = FactoryBot.create(:project, base_rfc5646_locale: 'en-US', targeted_rfc5646_locales: {'fr-CA' => true})
@@ -77,7 +76,6 @@
copy: nil,
translated: false,
approved: nil)
- regenerate_elastic_search_indexes
@request.env["devise.mapping"] = Devise.mappings[:user]
sign_in @user
@@ -127,7 +125,7 @@
expect(response.status).to eql(200)
translations = assigns(:translations)
expect(translations.map { |t| t.key.key }).
- to match_array([@approved.key.key, @new.key.key])
+ to match_array([@approved.key.key, @new.key.key, @rejected.key.key])
end
it "should filter with include_translated = true, include_approved = true" do
@@ -135,7 +133,7 @@
expect(response.status).to eql(200)
translations = assigns(:translations)
expect(translations.map { |t| t.key.key }).
- to match_array([@translated.key.key, @approved.key.key, @rejected.key.key])
+ to match_array([@translated.key.key, @approved.key.key])
end
it "should filter with include_translated = true, include_approved = true, include_new = true" do
@@ -163,7 +161,6 @@
sign_in user
allow_any_instance_of(Article).to receive(:import!) # prevent auto import
- reset_elastic_search
@project = FactoryBot.create(:project, repository_url: nil, job_type: :article)
@article = FactoryBot.create(:article, project: @project)
@@ -179,14 +176,11 @@
@translation3 = FactoryBot.create(:translation, key: @key3, copy: nil, rfc5646_locale: 'fr')
@translation4 = FactoryBot.create(:translation, key: @key4, copy: nil, rfc5646_locale: 'fr')
@translation5 = FactoryBot.create(:translation, key: @key5, copy: nil, rfc5646_locale: 'fr')
-
- regenerate_elastic_search_indexes
end
it "returns active keys in an article in the right order" do
@section2.update! active: false # inactive section
@key3.update! index_in_section: nil # inactive key
- regenerate_elastic_search_indexes
get :show, id: @project.to_param, article_id: @article.id, locale_id: 'fr', include_new: 'true'
expect(response.status).to eql(200)
@@ -199,11 +193,17 @@
expect(assigns(:translations).map(&:id)).to eql([@translation4.id])
end
- it "filters with include_block_tags" do
+ it "filters with include_block_tags = true" do
get :show, id: @project.to_param, article_id: @article.id, section_id: @section2.id, locale_id: 'fr', include_new: 'true', include_block_tags: 'true'
expect(response.status).to eql(200)
expect(assigns(:translations).map(&:id)).to eql([@translation4.id, @translation5.id])
end
+
+ it "filters with include_block_tags = false" do
+ get :show, id: @project.to_param, article_id: @article.id, section_id: @section2.id, locale_id: 'fr', include_new: 'false', include_block_tags: 'false'
+ expect(response.status).to eql(200)
+ expect(assigns(:translations).map(&:id)).to eql([@translation4.id])
+ end
end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index c72a6fd5..430c78b1 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -17,8 +17,6 @@
RSpec.describe SearchController do
describe "#translations" do
before :each do
- reset_elastic_search
-
update_date = DateTime.new(2014, 1, 1)
@user = FactoryBot.create(:user, :confirmed, role: 'translator')
@start_date = (update_date - 1.day).strftime('%m/%d/%Y')
@@ -42,7 +40,7 @@
end
end
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in @user
@@ -50,7 +48,7 @@
it "should filter by page" do
FactoryBot.create_list :translation, 50
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :translations, page: '2'
results = assigns(:results)
@@ -119,13 +117,12 @@
describe '#keys' do
before :each do
- reset_elastic_search
@user = FactoryBot.create(:user, :confirmed, role: 'translator')
@project = FactoryBot.create(:project)
5.times { |i| FactoryBot.create :key, project: @project, key: "t1_n#{i}" }
5.times { |i| FactoryBot.create :key, project: @project, key: "t2_n#{i}" }
- regenerate_elastic_search_indexes
+ KeysIndex.reset!
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in @user
@@ -141,7 +138,7 @@
it "should exlcude hidden key" do
FactoryBot.create(:key, project: @project, key: 'hide me', hidden_in_search: true)
- regenerate_elastic_search_indexes
+ KeysIndex.reset!
get :keys, project_id: @project.id, format: 'json'
expect(response.status).to eql(200)
@@ -151,9 +148,9 @@
it "should search for hidden key only" do
FactoryBot.create(:key, project: @project, key: 'hide me', hidden_in_search: true)
- regenerate_elastic_search_indexes
+ KeysIndex.reset!
- get :keys, project_id: @project.id, hidden_in_search: '', format: 'json'
+ get :keys, project_id: @project.id, hidden_in_search: '1', format: 'json'
expect(response.status).to eql(200)
results = JSON.parse(response.body)
expect(results.size).to eq(1)
@@ -210,8 +207,6 @@ def finish_sha(prefix="")
let(:prefix3) { "abc111" }
before :each do
- reset_elastic_search
-
@user = FactoryBot.create(:user, :confirmed, role: "translator")
@project1 = FactoryBot.create(:project)
@project2 = FactoryBot.create(:project)
@@ -221,7 +216,7 @@ def finish_sha(prefix="")
FactoryBot.create :commit, project: @project1, revision: finish_sha('abc111')
FactoryBot.create :commit, project: @project2, revision: finish_sha('abc111')
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
end
before :each do
diff --git a/spec/controllers/translations_controller_spec.rb b/spec/controllers/translations_controller_spec.rb
index 3b50fa25..befa82fe 100644
--- a/spec/controllers/translations_controller_spec.rb
+++ b/spec/controllers/translations_controller_spec.rb
@@ -152,7 +152,7 @@
expect(response.status).to eql(200)
expect(@translation.reload.copy).to eql('bye!')
expect(@translation).to be_approved
- expect(@translation.translator).to eql(@user)
+ expect(@translation.translator).to eql(translator)
expect(@translation.reviewer).to eql(@user)
expect(@translation.translation_changes.count).to eq(1)
@@ -163,6 +163,8 @@
end
it "should automatically approve reviewer changes to an untranslated string" do
+ translator = @translation.translator
+
patch :update,
project_id: @translation.key.project.to_param,
key_id: @translation.key.to_param,
@@ -173,7 +175,7 @@
expect(response.status).to eql(200)
expect(@translation.reload.copy).to eql('bye!')
expect(@translation).to be_approved
- expect(@translation.translator).to eql(@user)
+ expect(@translation.translator).to eql(translator)
expect(@translation.reviewer).to eql(@user)
expect(@translation.translation_changes.count).to eq(1)
@@ -366,7 +368,6 @@
end
before :each do
- reset_elastic_search
allow_any_instance_of(Locale).to receive(:fallbacks).and_return(
%w(fr-CA fr en).map { |l| Locale.from_rfc5646 l }
)
@@ -410,7 +411,7 @@
end
it "should 1. respond with a Translation with matching locale and source copy" do
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :match,
project_id: @project.to_param,
@@ -437,7 +438,7 @@
translation.update! modifier: @user
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :match,
project_id: @project.to_param,
@@ -452,7 +453,7 @@
it "should 3. respond with the Translation of the 1st fallback locale with matching project/key and source copy" do
@same_locale_sc.destroy
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :match,
project_id: @project.to_param,
@@ -465,7 +466,7 @@
it "should 4. respond with the Translation of the 1st fallback locale with source copy" do
[@same_locale_sc, @fallback1_sc].each(&:destroy)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :match,
project_id: @project.to_param,
@@ -478,7 +479,7 @@
it "should 5. respond with a 204" do
[@same_locale_sc, @fallback1_sc, @fallback2_sc].each(&:destroy)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :match,
project_id: @project.to_param,
@@ -592,7 +593,6 @@
before :each do
Translation.destroy_all
- reset_elastic_search
@translation = FactoryBot.create :translation,
source_copy: 'foo bar 1',
copy: 'something else',
@@ -611,7 +611,7 @@
copy: 'something else',
source_rfc5646_locale: 'en',
rfc5646_locale: 'fr'
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :fuzzy_match,
project_id: @translation.key.project.to_param,
@@ -631,7 +631,7 @@
approved: true,
source_rfc5646_locale: 'en',
rfc5646_locale: 'fr-CA'
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
# fr is a fallback of fr-CA
get :fuzzy_match,
@@ -661,7 +661,7 @@
copy: 'something else',
source_rfc5646_locale: 'en',
rfc5646_locale: 'fr'
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :fuzzy_match,
project_id: @translation.key.project.to_param,
@@ -683,7 +683,7 @@
source_rfc5646_locale: 'en',
rfc5646_locale: 'fr'
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :fuzzy_match,
project_id: @translation.key.project.to_param,
@@ -706,7 +706,7 @@
rfc5646_locale: 'fr'
end
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :fuzzy_match,
project_id: @translation.key.project.to_param,
@@ -729,7 +729,7 @@
rfc5646_locale: 'fr'
end
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :fuzzy_match,
project_id: @translation.key.project.to_param,
@@ -749,7 +749,7 @@
end
it "should truncate project name exceeds 30 chars" do
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :fuzzy_match,
project_id: @translation.key.project.to_param,
@@ -779,7 +779,7 @@
end
it "should not show in search result" do
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
get :fuzzy_match,
project_id: @translation.key.project.to_param,
@@ -812,6 +812,7 @@
%w(fr es).each do |locale|
FactoryBot.create(:translation, key: key, source_rfc5646_locale: 'en', rfc5646_locale: locale, source_copy: 'fake', copy: nil, approved: nil)
end
+ key.reload
end
@commit1 = FactoryBot.create(:commit, project: @project)
@@ -885,6 +886,7 @@
%w(fr es).each do |locale|
FactoryBot.create(:translation, key: key, source_rfc5646_locale: 'en', rfc5646_locale: locale, source_copy: 'fake', copy: nil, approved: nil)
end
+ key.reload
end
@project.keys.each { |k| k.recalculate_ready! }
diff --git a/spec/factories/edit_reason.rb b/spec/factories/edit_reason.rb
new file mode 100644
index 00000000..e11aef35
--- /dev/null
+++ b/spec/factories/edit_reason.rb
@@ -0,0 +1,7 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryBot.define do
+ factory :edit_reason do
+ association :translation_change
+ end
+end
diff --git a/spec/factories/reports.rb b/spec/factories/reports.rb
new file mode 100644
index 00000000..51954ee0
--- /dev/null
+++ b/spec/factories/reports.rb
@@ -0,0 +1,10 @@
+FactoryBot.define do
+ factory :report do
+ date "2019-06-20 21:48:02"
+ project "MyString"
+ locale "MyString"
+ strings 1
+ words 1
+ report_type "MyString"
+ end
+end
diff --git a/spec/helpers/comments_helper_spec.rb b/spec/helpers/comments_helper_spec.rb
index 19ddcee8..2ed57675 100644
--- a/spec/helpers/comments_helper_spec.rb
+++ b/spec/helpers/comments_helper_spec.rb
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-require "spec_helper"
+require "rails_helper"
RSpec.describe CommentsHelper do
describe "#issue_url" do
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 8143d0c3..a434ec1e 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-require "spec_helper"
+require "rails_helper"
RSpec.describe IssuesHelper do
describe "#issue_url" do
diff --git a/spec/lib/exporter/ios_spec.rb b/spec/lib/exporter/ios_spec.rb
index edec5f7f..e77fd82a 100644
--- a/spec/lib/exporter/ios_spec.rb
+++ b/spec/lib/exporter/ios_spec.rb
@@ -128,6 +128,48 @@
expect(entries).not_to include('/Resources/en-US.lproj/Some.xib')
end
+ it "should output in UTF-8 encoding when requested" do
+ io = StringIO.new
+ exporter = Exporter::Ios.new(@commit)
+ exporter.override_encoding = 'utf-8'
+ exporter.export(io, @de)
+ io.rewind
+
+ entries = Hash.new
+ Archive.read_open_memory(io.string, Archive::COMPRESSION_GZIP, Archive::FORMAT_TAR_GNUTAR) do |archive|
+ while (entry = archive.next_header)
+ expect(entry).to be_regular
+ contents = archive.read_data
+ expect(contents.force_encoding('UTF-8')).to eq(contents)
+ entries[entry.pathname] = contents
+ end
+ end
+
+ expect(entries.size).to eql(2)
+
+ body = entries['Resources/de-DE.lproj/Localizable-en-US.strings']
+ expect(body).to include(<<-C)
+/* This is a normal string. */
+"I'm a string!" = "Ich bin ein String!";
+ C
+ expect(body).to include(<<-C)
+/* This is also a normal string. */
+"I'm also a string!" = "Ich bin auch ein String!";
+ C
+
+ body = entries['Resources/de-DE.lproj/More.strings']
+ expect(body).to include(<<-C)
+/* Saying hello. */
+"Hello, world!" = "Hallo, Welt!";
+ C
+ expect(body).to include(<<-C)
+/* Saying goodbye. */
+"Goodbye, cruel world." = "Auf Wiedersehen, grausamer Welt.";
+ C
+
+ expect(entries).not_to include('/Resources/en-US.lproj/Some.xib')
+ end
+
describe ".valid?" do
it "should return true for a valid tar-gz file" do
smalltgz = "\x1F\x8B\b\b\xB1\x8E\xF8Q\x00\x03log.tar\x00\xED\x99\xCFO\xC20\x14\x80_'\xC6\x19/;\x19\x8F\xBDx\xF1\x80mi\xB7\xEBB\xF0hL\xDC\xC5\e\x121d\t?\x12\x1C\xF7\xFD\xE9\xB6\xF4I\x16\x10\x88\x89\e\"\xEFK\x9A\x0FX\xCB\xDE(\xAF\xEC\xD1\xF1lt\x0F5#\x84H\x8C\xE1\xD6J\xAB\xC4YH%\x96\xFE\x82K%\x93X\x8AD\x19\xCD\x85\x94\xC6$\xC0M\xDD\x819\x16\x1F\xC5`nC)\xF2\xC9\xCE~\x83\xE1$\x9F\xEE8\x8E\xD7\xB1\xF2\x910\xB6\xF3\xDF\xEE\xB7{Y?+f\xF3\xF7Z\xCEa?\x8FX\xEB\xED\xF3/\x95r\xF3o:\x89R\xB1\x8C\xED\xFCw\xB4Q\xC0E-\xD1\xACq\xE2\xF3\x0F\xE7\xD7\x17\x10\x00<\x0E\xDE\xF8S\xC6_8\xE2^\x83K\xDB\x94m\xDC6\xF7\xFC\xD9\rX\xF5\x88\x0E\x174\xF1[,\xF3\xBF\xD6\xEC\xDF\x97\xFFR\v\xA1\xD7\xF3_iI\xF9\xDF\x10\xAC\xBB\x18JX\xA6s\b\xDEp\xFB}\xD7\x10\xDB\x06A\xF5\xFD\x80\x96\x06\x82 \b\x828\x06\x98Wxu\xD80\b\x82\xF8\x83\xB8\xF5\x81\xA3St\xE9\xCD\xF0x\x80nU\xC6Dh\x8EN\xD1\xA57\xC3~\x01\xBA\x85\x0E\xD1\x11\x9A\xA3St\xE9\x8D\x8B\x16\xC3\xE2\x83\xE1\x99\x19V(\f\xAB\x10\xC6\xD1\xE9\x8F.\x99 N\x863\xAF\xC8\xFD\xFE?\xC0\xD6\xFA\x9F \x88\x7F\fk\xF5\xB2^\x17V\x05\xC1f\a\xDB^+\x8FK\xD8~\x13\x10\xF8?\vo*c9:E\x97\xDEt\#@\x10\x04\xD14\xCB\xFD\xBFQ^\xE4\xA3im\e\x80\xFB\xF6\xFF\x850\xEB\xFB\x7FF\xD3\xFE\x7F#\xDC\xB5\xED7\xE0\xD0A\x10\x04A\x10\x8D\xF3\t\n\xEE0\x8E\x00*\x00\x00"
diff --git a/spec/lib/fencer/android_spec.rb b/spec/lib/fencer/android_spec.rb
index 248b93ba..3a269672 100644
--- a/spec/lib/fencer/android_spec.rb
+++ b/spec/lib/fencer/android_spec.rb
@@ -35,5 +35,9 @@
it "should return false for a string that only contains any other character between { }" do
expect(Fencer::Android.valid?("String with {{two}} {tokens}.")).to be_falsey
end
+
+ it "should return true for a string that contains CDATA" do
+ expect(Fencer::Android.valid?("hello
]]>")).to be_truthy
+ end
end
end
diff --git a/spec/lib/fencer/intl_message_format_spec.rb b/spec/lib/fencer/intl_message_format_spec.rb
index 30aa4412..edbe97b1 100644
--- a/spec/lib/fencer/intl_message_format_spec.rb
+++ b/spec/lib/fencer/intl_message_format_spec.rb
@@ -15,19 +15,61 @@
require 'rails_helper'
RSpec.describe Fencer::IntlMessageFormat do
describe ".fence" do
+ # test cases cover all sample from this link: https://formatjs.io/guides/message-syntax
+
it "should fence a message format token" do
expect(Fencer::IntlMessageFormat.fence("String with {two} {tokens}.")).
- to eql('{two}' => [12..16], '{tokens}' => [18..25])
+ to eql(':two' => [-1..0], ':tokens' => [-1..0])
end
it "should fence a message format token at the beginning of the string" do
expect(Fencer::IntlMessageFormat.fence("{one} token in this string.")).
- to eql('{one}' => [0..4])
+ to eql(':one' => [-1..0])
+ end
+
+ it "should fence a message number type format" do
+ expect(Fencer::IntlMessageFormat.fence("Almost {pctBlack, number, percent} of them are black.")).
+ to eql(':pctBlack|number|percent' => [-1..0])
+ end
+
+ it "should fence a message date type format" do
+ expect(Fencer::IntlMessageFormat.fence("Sale begins {start, date, medium}")).
+ to eql(':start|date|medium' => [-1..0])
+ end
+
+ it "should fence a message time type format" do
+ expect(Fencer::IntlMessageFormat.fence("Coupon expires at {expires, time, short}")).
+ to eql(':expires|time|short' => [-1..0])
+ end
+
+ it "should fence a message select type format" do
+ expect(Fencer::IntlMessageFormat.fence("{fruit, select, apple {Apple} banana {Banana} other {Unknown}}")).
+ to eql(':fruit|select|apple' => [-1..0], ':fruit|select|banana' => [-1..0], ':fruit|select|other' => [-1..0])
+ end
+
+ it "should fence a message nested select type format" do
+ expect(Fencer::IntlMessageFormat.fence("{taxableArea, select, yes {An additional {taxRate, number, percent} tax will be collected.} other {No taxes apply.}}")).
+ to eql(':taxableArea|select|yes:taxRate|number|percent' => [-1..0], ':taxableArea|select|other' => [-1..0])
+ end
+
+ it "should fence a message plural format" do
+ expect(Fencer::IntlMessageFormat.fence("Cart: {itemCount} {itemCount, plural, one {item} other {items}}")).
+ to eql(':itemCount' => [-1..0], ':itemCount|plural|0|one' => [-1..0], ':itemCount|plural|0|other' => [-1..0])
+ end
+
+ it "should fence a message selectordinal format" do
+ expect(Fencer::IntlMessageFormat.fence("It's my cat's {year, selectordinal, one {st} two {nd} few {rd} other {3th}} birthday!")).
+ to eql(':year|selectordinal|0|one' => [-1..0], ':year|selectordinal|0|two' => [-1..0], ':year|selectordinal|0|few' => [-1..0], ':year|selectordinal|0|other' => [-1..0])
end
it "should not fence a string with escaped braces" do
expect(Fencer::IntlMessageFormat.fence("String with \\{two\\} \\{tokens\\}.")).
- to eql({})
+ to eql({})
+ end
+
+ it "should return empty set if mal-formatted" do
+ expect(Fencer::IntlMessageFormat.fence("Sale begins {start, date, mediu")).
+ to eql({})
end
end
@@ -35,7 +77,7 @@
it "should return true for a string with valid interpolations" do
expect(Fencer::IntlMessageFormat.valid?("String with {valid} {interpolations}.")).to be_truthy
expect(Fencer::IntlMessageFormat.valid?("{String} that starts with an interpolation.")).to be_truthy
- expect(Fencer::IntlMessageFormat.valid?("String with an escaped brace '\\{'.")).to be_truthy
+ expect(Fencer::IntlMessageFormat.valid?("String with an escaped brace \\{.")).to be_truthy
expect(Fencer::IntlMessageFormat.valid?("String with no interpolations.")).to be_truthy
end
diff --git a/spec/lib/importer/strings_spec.rb b/spec/lib/importer/strings_spec.rb
index 5b60f668..775260c3 100644
--- a/spec/lib/importer/strings_spec.rb
+++ b/spec/lib/importer/strings_spec.rb
@@ -35,6 +35,11 @@
expect(@project.keys.for_key("/apple/en-US.lproj/example.strings:Something\nwith\tescapes\\").first.translations.base.first.copy).to eql("Something\nwith\tescapes\\")
end
+ it "should properly joins multiple lines" do
+ importer = Importer::Strings.new(@commit.blobs.first, @commit)
+ expect(importer.send(:unescape, "first line, \\\ncontinue with the line")).to eq("first line, continue with the line")
+ end
+
it "should still import strings that end with " do
expect(@project.keys.for_key('/apple/en-US.lproj/example.strings:quote.charlie.1').first.translations.find_by_rfc5646_locale('en-US').copy).to eql("I'm a patriot. You've gotta give me that.")
end
diff --git a/spec/lib/localizer/android_spec.rb b/spec/lib/localizer/android_spec.rb
index 0ecf19a5..d4e1084f 100644
--- a/spec/lib/localizer/android_spec.rb
+++ b/spec/lib/localizer/android_spec.rb
@@ -38,7 +38,8 @@
'/java/basic-hdpi/strings.xml:attributed_array[0]' => 'Hallo',
'/java/basic-hdpi/strings.xml:attributed_array[1]' => 'Welt',
'/java/basic-hdpi/strings.xml:plural[one]' => 'Welt',
- '/java/basic-hdpi/strings.xml:plural[other]' => 'Welten'
+ '/java/basic-hdpi/strings.xml:plural[other]' => 'Welten',
+ '/java/basic-hdpi/strings.xml:cdata' => 'Hallo]]>'
}.each do |key, value|
key_obj = FactoryBot.create(:key, key: key, project: @project, source: '/java/basic-hdpi/strings.xml')
FactoryBot.create :translation, key: key_obj, copy: value, source_locale: @en, locale: @de
@@ -75,6 +76,7 @@
- Hello
- World
+ Hello]]>
XML
output_file = Localizer::File.new
@@ -110,6 +112,7 @@
- Hallo
- Welt
+ Hallo]]>
XML
end
diff --git a/spec/lib/paginatable_objects_spec.rb b/spec/lib/paginatable_objects_spec.rb
index a896ad35..df7ed856 100644
--- a/spec/lib/paginatable_objects_spec.rb
+++ b/spec/lib/paginatable_objects_spec.rb
@@ -18,55 +18,35 @@
before do
project = FactoryBot.create(:project)
- @commit1 = FactoryBot.create(:commit, project: project)
- @commit2 = FactoryBot.create(:commit, project: project)
- @commit3 = FactoryBot.create(:commit, project: project)
- @commit4 = FactoryBot.create(:commit, project: project)
- @commit5 = FactoryBot.create(:commit, project: project)
-
- @commits = [@commit1, @commit2, @commit3, @commit4, @commit5]
- @es_objects = [double('es_result', id: @commit4.id),
- double('es_result', id: @commit1.id),
- double('es_result', id: @commit5.id),
- double('es_result', id: @commit2.id),
- double('es_result', id: @commit3.id)]
-
- class << @es_objects
- def total() 10 end
- end
- end
-
- describe '#initialize' do
- it 'keeps objects sorted' do
- ordered_commits = PaginatableObjects.new(@commits, @es_objects, 1, 5).objects
- expect(ordered_commits).to eql([@commit4, @commit1, @commit5, @commit2, @commit3])
- end
+ FactoryBot.create_list :commit, 10, project: project
+ CommitsIndex.reset!
+ @es_objects = CommitsIndex.filter(term: {project_id: project.id})
end
describe '#offset_value' do
it 'finds the correct offset_value' do
- expect(PaginatableObjects.new([], @es_objects, 1, 5).offset_value).to eql(0)
- expect(PaginatableObjects.new([], @es_objects, 2, 5).offset_value).to eql(5)
- expect(PaginatableObjects.new([], @es_objects, 3, 5).offset_value).to eql(10)
+ expect(PaginatableObjects.new(@es_objects, 1, 5).offset_value).to eql(0)
+ expect(PaginatableObjects.new(@es_objects, 2, 5).offset_value).to eql(5)
+ expect(PaginatableObjects.new(@es_objects, 3, 5).offset_value).to eql(10)
end
end
describe '#total_pages' do
it 'returns the total number of pages' do
- expect(PaginatableObjects.new([], @es_objects, 1, 4).total_pages).to eql(3)
- expect(PaginatableObjects.new([], @es_objects, 1, 5).total_pages).to eql(2)
- expect(PaginatableObjects.new([], @es_objects, 1, 6).total_pages).to eql(2)
+ expect(PaginatableObjects.new(@es_objects, 1, 4).total_pages).to eql(3)
+ expect(PaginatableObjects.new(@es_objects, 1, 5).total_pages).to eql(2)
+ expect(PaginatableObjects.new(@es_objects, 1, 6).total_pages).to eql(2)
end
end
describe '#last_page?' do
it 'returns true if last page' do
- expect(PaginatableObjects.new([], @es_objects, 3, 4).last_page?).to be_truthy
+ expect(PaginatableObjects.new(@es_objects, 3, 4).last_page?).to be_truthy
end
it 'returns false if not page' do
- expect(PaginatableObjects.new([], @es_objects, 1, 4).last_page?).to be_falsey
- expect(PaginatableObjects.new([], @es_objects, 2, 4).last_page?).to be_falsey
+ expect(PaginatableObjects.new(@es_objects, 1, 4).last_page?).to be_falsey
+ expect(PaginatableObjects.new(@es_objects, 2, 4).last_page?).to be_falsey
end
end
end
diff --git a/spec/lib/reports/quality_report_spec.rb b/spec/lib/reports/quality_report_spec.rb
index af100156..7c4a3c21 100644
--- a/spec/lib/reports/quality_report_spec.rb
+++ b/spec/lib/reports/quality_report_spec.rb
@@ -137,20 +137,20 @@
expect(result[7]).to eql expected_results
end
- it 'has the expected row 8' do
- expected_results = [@end_date.strftime("%Y-%m-%d"), project, sha, @key4.key, "Hello, world", "IT", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Rebecca (#{@translator.id})", "hello!", "bye!", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Mark (#{@reviewer.id})", "", nil]
- expect(result[8]).to eql expected_results
- end
-
- it 'has the expected row 9' do
- expected_results = [@end_date.strftime("%Y-%m-%d"), @asset_project.name, asset, @key6.key, "Hello, world", "IT", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Rebecca (#{@translator.id})", "hello!", "bye!", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Mark (#{@reviewer.id})", "", nil]
- expect(result[9]).to eql expected_results
- end
-
- it 'has the expected row 10' do
- expected_results = [@end_date.strftime("%Y-%m-%d"), @article_project.name, article, @key5.key, "Hello, world", "IT", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Rebecca (#{@translator.id})", "hello!", "bye!", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Mark (#{@reviewer.id})", "", nil]
- expect(result[10]).to eql expected_results
- end
+ # it 'has the expected row 8' do
+ # expected_results = [@end_date.strftime("%Y-%m-%d"), project, sha, @key4.key, "Hello, world", "IT", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Rebecca (#{@translator.id})", "hello!", "bye!", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Mark (#{@reviewer.id})", "", nil]
+ # expect(result[8]).to eql expected_results
+ # end
+
+ # it 'has the expected row 9' do
+ # expected_results = [@end_date.strftime("%Y-%m-%d"), @asset_project.name, asset, @key6.key, "Hello, world", "IT", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Rebecca (#{@translator.id})", "hello!", "bye!", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Mark (#{@reviewer.id})", "", nil]
+ # expect(result[9]).to eql expected_results
+ # end
+
+ # it 'has the expected row 10' do
+ # expected_results = [@end_date.strftime("%Y-%m-%d"), @article_project.name, article, @key5.key, "Hello, world", "IT", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Rebecca (#{@translator.id})", "hello!", "bye!", @end_date.strftime("%Y-%m-%d 08:00:00 UTC"), "Mark (#{@reviewer.id})", "", nil]
+ # expect(result[10]).to eql expected_results
+ # end
it 'has the expected row 11' do
expect(result[11]).to eql nil
diff --git a/spec/lib/sorting_helper_spec.rb b/spec/lib/sorting_helper_spec.rb
deleted file mode 100644
index 383ccd64..00000000
--- a/spec/lib/sorting_helper_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright 2016 Square Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-require 'rails_helper'
-
-RSpec.describe SortingHelper do
- describe "#order_by_elasticsearch_result_order" do
- before do
- project = FactoryBot.create(:project)
- @commit1 = FactoryBot.create(:commit, project: project)
- @commit2 = FactoryBot.create(:commit, project: project)
- @commit3 = FactoryBot.create(:commit, project: project)
-
- @commits = [@commit1, @commit2, @commit3]
- @es_objects = [double('es_result', id: @commit2.id),
- double('es_result', id: @commit3.id),
- double('es_result', id: @commit1.id)]
- end
-
- it "orders items with the elasticsearch results order" do
- ordered_commits = SortingHelper.order_by_elasticsearch_result_order(@commits, @es_objects)
-
- expect(ordered_commits).to eql([@commit2, @commit3, @commit1])
- end
- end
-end
diff --git a/spec/lib/translation_validator/source_fencer_validator_spec.rb b/spec/lib/translation_validator/source_fencer_validator_spec.rb
new file mode 100644
index 00000000..c54e6ef5
--- /dev/null
+++ b/spec/lib/translation_validator/source_fencer_validator_spec.rb
@@ -0,0 +1,52 @@
+# Copyright 2014 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'rails_helper'
+
+RSpec.describe TranslationValidator::SourceFencerValidator do
+ let(:source_copy) { "this is a {{good-variable-name}}"}
+
+ before :each do
+ @project = FactoryBot.create(:project)
+ @commit = FactoryBot.create(:commit, project: @project, author: 'Foo Bar', author_email: "foo@example.com")
+
+ @key = FactoryBot.create(:key, project: @project, fencers: ['Mustache'], source_copy: source_copy)
+ @commit.keys << @key
+
+ @translation = FactoryBot.create(:translation, key: @key, translated: false, copy: nil)
+ @key.translations << @translation
+ end
+
+ describe "#run" do
+ subject { TranslationValidator::SourceFencerValidator.new(@commit).run }
+
+ context "without suspicious source strings" do
+ it "should not send email" do
+ expect_any_instance_of(FencerValidationMailer).to_not receive(:suspicious_source_found)
+
+ subject
+ end
+ end
+
+ context "with suspicious source strings" do
+ let(:source_copy) { "this is a {{bad variable name}}" }
+
+ it "should send email" do
+ expect_any_instance_of(FencerValidationMailer).to receive(:suspicious_source_found)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/translation_validator/translation_auto_migration_spec.rb b/spec/lib/translation_validator/translation_auto_migration_spec.rb
new file mode 100644
index 00000000..3b663446
--- /dev/null
+++ b/spec/lib/translation_validator/translation_auto_migration_spec.rb
@@ -0,0 +1,76 @@
+# Copyright 2014 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'rails_helper'
+
+RSpec.describe TranslationValidator::TranslationAutoMigration do
+ let(:source_copy) { "this is a testing string"}
+
+ before :each do
+ @project = FactoryBot.create(:project)
+ @commit = FactoryBot.create(:commit, project: @project, author: 'Foo Bar', author_email: "foo@example.com")
+
+ @key = FactoryBot.create(:key, project: @project, source_copy: source_copy)
+ @commit.keys << @key
+
+ @translation = FactoryBot.create(:translation, key: @key, source_copy: source_copy, copy: nil, translated: false, source_rfc5646_locale: 'en', rfc5646_locale: 'en-CA')
+ @key.translations << @translation
+ end
+
+ describe "#run" do
+ subject { TranslationValidator::TranslationAutoMigration.new(@commit).run }
+
+ context "without any matched translation" do
+ it "should not migrate TM" do
+ subject
+
+ @translation.reload
+ expect(@translation.translated).to be_falsey
+ expect(@translation.approved).to be_falsey
+ expect(@translation.copy).to be_nil
+ expect(@translation.notes).to be_nil
+ end
+ end
+
+ context "with non-approved translation" do
+ it "should not migrate TM" do
+ key = FactoryBot.create(:key, project: @project, original_key: '1', source_copy: source_copy)
+ FactoryBot.create(:translation, key: key, source_copy: source_copy, copy: 'hello', translated: true, approved: nil, source_rfc5646_locale: 'en', rfc5646_locale: 'en-CA')
+
+ subject
+
+ @translation.reload
+ expect(@translation.translated).to be_falsey
+ expect(@translation.approved).to be_falsey
+ expect(@translation.copy).to be_nil
+ expect(@translation.notes).to be_nil
+ end
+ end
+
+ context "with approved translation" do
+ it "should migrate TM" do
+ key = FactoryBot.create(:key, project: @project, original_key: '1', source_copy: source_copy)
+ translation = FactoryBot.create(:translation, key: key, source_copy: source_copy, copy: 'hello', translated: true, approved: true, source_rfc5646_locale: 'en', rfc5646_locale: 'en-CA')
+
+ subject
+
+ @translation.reload
+ expect(@translation.translated).to be_truthy
+ expect(@translation.approved).to be_falsey
+ expect(@translation.copy).to eq('hello')
+ expect(@translation.notes).to eq("AutoTM:#{translation.id} ")
+ end
+ end
+ end
+end
diff --git a/spec/mailers/comment_mailer_spec.rb b/spec/mailers/comment_mailer_spec.rb
index 9943aa2d..83bb5b6c 100644
--- a/spec/mailers/comment_mailer_spec.rb
+++ b/spec/mailers/comment_mailer_spec.rb
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-require "spec_helper"
+require "rails_helper"
RSpec.describe CommentMailer do
context "[comment_created]" do
diff --git a/spec/mailers/commit_mailer_spec.rb b/spec/mailers/commit_mailer_spec.rb
index 2b25fe11..c11d6136 100644
--- a/spec/mailers/commit_mailer_spec.rb
+++ b/spec/mailers/commit_mailer_spec.rb
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-require "spec_helper"
+require "rails_helper"
RSpec.describe CommitMailer do
describe "#notify_submitter_of_import_errors" do
diff --git a/spec/mailers/fencer_validation_mailer_spec.rb b/spec/mailers/fencer_validation_mailer_spec.rb
new file mode 100644
index 00000000..344ce82e
--- /dev/null
+++ b/spec/mailers/fencer_validation_mailer_spec.rb
@@ -0,0 +1,39 @@
+# Copyright 2014 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require "rails_helper"
+
+RSpec.describe FencerValidationMailer do
+ describe "#suspicious_source_found" do
+ before :each do
+ @project = FactoryBot.create(:project)
+ @commit = FactoryBot.create(:commit, project: @project, author: 'Foo Bar', author_email: "foo@example.com")
+ @key = FactoryBot.create(:key, project: @project)
+
+ @translation = FactoryBot.create(:translation, translated: false, key: @key)
+ @key.reload
+
+ ActionMailer::Base.deliveries.clear
+ end
+
+ it 'sends an email to shuttle and localization teams' do
+ fake_suspicious_keys_errors = [[@key, 'Violate fencers: FakeFencer']]
+
+ mail = FencerValidationMailer.suspicious_source_found(@commit, fake_suspicious_keys_errors).deliver_now
+
+ expect(mail.subject).to eq('[NO ACTION REQUIRED] [Shuttle Staging] Found suspicious source strings')
+ expect(mail.body).to include('Violate fencers: FakeFencer')
+ end
+ end
+end
diff --git a/spec/mailers/issue_mailer_spec.rb b/spec/mailers/issue_mailer_spec.rb
index a27c7bc0..0f236b3d 100644
--- a/spec/mailers/issue_mailer_spec.rb
+++ b/spec/mailers/issue_mailer_spec.rb
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-require "spec_helper"
+require "rails_helper"
RSpec.describe IssueMailer do
context "issue_created" do
diff --git a/spec/mailers/screenshot_mailer_spec.rb b/spec/mailers/screenshot_mailer_spec.rb
index 0034f490..eb6aff96 100644
--- a/spec/mailers/screenshot_mailer_spec.rb
+++ b/spec/mailers/screenshot_mailer_spec.rb
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-require "spec_helper"
+require "rails_helper"
RSpec.describe ScreenshotMailer do
describe 'request_screenshot' do
diff --git a/spec/mediators/translation_update_mediator_spec.rb b/spec/mediators/translation_update_mediator_spec.rb
index 8ef1a0aa..5fba216e 100644
--- a/spec/mediators/translation_update_mediator_spec.rb
+++ b/spec/mediators/translation_update_mediator_spec.rb
@@ -43,7 +43,7 @@
expect(@key.reload).to_not be_ready
end
- it "updates a single translation; approve translation if translator is a reviewer; key becomes ready" do
+ it "updates a single translation; approve not-translated string" do
TranslationUpdateMediator.new(@fr_translation, reviewer, @params).update!
expect(@fr_translation.copy).to eql("test copy")
expect(@fr_translation.translator).to eql(reviewer)
@@ -52,6 +52,17 @@
expect(@key.reload).to be_ready
end
+ it "updates a single translation; approve translated string" do
+ @fr_translation.update(copy: 'this is old translation', translator: translator)
+
+ TranslationUpdateMediator.new(@fr_translation, reviewer, @params).update!
+ expect(@fr_translation.copy).to eql("test copy")
+ expect(@fr_translation.translator).to eql(translator)
+ expect(@fr_translation.approved).to be_truthy
+ expect(@fr_translation.reviewer).to eql(reviewer)
+ expect(@key.reload).to be_ready
+ end
+
it "updates a single translation; review_date is set if the translator is a reviewer" do
TranslationUpdateMediator.new(@fr_translation, reviewer, @params).update!
expect(@fr_translation.review_date).to_not be_nil
@@ -141,13 +152,11 @@
end
it "sets the tm_match" do
- reset_elastic_search
-
# create a translation that will be used for lookup for tm_match
FactoryBot.create(:translation, copy: "test", source_copy: 'test', approved: true, translated: true, rfc5646_locale: 'fr')
# finding the fuzzy match for a translation requires elasticsearch, update the index since we just created a translation
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
TranslationUpdateMediator.new(@fr_translation, reviewer, @params).update!
@@ -224,6 +233,7 @@
translation1 = FactoryBot.create(:translation, key: key, rfc5646_locale: 'fr')
translation2 = FactoryBot.create(:translation, key: key, rfc5646_locale: 'fr-CA')
translation3 = FactoryBot.create(:translation, key: key, rfc5646_locale: 'fr-FR')
+ key.reload
expect(TranslationUpdateMediator.multi_updateable_translations_to_locale_associations_hash(translation1)).to eql(translation2 => la1, translation3 => la2)
end
@@ -236,6 +246,7 @@
translation1 = FactoryBot.create(:translation, key: key, source_rfc5646_locale: 'en', rfc5646_locale: 'en-XX')
translation2 = FactoryBot.create(:translation, key: key, source_rfc5646_locale: 'en', rfc5646_locale: 'en')
translation3 = FactoryBot.create(:translation, key: key, source_rfc5646_locale: 'en', rfc5646_locale: 'en-YY')
+ key.reload
expect(TranslationUpdateMediator.multi_updateable_translations_to_locale_associations_hash(translation1)).to eql(translation3 => la2)
end
end
@@ -251,6 +262,7 @@
it "returns the Translation objects corresponding to the user provided copyToLocales param; doesn't add an error if all are valid" do
fr_CA_translation = FactoryBot.create(:translation, key: @key, copy: nil, translator: nil, rfc5646_locale: 'fr-CA')
fr_FR_translation = FactoryBot.create(:translation, key: @key, copy: nil, translator: nil, rfc5646_locale: 'fr-FR')
+ @key.reload
mediator = TranslationUpdateMediator.new(@fr_translation, translator, ActionController::Parameters.new( copyToLocales: %w(fr-CA fr-FR) ))
expect(mediator.send(:translations_that_should_be_multi_updated)).to eql([fr_CA_translation, fr_FR_translation])
expect(mediator.success?).to be_truthy
@@ -266,6 +278,7 @@
it "adds an error to the mediator if one of the locales user wanted to copy to is not valid because there is no Translation in one of those locales" do
fr_CA_translation = FactoryBot.create(:translation, key: @key, copy: nil, translator: nil, rfc5646_locale: 'fr-CA')
+ @key.reload
mediator = TranslationUpdateMediator.new(@fr_translation, translator, ActionController::Parameters.new( copyToLocales: %w(fr-CA fr-FR)))
mediator.send(:translations_that_should_be_multi_updated)
expect(mediator.success?).to be_falsey
diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb
index bc804459..d94f07e0 100644
--- a/spec/models/article_spec.rb
+++ b/spec/models/article_spec.rb
@@ -377,7 +377,11 @@
allow(article).to receive(:import!) # prevent the import because we want to create the related keys manually
article.save!
- article.import_batch.jobs { regenerate_elastic_search_indexes }
+ article.import_batch.jobs do
+ CommitsIndex.reset!
+ KeysIndex.reset!
+ TranslationsIndex.reset!
+ end
bid = article.import_batch_id
article.import_batch # this should re-use the existing batch
expect(article.import_batch_id).to eql(bid)
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index cca1c2ab..8b0220ee 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -487,4 +487,21 @@
end
end
end
+
+ describe "#elastic_search" do
+ let!(:project) { FactoryBot.create(:project, repository_url: "https://github.com/example/my-project.git") }
+ let!(:commit) { FactoryBot.create(:commit, revision: 'abc123', project: project) }
+
+ it "should appear in both ES and DB after creation" do
+ expect(Commit.where(id: commit.id).count).to eq(1)
+ expect(CommitsIndex.query(term: { id: commit.id }).count).to eq(1)
+ end
+
+ it "should disappear in both ES and DB after destroy" do
+ commit.destroy
+
+ expect(Commit.where(id: commit.id).count).to eq(0)
+ expect(CommitsIndex.query(term: { id: commit.id }).count).to eq(0)
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 9764b7c9..47ce89f4 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -208,7 +208,7 @@
FactoryBot.create :translation, key: key1, rfc5646_locale: 'fr', approved: true, source_copy: 'yes', copy: 'oui'
# finding the fuzzy match for a translation requires elasticsearch, update the index since we just created a translation
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
# this is a new key
key2 = FactoryBot.create(:key, project: project, source_copy: 'yes')
@@ -409,6 +409,8 @@
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'de', key: key
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'de', key: excluded, copy: nil
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'fr', key: excluded, copy: nil
+ key.reload
+ excluded.reload
key.remove_excluded_pending_translations
excluded.remove_excluded_pending_translations
@@ -428,6 +430,8 @@
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'de', key: included, copy: nil
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'de', key: excluded, copy: nil
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'fr', key: excluded, copy: "hello!"
+ included.reload
+ excluded.reload
included.remove_excluded_pending_translations
excluded.remove_excluded_pending_translations
@@ -447,6 +451,8 @@
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'de', key: key, copy: nil
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'de', key: excluded, copy: nil
FactoryBot.create :translation, source_rfc5646_locale: 'en-US', rfc5646_locale: 'fr', key: excluded, copy: "hello!"
+ key.reload
+ excluded.reload
key.remove_excluded_pending_translations
excluded.remove_excluded_pending_translations
@@ -608,4 +614,21 @@
expect(@key2.reload).to_not be_ready
end
end
+
+ describe "#elastic_search" do
+ let!(:project) { FactoryBot.create(:project, targeted_rfc5646_locales: {'fr' => true}, base_rfc5646_locale: 'en') }
+ let!(:key) { FactoryBot.create(:key, project: project) }
+
+ it "should appear in both ES and DB after creation" do
+ expect(Key.where(id: key.id).count).to eq(1)
+ expect(KeysIndex.query(term: { id: key.id }).count).to eq(1)
+ end
+
+ it "should disappear in both ES and DB after destroy" do
+ key.destroy
+
+ expect(Key.where(id: key.id).count).to eq(0)
+ expect(KeysIndex.query(term: { id: key.id }).count).to eq(0)
+ end
+ end
end
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
new file mode 100644
index 00000000..92088eed
--- /dev/null
+++ b/spec/models/report_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Report, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb
index eea51c58..313ff051 100644
--- a/spec/models/translation_spec.rb
+++ b/spec/models/translation_spec.rb
@@ -257,30 +257,6 @@
end
end
- describe "#batch_refresh_elastic_search" do
- it "refreshes the ElasticSearch index (section_active field in this test) of Article's Translations" do
- allow_any_instance_of(Article).to receive(:import!) # prevent auto import
- reset_elastic_search
-
- article = FactoryBot.create(:article)
- section = FactoryBot.create(:section, article: article, active: true)
- key = FactoryBot.create(:key, section: section, index_in_section: 0, project: article.project)
- translation = FactoryBot.create(:translation, key: key)
-
- regenerate_elastic_search_indexes
-
- expect(Elasticsearch::Model.search(section_active_query(true), Translation).results.first.id.to_i).to eql(translation.id)
- expect(Elasticsearch::Model.search(section_active_query(false), Translation).results.first).to be_nil
-
- section.update! active: false
- Translation.batch_refresh_elastic_search(article)
- regenerate_elastic_search_indexes
-
- expect(Elasticsearch::Model.search(section_active_query(true), Translation).results.first).to be_nil
- expect(Elasticsearch::Model.search(section_active_query(false), Translation).results.first.id.to_i).to eql(translation.id)
- end
- end
-
describe "#shared?" do
let(:article) { FactoryBot.create(:article) }
let(:section) { FactoryBot.create(:section, article: article, active: true) }
@@ -313,6 +289,40 @@
end
end
+ describe "#destroy" do
+ it "cleans all child objects properly" do
+ translation = FactoryBot.create(:translation)
+ translation_change = FactoryBot.create(:translation_change, translation: translation)
+ edit_reason = FactoryBot.create(:edit_reason, translation_change: translation_change)
+
+ expect(Translation.where(id: translation.id).count).to eq(1)
+ expect(TranslationChange.where(id: translation_change.id).count).to eq(1)
+ expect(EditReason.where(id: edit_reason.id).count).to eq(1)
+
+ translation.destroy
+
+ expect(Translation.where(id: translation.id).count).to eq(0)
+ expect(TranslationChange.where(id: translation_change.id).count).to eq(0)
+ expect(EditReason.where(id: edit_reason.id).count).to eq(0)
+ end
+ end
+
+ describe "#elastic_search" do
+ let!(:translation) { FactoryBot.create(:translation) }
+
+ it "should appear in both ES and DB after creation" do
+ expect(Translation.where(id: translation.id).count).to eq(1)
+ expect(TranslationsIndex.query(term: { id: translation.id }).count).to eq(1)
+ end
+
+ it "should disappear in both ES and DB after destroy" do
+ translation.destroy
+
+ expect(Translation.where(id: translation.id).count).to eq(0)
+ expect(TranslationsIndex.query(term: { id: translation.id }).count).to eq(0)
+ end
+ end
+
def section_active_query(active)
{
filter: {
diff --git a/spec/presenters/locale_projects_show_presenter_spec.rb b/spec/presenters/locale_projects_show_presenter_spec.rb
index e600e4de..3f5a9e66 100644
--- a/spec/presenters/locale_projects_show_presenter_spec.rb
+++ b/spec/presenters/locale_projects_show_presenter_spec.rb
@@ -48,6 +48,14 @@
end
end
+ describe "#selected_group" do
+ it "returns the selected Group if there is one" do
+ group = FactoryBot.create(:group, name: "hello", display_name: 'test display name')
+ presenter = LocaleProjectsShowPresenter.new(group.project, @user, { group: group.display_name })
+ expect(presenter.selected_group).to eql(group)
+ end
+ end
+
describe "#selectable_sections" do
before :each do
@project = FactoryBot.create(:project)
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index a3c7b066..16a6fef2 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -8,6 +8,7 @@
# Add additional requires below this line. Rails is not loaded until this point!
require 'sidekiq/testing'
require 'paperclip/matchers'
+require 'chewy/rspec'
Dir[Rails.root.join('spec/spec_support/**/*.rb')].each { |f| require f }
@@ -88,8 +89,17 @@
DatabaseCleaner.strategy = :deletion
DatabaseCleaner.clean_with :truncation
end
+
config.around(:each) do |example|
- DatabaseCleaner.cleaning { example.run }
+ DatabaseCleaner.start
+ example.run
+
+ # Delete the table relationship individually.
+ models = [AssetsKey, ArticleGroup, BlobsCommit, TranslationChange]
+ models.each { |model| model.all.destroy_all }
+
+ DatabaseCleaner.clean
+ [CommitsIndex, KeysIndex, TranslationsIndex].each { |index| index.reset! }
end
# Paperclip
@@ -115,22 +125,7 @@
end
# ElasticSearch
- config.before(:suite) { reset_elastic_search }
-end
-
-def reset_elastic_search
- ActiveRecord::Base.subclasses.each do |model|
- next unless model.respond_to?(:__elasticsearch__)
- model.__elasticsearch__.create_index! force: true
- model.import(force: true)
- model.__elasticsearch__.client.indices.flush(index: model.__elasticsearch__.index_name, force: true)
- end
-end
-
-def regenerate_elastic_search_indexes
- ActiveRecord::Base.subclasses.each do |model|
- next unless model.respond_to?(:__elasticsearch__)
- model.import(refresh: true)
- model.__elasticsearch__.client.indices.flush(index: model.__elasticsearch__.index_name, force: true)
+ config.before(:suite) do
+ Chewy::strategy :urgent
end
end
diff --git a/spec/services/fuzzy_match_translations_finder_spec.rb b/spec/services/fuzzy_match_translations_finder_spec.rb
index 9757cebd..cf50d1d0 100644
--- a/spec/services/fuzzy_match_translations_finder_spec.rb
+++ b/spec/services/fuzzy_match_translations_finder_spec.rb
@@ -25,7 +25,7 @@
FactoryBot.create(:translation, copy: "oui", source_copy: source_copy, approved: true, translated: true, rfc5646_locale: 'fr')
# finding the fuzzy match for a translation requires elasticsearch, update the index since we just created a translation
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
translation = FactoryBot.build(:translation, source_copy: source_copy, rfc5646_locale: 'fr')
finder = FuzzyMatchTranslationsFinder.new(source_copy, translation)
@@ -44,7 +44,7 @@
FactoryBot.create(:translation, copy: "oui monsieur ", source_copy: 'yes sir', approved: true, translated: true, rfc5646_locale: 'fr')
# finding the fuzzy match for a translation requires elasticsearch, update the index since we just created a translation
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
# create a translation that will matches the above one with 57%
translation = FactoryBot.build(:translation, source_copy: 'yes madam', rfc5646_locale: 'fr')
diff --git a/spec/services/search_translations_finder_spec.rb b/spec/services/search_translations_finder_spec.rb
index bab966d4..839194bf 100644
--- a/spec/services/search_translations_finder_spec.rb
+++ b/spec/services/search_translations_finder_spec.rb
@@ -19,7 +19,6 @@
describe "#find_translations" do
before :each do
Translation.destroy_all
- reset_elastic_search
@project = FactoryBot.create(:project, repository_url: Rails.root.join('spec', 'fixtures', 'repository.git').to_s)
@key = create_key(@project)
@translation = create_translation(@key, copy: 'some copy here', rfc5646_locale: Locale.new('de-DE').rfc5646)
@@ -27,19 +26,19 @@
end
it "should include new translation" do
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(1)
new_key = create_key(@project)
create_translation(new_key)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(2)
end
it "should filter target locales" do
create_translation(@key)
new_finder = create_finder(target_locales: [Locale.new('zh-CN'), Locale.new('ja-JP')])
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(2)
expect(new_finder.find_translations.total_count).to eq(1)
@@ -50,7 +49,7 @@
new_key = create_key(new_project)
create_translation(new_key)
new_finder = create_finder(project_id: new_project.id)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(2)
expect(new_finder.find_translations.total_count).to eq(1)
@@ -58,7 +57,15 @@
it "should filter translation id" do
new_finder = create_finder(translator_id: 15875)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
+
+ expect(@finder.find_translations.total_count).to eq(1)
+ expect(new_finder.find_translations.total_count).to eq(0)
+ end
+
+ it "should filter reviewer id" do
+ new_finder = create_finder(reviewer_id: 15875)
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(1)
expect(new_finder.find_translations.total_count).to eq(0)
@@ -67,7 +74,7 @@
it "should filter start date" do
create_translation(@key, updated_at: Time.current - 7.days)
new_finder = create_finder(start_date: Time.current - 3.days)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(2)
expect(new_finder.find_translations.total_count).to eq(1)
@@ -76,7 +83,7 @@
it "should filter end date" do
create_translation(@key, updated_at: Time.current + 7.days)
new_finder = create_finder(end_date: Time.current + 3.days)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(2)
expect(new_finder.find_translations.total_count).to eq(1)
@@ -85,7 +92,7 @@
it "should filter hidden keys" do
hidden_key = create_key(@project, hidden_in_search: true)
create_translation(hidden_key)
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(1)
end
@@ -96,7 +103,7 @@
hidden_key = create_key(@project, hidden_in_search: true)
create_translation(hidden_key)
end
- regenerate_elastic_search_indexes
+ TranslationsIndex.reset!
expect(@finder.find_translations.total_count).to eq(1)
expect(new_finder.find_translations.total_count).to eq(3)
end
diff --git a/spec/workers/article_importer_spec.rb b/spec/workers/article_importer_spec.rb
index ea5896b1..b211e7b2 100644
--- a/spec/workers/article_importer_spec.rb
+++ b/spec/workers/article_importer_spec.rb
@@ -16,7 +16,7 @@
RSpec.describe ArticleImporter do
describe "#perform" do
- before :each do
+ before do
allow_any_instance_of(Article).to receive(:import!) # prevent auto imports
@article = FactoryBot.create(:article, sections_hash: { "title" => "a", "body" => "b
c
" })
ArticleImporter.new.perform(@article.id) # first import
@@ -117,7 +117,7 @@
RSpec.describe ArticleImporter::Finisher do
describe "#on_success" do
- before :each do
+ before do
# creation triggers the initial import
@article = FactoryBot.create(:article, sections_hash: { "main" => "hello
world
" }, base_rfc5646_locale: 'en', targeted_rfc5646_locales: { 'fr' => true, 'es' => true, 'ja' => false })
expect(@article.reload.keys.count).to eql(6)
diff --git a/spec/workers/auto_importer_spec.rb b/spec/workers/auto_importer_spec.rb
index 9478cb6a..7db57ad3 100644
--- a/spec/workers/auto_importer_spec.rb
+++ b/spec/workers/auto_importer_spec.rb
@@ -17,7 +17,7 @@
RSpec.describe AutoImporter do
describe "#perform" do
context "[watched branches]" do
- it "calls ProjectAutoImporter on the projects with watched_branches, removes the watched branch if it doesn't exist" do
+ it "calls ProjectAutoImporter on the projects with watched_branches, does not remove the watched branch if it doesn't exist" do
project1 = FactoryBot.create(:project, skip_imports: (Importer::Base.implementations.map(&:ident) - %w(yaml)), repository_url: Rails.root.join('spec', 'fixtures', 'repository.git').to_s, watched_branches: %w(master non_existent_branch))
project2 = FactoryBot.create(:project, watched_branches: [])
@@ -28,7 +28,7 @@
expect { AutoImporter.new.perform }.to_not raise_error
- expect(project1.reload.watched_branches).to eql(%w(master))
+ expect(project1.reload.watched_branches).to eql(%w(master non_existent_branch))
expect(project2.reload.watched_branches).to be_blank
end
end
@@ -47,12 +47,12 @@
describe "#perform" do
context "[watched branches]" do
context "[rescue Git::CommitNotFoundError]" do
- it "removes a watched branch if the branch doesn't exist anymore" do
+ it "does not remove a watched branch if the branch doesn't exist anymore" do
project = FactoryBot.create(:project, skip_imports: (Importer::Base.implementations.map(&:ident) - %w(yaml)), repository_url: Rails.root.join('spec', 'fixtures', 'repository.git').to_s, watched_branches: %w(master non_existent_branch))
expect(project.watched_branches).to eql(%w(master non_existent_branch))
expect { AutoImporter::ProjectAutoImporter.new.perform(project.id) }.to_not raise_error
- expect(project.reload.watched_branches).to eql(%w(master))
+ expect(project.reload.watched_branches).to eql(%w(master non_existent_branch))
end
end
end
diff --git a/spec/workers/commit_importer_spec.rb b/spec/workers/commit_importer_spec.rb
index ec5b8f55..ee3b65fd 100644
--- a/spec/workers/commit_importer_spec.rb
+++ b/spec/workers/commit_importer_spec.rb
@@ -19,11 +19,10 @@
describe "#perform" do
context "[rescue Git::CommitNotFoundError]" do
it "deletes the commit when commit importer fails due to a Git::CommitNotFoundError" do
- skip 'ElasticSearch flaky - deletion throws NotFound exception'
allow_any_instance_of(Project).to receive(:find_or_fetch_git_object).and_return(nil)
project = FactoryBot.create(:project)
commit = FactoryBot.create(:commit, revision: "abc123", project: project)
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
CommitImporter.new.perform commit.id
expect { commit.reload }.to raise_error(ActiveRecord::RecordNotFound)
diff --git a/spec/workers/commits_cleaner_spec.rb b/spec/workers/commits_cleaner_spec.rb
index f766dced..20b64900 100644
--- a/spec/workers/commits_cleaner_spec.rb
+++ b/spec/workers/commits_cleaner_spec.rb
@@ -29,9 +29,8 @@
end
it "should destory all commits" do
- skip 'ElasticSearch flaky - deletion throws NotFound exception'
FactoryBot.create_list :commit, 3, project: @project
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
expect(@project.commits.count).to eq(3)
@commits_cleaner.destroy_dangling_commits
@@ -56,10 +55,9 @@
describe "#destroy_old_commits_which_errored_during_import" do
it "should destroy all errored commits older than 2 days during import" do
- skip 'ElasticSearch flaky - deletion throws NotFound exception'
FactoryBot.create_list(:commit, 3, project: @project, created_at: 3.days.ago).
each { |c| c.add_import_error StandardError.new("This is a fake error") }
- regenerate_elastic_search_indexes
+ CommitsIndex.reset!
expect(@project.commits.count).to eq(3)
@commits_cleaner.destroy_old_commits_which_errored_during_import
diff --git a/spec/workers/post_loading_checker_spec.rb b/spec/workers/post_loading_checker_spec.rb
new file mode 100644
index 00000000..d560d604
--- /dev/null
+++ b/spec/workers/post_loading_checker_spec.rb
@@ -0,0 +1,31 @@
+# Copyright 2014 Square Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'rails_helper'
+
+RSpec.describe PostLoadingChecker do
+ describe "#perform" do
+ before :each do
+ @project = FactoryBot.create(:project)
+ @commit = FactoryBot.create(:commit, project: @project, author: 'Foo Bar', author_email: "foo@example.com")
+ end
+
+ it "calls each validator" do
+ expect_any_instance_of(TranslationValidator::SourceFencerValidator).to receive(:run)
+ expect_any_instance_of(TranslationValidator::TranslationAutoMigration).to receive(:run)
+
+ PostLoadingChecker.new.perform('commit', @commit.id)
+ end
+ end
+end
diff --git a/spec/workers/stash_webhook_pinger_spec.rb b/spec/workers/stash_webhook_pinger_spec.rb
index f2014f25..bf047b21 100644
--- a/spec/workers/stash_webhook_pinger_spec.rb
+++ b/spec/workers/stash_webhook_pinger_spec.rb
@@ -16,6 +16,9 @@
RSpec.describe StashWebhookPinger do
include Rails.application.routes.url_helpers
+ let(:http_response_code) { 204 }
+ let(:http_response) { Net::HTTPResponse.new(1.0, http_response_code, "OK") }
+
before(:each) do
allow(Kernel).to receive(:sleep)
allow(HTTParty).to receive(:post)
@@ -36,7 +39,7 @@
expect(HTTParty).to receive(:post).with(
"#{url}/#{@commit.revision}",
anything()
- ).exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times
+ ).exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times.and_return(http_response)
subject.perform(@commit.id)
end
@@ -50,7 +53,7 @@
expected_state = @commit.ready? ? 'SUCCESSFUL' : 'INPROGRESS'
expect(commit_state).to eql(expected_state)
@commit.update_column :ready, !@commit.ready
- end.exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times
+ end.exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times.and_return(http_response)
subject.perform(@commit.id)
end
@@ -65,6 +68,20 @@
expect(HTTParty).not_to receive(:post)
expect { subject.perform(@commit.id) }.to raise_error(Project::NotLinkedToAGitRepositoryError)
end
+
+ context "with failure http response code" do
+ let(:http_response_code) { 400 }
+
+ it "when http returns failure" do
+ url = "http://www.example.com"
+ @commit.project.stash_webhook_url = url
+ @commit.project.save!
+
+ expect(HTTParty).to receive(:post).and_return(http_response)
+
+ expect { subject.perform(@commit.id) }.to raise_error(RuntimeError, "[StashWebhookHelper] Failed to ping stash for commit #{@commit.id}, revision: #{@commit.revision}, code: #{http_response_code}")
+ end
+ end
end
context "on_create" do
@@ -84,7 +101,7 @@
protocol: Shuttle::Configuration.default_url_options['protocol'] || 'http'),
state: 'INPROGRESS',
description: 'Currently loading',
- }.to_json))
+ }.to_json)).exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times.and_return(http_response)
@commit.save!
end
end
@@ -92,6 +109,8 @@
context "on_update" do
before(:each) do
+ expect(HTTParty).to receive(:post).exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times.and_return(http_response)
+
@commit = FactoryBot.build(:commit, ready: false, loading: true)
@url = "http://www.example.com"
@commit.project.stash_webhook_url = @url
@@ -110,7 +129,7 @@
protocol: Shuttle::Configuration.default_url_options['protocol'] || 'http'),
state: 'INPROGRESS',
description: 'Currently translating',
- }.to_json))
+ }.to_json)).exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times.and_return(http_response)
@commit.loading = false
# force commit not to be ready
@@ -130,7 +149,7 @@
protocol: Shuttle::Configuration.default_url_options['protocol'] || 'http'),
state: 'SUCCESSFUL',
description: 'Translations completed',
- }.to_json))
+ }.to_json)).exactly(StashWebhookHelper::DEFAULT_NUM_TIMES).times.and_return(http_response)
@commit.loading = false
@commit.ready = true # redundant since CSR will do this anyway
@commit.save!
diff --git a/square_primary_certificate_authority_g2.crt b/square_primary_certificate_authority_g2.crt
new file mode 100644
index 00000000..71fa6fe5
--- /dev/null
+++ b/square_primary_certificate_authority_g2.crt
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGEzCCA/ugAwIBAgIQbOEgSriEcg18Vw6n58DlXDANBgkqhkiG9w0BAQ0FADCB
+hDEyMDAGA1UEAwwpU3F1YXJlIFByaW1hcnkgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
+IC0gRzIxFDASBgNVBAoMC1NxdWFyZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlh
+MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQswCQYDVQQGEwJVUzAeFw0xMjA1MDMx
+NzQ4NDVaFw0zNzA0MjcxNzQ4NDlaMIGEMTIwMAYDVQQDDClTcXVhcmUgUHJpbWFy
+eSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjEUMBIGA1UECgwLU3F1YXJlIElu
+Yy4xEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28x
+CzAJBgNVBAYTAlVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1dK2
+hdAwnTyw+ja7Jm1r2ft3OOp4vX5ATIvt53OSUhxjWWZXmLYoIC2yXKg6ru0D6Pw+
+pc9pOHTXRNPRaCiMEZ+AzwdMHHgGpRZGTN+e5HUIVfYpJE9mcs1Q9KX/GWw05uAC
+xGyk7sbBqXSj/aJB6itydxhb2FXiRmn+QktYfjjjFgcPD2LK1YbuDRia7rkUoLLu
+EZQk0AkuNMNJ9TmdNFBsEb1wiZCYfYx1bF8d1adhgRLbeWLbV5aueVBeczkOcO3s
+XqvyoJSNq1AYx6T2xdDS2Pi2oN6//UnRk5h22M+ROfeVSGyIX7pAiJ8QojD0ajNn
+TfESy1FwQnHIadAxeqgkEZezr5NhzzgAczL0L0acXlve6sIPdaIREjn4XNxJS5W9
+WUdzqux64dVcGTLNshvArS/yLbCSC9M5t4M+OAI+FPt4wn8Uk3auEWvIi5jfWwwY
+nxitejb2kT2rKSb/ETji1lZorQEnlemqylEWfzG21P0weUa+ghKlYk+F2d5nJBly
+YakFgheSnzs9zoz1LISDBP1v8GY+xJJhZIKBA9np4HCGMUx5nXdj+ohXvN3oSzsU
+p6foRnaiRpY5vfIy3LITVdAZ3R/BJFUcjXXSH9ddI2tPyo470l+L5qTn2LAXGOzH
+wRaGDeFnJNU768PC7CckcqsiUkHouLnBNLz7g3UCAwEAAaN/MH0wDAYDVR0TBAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFH0MAs3/FcUHwggOwfwOFamH
+kHDbMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAfBgNVHSMEGDAWgBR9
+DALN/xXFB8IIDsH8DhWph5Bw2zANBgkqhkiG9w0BAQ0FAAOCAgEAKB8LQZme3mnV
+biFZknUt6htb/iSBYtT+45CDnM/q57R7ucme59CHiOAaAyVp8TlS5GfMLfdWaj2q
+wvdaEtLMunIco8qmJ52uQ7omi5SC0hqru8x1CcOdddiPeETeF5Ixg5Oa+uwsLyss
+ez+mkm0+jrUZaAQUd4Myhjb0qKZWv2gCr5pBoouwelThz4TTFW95Mm1zkqoxuiHi
+w/17w9ggiQA+cgb+/6cTv2HRdTTY36FLePATZa9hd6YDA8j7cDFdUmmMMeTTm7+1
+YKDgH2S44v2C28E4Kv9/ip5thRLirkiyIsXXF+And62Vm1O0q2CK7rsKyhJW4gSn
+8oTa7RAq2mABfFEIkVNjhNY3xE8Ij1D0FK7a5mo7G3xLJSRbcMvyqlQER4d32/uT
+/aN+xpyO8ug8QYLpoPaZDoa06L19IWkoz4CI152ZBOwPVjX38jenoNEpmvwroJ4Y
+Wr8VNy6xl95GjkZXp9lwYLByM5pt3S7+d2L9xUMZATC8tR4vgdK6GeUbj+O33IHT
+0Mx3+ZSh03C4/WdHKQPOWz0coBjMf9/1zrLrYSHmMXM/AishIzq8iPF6RtaFjDjQ
+fSJ/Zcz6z3KLFO4oHE6n5D284XuZoiCovHJT1NZGRPgBoQ5mlvTrLnX+DSufesLs
+8XVOleJ/4+k35H8d59s9vykPpVJxYa0=
+-----END CERTIFICATE-----
diff --git a/square_service_authority_g2.crt b/square_service_authority_g2.crt
new file mode 100644
index 00000000..aaf36811
--- /dev/null
+++ b/square_service_authority_g2.crt
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE6DCCAtCgAwIBAgIJAK17JkYW1k4SMA0GCSqGSIb3DQEBCwUAMIGEMTIwMAYD
+VQQDDClTcXVhcmUgUHJpbWFyeSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjEU
+MBIGA1UECgwLU3F1YXJlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNV
+BAcMDVNhbiBGcmFuY2lzY28xCzAJBgNVBAYTAlVTMB4XDTE2MDQyMTIxMzczN1oX
+DTE5MTAyMzIxMzczN1owfTErMCkGA1UEAwwiU3F1YXJlIFNlcnZpY2UgQXV0aG9y
+aXR5IC0gRzIgMjAxNTEUMBIGA1UECgwLU3F1YXJlIEluYy4xEzARBgNVBAgMCkNh
+bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xCzAJBgNVBAYTAlVTMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1mZV8i8jLplpIqbIOT/CJ2Sw
+AvhoGx3OfrTGRMemGtKQ3Ne0qa7BeyLeIG5Hd2dv5IkNEdyNuP0zH73ji0SB+MdC
+RZPRa0h/1zRWdsD1FGMvhOP0yowMRKVMAHZTSMzCbJJW8qVFe61uNzWPXuxwSfy7
+gbjPcNRbR1o1AJPELuqNHfCEinL93mIvvLNM2u8w889IdQJVY9a0hQaKsIMNwh3G
+PcEoxsZbNPUjS6nseX1lAHFVd51qcU3zxpNXYxzH76Eqj/ltXgYmkggI7GZs6jDk
+9TvjwfU1hVoB+mCydXlhJAZkQIewnsDbHU+jH3Hx2vNvovTdGw6X+AhTI4NsAQID
+AQABo2MwYTAdBgNVHQ4EFgQUeNr6yOZXK2lPaoqKm8XiyNlZbAgwDwYDVR0TAQH/
+BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHwYDVR0jBBgwFoAUfQwCzf8VxQfCCA7B
+/A4VqYeQcNswDQYJKoZIhvcNAQELBQADggIBAIxUdCPzazg9rj9AxRy9kUdExc73
+/Wh5HecEECyQOWI/+wPZAyGFseMpGYT+4c7Y/hvBYYI2UHhD4+3knZmgh69wI9g2
+Drmzo5KSHK6aQVofPHdLVUPid+QfI2NvaJfv2KK2wJuBKR0JueTBBaml8L8jW/aA
+m2MhCW6X5PFS8rS8S5FtL1a2AHsHD31xgGM3/Ku7JJOuYcE0jpzlX3Tk/LzpchuV
+tSdJKBRbP14q/U/2IDzMLJ168plfbo7QFwkq7HOrgNMCJJJcRHoZZOjHv3NalPlw
+CM1QiM1uqVgCna33eA/W6PFNGn6iXS+W5Pdh3p7n1hMB6UTmnuB4l0n8eQuZU/SJ
+J14D6VNPVDfsa1y0rDbqx7WuJkVwDUK1JJhU7cdBHy4KjInMI7niaHdMXqt/Mn4A
+4Lmjg0W99zqda9gCVskKXlDne/9N2Z8LIhPePv9iRUCVV/riuy9Yhh3O7y+pYB+f
+A8GhlglC/B8Aps26YS8/Mlj/3l/uR1m/Hw73+h3IQCEWncwmX8TdCuHRpMbuZQLc
+OaYFMoikk+dBs32c4x4wD/FOoY/uBmFzTc6I96ZD58JKn3yG+0i0/fUMGsoBgRWt
+95o2Nq3peW86Htz8OjwHXEmwiU6LBKa2uBLUQ7d0Mwn+55Hku0EmkXiHMCR8F8cY
+mtqI2NT3nLrtwbSl
+-----END CERTIFICATE-----