Commit f8fb343a by jimabramson Committed by jsa

set up i18n

* add i18n gem
* configure default_locale (en-US)
* externalize strings
* configure locale fallbacks
* add tests
parent 735334e4
...@@ -55,3 +55,4 @@ gem 'newrelic_rpm' ...@@ -55,3 +55,4 @@ gem 'newrelic_rpm'
gem 'newrelic_moped' gem 'newrelic_moped'
gem 'unicorn' gem 'unicorn'
gem "rack-timeout", "0.1.0beta3" gem "rack-timeout", "0.1.0beta3"
gem "i18n"
...@@ -178,6 +178,7 @@ DEPENDENCIES ...@@ -178,6 +178,7 @@ DEPENDENCIES
faker faker
guard guard
guard-unicorn guard-unicorn
i18n
kaminari! kaminari!
mongo mongo
mongoid (~> 3.0) mongoid (~> 3.0)
......
...@@ -59,6 +59,17 @@ Mongoid.load!("config/mongoid.yml", environment) ...@@ -59,6 +59,17 @@ Mongoid.load!("config/mongoid.yml", environment)
Mongoid.logger.level = Logger::INFO Mongoid.logger.level = Logger::INFO
Moped.logger.level = ENV["ENABLE_MOPED_DEBUGGING"] ? Logger::DEBUG : Logger::INFO Moped.logger.level = ENV["ENABLE_MOPED_DEBUGGING"] ? Logger::DEBUG : Logger::INFO
# set up i18n
I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'locale', '*.yml').to_s]
I18n.default_locale = CommentService.config[:default_locale]
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
helpers do
def t(*args)
I18n.t(*args)
end
end
Dir[File.dirname(__FILE__) + '/lib/**/*.rb'].each {|file| require file} Dir[File.dirname(__FILE__) + '/lib/**/*.rb'].each {|file| require file}
Dir[File.dirname(__FILE__) + '/models/*.rb'].each {|file| require file} Dir[File.dirname(__FILE__) + '/models/*.rb'].each {|file| require file}
Dir[File.dirname(__FILE__) + '/presenters/*.rb'].each {|file| require file} Dir[File.dirname(__FILE__) + '/presenters/*.rb'].each {|file| require file}
...@@ -124,11 +135,11 @@ if RACK_ENV.to_s == "development" ...@@ -124,11 +135,11 @@ if RACK_ENV.to_s == "development"
end end
error Moped::Errors::InvalidObjectId do error Moped::Errors::InvalidObjectId do
error 400, ["requested object not found"].to_json error 400, [t(:requested_object_not_found)].to_json
end end
error Mongoid::Errors::DocumentNotFound do error Mongoid::Errors::DocumentNotFound do
error 400, ["requested object not found"].to_json error 400, [t(:requested_object_not_found)].to_json
end end
error ArgumentError do error ArgumentError do
......
...@@ -2,3 +2,4 @@ level_limit: 3 ...@@ -2,3 +2,4 @@ level_limit: 3
api_key: <%= ENV['API_KEY'] || 'PUT_YOUR_API_KEY_HERE' %> api_key: <%= ENV['API_KEY'] || 'PUT_YOUR_API_KEY_HERE' %>
elasticsearch_server: <%= ENV['SEARCH_SERVER'] || 'http://localhost:9200' %> elasticsearch_server: <%= ENV['SEARCH_SERVER'] || 'http://localhost:9200' %>
max_deep_search_comment_count: 5000 max_deep_search_comment_count: 5000
default_locale: en-US
...@@ -7,7 +7,7 @@ helpers do ...@@ -7,7 +7,7 @@ helpers do
end end
def user # TODO handle 404 if integrated user service def user # TODO handle 404 if integrated user service
raise ArgumentError, "User id is required" unless @user || params[:user_id] raise ArgumentError, t(:user_id_is_required) unless @user || params[:user_id]
@user ||= User.find_by(external_id: params[:user_id]) @user ||= User.find_by(external_id: params[:user_id])
end end
...@@ -28,27 +28,27 @@ helpers do ...@@ -28,27 +28,27 @@ helpers do
when "other" when "other"
Commentable.find(params["source_id"]) Commentable.find(params["source_id"])
else else
raise ArgumentError, "Source type must be 'user', 'thread' or 'other'" raise ArgumentError, t(:source_type_must_be_user_thread_or_other)
end end
end end
def vote_for(obj) def vote_for(obj)
raise ArgumentError, "User id is required" unless user raise ArgumentError, t(:user_id_is_required) unless user
raise ArgumentError, "Value is required" unless params["value"] raise ArgumentError, t(:value_is_required) unless params["value"]
raise ArgumentError, "Value is invalid" unless %w[up down].include? params["value"] raise ArgumentError, t(:value_is_invalid) unless %w[up down].include? params["value"]
user.vote(obj, params["value"].to_sym) user.vote(obj, params["value"].to_sym)
obj.reload.to_hash.to_json obj.reload.to_hash.to_json
end end
def flag_as_abuse(obj) def flag_as_abuse(obj)
raise ArgumentError, "User id is required" unless user raise ArgumentError, t(:user_id_is_required) unless user
obj.abuse_flaggers << user.id unless obj.abuse_flaggers.include? user.id obj.abuse_flaggers << user.id unless obj.abuse_flaggers.include? user.id
obj.save obj.save
obj.reload.to_hash.to_json obj.reload.to_hash.to_json
end end
def un_flag_as_abuse(obj) def un_flag_as_abuse(obj)
raise ArgumentError, "User id is required" unless user raise ArgumentError, t(:user_id_is_required) unless user
if params["all"] if params["all"]
obj.historical_abuse_flaggers += obj.abuse_flaggers obj.historical_abuse_flaggers += obj.abuse_flaggers
obj.historical_abuse_flaggers = obj.historical_abuse_flaggers.uniq obj.historical_abuse_flaggers = obj.historical_abuse_flaggers.uniq
...@@ -62,21 +62,21 @@ helpers do ...@@ -62,21 +62,21 @@ helpers do
end end
def undo_vote_for(obj) def undo_vote_for(obj)
raise ArgumentError, "must provide user id" unless user raise ArgumentError, t(:user_id_is_required) unless user
user.unvote(obj) user.unvote(obj)
obj.reload.to_hash.to_json obj.reload.to_hash.to_json
end end
def pin(obj) def pin(obj)
raise ArgumentError, "User id is required" unless user raise ArgumentError, t(:user_id_is_required) unless user
obj.pinned = true obj.pinned = true
obj.save obj.save
obj.reload.to_hash.to_json obj.reload.to_hash.to_json
end end
def unpin(obj) def unpin(obj)
raise ArgumentError, "User id is required" unless user raise ArgumentError, t(:user_id_is_required) unless user
obj.pinned = nil obj.pinned = nil
obj.save obj.save
obj.reload.to_hash.to_json obj.reload.to_hash.to_json
...@@ -268,7 +268,7 @@ helpers do ...@@ -268,7 +268,7 @@ helpers do
end end
content_obj = {} content_obj = {}
content_obj["username"] = c.author_with_anonymity(:username, "(anonymous)") content_obj["username"] = c.author_with_anonymity(:username, t(:anonymous))
content_obj["updated_at"] = c.updated_at content_obj["updated_at"] = c.updated_at
content_obj["body"] = c.body content_obj["body"] = c.body
t["content"] << content_obj t["content"] << content_obj
...@@ -289,8 +289,9 @@ helpers do ...@@ -289,8 +289,9 @@ helpers do
return return
end end
if CommentService.blocked_hashes.include? hash then if CommentService.blocked_hashes.include? hash then
logger.warn "blocked content with body hash [#{hash}]" msg = t(:blocked_content_with_body_hash, :hash => hash)
error 503 logger.warn msg
error 503, [msg].to_json
end end
end end
......
en-US:
requested_object_not_found: "requested object not found"
user_id_is_required: "User id is required"
source_type_must_be_user_thread_or_other: "Source type must be 'user', 'thread' or 'other'"
value_is_required: "Value is required"
value_is_invalid: "Value is invalid"
anonymous: "anonymous"
blocked_content_with_body_hash: blocked content with body hash %{hash}
...@@ -52,10 +52,12 @@ describe "app" do ...@@ -52,10 +52,12 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
create_comment_flag("does_not_exist", User.first.id) create_comment_flag("does_not_exist", User.first.id)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
create_comment_flag("#{Comment.first.id}", nil) create_comment_flag("#{Comment.first.id}", nil)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
#Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object #Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object
#it "has a correct hash" do #it "has a correct hash" do
...@@ -78,10 +80,12 @@ describe "app" do ...@@ -78,10 +80,12 @@ describe "app" do
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
create_thread_flag("does_not_exist", User.first.id) create_thread_flag("does_not_exist", User.first.id)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
create_thread_flag("#{Comment.first.comment_thread.id}", nil) create_thread_flag("#{Comment.first.comment_thread.id}", nil)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
#Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object #Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object
#it "has a correct hash" do #it "has a correct hash" do
...@@ -106,13 +110,20 @@ describe "app" do ...@@ -106,13 +110,20 @@ describe "app" do
comment.abuse_flaggers.count.should == prev_abuse_flaggers_count - 1 comment.abuse_flaggers.count.should == prev_abuse_flaggers_count - 1
comment.abuse_flaggers.to_a.should_not include User.first.id comment.abuse_flaggers.to_a.should_not include User.first.id
end end
it "returns 400 when the thread does not exist" do it "returns 400 when the comment does not exist" do
remove_comment_flag("does_not_exist", User.first.id) remove_comment_flag("does_not_exist", User.first.id)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end
it "returns 400 when the thread does not exist" do
remove_thread_flag("does_not_exist", User.first.id)
last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
remove_comment_flag("#{Comment.first.comment_thread.id}", nil) remove_thread_flag("#{Comment.first.comment_thread.id}", nil)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
#Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object #Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object
#it "has a correct hash" do #it "has a correct hash" do
...@@ -140,10 +151,12 @@ describe "app" do ...@@ -140,10 +151,12 @@ describe "app" do
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
remove_thread_flag("does_not_exist", User.first.id) remove_thread_flag("does_not_exist", User.first.id)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
remove_thread_flag("#{Comment.first.comment_thread.id}", nil) remove_thread_flag("#{Comment.first.comment_thread.id}", nil)
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
#Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object #Would like to test the output of to_hash, but not sure how to deal with a Moped::BSON::Document object
#it "has a correct hash" do #it "has a correct hash" do
......
...@@ -37,6 +37,7 @@ describe "app" do ...@@ -37,6 +37,7 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
get "/api/v1/comments/does_not_exist" get "/api/v1/comments/does_not_exist"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
end end
describe "PUT /api/v1/comments/:comment_id" do describe "PUT /api/v1/comments/:comment_id" do
...@@ -51,11 +52,13 @@ describe "app" do ...@@ -51,11 +52,13 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
put "/api/v1/comments/does_not_exist", body: "new body", endorsed: true put "/api/v1/comments/does_not_exist", body: "new body", endorsed: true
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 503 when the post hash is blocked" do it "returns 503 when the post hash is blocked" do
comment = Comment.first comment = Comment.first
put "/api/v1/comments/#{comment.id}", body: "BLOCKED POST", endorsed: true put "/api/v1/comments/#{comment.id}", body: "BLOCKED POST", endorsed: true
last_response.status.should == 503 last_response.status.should == 503
parse(last_response.body).first.should == I18n.t(:blocked_content_with_body_hash, :hash => Digest::MD5.hexdigest("blocked post"))
end end
end end
describe "POST /api/v1/comments/:comment_id" do describe "POST /api/v1/comments/:comment_id" do
...@@ -73,12 +76,14 @@ describe "app" do ...@@ -73,12 +76,14 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
post "/api/v1/comments/does_not_exist", body: "new comment", course_id: "1", user_id: User.first.id post "/api/v1/comments/does_not_exist", body: "new comment", course_id: "1", user_id: User.first.id
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 503 when the post hash is blocked" do it "returns 503 when the post hash is blocked" do
comment = Comment.first.to_hash(recursive: true) comment = Comment.first.to_hash(recursive: true)
user = User.first user = User.first
post "/api/v1/comments/#{comment["id"]}", body: "BLOCKED POST", course_id: "1", user_id: User.first.id post "/api/v1/comments/#{comment["id"]}", body: "BLOCKED POST", course_id: "1", user_id: User.first.id
last_response.status.should == 503 last_response.status.should == 503
parse(last_response.body).first.should == I18n.t(:blocked_content_with_body_hash, :hash => Digest::MD5.hexdigest("blocked post"))
end end
end end
describe "DELETE /api/v1/comments/:comment_id" do describe "DELETE /api/v1/comments/:comment_id" do
...@@ -93,6 +98,7 @@ describe "app" do ...@@ -93,6 +98,7 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
delete "/api/v1/comments/does_not_exist" delete "/api/v1/comments/does_not_exist"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
end end
end end
......
...@@ -366,8 +366,10 @@ describe "app" do ...@@ -366,8 +366,10 @@ describe "app" do
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
get "/api/v1/threads/does_not_exist" get "/api/v1/threads/does_not_exist"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
get "/api/v1/threads/5016a3caec5eb9a12300000b1" get "/api/v1/threads/5016a3caec5eb9a12300000b1"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "get information of a single comment thread with its tags" do it "get information of a single comment thread with its tags" do
...@@ -406,6 +408,7 @@ describe "app" do ...@@ -406,6 +408,7 @@ describe "app" do
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
put "/api/v1/threads/does_not_exist", body: "new body", title: "new title" put "/api/v1/threads/does_not_exist", body: "new body", title: "new title"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 503 if the post body has been blocked" do it "returns 503 if the post body has been blocked" do
thread = CommentThread.first thread = CommentThread.first
...@@ -465,6 +468,7 @@ describe "app" do ...@@ -465,6 +468,7 @@ describe "app" do
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
post "/api/v1/threads/does_not_exist/comments", default_params post "/api/v1/threads/does_not_exist/comments", default_params
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns error when body or course_id does not exist, or when body is blank" do it "returns error when body or course_id does not exist, or when body is blank" do
post "/api/v1/threads/#{CommentThread.first.id}/comments", default_params.merge(body: nil) post "/api/v1/threads/#{CommentThread.first.id}/comments", default_params.merge(body: nil)
...@@ -489,6 +493,7 @@ describe "app" do ...@@ -489,6 +493,7 @@ describe "app" do
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
delete "/api/v1/threads/does_not_exist" delete "/api/v1/threads/does_not_exist"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
end end
end end
......
...@@ -99,6 +99,7 @@ describe "app" do ...@@ -99,6 +99,7 @@ describe "app" do
it "returns 503 when the post content is blocked" do it "returns 503 when the post content is blocked" do
post '/api/v1/question_1/threads', default_params.merge(body: "BLOCKED POST") post '/api/v1/question_1/threads', default_params.merge(body: "BLOCKED POST")
last_response.status.should == 503 last_response.status.should == 503
parse(last_response.body).first.should == I18n.t(:blocked_content_with_body_hash, :hash => Digest::MD5.hexdigest("blocked post"))
end end
it "create a new comment thread with tag" do it "create a new comment thread with tag" do
old_count = CommentThread.count old_count = CommentThread.count
......
...@@ -17,16 +17,20 @@ describe "app" do ...@@ -17,16 +17,20 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
put "/api/v1/comments/does_not_exist/votes", user_id: User.first.id, value: "down" put "/api/v1/comments/does_not_exist/votes", user_id: User.first.id, value: "down"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
put "/api/v1/comments/#{Comment.first.id}/votes", value: "down" put "/api/v1/comments/#{Comment.first.id}/votes", value: "down"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
it "returns 400 when value is not provided or invalid" do it "returns 400 when value is not provided or invalid" do
put "/api/v1/comments/#{Comment.first.id}/votes", user_id: User.first.id put "/api/v1/comments/#{Comment.first.id}/votes", user_id: User.first.id
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:value_is_required)
put "/api/v1/comments/#{Comment.first.id}/votes", user_id: User.first.id, value: "superdown" put "/api/v1/comments/#{Comment.first.id}/votes", user_id: User.first.id, value: "superdown"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:value_is_invalid)
end end
end end
describe "DELETE /api/v1/comments/:comment_id/votes" do describe "DELETE /api/v1/comments/:comment_id/votes" do
...@@ -43,10 +47,12 @@ describe "app" do ...@@ -43,10 +47,12 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
delete "/api/v1/comments/does_not_exist/votes", user_id: User.first.id delete "/api/v1/comments/does_not_exist/votes", user_id: User.first.id
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
delete "/api/v1/comments/#{Comment.first.id}/votes" delete "/api/v1/comments/#{Comment.first.id}/votes"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
end end
describe "PUT /api/v1/threads/:thread_id/votes" do describe "PUT /api/v1/threads/:thread_id/votes" do
...@@ -63,16 +69,20 @@ describe "app" do ...@@ -63,16 +69,20 @@ describe "app" do
it "returns 400 when the thread does not exist" do it "returns 400 when the thread does not exist" do
put "/api/v1/threads/does_not_exist/votes", user_id: User.first.id, value: "down" put "/api/v1/threads/does_not_exist/votes", user_id: User.first.id, value: "down"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
put "/api/v1/threads/#{CommentThread.first.id}/votes", value: "down" put "/api/v1/threads/#{CommentThread.first.id}/votes", value: "down"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
it "returns 400 when value is not provided or invalid" do it "returns 400 when value is not provided or invalid" do
put "/api/v1/threads/#{CommentThread.first.id}/votes", user_id: User.first.id put "/api/v1/threads/#{CommentThread.first.id}/votes", user_id: User.first.id
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:value_is_required)
put "/api/v1/threads/#{CommentThread.first.id}/votes", user_id: User.first.id, value: "superdown" put "/api/v1/threads/#{CommentThread.first.id}/votes", user_id: User.first.id, value: "superdown"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:value_is_invalid)
end end
end end
describe "DELETE /api/v1/threads/:thread_id/votes" do describe "DELETE /api/v1/threads/:thread_id/votes" do
...@@ -89,10 +99,12 @@ describe "app" do ...@@ -89,10 +99,12 @@ describe "app" do
it "returns 400 when the comment does not exist" do it "returns 400 when the comment does not exist" do
delete "/api/v1/threads/does_not_exist/votes", user_id: User.first.id delete "/api/v1/threads/does_not_exist/votes", user_id: User.first.id
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:requested_object_not_found)
end end
it "returns 400 when user_id is not provided" do it "returns 400 when user_id is not provided" do
delete "/api/v1/threads/#{CommentThread.first.id}/votes" delete "/api/v1/threads/#{CommentThread.first.id}/votes"
last_response.status.should == 400 last_response.status.should == 400
parse(last_response.body).first.should == I18n.t(:user_id_is_required)
end end
end end
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment