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.
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