TextTractor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +66 -0
  5. data/Rakefile +6 -0
  6. data/assets/images/blankpad.png +0 -0
  7. data/assets/images/stale.png +0 -0
  8. data/assets/images/translated.png +0 -0
  9. data/assets/images/untranslated.png +0 -0
  10. data/assets/js/application.js +38 -0
  11. data/assets/js/jquery.js +16 -0
  12. data/assets/js/jquery.pjax.js +188 -0
  13. data/config.ru +20 -0
  14. data/lib/text_tractor.rb +31 -0
  15. data/lib/text_tractor/api_server.rb +93 -0
  16. data/lib/text_tractor/base.rb +25 -0
  17. data/lib/text_tractor/config.rb +48 -0
  18. data/lib/text_tractor/phrase.rb +67 -0
  19. data/lib/text_tractor/projects.rb +202 -0
  20. data/lib/text_tractor/ui_server.rb +152 -0
  21. data/lib/text_tractor/users.rb +49 -0
  22. data/lib/text_tractor/users_spec.rb +10 -0
  23. data/lib/text_tractor/version.rb +3 -0
  24. data/spec/api_server_spec.rb +224 -0
  25. data/spec/config_spec.rb +56 -0
  26. data/spec/phrase_spec.rb +71 -0
  27. data/spec/project_spec.rb +292 -0
  28. data/spec/spec_helper.rb +51 -0
  29. data/spec/ui_server/authentication_spec.rb +60 -0
  30. data/spec/ui_server/project_management_spec.rb +103 -0
  31. data/spec/ui_server/project_viewing_spec.rb +137 -0
  32. data/spec/ui_server_spec.rb +6 -0
  33. data/spec/users_spec.rb +123 -0
  34. data/text_tractor.gemspec +33 -0
  35. data/views/blurbs/_blurb.haml +1 -0
  36. data/views/blurbs/edit.haml +5 -0
  37. data/views/blurbs/value.haml +9 -0
  38. data/views/index.haml +13 -0
  39. data/views/layout.haml +26 -0
  40. data/views/projects/getting_started.haml +16 -0
  41. data/views/projects/new.haml +18 -0
  42. data/views/projects/show.haml +23 -0
  43. data/views/styles.scss +218 -0
  44. data/views/users.haml +29 -0
  45. data/watchr.rb +8 -0
  46. 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,3 @@
1
+ module TextTractor
2
+ VERSION = "0.1.0"
3
+ 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
@@ -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
@@ -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