TextTractor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +66 -0
- data/Rakefile +6 -0
- data/assets/images/blankpad.png +0 -0
- data/assets/images/stale.png +0 -0
- data/assets/images/translated.png +0 -0
- data/assets/images/untranslated.png +0 -0
- data/assets/js/application.js +38 -0
- data/assets/js/jquery.js +16 -0
- data/assets/js/jquery.pjax.js +188 -0
- data/config.ru +20 -0
- data/lib/text_tractor.rb +31 -0
- data/lib/text_tractor/api_server.rb +93 -0
- data/lib/text_tractor/base.rb +25 -0
- data/lib/text_tractor/config.rb +48 -0
- data/lib/text_tractor/phrase.rb +67 -0
- data/lib/text_tractor/projects.rb +202 -0
- data/lib/text_tractor/ui_server.rb +152 -0
- data/lib/text_tractor/users.rb +49 -0
- data/lib/text_tractor/users_spec.rb +10 -0
- data/lib/text_tractor/version.rb +3 -0
- data/spec/api_server_spec.rb +224 -0
- data/spec/config_spec.rb +56 -0
- data/spec/phrase_spec.rb +71 -0
- data/spec/project_spec.rb +292 -0
- data/spec/spec_helper.rb +51 -0
- data/spec/ui_server/authentication_spec.rb +60 -0
- data/spec/ui_server/project_management_spec.rb +103 -0
- data/spec/ui_server/project_viewing_spec.rb +137 -0
- data/spec/ui_server_spec.rb +6 -0
- data/spec/users_spec.rb +123 -0
- data/text_tractor.gemspec +33 -0
- data/views/blurbs/_blurb.haml +1 -0
- data/views/blurbs/edit.haml +5 -0
- data/views/blurbs/value.haml +9 -0
- data/views/index.haml +13 -0
- data/views/layout.haml +26 -0
- data/views/projects/getting_started.haml +16 -0
- data/views/projects/new.haml +18 -0
- data/views/projects/show.haml +23 -0
- data/views/styles.scss +218 -0
- data/views/users.haml +29 -0
- data/watchr.rb +8 -0
- metadata +225 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module TextTractor
|
4
|
+
module Users
|
5
|
+
class DuplicateUserError < Exception; end
|
6
|
+
|
7
|
+
def self.redis
|
8
|
+
TextTractor.redis
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.all
|
12
|
+
redis.smembers("users").collect { |u| JSON.parse(redis.get("users:#{u}")) }.sort { |a, b| a["name"] <=> b["name"] }
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.exists?(username)
|
16
|
+
redis.sismember("users", username)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.authenticate(username, password)
|
20
|
+
redis.sismember("user_hashes", hash_user(username, password))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.create(attributes = {})
|
24
|
+
attributes = TextTractor.stringify_keys(attributes)
|
25
|
+
|
26
|
+
password = attributes.delete("password")
|
27
|
+
attributes["superuser"] ||= false
|
28
|
+
|
29
|
+
if redis.setnx("users:#{attributes["username"]}", attributes.to_json)
|
30
|
+
redis.sadd("users", attributes["username"])
|
31
|
+
redis.sadd("user_hashes", hash_user(attributes["username"], password))
|
32
|
+
else
|
33
|
+
raise DuplicateUserError.new
|
34
|
+
end
|
35
|
+
|
36
|
+
attributes
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.get(username)
|
40
|
+
raw = redis.get("users:#{username}")
|
41
|
+
return JSON.parse(raw) if raw
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def self.hash_user(username, password)
|
46
|
+
Digest::MD5.hexdigest("#{username}.#{password}.#{TextTractor.configuration.salt}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TextTractor::Users do
|
4
|
+
it { should_not be_nil }
|
5
|
+
|
6
|
+
specify { TextTractor::Users.should respond_to(:all) }
|
7
|
+
specify { TextTractor::Users.should respond_to(:authenticate) }
|
8
|
+
specify { TextTractor::Users.should respond_to(:create) }
|
9
|
+
specify { TextTractor::Users.should respond_to(:get) }
|
10
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
describe "the API server" do
|
5
|
+
def app
|
6
|
+
TextTractor::ApiServer
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "creating and listing projects" do
|
10
|
+
it "allows the creation of a new project, returning the details" do
|
11
|
+
post "/", :name => "Test Project", :api_key => "49032804328090f8sd0fas0jds"
|
12
|
+
|
13
|
+
last_response.should be_ok
|
14
|
+
last_response.body.should == TextTractor::Project.new(
|
15
|
+
name: "Test Project",
|
16
|
+
api_key: "49032804328090f8sd0fas0jds",
|
17
|
+
default_locale: "en"
|
18
|
+
).to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
it "includes a newly created project in the projects list" do
|
22
|
+
post "/", :name => "Test Project", :api_key => "bob"
|
23
|
+
get "/"
|
24
|
+
|
25
|
+
last_response.body.should == [
|
26
|
+
{ name: "Test Project", api_key: "bob", default_locale: "en", users: [] }
|
27
|
+
].to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
it "rejects a project with the same name as an existing project" do
|
31
|
+
post "/", :name => "Test Project"
|
32
|
+
post "/", :name => "Test Project"
|
33
|
+
|
34
|
+
last_response.should_not be_ok
|
35
|
+
last_response.status.should eq 422
|
36
|
+
JSON.parse(last_response.body).should == {
|
37
|
+
"error" => "The project name you specified is already in use."
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:example_phrases) do
|
43
|
+
{
|
44
|
+
"en.application.home.title" => "Home Page",
|
45
|
+
"en.application.home.body" => "This is the home page."
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "registering draft blurbs for a project" do
|
50
|
+
context "when the project exists" do
|
51
|
+
before(:each) do
|
52
|
+
post "/", :name => "Test Project", :api_key => "test"
|
53
|
+
post "/test/draft_blurbs", example_phrases.to_json
|
54
|
+
end
|
55
|
+
|
56
|
+
it "shows the update was OK" do
|
57
|
+
last_response.should be_ok
|
58
|
+
last_response.body.should == "OK"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when the project does not exist" do
|
63
|
+
it "returns a 404 error code" do
|
64
|
+
post "/test/draft_blurbs"
|
65
|
+
|
66
|
+
last_response.status.should eq 404
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "returning the draft blurbs for a project" do
|
72
|
+
context "when the project exists" do
|
73
|
+
before(:each) do
|
74
|
+
TextTractor::Projects.create name: "Test Project", api_key: "test"
|
75
|
+
|
76
|
+
# That's the format we get it in from copycopter_client
|
77
|
+
post "/test/draft_blurbs", example_phrases.to_json
|
78
|
+
end
|
79
|
+
|
80
|
+
it "returns all the translations if the ETag does not match" do
|
81
|
+
get "/test/draft_blurbs"
|
82
|
+
|
83
|
+
last_response.should be_ok
|
84
|
+
JSON.parse(last_response.body).should == example_phrases
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns a status code of 302, with an empty body, if the ETag does match" do
|
88
|
+
header "If-None-Match", redis.get("projects:test:draft_blurbs_etag")
|
89
|
+
get "/test/draft_blurbs"
|
90
|
+
|
91
|
+
last_response.status.should eq 304
|
92
|
+
last_response.body.should be_empty
|
93
|
+
end
|
94
|
+
|
95
|
+
it "does not load the translation list if the ETag matches" do
|
96
|
+
blurbs = stub(:bytesize => 0, :etag => "foo")
|
97
|
+
TextTractor::ApiServer::BlurbList.should_receive(:new).and_return(blurbs)
|
98
|
+
blurbs.should_not_receive(:each)
|
99
|
+
|
100
|
+
header "If-None-Match", "foo"
|
101
|
+
get "/test/draft_blurbs"
|
102
|
+
|
103
|
+
last_response.status.should eq 304
|
104
|
+
last_response.body.should be_empty
|
105
|
+
end
|
106
|
+
|
107
|
+
it "sets the ETag to be set" do
|
108
|
+
get "/test/draft_blurbs"
|
109
|
+
|
110
|
+
last_response.headers["ETag"].should_not be_nil
|
111
|
+
last_response.headers["ETag"].should == redis.get("projects:test:draft_blurbs_etag")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "when the project does not exist" do
|
116
|
+
it "returns a 404 error code" do
|
117
|
+
get "/test/draft_blurbs"
|
118
|
+
|
119
|
+
last_response.status.should eq 404
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "returning the published blurbs for a project" do
|
125
|
+
context "when the project exists" do
|
126
|
+
before(:each) do
|
127
|
+
TextTractor::Projects.create name: "Test Project", api_key: "test"
|
128
|
+
|
129
|
+
post "/test/published_blurbs", example_phrases.to_json
|
130
|
+
end
|
131
|
+
|
132
|
+
it "returns all the translations if the ETag does not match" do
|
133
|
+
get "/test/published_blurbs"
|
134
|
+
|
135
|
+
last_response.should be_ok
|
136
|
+
JSON.parse(last_response.body).should == example_phrases
|
137
|
+
end
|
138
|
+
|
139
|
+
it "returns a status code of 302, with an empty body, if the ETag does match" do
|
140
|
+
header "If-None-Match", redis.get("projects:test:published_blurbs_etag")
|
141
|
+
get "/test/published_blurbs"
|
142
|
+
|
143
|
+
last_response.status.should eq 304
|
144
|
+
last_response.body.should be_empty
|
145
|
+
end
|
146
|
+
|
147
|
+
it "does not load the translation list if the ETag matches" do
|
148
|
+
blurbs = stub(:bytesize => 0, :etag => "foo")
|
149
|
+
TextTractor::ApiServer::BlurbList.should_receive(:new).and_return(blurbs)
|
150
|
+
blurbs.should_not_receive(:each)
|
151
|
+
|
152
|
+
header "If-None-Match", "foo"
|
153
|
+
get "/test/published_blurbs"
|
154
|
+
|
155
|
+
last_response.status.should eq 304
|
156
|
+
last_response.body.should be_empty
|
157
|
+
end
|
158
|
+
|
159
|
+
it "sets the ETag to be set" do
|
160
|
+
get "/test/published_blurbs"
|
161
|
+
|
162
|
+
last_response.headers["ETag"].should_not be_nil
|
163
|
+
last_response.headers["ETag"].should == redis.get("projects:test:published_blurbs_etag")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "when the project does not exist" do
|
168
|
+
it "returns a 404 error code" do
|
169
|
+
get "/test/draft_blurbs"
|
170
|
+
|
171
|
+
last_response.status.should eq 404
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "publishing blurbs" do
|
177
|
+
context "when the project does exist" do
|
178
|
+
before(:each) do
|
179
|
+
post "/", :name => "Test Project", :api_key => "test"
|
180
|
+
post "/test/draft_blurbs", example_phrases.to_json
|
181
|
+
end
|
182
|
+
|
183
|
+
it "has a 200 response code" do
|
184
|
+
post "/test/deploys"
|
185
|
+
last_response.status.should eq 200
|
186
|
+
end
|
187
|
+
|
188
|
+
it "marks the draft blurbs as published" do
|
189
|
+
get "/test/published_blurbs"
|
190
|
+
JSON.parse(last_response.body).should be_empty
|
191
|
+
|
192
|
+
post "/test/deploys"
|
193
|
+
get "/test/published_blurbs"
|
194
|
+
JSON.parse(last_response.body).should == example_phrases
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "when the project does not exist" do
|
199
|
+
it "returns a 404 error code" do
|
200
|
+
post "/test/publish"
|
201
|
+
last_response.status.should eq 404
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe "registering multi-lingual blurbs" do
|
207
|
+
let(:example_phrases) do
|
208
|
+
{
|
209
|
+
"en.application.home.title" => "Home Page",
|
210
|
+
"cy.application.home.title" => "Hafan",
|
211
|
+
"en.application.home.body" => "This is the home page.",
|
212
|
+
"cy.application.home.body" => "Dyma hafan."
|
213
|
+
}
|
214
|
+
end
|
215
|
+
|
216
|
+
it "registers all the translations provided" do
|
217
|
+
post "/", :name => "Test Project", :api_key => "test"
|
218
|
+
post "/test/draft_blurbs", example_phrases.to_json
|
219
|
+
get "/test/draft_blurbs"
|
220
|
+
|
221
|
+
JSON.parse(last_response.body).should == example_phrases
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TextTractor::Config do
|
4
|
+
describe "setting the application configuration" do
|
5
|
+
specify { TextTractor.should respond_to(:config) }
|
6
|
+
|
7
|
+
it "yields an instance of TextTractor::Config" do
|
8
|
+
TextTractor.config.should do |config|
|
9
|
+
config.should be_a TextTractor::Config
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "available options" do
|
15
|
+
it { should respond_to(:redis) }
|
16
|
+
it { should respond_to(:default_username) }
|
17
|
+
it { should respond_to(:default_password) }
|
18
|
+
it { should respond_to(:environment) }
|
19
|
+
it { should respond_to(:salt) }
|
20
|
+
it { should respond_to(:hostname) }
|
21
|
+
it { should respond_to(:port) }
|
22
|
+
it { should respond_to(:ssl) }
|
23
|
+
|
24
|
+
describe "setting the redis option" do
|
25
|
+
it "defaults to an empty hash" do
|
26
|
+
subject.redis.should == {}
|
27
|
+
end
|
28
|
+
|
29
|
+
it "uses the provided value if it is a Hash" do
|
30
|
+
subject.redis = { :server => "foo" }
|
31
|
+
subject.redis.should == { :server => "foo" }
|
32
|
+
end
|
33
|
+
|
34
|
+
it "extracts the relevant details is it is a String" do
|
35
|
+
subject.redis = "redis://user:password@example.org:1234/namespace"
|
36
|
+
subject.redis.should == {
|
37
|
+
host: "example.org",
|
38
|
+
port: 1234,
|
39
|
+
username: "user",
|
40
|
+
password: "password",
|
41
|
+
ns: "namespace"
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "client configuration options" do
|
48
|
+
it "defaults the port to 80" do
|
49
|
+
subject.port.should eq 80
|
50
|
+
end
|
51
|
+
|
52
|
+
it "defaults ssl to true" do
|
53
|
+
subject.ssl.should eq true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/spec/phrase_spec.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TextTractor::Phrase do
|
4
|
+
let(:project) { TextTractor::Project.new(default_locale: "en", name: "Test Project", api_key: "test") }
|
5
|
+
subject do
|
6
|
+
TextTractor::Phrase.new(project, {
|
7
|
+
en: { "text" => "An example", "translated_at" => Time.new(2011, 01, 03, 00, 32, 00).to_s },
|
8
|
+
cy: { "text" => "An example in Welsh", "translated_at" => Time.new(2011, 01, 03, 00, 12, 00).to_s }
|
9
|
+
})
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should set the phrase's project" do
|
13
|
+
subject.project.should == project
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "converting to a hash for saving" do
|
17
|
+
specify do
|
18
|
+
subject.to_hash.should == {
|
19
|
+
"en" => { "text" => "An example", "translated_at" => Time.new(2011, 01, 03, 00, 32, 00).to_s },
|
20
|
+
"cy" => { "text" => "An example in Welsh", "translated_at" => Time.new(2011, 01, 03, 00, 12, 00).to_s }
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it { should respond_to(:[]) }
|
26
|
+
it { should respond_to(:[]=) }
|
27
|
+
|
28
|
+
describe "accessing the individual translations" do
|
29
|
+
it "allows direct access to the translated string" do
|
30
|
+
subject["en"].to_s.should == "An example"
|
31
|
+
subject["cy"].to_s.should == "An example in Welsh"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "allows the translation of a phrase to be set" do
|
35
|
+
subject["cy"] = "A new translation"
|
36
|
+
subject["cy"].to_s.should == "A new translation"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns an empty string when the translation has not been made" do
|
40
|
+
subject["de"].to_s.should == ""
|
41
|
+
subject["de"].translated_at.should be_nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "setting a translation" do
|
46
|
+
before(:each) do
|
47
|
+
Time.stub(:now).and_return(Time.new(2011, 04, 11))
|
48
|
+
end
|
49
|
+
|
50
|
+
it "sets the translation time" do
|
51
|
+
subject["de"] = "Hello!"
|
52
|
+
subject["de"].translated_at.should == Time.new(2011, 04, 11)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "translaion states" do
|
57
|
+
it "is considered translated if the default locale was translated before this translation" do
|
58
|
+
subject["cy"].translated_at = subject["en"].translated_at + 40
|
59
|
+
subject["cy"].state.should == :translated
|
60
|
+
end
|
61
|
+
|
62
|
+
it "is considered untranslated if no translation time is set" do
|
63
|
+
subject["de"].state.should == :untranslated
|
64
|
+
end
|
65
|
+
|
66
|
+
it "is considered stale if the translation time is earlier then that on the default locale" do
|
67
|
+
subject["cy"].translated_at = subject["en"].translated_at - 40
|
68
|
+
subject["cy"].state.should == :stale
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TextTractor::Project do
|
4
|
+
it { should_not be_nil }
|
5
|
+
it { should respond_to :name }
|
6
|
+
it { should respond_to :api_key }
|
7
|
+
it { should respond_to :default_locale }
|
8
|
+
it { should respond_to :users }
|
9
|
+
|
10
|
+
it "defaults the api_key to a random key" do
|
11
|
+
TextTractor::Projects.stub(:random_key).and_return("4") # Chosen by fair dice roll.
|
12
|
+
subject.api_key.should == "4"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "defaults the locale to 'en'" do
|
16
|
+
subject.default_locale.should == "en"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "defaults the users to an empty array" do
|
20
|
+
subject.users.should == []
|
21
|
+
end
|
22
|
+
|
23
|
+
specify { should respond_to(:update_draft_blurbs) }
|
24
|
+
describe "updating the draft blurbs" do
|
25
|
+
before(:each) do
|
26
|
+
Time.stub(:now).and_return(Time.new(2011, 01, 01, 00, 00, 00))
|
27
|
+
|
28
|
+
@project = TextTractor::Projects.create(name: "Test", api_key: "test")
|
29
|
+
@project.update_draft_blurbs(
|
30
|
+
"en.application.home.title" => "Home Page",
|
31
|
+
"en.application.home.body" => "This is the home page.",
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "saves the new translations" do
|
36
|
+
JSON.parse(redis.get("projects:test:draft_blurbs:application.home.title")).should == TextTractor::Phrase.new(@project, {
|
37
|
+
"en" => { "text" => "Home Page", "translated_at" => Time.now.to_s }
|
38
|
+
}).to_hash
|
39
|
+
JSON.parse(redis.get("projects:test:draft_blurbs:application.home.body")).should == TextTractor::Phrase.new(@project, {
|
40
|
+
"en" => { "text" => "This is the home page.", "translated_at" => Time.now.to_s }
|
41
|
+
}).to_hash
|
42
|
+
end
|
43
|
+
|
44
|
+
it "correctly retains quotes" do
|
45
|
+
@project.update_draft_blurbs(
|
46
|
+
"en.application.home.quoted" => %q{"I would like to test quoting." said Jon.}
|
47
|
+
)
|
48
|
+
|
49
|
+
JSON.parse(redis.get("projects:test:draft_blurbs:application.home.quoted")).should == TextTractor::Phrase.new(@project, {
|
50
|
+
"en" => { "text" => %q{"I would like to test quoting." said Jon.}, "translated_at" => Time.now.to_s }
|
51
|
+
}).to_hash
|
52
|
+
end
|
53
|
+
|
54
|
+
it "generates a new ETag if the translations have changed" do
|
55
|
+
redis.get("projects:test:draft_blurbs_etag").should_not be_nil
|
56
|
+
end
|
57
|
+
|
58
|
+
it "does not replace the content of existing translations by default" do
|
59
|
+
@project.update_draft_blurbs(
|
60
|
+
"en.application.home.title" => "A different title"
|
61
|
+
)
|
62
|
+
|
63
|
+
JSON.parse(redis.get("projects:test:draft_blurbs:application.home.title")).should == TextTractor::Phrase.new(@project, {
|
64
|
+
"en" => { "text" => "Home Page", "translated_at" => Time.now.to_s }
|
65
|
+
}).to_hash
|
66
|
+
end
|
67
|
+
|
68
|
+
it "replaces the content of existing translations if :overwrite is set" do
|
69
|
+
@project.update_draft_blurbs({
|
70
|
+
"en.application.home.title" => "A different title"
|
71
|
+
}, :overwrite => true)
|
72
|
+
|
73
|
+
JSON.parse(redis.get("projects:test:draft_blurbs:application.home.title")).should == TextTractor::Phrase.new(@project, {
|
74
|
+
"en" => { "text" => "A different title", "translated_at" => Time.now.to_s }
|
75
|
+
}).to_hash
|
76
|
+
end
|
77
|
+
|
78
|
+
it "does not generate a new ETag if the translations did not change" do
|
79
|
+
previous_etag = redis.get("projects:test:draft_blurbs_etag")
|
80
|
+
|
81
|
+
@project.update_draft_blurbs(
|
82
|
+
"en.application.home.title" => "A different title"
|
83
|
+
)
|
84
|
+
redis.get("projects:test:draft_blurbs_etag").should == previous_etag
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
specify { should respond_to(:draft_blurbs) }
|
89
|
+
describe "getting the draft blurbs for a project" do
|
90
|
+
context "when the project exists" do
|
91
|
+
before(:each) do
|
92
|
+
@project = TextTractor::Projects.create(name: "Test Project", api_key: "test")
|
93
|
+
@project.update_draft_blurbs({
|
94
|
+
"en.application.home.title" => "Home Page",
|
95
|
+
"en.application.home.body" => "This is the home page."
|
96
|
+
})
|
97
|
+
end
|
98
|
+
|
99
|
+
subject { @project.draft_blurbs }
|
100
|
+
|
101
|
+
it "returns all the translations" do
|
102
|
+
subject.should == {
|
103
|
+
"en.application.home.title" => "Home Page",
|
104
|
+
"en.application.home.body" => "This is the home page."
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
specify { should respond_to(:draft_phrases) }
|
111
|
+
describe "getting the phrase list for a project" do
|
112
|
+
before(:each) do
|
113
|
+
Time.stub(:now).and_return(Time.new(2011, 01, 01, 00, 00, 00))
|
114
|
+
@project = TextTractor::Projects.create(name: "Test Project", api_key: "test")
|
115
|
+
@project.update_draft_blurbs({
|
116
|
+
"en.application.home.title" => "Home Page",
|
117
|
+
"cy.application.home.title" => "Dafan",
|
118
|
+
"en.application.home.body" => "This is the home page."
|
119
|
+
})
|
120
|
+
end
|
121
|
+
|
122
|
+
it "returns the list of phrases, in all known languages" do
|
123
|
+
@project.draft_phrases.inject({}) { |memo, value| memo[value[0]] = value[1].to_hash; memo }.should == {
|
124
|
+
"application.home.title" => TextTractor::Phrase.new(@project, {
|
125
|
+
"en" => { "text" => "Home Page", "translated_at" => Time.now.to_s },
|
126
|
+
"cy" => { "text" => "Dafan", "translated_at" => Time.now.to_s }
|
127
|
+
}).to_hash,
|
128
|
+
"application.home.body" => TextTractor::Phrase.new(@project, {
|
129
|
+
"en" => { "text" => "This is the home page.", "translated_at" => Time.now.to_s }
|
130
|
+
}).to_hash
|
131
|
+
}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
specify { should respond_to(:locales) }
|
136
|
+
describe "getting the list of known locales" do
|
137
|
+
before(:each) do
|
138
|
+
@project = TextTractor::Projects.create(name: "Test Project", api_key: "test")
|
139
|
+
@project.update_draft_blurbs({
|
140
|
+
"en.application.home.title" => "Home Page",
|
141
|
+
"cy.application.home.title" => "Dafan",
|
142
|
+
"en.application.home.body" => "This is the home page."
|
143
|
+
})
|
144
|
+
end
|
145
|
+
|
146
|
+
it "returns the known locales" do
|
147
|
+
@project.locales.should == [ "cy", "en" ]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe TextTractor::Projects do
|
153
|
+
specify { TextTractor::Projects.should respond_to(:create) }
|
154
|
+
|
155
|
+
describe "creating a new project" do
|
156
|
+
context "when succesful" do
|
157
|
+
before(:each) { @project = TextTractor::Projects.create name: "Test Project", api_key: "49032804328090f8sd0fas0jds", users: [ "jon@blankpad.net", "bob@example.org" ] }
|
158
|
+
subject { @project }
|
159
|
+
|
160
|
+
it "returns the details as a project instance" do
|
161
|
+
subject.should be_instance_of TextTractor::Project
|
162
|
+
|
163
|
+
subject.name.should == "Test Project"
|
164
|
+
subject.api_key.should == "49032804328090f8sd0fas0jds"
|
165
|
+
end
|
166
|
+
|
167
|
+
it "saves the projects details for later use" do
|
168
|
+
TextTractor.redis.get("projects:49032804328090f8sd0fas0jds").should == subject.to_json
|
169
|
+
end
|
170
|
+
|
171
|
+
it "adds the API key to the project index" do
|
172
|
+
TextTractor.redis.sismember("projects", "49032804328090f8sd0fas0jds").should be_true
|
173
|
+
end
|
174
|
+
|
175
|
+
it "places the project name in a set for quick reference" do
|
176
|
+
TextTractor.redis.sismember("project_names", "Test Project").should be_true
|
177
|
+
end
|
178
|
+
|
179
|
+
it "assigns any provided users to the project" do
|
180
|
+
TextTractor.redis.sismember("project_users:49032804328090f8sd0fas0jds", "bob@example.org").should be_true
|
181
|
+
TextTractor.redis.sismember("project_users:49032804328090f8sd0fas0jds", "jon@blankpad.net").should be_true
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
it "rejects a project with the same name as an existing project" do
|
186
|
+
TextTractor::Projects.create name: "Test Project"
|
187
|
+
|
188
|
+
lambda { TextTractor::Projects.create name: "Test Project" }.should raise_error(TextTractor::Projects::DuplicateProjectName)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "getting an existing project" do
|
193
|
+
it "returns the project on success" do
|
194
|
+
TextTractor::Projects.create name: "Test Project", api_key: "test"
|
195
|
+
|
196
|
+
project = TextTractor::Projects.get("test")
|
197
|
+
project.should be_instance_of TextTractor::Project
|
198
|
+
project.name.should == "Test Project"
|
199
|
+
project.api_key.should == "test"
|
200
|
+
end
|
201
|
+
|
202
|
+
it "returns nil if the project did not exist" do
|
203
|
+
TextTractor::Projects.get("test").should be_nil
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "listing projects for a user" do
|
208
|
+
before(:each) do
|
209
|
+
TextTractor::Projects.create(name: "Assigned Project", users: [ "example" ])
|
210
|
+
TextTractor::Projects.create(name: "Unassigned Project")
|
211
|
+
end
|
212
|
+
|
213
|
+
let(:user) do
|
214
|
+
{ "username" => "example",
|
215
|
+
"superuser" => false }
|
216
|
+
end
|
217
|
+
|
218
|
+
specify { TextTractor::Projects.should respond_to(:for_user) }
|
219
|
+
|
220
|
+
it "returns all projects if the user is a superuser" do
|
221
|
+
user["superuser"] = true
|
222
|
+
projects = TextTractor::Projects.for_user(user)
|
223
|
+
|
224
|
+
projects.should have(2).projects
|
225
|
+
projects.first["name"].should eq "Assigned Project"
|
226
|
+
projects.last["name"].should eq "Unassigned Project"
|
227
|
+
end
|
228
|
+
|
229
|
+
it "returns only projects the user has been added to for standard users" do
|
230
|
+
user["superuser"] = false
|
231
|
+
projects = TextTractor::Projects.for_user(user)
|
232
|
+
|
233
|
+
projects.should have(1).project
|
234
|
+
projects.first["name"].should eq "Assigned Project"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
specify { TextTractor::Projects.should respond_to(:authorised?) }
|
239
|
+
describe "checking authorisation for a project" do
|
240
|
+
before(:each) do
|
241
|
+
TextTractor::Projects.create(name: "Test", api_key: "test", users: [ "bob@example.org" ])
|
242
|
+
end
|
243
|
+
|
244
|
+
it "returns true if the user is a super user" do
|
245
|
+
TextTractor::Projects.authorised?({ "superuser" => true }, "test")
|
246
|
+
end
|
247
|
+
|
248
|
+
it "returns true if the user is in the list of assigned users for the project" do
|
249
|
+
TextTractor::Projects.authorised?({ "superuser" => false, "username" => "bob@example.org" }, "test")
|
250
|
+
end
|
251
|
+
|
252
|
+
it "returns false if the user is not in the list of assigned users for the project" do
|
253
|
+
TextTractor::Projects.authorised?({ "superuser" => false, "username" => "frank@example.org" }, "test")
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
specify { TextTractor::Projects.should respond_to(:update_datastore) }
|
258
|
+
describe "migrating a data store to the current version" do
|
259
|
+
before(:each) do
|
260
|
+
Time.stub(:now).and_return(Time.new(2011, 01, 01, 00, 00, 00))
|
261
|
+
TextTractor::Projects.create(name: "Test", api_key: "test")
|
262
|
+
|
263
|
+
redis.set("projects:test:draft_blurbs:en.application.home.title", "Home page")
|
264
|
+
redis.sadd("projects:test:draft_blurb_keys", "en.application.home.title")
|
265
|
+
redis.set("projects:test:draft_blurbs:cy.application.home.title", "Hafan")
|
266
|
+
redis.sadd("projects:test:draft_blurb_keys", "cy.application.home.title")
|
267
|
+
redis.set "projects:test:draft_blurbs_etag", "old_etag"
|
268
|
+
|
269
|
+
TextTractor::Projects.update_datastore
|
270
|
+
end
|
271
|
+
|
272
|
+
it "updates the ETag" do
|
273
|
+
redis.get("projects:test:draft_blurbs_etag").should_not == "old_etag"
|
274
|
+
end
|
275
|
+
|
276
|
+
it "places all translations of a phrase under one key" do
|
277
|
+
redis.sismember("projects:test:draft_blurb_keys", "application.home.title").should be_true
|
278
|
+
redis.get("projects:test:draft_blurbs:application.home.title").should == {
|
279
|
+
"en" => { "text" => "Home page", "translated_at" => Time.now.to_s },
|
280
|
+
"cy" => { "text" => "Hafan", "translated_at" => Time.now.to_s }
|
281
|
+
}.to_json
|
282
|
+
end
|
283
|
+
|
284
|
+
it "removes the old blurbs" do
|
285
|
+
redis.sismember("projects:test:draft_blurb_keys", "en.application.home.title").should_not be_true
|
286
|
+
redis.sismember("projects:test:draft_blurb_keys", "cy.application.home.title").should_not be_true
|
287
|
+
|
288
|
+
redis.get("projects:test:draft_blurbs:en.application.home.title").should be_nil
|
289
|
+
redis.get("projects:test:draft_blurbs:cy.application.home.title").should be_nil
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|