From 89e9154499f67fd8c63e1098b3e50b317c690dd0 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 5 May 2014 12:22:52 +0200 Subject: custom error pages for 404 and 500 errors --- app/controllers/errors_controller.rb | 9 +++++++++ app/views/errors/not_found.html.haml | 7 +++++++ app/views/errors/server_error.html.haml | 7 +++++++ config/application.rb | 2 ++ config/routes.rb | 3 +++ 5 files changed, 28 insertions(+) create mode 100644 app/controllers/errors_controller.rb create mode 100644 app/views/errors/not_found.html.haml create mode 100644 app/views/errors/server_error.html.haml diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb new file mode 100644 index 0000000..bf9329c --- /dev/null +++ b/app/controllers/errors_controller.rb @@ -0,0 +1,9 @@ +class ErrorsController < ApplicationController + + def not_found + end + + def server_error + end + +end diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml new file mode 100644 index 0000000..22b6a55 --- /dev/null +++ b/app/views/errors/not_found.html.haml @@ -0,0 +1,7 @@ +.hero-unit + %h1 Page not found. + %h2 The page you were looking for doesn't exist. + %p.lead You may have mistyped the address or the page may have moved. + %a.btn.btn-primary.btn-large{href:'/'} + %i.icon-home.icon-white + Home diff --git a/app/views/errors/server_error.html.haml b/app/views/errors/server_error.html.haml new file mode 100644 index 0000000..173cdad --- /dev/null +++ b/app/views/errors/server_error.html.haml @@ -0,0 +1,7 @@ +.hero-unit + %h1 Ouch! + %h2 We ran into a server error. + %p.lead The problem has been logged and we will look into it. + %a.btn.btn-primary.btn-large{href:'/'} + %i.icon-home.icon-white + Home diff --git a/config/application.rb b/config/application.rb index 2c9c55a..1077198 100644 --- a/config/application.rb +++ b/config/application.rb @@ -91,5 +91,7 @@ module LeapWeb ## see initializers/customization.rb ## config.paths['app/views'].unshift "config/customization/views" + + config.exceptions_app = self.routes end end diff --git a/config/routes.rb b/config/routes.rb index 745b97d..f92c704 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,9 @@ LeapWeb::Application.routes.draw do root :to => "home#index" get '(:locale)' => 'home#index', :locale => MATCH_LOCALE, :as => 'home' + match '/404' => 'errors#not_found' + match '/500' => 'errors#server_error' + scope "(:locale)", :locale => MATCH_LOCALE, :controller => 'pages', :action => 'show' do get 'privacy-policy', :as => 'privacy_policy' get 'terms-of-service', :as => 'terms_of_service' -- cgit v1.2.3 From 97e30cc136a72092abba19f1b601ad9d5ebd5257 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 5 May 2014 12:29:21 +0200 Subject: i18n for error pages --- app/views/errors/not_found.html.haml | 8 ++++---- app/views/errors/server_error.html.haml | 8 ++++---- config/locales/en.yml | 9 +++++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 22b6a55..75cb889 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,7 +1,7 @@ .hero-unit - %h1 Page not found. - %h2 The page you were looking for doesn't exist. - %p.lead You may have mistyped the address or the page may have moved. + %h1=t :not_found_title + %h2=t :not_found_subtitle + %p.lead=t :not_found_lead %a.btn.btn-primary.btn-large{href:'/'} %i.icon-home.icon-white - Home + =t :home diff --git a/app/views/errors/server_error.html.haml b/app/views/errors/server_error.html.haml index 173cdad..68baf20 100644 --- a/app/views/errors/server_error.html.haml +++ b/app/views/errors/server_error.html.haml @@ -1,7 +1,7 @@ .hero-unit - %h1 Ouch! - %h2 We ran into a server error. - %p.lead The problem has been logged and we will look into it. + %h1=t :server_error_title + %h2=t :server_error_subtitle + %p.lead=t :server_error_lead %a.btn.btn-primary.btn-large{href:'/'} %i.icon-home.icon-white - Home + =t :home diff --git a/config/locales/en.yml b/config/locales/en.yml index cebf075..899d659 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,10 +1,19 @@ en: + home: Home privacy_policy: Privacy Policy terms_of_service: Terms of Service pricing: Pricing about: About Us contact: Contact + + not_found_title: Page not found. + not_found_subtitle: "The page you were looking for doesn't exist." + not_found_lead: "You may have mistyped the address or the page may have moved." + server_error_title: Ouch! + server_error_subtitle: We ran into a server error. + server_error_lead: The problem has been logged and we will look into it. no_such_thing: "No such %{thing}." + thing_was_successfully_created: "%{thing} was successfully created." create_thing: "Create %{thing}" -- cgit v1.2.3 From 8f9e6d294c78a4c3c2336ca4386fda3e20b2d365 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 6 May 2014 09:50:35 +0200 Subject: minor: remove unused error pages --- public/404.html | 26 -------------------------- public/500.html | 25 ------------------------- 2 files changed, 51 deletions(-) delete mode 100644 public/404.html delete mode 100644 public/500.html diff --git a/public/404.html b/public/404.html deleted file mode 100644 index 9a48320..0000000 --- a/public/404.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - -
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
- - diff --git a/public/500.html b/public/500.html deleted file mode 100644 index f3648a0..0000000 --- a/public/500.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - -
-

We're sorry, but something went wrong.

-
- - -- cgit v1.2.3 From b298cea527f74f682d24defee360e0f45f47d125 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 6 May 2014 09:50:46 +0200 Subject: little bit of documentation --- app/controllers/errors_controller.rb | 5 +++-- config/application.rb | 1 + config/routes.rb | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index bf9329c..6c659e6 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -1,9 +1,10 @@ +# We render http errors ourselves so we can customize them class ErrorsController < ApplicationController - + # 404 def not_found end + # 500 def server_error end - end diff --git a/config/application.rb b/config/application.rb index 1077198..8555f48 100644 --- a/config/application.rb +++ b/config/application.rb @@ -92,6 +92,7 @@ module LeapWeb ## config.paths['app/views'].unshift "config/customization/views" + # handle http errors ourselves config.exceptions_app = self.routes end end diff --git a/config/routes.rb b/config/routes.rb index f92c704..9e0b72d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,10 @@ LeapWeb::Application.routes.draw do root :to => "home#index" get '(:locale)' => 'home#index', :locale => MATCH_LOCALE, :as => 'home' + # + # HTTP Error Handling + # instead of the default error pages use the errors controller and views + # match '/404' => 'errors#not_found' match '/500' => 'errors#server_error' -- cgit v1.2.3 From e94b9471c0bc30cd6a1a5bf5b6b22b746d242e31 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 9 May 2014 16:11:20 +0200 Subject: calculate cert fingerprints to store for leap_mx stelfox.net/blog/2014/04/calculating-rsa-key-fingerprints-in-ruby/ --- app/models/client_certificate.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/client_certificate.rb b/app/models/client_certificate.rb index 76b07a2..63de9e1 100644 --- a/app/models/client_certificate.rb +++ b/app/models/client_certificate.rb @@ -43,8 +43,16 @@ class ClientCertificate self.key.to_pem + self.cert.to_pem end + def fingerprint + OpenSSL::Digest::SHA1.hexdigest(openssl_cert.to_der).scan(/../).join(':') + end + private + def openssl_cert + cert.openssl_body + end + def self.root_ca @root_ca ||= begin crt = File.read(APP_CONFIG[:client_ca_cert]) -- cgit v1.2.3 From 1241cb8f13e6d0752b67521e8385b62d7fbcc882 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 15 May 2014 10:40:21 +0200 Subject: basic integration test for cert API --- test/integration/api/cert_test.rb | 30 ++++++++++++++++++++++++++++++ test/support/api_integration_test.rb | 23 +++++++++++++++++++++++ test/support/assert_responses.rb | 30 +++++++++++++++++++++++++----- test/support/browser_integration_test.rb | 1 + 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 test/integration/api/cert_test.rb create mode 100644 test/support/api_integration_test.rb diff --git a/test/integration/api/cert_test.rb b/test/integration/api/cert_test.rb new file mode 100644 index 0000000..74d439a --- /dev/null +++ b/test/integration/api/cert_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' + +class CertTest < ApiIntegrationTest + + test "retrieve eip cert" do + login + get '/1/cert', {}, RACK_ENV + assert_text_response + assert_response_includes "BEGIN RSA PRIVATE KEY" + assert_response_includes "END RSA PRIVATE KEY" + assert_response_includes "BEGIN CERTIFICATE" + assert_response_includes "END CERTIFICATE" + end + + test "fetching certs requires login by default" do + get '/1/cert', {}, RACK_ENV + assert_json_response error: I18n.t(:not_authorized) + end + + test "retrieve anonymous eip cert" do + with_config allow_anonymous_certs: true do + get '/1/cert', {}, RACK_ENV + assert_text_response + assert_response_includes "BEGIN RSA PRIVATE KEY" + assert_response_includes "END RSA PRIVATE KEY" + assert_response_includes "BEGIN CERTIFICATE" + assert_response_includes "END CERTIFICATE" + end + end +end diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb new file mode 100644 index 0000000..50c528b --- /dev/null +++ b/test/support/api_integration_test.rb @@ -0,0 +1,23 @@ +class ApiIntegrationTest < ActionDispatch::IntegrationTest + + DUMMY_TOKEN = Token.new + RACK_ENV = {'HTTP_AUTHORIZATION' => %Q(Token token="#{DUMMY_TOKEN.to_s}")} + + def login(user = nil) + @user ||= user ||= FactoryGirl.create(:user) + @token ||= DUMMY_TOKEN + @token.user_id = @user.id + @token.last_seen_at = Time.now + @token.save + end + + teardown do + if @user && @user.persisted? + Identity.destroy_all_for @user + @user.reload.destroy + end + if @token && @token.persisted? + @token.reload.destroy + end + end +end diff --git a/test/support/assert_responses.rb b/test/support/assert_responses.rb index b01166f..19c2768 100644 --- a/test/support/assert_responses.rb +++ b/test/support/assert_responses.rb @@ -8,21 +8,27 @@ module AssertResponses @response || last_response end - def assert_attachement_filename(name) - assert_equal %Q(attachment; filename="#{name}"), - get_response.headers["Content-Disposition"] + def content_type + get_response.content_type.to_s.split(';').first end def json_response + return nil unless content_type == 'application/json' response = JSON.parse(get_response.body) response.respond_to?(:with_indifferent_access) ? response.with_indifferent_access : response end + def assert_text_response(body = nil) + assert_equal 'text/plain', content_type + unless body.nil? + assert_equal body, get_response.body + end + end + def assert_json_response(object) - assert_equal 'application/json', - get_response.content_type.to_s.split(';').first + assert_equal 'application/json', content_type if object.is_a? Hash object.stringify_keys! if object.respond_to? :stringify_keys! assert_equal object, json_response @@ -35,6 +41,20 @@ module AssertResponses object.stringify_keys! if object.respond_to? :stringify_keys! assert_json_response :errors => object end + + # checks for the presence of a key in a json response + # or a string in a text response + def assert_response_includes(string_or_key) + response = json_response || get_response.body + assert response.include?(string_or_key), + "response should have included #{string_or_key}" + end + + def assert_attachement_filename(name) + assert_equal %Q(attachment; filename="#{name}"), + get_response.headers["Content-Disposition"] + end + end class ::ActionController::TestCase diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb index 1c872ff..4fec59f 100644 --- a/test/support/browser_integration_test.rb +++ b/test/support/browser_integration_test.rb @@ -54,6 +54,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest end # currently this only works for tests with poltergeist. + # ApiIntegrationTest has a working implementation for RackTest def login(user = nil) @user ||= user ||= FactoryGirl.create(:user) token = Token.create user_id: user.id -- cgit v1.2.3 From 5dd6c1529f8f4fc5089c71b0a44e360acaea900d Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 15 May 2014 11:04:56 +0200 Subject: fix Email so User.new.valid? does not crash Email.new(nil) now returns an invalid email rather than crashing. --- app/models/email.rb | 5 +++++ test/unit/user_test.rb | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/app/models/email.rb b/app/models/email.rb index a9a503f..4090275 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -7,6 +7,11 @@ class Email < String :message => "needs to be a valid email address" } + # Make sure we can call Email.new(nil) and get an invalid email address + def initialize(s) + super(s.to_s) + end + def to_partial_path "emails/email" end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index ffbb7d8..b3c831b 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -65,4 +65,11 @@ class UserTest < ActiveSupport::TestCase assert_equal key, @user.public_key end + # + ## Regression tests + # + test "make sure valid does not crash" do + assert !User.new.valid? + end + end -- cgit v1.2.3 From 71dcf3f4e5d423b78b47f675297fc98b28ef3442 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 15 May 2014 11:17:47 +0200 Subject: SmtpCertsController, routes and tests --- app/controllers/v1/smtp_certs_controller.rb | 21 ++++++++++ config/routes.rb | 1 + test/functional/v1/smtp_certs_controller_test.rb | 35 ++++++++++++++++ test/integration/api/smtp_cert_test.rb | 51 ++++++++++++++++++++++++ test/support/api_integration_test.rb | 2 + 5 files changed, 110 insertions(+) create mode 100644 app/controllers/v1/smtp_certs_controller.rb create mode 100644 test/functional/v1/smtp_certs_controller_test.rb create mode 100644 test/integration/api/smtp_cert_test.rb diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb new file mode 100644 index 0000000..001425d --- /dev/null +++ b/app/controllers/v1/smtp_certs_controller.rb @@ -0,0 +1,21 @@ +class V1::SmtpCertsController < ApplicationController + + before_filter :require_login + before_filter :require_email_account + + # GET /cert + def show + @cert = ClientCertificate.new prefix: current_user.email_address + render text: @cert.to_s, content_type: 'text/plain' + end + + protected + + def require_email_account + access_denied unless service_level.provides? 'email' + end + + def service_level + current_user.effective_service_level + end +end diff --git a/config/routes.rb b/config/routes.rb index 745b97d..ff2d2cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,7 @@ LeapWeb::Application.routes.draw do resources :users, :only => [:create, :update, :destroy, :index] resources :messages, :only => [:index, :update] resource :cert, :only => [:show] + resource :smtp_cert, :only => [:show] resource :service, :only => [:show] end diff --git a/test/functional/v1/smtp_certs_controller_test.rb b/test/functional/v1/smtp_certs_controller_test.rb new file mode 100644 index 0000000..f9ba26f --- /dev/null +++ b/test/functional/v1/smtp_certs_controller_test.rb @@ -0,0 +1,35 @@ +require 'test_helper' + +class V1::SmtpCertsControllerTest < ActionController::TestCase + + test "no smtp cert without login" do + with_config allow_anonymous_certs: true do + get :show, format: 'json' + assert_access_denied + end + end + + test "require service level with email" do + login + get :show + assert_access_denied + end + + test "send cert with username" do + login effective_service_level: ServiceLevel.new(id: 2) + cert = expect_cert(@current_user.email_address) + get :show + assert_response :success + assert_equal cert.to_s, @response.body + end + + protected + + def expect_cert(prefix) + cert = stub :to_s => "#{prefix.downcase} cert" + ClientCertificate.expects(:new). + with(:prefix => prefix). + returns(cert) + return cert + end +end diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb new file mode 100644 index 0000000..a579d93 --- /dev/null +++ b/test/integration/api/smtp_cert_test.rb @@ -0,0 +1,51 @@ +require 'test_helper' +require 'openssl' + +class SmtpCertTest < ApiIntegrationTest + + test "retrieve smtp cert" do + @user = FactoryGirl.create :user, effective_service_level_code: 2 + login + get '/1/smtp_cert', {}, RACK_ENV + assert_text_response + assert_response_includes "BEGIN RSA PRIVATE KEY" + assert_response_includes "END RSA PRIVATE KEY" + assert_response_includes "BEGIN CERTIFICATE" + assert_response_includes "END CERTIFICATE" + end + + test "key matches the cert" do + @user = FactoryGirl.create :user, effective_service_level_code: 2 + login + get '/1/smtp_cert', {}, RACK_ENV + assert_text_response + cert = OpenSSL::X509::Certificate.new(get_response.body) + key = OpenSSL::PKey::RSA.new(get_response.body) + assert cert.check_private_key(key) + end + + # we'll store the fingerprint later. + test "fingerprint matches" do + @user = FactoryGirl.create :user, effective_service_level_code: 2 + login + get '/1/smtp_cert', {}, RACK_ENV + assert_text_response + cert = OpenSSL::X509::Certificate.new(get_response.body) + fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':') + skip "we're not storing the fingerprints yet" + assert_equal fingerprint, @user.identity.cert_fingerprints.last + end + + test "fetching smtp certs requires email account" do + login + get '/1/smtp_cert', {}, RACK_ENV + assert_json_response error: I18n.t(:not_authorized) + end + + test "no anonymous smtp certs" do + with_config allow_anonymous_certs: true do + get '/1/smtp_cert', {}, RACK_ENV + assert_json_response error: I18n.t(:not_authorized) + end + end +end diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb index 50c528b..aa9c00d 100644 --- a/test/support/api_integration_test.rb +++ b/test/support/api_integration_test.rb @@ -6,6 +6,8 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest def login(user = nil) @user ||= user ||= FactoryGirl.create(:user) @token ||= DUMMY_TOKEN + # make sure @token is up to date if it already exists + @token.reload if @token.persisted @token.user_id = @user.id @token.last_seen_at = Time.now @token.save -- cgit v1.2.3 From 17b67aeda81dee2273ce1161ac7292a328c3efaa Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 15 May 2014 16:29:49 +0200 Subject: store cert fingerprint with main user identity --- app/controllers/v1/smtp_certs_controller.rb | 2 ++ app/models/identity.rb | 1 + test/integration/api/smtp_cert_test.rb | 8 ++++---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb index 001425d..258b391 100644 --- a/app/controllers/v1/smtp_certs_controller.rb +++ b/app/controllers/v1/smtp_certs_controller.rb @@ -6,6 +6,8 @@ class V1::SmtpCertsController < ApplicationController # GET /cert def show @cert = ClientCertificate.new prefix: current_user.email_address + current_user.identity.cert_fingerprints << @cert.fingerprint + current_user.identity.save render text: @cert.to_s, content_type: 'text/plain' end diff --git a/app/models/identity.rb b/app/models/identity.rb index ad8c01e..2f8d4eb 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -8,6 +8,7 @@ class Identity < CouchRest::Model::Base property :address, LocalEmail property :destination, Email property :keys, HashWithIndifferentAccess + property :cert_fingerprints, [String] validate :unique_forward validate :alias_available diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb index a579d93..4f0f4a6 100644 --- a/test/integration/api/smtp_cert_test.rb +++ b/test/integration/api/smtp_cert_test.rb @@ -14,7 +14,7 @@ class SmtpCertTest < ApiIntegrationTest assert_response_includes "END CERTIFICATE" end - test "key matches the cert" do + test "cert and key" do @user = FactoryGirl.create :user, effective_service_level_code: 2 login get '/1/smtp_cert', {}, RACK_ENV @@ -22,17 +22,17 @@ class SmtpCertTest < ApiIntegrationTest cert = OpenSSL::X509::Certificate.new(get_response.body) key = OpenSSL::PKey::RSA.new(get_response.body) assert cert.check_private_key(key) + prefix = "/CN=#{@user.email_address}" + assert_equal prefix, cert.subject.to_s.slice(0,prefix.size) end - # we'll store the fingerprint later. - test "fingerprint matches" do + test "fingerprint is stored with identity" do @user = FactoryGirl.create :user, effective_service_level_code: 2 login get '/1/smtp_cert', {}, RACK_ENV assert_text_response cert = OpenSSL::X509::Certificate.new(get_response.body) fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':') - skip "we're not storing the fingerprints yet" assert_equal fingerprint, @user.identity.cert_fingerprints.last end -- cgit v1.2.3 From e8ba98df64cb537e85de8624c0ebb08c4135ccca Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 19 May 2014 14:50:16 +0200 Subject: minor: fix tests --- app/controllers/v1/smtp_certs_controller.rb | 2 +- test/functional/v1/smtp_certs_controller_test.rb | 1 + test/support/api_integration_test.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb index 258b391..533a19a 100644 --- a/app/controllers/v1/smtp_certs_controller.rb +++ b/app/controllers/v1/smtp_certs_controller.rb @@ -3,7 +3,7 @@ class V1::SmtpCertsController < ApplicationController before_filter :require_login before_filter :require_email_account - # GET /cert + # GET /1/smtp_cert def show @cert = ClientCertificate.new prefix: current_user.email_address current_user.identity.cert_fingerprints << @cert.fingerprint diff --git a/test/functional/v1/smtp_certs_controller_test.rb b/test/functional/v1/smtp_certs_controller_test.rb index f9ba26f..169f414 100644 --- a/test/functional/v1/smtp_certs_controller_test.rb +++ b/test/functional/v1/smtp_certs_controller_test.rb @@ -18,6 +18,7 @@ class V1::SmtpCertsControllerTest < ActionController::TestCase test "send cert with username" do login effective_service_level: ServiceLevel.new(id: 2) cert = expect_cert(@current_user.email_address) + cert.expects(:fingerprint).returns('fingerprint') get :show assert_response :success assert_equal cert.to_s, @response.body diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb index aa9c00d..0e8e261 100644 --- a/test/support/api_integration_test.rb +++ b/test/support/api_integration_test.rb @@ -7,7 +7,7 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest @user ||= user ||= FactoryGirl.create(:user) @token ||= DUMMY_TOKEN # make sure @token is up to date if it already exists - @token.reload if @token.persisted + @token.reload if @token.persisted? @token.user_id = @user.id @token.last_seen_at = Time.now @token.save -- cgit v1.2.3 From 3a84578cf33685800c9216cfb4da12ea1fb0032f Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 19 May 2014 15:07:02 +0200 Subject: store fingerprints with timestamp Only storing the date as that should suffice for normal expiry and is less useful for identifying users by timestamps --- app/controllers/v1/smtp_certs_controller.rb | 18 ++++++++++++++++-- app/models/identity.rb | 12 +++++++++++- test/integration/api/smtp_cert_test.rb | 3 ++- test/support/api_integration_test.rb | 3 ++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb index 533a19a..fcc00b8 100644 --- a/app/controllers/v1/smtp_certs_controller.rb +++ b/app/controllers/v1/smtp_certs_controller.rb @@ -2,22 +2,36 @@ class V1::SmtpCertsController < ApplicationController before_filter :require_login before_filter :require_email_account + before_filter :fetch_identity # GET /1/smtp_cert def show @cert = ClientCertificate.new prefix: current_user.email_address - current_user.identity.cert_fingerprints << @cert.fingerprint - current_user.identity.save + @identity.register_cert(@cert) + @identity.save render text: @cert.to_s, content_type: 'text/plain' end protected + # + # Filters + # + def require_email_account access_denied unless service_level.provides? 'email' end + def fetch_identity + @identity = current_user.identity + end + + # + # Helper methods + # + def service_level current_user.effective_service_level end + end diff --git a/app/models/identity.rb b/app/models/identity.rb index 2f8d4eb..a4225e7 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -8,7 +8,7 @@ class Identity < CouchRest::Model::Base property :address, LocalEmail property :destination, Email property :keys, HashWithIndifferentAccess - property :cert_fingerprints, [String] + property :cert_fingerprints, Hash validate :unique_forward validate :alias_available @@ -108,6 +108,16 @@ class Identity < CouchRest::Model::Base write_attribute('keys', keys.merge(type => key.to_s)) end + def cert_fingerprints + read_attribute('cert_fingerprints') || Hash.new + end + + def register_cert(cert) + today = DateTime.now.to_date.to_s + write_attribute 'cert_fingerprints', + cert_fingerprints.merge(cert.fingerprint => today) + end + # for LoginFormatValidation def login self.address.handle diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb index 4f0f4a6..992249b 100644 --- a/test/integration/api/smtp_cert_test.rb +++ b/test/integration/api/smtp_cert_test.rb @@ -33,7 +33,8 @@ class SmtpCertTest < ApiIntegrationTest assert_text_response cert = OpenSSL::X509::Certificate.new(get_response.body) fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':') - assert_equal fingerprint, @user.identity.cert_fingerprints.last + today = DateTime.now.to_date.to_s + assert_equal({fingerprint => today}, @user.identity.cert_fingerprints) end test "fetching smtp certs requires email account" do diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb index 0e8e261..bd10f11 100644 --- a/test/support/api_integration_test.rb +++ b/test/support/api_integration_test.rb @@ -5,7 +5,8 @@ class ApiIntegrationTest < ActionDispatch::IntegrationTest def login(user = nil) @user ||= user ||= FactoryGirl.create(:user) - @token ||= DUMMY_TOKEN + # DUMMY_TOKEN will be frozen. So let's use a dup + @token ||= DUMMY_TOKEN.dup # make sure @token is up to date if it already exists @token.reload if @token.persisted? @token.user_id = @user.id -- cgit v1.2.3 From d2f4bd40342675717c6681e4ce845c316468c8b1 Mon Sep 17 00:00:00 2001 From: elijah Date: Tue, 20 May 2014 13:33:30 -0700 Subject: better detection if price link should be shown in the footer --- app/helpers/core_helper.rb | 7 +++++++ app/views/layouts/_footer.html.haml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/helpers/core_helper.rb b/app/helpers/core_helper.rb index 4126906..46e8fa4 100644 --- a/app/helpers/core_helper.rb +++ b/app/helpers/core_helper.rb @@ -10,4 +10,11 @@ module CoreHelper render 'common/home_page_buttons' end + # + # returns true if the configured service levels contain a level with a price attached + # + def paid_service_level? + APP_CONFIG[:service_levels].present? && APP_CONFIG[:service_levels].detect{|k,v| v['rate'].present?} + end + end diff --git a/app/views/layouts/_footer.html.haml b/app/views/layouts/_footer.html.haml index 340d36c..de53667 100644 --- a/app/views/layouts/_footer.html.haml +++ b/app/views/layouts/_footer.html.haml @@ -6,5 +6,5 @@ = link_to icon('info-sign') + t(:about), about_path - if lookup_context.exists?('pages/contact') = link_to icon('comment') + t(:contact), contact_path - - if APP_CONFIG[:service_levels].present? + - if paid_service_level? = link_to icon('shopping-cart') + t(:pricing), pricing_path -- cgit v1.2.3 From 00d5adc90ccadc7f4a2a0d54a5a31a1ad02f05be Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 26 May 2014 09:31:36 +0200 Subject: change from GET to POST for certs We create them. let's reflect that in the verb. --- app/controllers/v1/certs_controller.rb | 8 ++++++++ app/controllers/v1/smtp_certs_controller.rb | 4 ++-- config/routes.rb | 4 ++-- test/functional/v1/certs_controller_test.rb | 20 ++++++++++++++------ test/functional/v1/smtp_certs_controller_test.rb | 6 +++--- test/integration/api/smtp_cert_test.rb | 10 +++++----- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb index 73409ef..b6d1d0b 100644 --- a/app/controllers/v1/certs_controller.rb +++ b/app/controllers/v1/certs_controller.rb @@ -3,7 +3,15 @@ class V1::CertsController < ApplicationController before_filter :require_login, :unless => :anonymous_certs_allowed? # GET /cert + # deprecated - we actually create a new cert and that can + # be reflected in the action. GET /cert will eventually go + # away and be replaced by POST /cert def show + create + end + + # POST /cert + def create @cert = ClientCertificate.new(:prefix => service_level.cert_prefix) render text: @cert.to_s, content_type: 'text/plain' end diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb index fcc00b8..377a49c 100644 --- a/app/controllers/v1/smtp_certs_controller.rb +++ b/app/controllers/v1/smtp_certs_controller.rb @@ -4,8 +4,8 @@ class V1::SmtpCertsController < ApplicationController before_filter :require_email_account before_filter :fetch_identity - # GET /1/smtp_cert - def show + # POST /1/smtp_cert + def create @cert = ClientCertificate.new prefix: current_user.email_address @identity.register_cert(@cert) @identity.save diff --git a/config/routes.rb b/config/routes.rb index ff2d2cc..2853fbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,8 +25,8 @@ LeapWeb::Application.routes.draw do delete "logout" => "sessions#destroy", :as => "logout" resources :users, :only => [:create, :update, :destroy, :index] resources :messages, :only => [:index, :update] - resource :cert, :only => [:show] - resource :smtp_cert, :only => [:show] + resource :cert, :only => [:show, :create] + resource :smtp_cert, :only => [:create] resource :service, :only => [:show] end diff --git a/test/functional/v1/certs_controller_test.rb b/test/functional/v1/certs_controller_test.rb index fb8e9c4..ec34b01 100644 --- a/test/functional/v1/certs_controller_test.rb +++ b/test/functional/v1/certs_controller_test.rb @@ -2,26 +2,34 @@ require 'test_helper' class V1::CertsControllerTest < ActionController::TestCase - test "send unlimited cert without login" do + test "create unlimited cert without login" do with_config allow_anonymous_certs: true do cert = expect_cert('UNLIMITED') - get :show + post :create assert_response :success assert_equal cert.to_s, @response.body end end - test "send limited cert" do + test "create limited cert" do with_config allow_limited_certs: true do login cert = expect_cert('LIMITED') - get :show + post :create assert_response :success assert_equal cert.to_s, @response.body end end - test "send unlimited cert" do + test "create unlimited cert" do + login effective_service_level: ServiceLevel.new(id: 2) + cert = expect_cert('UNLIMITED') + post :create + assert_response :success + assert_equal cert.to_s, @response.body + end + + test "GET still works as an alias" do login effective_service_level: ServiceLevel.new(id: 2) cert = expect_cert('UNLIMITED') get :show @@ -30,7 +38,7 @@ class V1::CertsControllerTest < ActionController::TestCase end test "redirect if no eip service offered" do - get :show + post :create assert_response :redirect end diff --git a/test/functional/v1/smtp_certs_controller_test.rb b/test/functional/v1/smtp_certs_controller_test.rb index 169f414..ae1a214 100644 --- a/test/functional/v1/smtp_certs_controller_test.rb +++ b/test/functional/v1/smtp_certs_controller_test.rb @@ -4,14 +4,14 @@ class V1::SmtpCertsControllerTest < ActionController::TestCase test "no smtp cert without login" do with_config allow_anonymous_certs: true do - get :show, format: 'json' + post :create assert_access_denied end end test "require service level with email" do login - get :show + post :create assert_access_denied end @@ -19,7 +19,7 @@ class V1::SmtpCertsControllerTest < ActionController::TestCase login effective_service_level: ServiceLevel.new(id: 2) cert = expect_cert(@current_user.email_address) cert.expects(:fingerprint).returns('fingerprint') - get :show + post :create assert_response :success assert_equal cert.to_s, @response.body end diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb index 992249b..04e6f31 100644 --- a/test/integration/api/smtp_cert_test.rb +++ b/test/integration/api/smtp_cert_test.rb @@ -6,7 +6,7 @@ class SmtpCertTest < ApiIntegrationTest test "retrieve smtp cert" do @user = FactoryGirl.create :user, effective_service_level_code: 2 login - get '/1/smtp_cert', {}, RACK_ENV + post '/1/smtp_cert', {}, RACK_ENV assert_text_response assert_response_includes "BEGIN RSA PRIVATE KEY" assert_response_includes "END RSA PRIVATE KEY" @@ -17,7 +17,7 @@ class SmtpCertTest < ApiIntegrationTest test "cert and key" do @user = FactoryGirl.create :user, effective_service_level_code: 2 login - get '/1/smtp_cert', {}, RACK_ENV + post '/1/smtp_cert', {}, RACK_ENV assert_text_response cert = OpenSSL::X509::Certificate.new(get_response.body) key = OpenSSL::PKey::RSA.new(get_response.body) @@ -29,7 +29,7 @@ class SmtpCertTest < ApiIntegrationTest test "fingerprint is stored with identity" do @user = FactoryGirl.create :user, effective_service_level_code: 2 login - get '/1/smtp_cert', {}, RACK_ENV + post '/1/smtp_cert', {}, RACK_ENV assert_text_response cert = OpenSSL::X509::Certificate.new(get_response.body) fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':') @@ -39,13 +39,13 @@ class SmtpCertTest < ApiIntegrationTest test "fetching smtp certs requires email account" do login - get '/1/smtp_cert', {}, RACK_ENV + post '/1/smtp_cert', {}, RACK_ENV assert_json_response error: I18n.t(:not_authorized) end test "no anonymous smtp certs" do with_config allow_anonymous_certs: true do - get '/1/smtp_cert', {}, RACK_ENV + post '/1/smtp_cert', {}, RACK_ENV assert_json_response error: I18n.t(:not_authorized) end end -- cgit v1.2.3 From f221e5313fe54a2efa127b547916c7c812110449 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 26 May 2014 09:56:11 +0200 Subject: fix test to require login --- test/functional/v1/smtp_certs_controller_test.rb | 2 +- test/support/auth_test_helper.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/functional/v1/smtp_certs_controller_test.rb b/test/functional/v1/smtp_certs_controller_test.rb index ae1a214..9281ae6 100644 --- a/test/functional/v1/smtp_certs_controller_test.rb +++ b/test/functional/v1/smtp_certs_controller_test.rb @@ -5,7 +5,7 @@ class V1::SmtpCertsControllerTest < ActionController::TestCase test "no smtp cert without login" do with_config allow_anonymous_certs: true do post :create - assert_access_denied + assert_login_required end end diff --git a/test/support/auth_test_helper.rb b/test/support/auth_test_helper.rb index 57f9f9b..e1961aa 100644 --- a/test/support/auth_test_helper.rb +++ b/test/support/auth_test_helper.rb @@ -19,6 +19,10 @@ module AuthTestHelper return @current_user end + def assert_login_required + assert_access_denied(true, false) + end + def assert_access_denied(denied = true, logged_in = true) if denied if @response.content_type == 'application/json' -- cgit v1.2.3 From 5764daae090227bf4c5967900b708392c967be47 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 1 May 2014 10:45:57 +0200 Subject: hash token with sha512 against timing attacs #3398 --- .../controller_extension/token_authentication.rb | 4 ++-- app/models/token.rb | 13 ++++++++++-- test/functional/test_helpers_test.rb | 2 +- test/functional/v1/sessions_controller_test.rb | 2 +- test/integration/api/token_test.rb | 15 ++++++++++++++ test/support/auth_test_helper.rb | 5 +++-- test/unit/token_test.rb | 23 +++++++++++++--------- 7 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 test/integration/api/token_test.rb diff --git a/app/controllers/controller_extension/token_authentication.rb b/app/controllers/controller_extension/token_authentication.rb index 6e0a6ce..b0ed624 100644 --- a/app/controllers/controller_extension/token_authentication.rb +++ b/app/controllers/controller_extension/token_authentication.rb @@ -2,8 +2,8 @@ module ControllerExtension::TokenAuthentication extend ActiveSupport::Concern def token - @token ||= authenticate_with_http_token do |token_id, options| - Token.find(token_id) + @token ||= authenticate_with_http_token do |token, options| + Token.find_by_token(token) end end diff --git a/app/models/token.rb b/app/models/token.rb index e759ee3..ff2ad12 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,3 +1,5 @@ +require 'digest/sha2' + class Token < CouchRest::Model::Base use_database :tokens @@ -11,10 +13,16 @@ class Token < CouchRest::Model::Base validates :user_id, presence: true + attr_accessor :token + design do view :by_last_seen_at end + def self.find_by_token(token) + self.find Digest::SHA512.hexdigest(token) + end + def self.expires_after APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] end @@ -31,7 +39,7 @@ class Token < CouchRest::Model::Base end def to_s - id + token end def authenticate @@ -65,7 +73,8 @@ class Token < CouchRest::Model::Base def initialize(*args) super if new_record? - self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') + self.token = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') + self.id = Digest::SHA512.hexdigest(self.token) self.last_seen_at = Time.now end end diff --git a/test/functional/test_helpers_test.rb b/test/functional/test_helpers_test.rb index 845e516..ca85482 100644 --- a/test/functional/test_helpers_test.rb +++ b/test/functional/test_helpers_test.rb @@ -27,7 +27,7 @@ class TestHelpersTest < ActionController::TestCase def test_login_adds_token_header login token_present = @controller.authenticate_with_http_token do |token, options| - assert_equal @token.id, token + assert_equal @token.to_s, token end # authenticate_with_http_token just returns nil and does not # execute the block if there is no token. So we have to also diff --git a/test/functional/v1/sessions_controller_test.rb b/test/functional/v1/sessions_controller_test.rb index df0d681..8bb6acd 100644 --- a/test/functional/v1/sessions_controller_test.rb +++ b/test/functional/v1/sessions_controller_test.rb @@ -48,7 +48,7 @@ class V1::SessionsControllerTest < ActionController::TestCase assert_response :success assert json_response.keys.include?("id") assert json_response.keys.include?("token") - assert token = Token.find(json_response['token']) + assert token = Token.find_by_token(json_response['token']) assert_equal @user.id, token.user_id end diff --git a/test/integration/api/token_test.rb b/test/integration/api/token_test.rb new file mode 100644 index 0000000..ad3ac22 --- /dev/null +++ b/test/integration/api/token_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' +require_relative 'srp_test' + +class TokenTest < SrpTest + + setup do + register_user + end + + test "stores token SHA512 encoded" do + authenticate + token = server_auth['token'] + assert Token.find(Digest::SHA512.hexdigest(token)) + end +end diff --git a/test/support/auth_test_helper.rb b/test/support/auth_test_helper.rb index 57f9f9b..28e9633 100644 --- a/test/support/auth_test_helper.rb +++ b/test/support/auth_test_helper.rb @@ -46,8 +46,9 @@ module AuthTestHelper protected def header_for_token_auth - @token = find_record(:token, :authenticate => @current_user) - ActionController::HttpAuthentication::Token.encode_credentials @token.id + @token = stub_record(:token, :authenticate => @current_user) + Token.stubs(:find_by_token).with(@token.token).returns(@token) + ActionController::HttpAuthentication::Token.encode_credentials @token.token end def expect_warden_logout diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb index a3c6cf6..b143345 100644 --- a/test/unit/token_test.rb +++ b/test/unit/token_test.rb @@ -14,17 +14,22 @@ class ClientCertificateTest < ActiveSupport::TestCase assert_equal @user, sample.authenticate end - test "token id is secure" do + test "token is secure" do sample = Token.new(:user_id => @user.id) other = Token.new(:user_id => @user.id) - assert sample.id, - "id is set on initialization" - assert sample.id[0..10] != other.id[0..10], - "token id prefixes should not repeat" - assert /[g-zG-Z]/.match(sample.id), - "should use non hex chars in the token id" - assert sample.id.size > 16, - "token id should be more than 16 chars long" + assert sample.token, + "token is set on initialization" + assert sample.token[0..10] != other.token[0..10], + "token prefixes should not repeat" + assert /[g-zG-Z]/.match(sample.token), + "should use non hex chars in the token" + assert sample.token.size > 16, + "token should be more than 16 chars long" + end + + test "token id is hash of the token" do + sample = Token.new(:user_id => @user.id) + assert_equal Digest::SHA512.hexdigest(sample.token), sample.id end test "token checks for user" do -- cgit v1.2.3 From 02426b7f143e82447ff41a604c286b556ab8d3a5 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 09:06:31 +0200 Subject: use i18n.missing_translations This will print missing translation keys at the end of the tests --- Gemfile | 1 + Gemfile.lock | 3 +++ test/test_helper.rb | 2 ++ 3 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index 816cc4a..ae11e0e 100644 --- a/Gemfile +++ b/Gemfile @@ -60,6 +60,7 @@ end group :test, :development do gem 'thin' + gem 'i18n-missing_translations' end group :assets do diff --git a/Gemfile.lock b/Gemfile.lock index 53e0d0b..2c229f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,6 +122,8 @@ GEM hike (1.2.3) http_accept_language (2.0.0) i18n (0.6.9) + i18n-missing_translations (0.0.2) + i18n (~> 0.6.0) journey (1.0.4) jquery-rails (3.0.4) railties (>= 3.0, < 5.0) @@ -254,6 +256,7 @@ DEPENDENCIES haml (~> 3.1.7) haml-rails (~> 0.3.4) http_accept_language + i18n-missing_translations jquery-rails json kaminari (= 0.13.0) diff --git a/test/test_helper.rb b/test/test_helper.rb index d001ac7..7959ddb 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,4 +27,6 @@ class ActiveSupport::TestCase File.join(Rails.root, 'test', 'files', name) end + require 'i18n/missing_translations' + at_exit { I18n.missing_translations.dump } end -- cgit v1.2.3 From e7d6bcce2a04a049926e75074605a2e7f91ede1a Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 09:08:42 +0200 Subject: move comment related tests out of TicketControllerTest This controller does too much - so the tests are also getting large and hard to keep track of --- .../test/functional/ticket_comments_test.rb | 89 ++++++++++++++++++++++ .../test/functional/tickets_controller_test.rb | 78 ------------------- 2 files changed, 89 insertions(+), 78 deletions(-) create mode 100644 engines/support/test/functional/ticket_comments_test.rb diff --git a/engines/support/test/functional/ticket_comments_test.rb b/engines/support/test/functional/ticket_comments_test.rb new file mode 100644 index 0000000..e30a018 --- /dev/null +++ b/engines/support/test/functional/ticket_comments_test.rb @@ -0,0 +1,89 @@ +require 'test_helper' + +class TicketsCommentsTest < ActionController::TestCase + tests TicketsController + + teardown do + # destroy all tickets that were created during the test + Ticket.all.each{|t| t.destroy} + end + + test "add comment to unauthenticated ticket" do + ticket = FactoryGirl.create :ticket, :created_by => nil + + assert_difference('Ticket.find(ticket.id).comments.count') do + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + end + + assert_equal ticket, assigns(:ticket) # still same ticket, with different comments + assert_not_equal ticket.comments, assigns(:ticket).comments # ticket == assigns(:ticket), but they have different comments (which we want) + + end + + + test "add comment to own authenticated ticket" do + + login + ticket = FactoryGirl.create :ticket, :created_by => @current_user.id + + #they should be able to comment if it is their ticket: + assert_difference('Ticket.find(ticket.id).comments.count') do + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + end + assert_not_equal ticket.comments, assigns(:ticket).comments + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + + end + + + test "cannot comment if it is not your ticket" do + + other_user = find_record :user + login :is_admin? => false, :email => nil + ticket = FactoryGirl.create :ticket, :created_by => other_user.id + # they should *not* be able to comment if it is not their ticket + put :update, :id => ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"not allowed comment"}} } + assert_response :redirect + assert_access_denied + + assert_equal ticket.comments.map(&:body), assigns(:ticket).comments.map(&:body) + + end + + + test "admin add comment to authenticated ticket" do + + other_user = find_record :user + login :is_admin? => true + + ticket = FactoryGirl.create :ticket, :created_by => other_user.id + + #admin should be able to comment: + assert_difference('Ticket.find(ticket.id).comments.count') do + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + end + assert_not_equal ticket.comments, assigns(:ticket).comments + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + end + + test "commenting on a ticket adds to tickets that are mine" do + testticket = FactoryGirl.create :ticket + user = find_record :admin_user + login user + get :index, {:user_id => user.id, :open_status => "open"} + assert_difference('assigns[:all_tickets].count') do + put :update, :id => testticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} + get :index, {:user_id => user.id, :open_status => "open"} + end + + assert assigns(:all_tickets).include?(assigns(:ticket)) + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + end + +end diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index fc4a6f8..bb86dad 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -104,69 +104,6 @@ class TicketsControllerTest < ActionController::TestCase assert_equal assigns(:ticket).comments.first.posted_by, @current_user.id end - test "add comment to unauthenticated ticket" do - ticket = FactoryGirl.create :ticket, :created_by => nil - - assert_difference('Ticket.find(ticket.id).comments.count') do - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - end - - assert_equal ticket, assigns(:ticket) # still same ticket, with different comments - assert_not_equal ticket.comments, assigns(:ticket).comments # ticket == assigns(:ticket), but they have different comments (which we want) - - end - - - test "add comment to own authenticated ticket" do - - login - ticket = FactoryGirl.create :ticket, :created_by => @current_user.id - - #they should be able to comment if it is their ticket: - assert_difference('Ticket.find(ticket.id).comments.count') do - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - end - assert_not_equal ticket.comments, assigns(:ticket).comments - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id - - end - - - test "cannot comment if it is not your ticket" do - - other_user = find_record :user - login :is_admin? => false, :email => nil - ticket = FactoryGirl.create :ticket, :created_by => other_user.id - # they should *not* be able to comment if it is not their ticket - put :update, :id => ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"not allowed comment"}} } - assert_response :redirect - assert_access_denied - - assert_equal ticket.comments.map(&:body), assigns(:ticket).comments.map(&:body) - - end - - - test "admin add comment to authenticated ticket" do - - other_user = find_record :user - login :is_admin? => true - - ticket = FactoryGirl.create :ticket, :created_by => other_user.id - - #admin should be able to comment: - assert_difference('Ticket.find(ticket.id).comments.count') do - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - end - assert_not_equal ticket.comments, assigns(:ticket).comments - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id - end - test "tickets by admin" do other_user = find_record :user ticket = FactoryGirl.create :ticket, :created_by => other_user.id @@ -196,21 +133,6 @@ class TicketsControllerTest < ActionController::TestCase assert !assigns(:all_tickets).include?(testticket) end - test "commenting on a ticket adds to tickets that are mine" do - testticket = FactoryGirl.create :ticket - user = find_record :admin_user - login user - get :index, {:user_id => user.id, :open_status => "open"} - assert_difference('assigns[:all_tickets].count') do - put :update, :id => testticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} - get :index, {:user_id => user.id, :open_status => "open"} - end - - assert assigns(:all_tickets).include?(assigns(:ticket)) - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id - end - test "admin ticket ordering" do tickets = FactoryGirl.create_list :ticket, 2 -- cgit v1.2.3 From 730e31017109994c24db431fde12f575ed5c1467 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 09:13:25 +0200 Subject: FlashResponder will automagically add flash messages --- app/views/layouts/_messages.html.haml | 2 +- config/initializers/i18n.rb | 4 ++++ config/locales/en.yml | 6 ++++++ engines/support/app/controllers/tickets_controller.rb | 13 ++++++------- lib/extensions/couchrest.rb | 3 +++ 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/views/layouts/_messages.html.haml b/app/views/layouts/_messages.html.haml index 7ff985f..18be54f 100644 --- a/app/views/layouts/_messages.html.haml +++ b/app/views/layouts/_messages.html.haml @@ -1,6 +1,6 @@ #messages - flash.each do |name, msg| - if msg.is_a?(String) - %div{:class => "alert alert-#{name == :notice ? "success" : "error"}"} + %div{:class => "alert alert-#{name == :notice ? "success" : name}"} %a.close{"data-dismiss" => "alert"} × = content_tag :div, format_flash(msg), :id => "flash_#{name}" diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb index c277a22..b209d00 100644 --- a/config/initializers/i18n.rb +++ b/config/initializers/i18n.rb @@ -8,3 +8,7 @@ MATCH_LOCALE = /(#{I18n.available_locales.join('|')})/ # I18n.available_locales is always an array of symbols, but for comparison with # params we need it to be an array of strings. LOCALES_STRING = I18n.available_locales.map(&:to_s) + +# enable using the cascade option +# see svenfuchs.com/2011/2/11/organizing-translations-with-i18n-cascade-and-i18n-missingtranslations +I18n::Backend::Simple.send(:include, I18n::Backend::Cascade) diff --git a/config/locales/en.yml b/config/locales/en.yml index 899d659..a7a76a8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,4 +1,10 @@ en: + flash: + success: "%{resource} was successfully saved." + create: + success: "%{resource} was successfully created." + update: + success: "%{resource} was successfully updated." home: Home privacy_policy: Privacy Policy terms_of_service: Terms of Service diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 99357ab..19663c3 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -23,11 +23,8 @@ class TicketsController < ApplicationController @ticket.comments.last.posted_by = current_user.id @ticket.comments.last.private = false unless admin? @ticket.created_by = current_user.id - if @ticket.save - flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket)) - if !logged_in? - flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) - end + if @ticket.save && !logged_in? + flash[:success] = t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) end respond_with(@ticket, :location => auto_ticket_path(@ticket)) end @@ -62,10 +59,8 @@ class TicketsController < ApplicationController end if @ticket.changed? and @ticket.save - flash[:notice] = t(:changes_saved) redirect_to_tickets else - flash[:error] = @ticket.errors.full_messages.join(". ") if @ticket.changed? redirect_to auto_ticket_path(@ticket) end end @@ -88,6 +83,10 @@ class TicketsController < ApplicationController @title = t(:tickets) end + def self.responder + Responders::FlashResponder + end + private # diff --git a/lib/extensions/couchrest.rb b/lib/extensions/couchrest.rb index 95f5d92..0e5051a 100644 --- a/lib/extensions/couchrest.rb +++ b/lib/extensions/couchrest.rb @@ -1,5 +1,8 @@ module CouchRest module Model + class Base + extend ActiveModel::Naming + end module Designs class View -- cgit v1.2.3 From 29fd85f605ee41275e383681a6b3e8ea0615d525 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 11:18:28 +0200 Subject: splitting up long functional test case --- engines/support/test/factories.rb | 12 +++- .../test/functional/ticket_comments_test.rb | 16 ++++- .../test/functional/tickets_controller_test.rb | 74 +++++++++++++--------- 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/engines/support/test/factories.rb b/engines/support/test/factories.rb index be04f15..bcf41e8 100644 --- a/engines/support/test/factories.rb +++ b/engines/support/test/factories.rb @@ -6,13 +6,23 @@ FactoryGirl.define do factory :ticket_with_comment do comments_attributes do - { "0" => { "body" => Faker::Lorem.sentences.join(" ") } } + { "0" => { + "body" => Faker::Lorem.sentences.join(" "), + "posted_by" => created_by + } } end end factory :ticket_with_creator do created_by { FactoryGirl.create(:user).id } end + + end + + # TicketComments can not be saved. so only use this with build + # and add to a ticket afterwards + factory :ticket_comment do + body { Faker::Lorem.sentences.join(" ") } end end diff --git a/engines/support/test/functional/ticket_comments_test.rb b/engines/support/test/functional/ticket_comments_test.rb index e30a018..5cbe233 100644 --- a/engines/support/test/functional/ticket_comments_test.rb +++ b/engines/support/test/functional/ticket_comments_test.rb @@ -39,8 +39,7 @@ class TicketsCommentsTest < ActionController::TestCase end - test "cannot comment if it is not your ticket" do - + test "cannot comment if it is another users ticket" do other_user = find_record :user login :is_admin? => false, :email => nil ticket = FactoryGirl.create :ticket, :created_by => other_user.id @@ -50,10 +49,23 @@ class TicketsCommentsTest < ActionController::TestCase assert_access_denied assert_equal ticket.comments.map(&:body), assigns(:ticket).comments.map(&:body) + end + test "authenticated comment on an anonymous ticket adds to my tickets" do + login + ticket = FactoryGirl.create :ticket + other_ticket = FactoryGirl.create :ticket + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + assert_not_nil assigns(:ticket).comments.last.posted_by + assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + visible_tickets = Ticket.search admin_status: 'mine', + user_id: @current_user.id, is_admin: false + assert_equal [ticket], visible_tickets.all end + test "admin add comment to authenticated ticket" do other_user = find_record :user diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index bb86dad..acc088f 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -155,51 +155,63 @@ class TicketsControllerTest < ActionController::TestCase end - test "tickets for regular user" do + test "own tickets include tickets commented upon" do login ticket = FactoryGirl.create :ticket other_ticket = FactoryGirl.create :ticket - - put :update, :id => ticket.id, - :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } - assert_not_nil assigns(:ticket).comments.last.posted_by - assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id + comment = FactoryGirl.build(:ticket_comment, posted_by: @current_user.id) + ticket.comments << comment + ticket.save get :index, {:open_status => "open"} assert assigns(:all_tickets).count > 0 assert assigns(:all_tickets).include?(ticket) assert !assigns(:all_tickets).include?(other_ticket) + end - # user should have one more ticket if a new tick gets a comment by this user - assert_difference('assigns[:all_tickets].count') do - put :update, :id => other_ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} - get :index, {:open_status => "open"} - end - assert assigns(:all_tickets).include?(other_ticket) - - # if we close one ticket, the user should have 1 less open ticket - assert_difference('assigns[:all_tickets].count', -1) do - other_ticket.reload - other_ticket.close - other_ticket.save - get :index, {:open_status => "open"} - end + test "list all tickets created by user" do + login + ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + other_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + get :index, {:open_status => "open"} + assert_equal 2, assigns[:all_tickets].count + end - number_open_tickets = assigns(:all_tickets).count + test "closing ticket removes from open tickets list" do + login + ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + other_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + other_ticket.reload + other_ticket.close + other_ticket.save + get :index, {:open_status => "open"} + assert_equal 1, assigns[:all_tickets].count + end - # look at closed tickets: + test "list closed tickets only" do + login + open_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + closed_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id, is_open: false get :index, {:open_status => "closed"} - assert !assigns(:all_tickets).include?(ticket) - assert assigns(:all_tickets).include?(other_ticket) - number_closed_tickets = assigns(:all_tickets).count + assert_equal [closed_ticket], assigns(:all_tickets).all + end - # all tickets should equal closed + open + test "list all tickets inludes closed + open" do + login + open_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + closed_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id, is_open: false get :index, {:open_status => "all"} - assert assigns(:all_tickets).include?(ticket) - assert assigns(:all_tickets).include?(other_ticket) - assert_equal assigns(:all_tickets).count, number_closed_tickets + number_open_tickets - - + assert_equal 2, assigns(:all_tickets).count + assert assigns(:all_tickets).include?(open_ticket) + assert assigns(:all_tickets).include?(closed_ticket) end end -- cgit v1.2.3 From 7638970e233eaebc48abd499c37c274b48c97a96 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 11:41:26 +0200 Subject: separate tests for the ticket list from main controller test --- .../test/functional/tickets_controller_test.rb | 120 ++------------------ .../support/test/functional/tickets_list_test.rb | 122 +++++++++++++++++++++ 2 files changed, 131 insertions(+), 111 deletions(-) create mode 100644 engines/support/test/functional/tickets_list_test.rb diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index acc088f..e103d01 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -1,5 +1,14 @@ require 'test_helper' +# +# Tests for the basic actions in the TicketsController +# +# Also see +# TicketCommentsTest +# TicketsListTest +# +# for detailed functional tests for comments and index action. +# class TicketsControllerTest < ActionController::TestCase teardown do @@ -103,116 +112,5 @@ class TicketsControllerTest < ActionController::TestCase assert_not_nil assigns(:ticket).comments.first.posted_by assert_equal assigns(:ticket).comments.first.posted_by, @current_user.id end - - test "tickets by admin" do - other_user = find_record :user - ticket = FactoryGirl.create :ticket, :created_by => other_user.id - - login :is_admin? => true - - get :index, {:admin_status => "all", :open_status => "open"} - assert assigns(:all_tickets).count > 0 - - # if we close one ticket, the admin should have 1 less open ticket - assert_difference('assigns[:all_tickets].count', -1) do - assigns(:tickets).first.close - assigns(:tickets).first.save - get :index, {:admin_status => "all", :open_status => "open"} - end - end - - - test "admin_status mine vs all" do - testticket = FactoryGirl.create :ticket - user = find_record :user - login :is_admin? => true, :email => nil - - get :index, {:open_status => "open"} - assert assigns(:all_tickets).include?(testticket) - get :index, {:user_id => user.id, :open_status => "open"} - assert !assigns(:all_tickets).include?(testticket) - end - - test "admin ticket ordering" do - tickets = FactoryGirl.create_list :ticket, 2 - - login :is_admin? => true, :email => nil - get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_desc'} - - # this will consider all tickets, not just those on first page - first_tick = assigns(:all_tickets).all.first - last_tick = assigns(:all_tickets).all.last - assert first_tick.created_at > last_tick.created_at - - # and now reverse order: - get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_asc'} - - assert_equal first_tick, assigns(:all_tickets).last - assert_equal last_tick, assigns(:all_tickets).first - - assert_not_equal first_tick, assigns(:all_tickets).first - assert_not_equal last_tick, assigns(:all_tickets).last - - end - - test "own tickets include tickets commented upon" do - login - ticket = FactoryGirl.create :ticket - other_ticket = FactoryGirl.create :ticket - comment = FactoryGirl.build(:ticket_comment, posted_by: @current_user.id) - ticket.comments << comment - ticket.save - - get :index, {:open_status => "open"} - assert assigns(:all_tickets).count > 0 - assert assigns(:all_tickets).include?(ticket) - assert !assigns(:all_tickets).include?(other_ticket) - end - - test "list all tickets created by user" do - login - ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id - other_ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id - get :index, {:open_status => "open"} - assert_equal 2, assigns[:all_tickets].count - end - - test "closing ticket removes from open tickets list" do - login - ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id - other_ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id - other_ticket.reload - other_ticket.close - other_ticket.save - get :index, {:open_status => "open"} - assert_equal 1, assigns[:all_tickets].count - end - - test "list closed tickets only" do - login - open_ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id - closed_ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id, is_open: false - get :index, {:open_status => "closed"} - assert_equal [closed_ticket], assigns(:all_tickets).all - end - - test "list all tickets inludes closed + open" do - login - open_ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id - closed_ticket = FactoryGirl.create :ticket_with_comment, - created_by: @current_user.id, is_open: false - get :index, {:open_status => "all"} - assert_equal 2, assigns(:all_tickets).count - assert assigns(:all_tickets).include?(open_ticket) - assert assigns(:all_tickets).include?(closed_ticket) - end - end diff --git a/engines/support/test/functional/tickets_list_test.rb b/engines/support/test/functional/tickets_list_test.rb new file mode 100644 index 0000000..4c4cdef --- /dev/null +++ b/engines/support/test/functional/tickets_list_test.rb @@ -0,0 +1,122 @@ +require 'test_helper' + +class TicketsListTest < ActionController::TestCase + tests TicketsController + + teardown do + # destroy all records that were created during the test + Ticket.all.each{|t| t.destroy} + User.all.each{|u| u.account.destroy} + end + + + test "tickets by admin" do + other_user = find_record :user + ticket = FactoryGirl.create :ticket, :created_by => other_user.id + + login :is_admin? => true + + get :index, {:admin_status => "all", :open_status => "open"} + assert assigns(:all_tickets).count > 0 + + # if we close one ticket, the admin should have 1 less open ticket + assert_difference('assigns[:all_tickets].count', -1) do + assigns(:tickets).first.close + assigns(:tickets).first.save + get :index, {:admin_status => "all", :open_status => "open"} + end + end + + + test "admin_status mine vs all" do + testticket = FactoryGirl.create :ticket + user = find_record :user + login :is_admin? => true, :email => nil + + get :index, {:open_status => "open"} + assert assigns(:all_tickets).include?(testticket) + get :index, {:user_id => user.id, :open_status => "open"} + assert !assigns(:all_tickets).include?(testticket) + end + + test "admin ticket ordering" do + tickets = FactoryGirl.create_list :ticket, 2 + + login :is_admin? => true, :email => nil + get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_desc'} + + # this will consider all tickets, not just those on first page + first_tick = assigns(:all_tickets).all.first + last_tick = assigns(:all_tickets).all.last + assert first_tick.created_at > last_tick.created_at + + # and now reverse order: + get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_asc'} + + assert_equal first_tick, assigns(:all_tickets).last + assert_equal last_tick, assigns(:all_tickets).first + + assert_not_equal first_tick, assigns(:all_tickets).first + assert_not_equal last_tick, assigns(:all_tickets).last + + end + + test "own tickets include tickets commented upon" do + login + ticket = FactoryGirl.create :ticket + other_ticket = FactoryGirl.create :ticket + comment = FactoryGirl.build(:ticket_comment, posted_by: @current_user.id) + ticket.comments << comment + ticket.save + + get :index, {:open_status => "open"} + assert assigns(:all_tickets).count > 0 + assert assigns(:all_tickets).include?(ticket) + assert !assigns(:all_tickets).include?(other_ticket) + end + + test "list all tickets created by user" do + login + ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + other_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + get :index, {:open_status => "open"} + assert_equal 2, assigns[:all_tickets].count + end + + test "closing ticket removes from open tickets list" do + login + ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + other_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + other_ticket.reload + other_ticket.close + other_ticket.save + get :index, {:open_status => "open"} + assert_equal 1, assigns[:all_tickets].count + end + + test "list closed tickets only" do + login + open_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + closed_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id, is_open: false + get :index, {:open_status => "closed"} + assert_equal [closed_ticket], assigns(:all_tickets).all + end + + test "list all tickets inludes closed + open" do + login + open_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + closed_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id, is_open: false + get :index, {:open_status => "all"} + assert_equal 2, assigns(:all_tickets).count + assert assigns(:all_tickets).include?(open_ticket) + assert assigns(:all_tickets).include?(closed_ticket) + end +end -- cgit v1.2.3 From 467dd712a19d48fc653cfc0e58201e6657d2c1f9 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 12:30:55 +0200 Subject: split up and refactor TicketController#update close and open actions for plain opening and closing the tickets respond_with so fields are not cleared on invalid update the custom actions are not strictly restful. But adding a subresource felt like too much overhead and is conceptually hard to grasp (so we destroy the openess of the ticket to close it?). --- .../support/app/controllers/tickets_controller.rb | 66 ++++++++++------------ engines/support/app/models/ticket.rb | 2 + .../support/app/views/tickets/_comments.html.haml | 8 +++ .../support/app/views/tickets/_edit_form.html.haml | 7 ++- engines/support/app/views/tickets/edit.html.haml | 5 ++ engines/support/app/views/tickets/show.html.haml | 9 +-- engines/support/config/routes.rb | 14 ++++- .../test/functional/tickets_controller_test.rb | 17 ++++++ 8 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 engines/support/app/views/tickets/_comments.html.haml create mode 100644 engines/support/app/views/tickets/edit.html.haml diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 19663c3..bb98277 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -5,8 +5,8 @@ class TicketsController < ApplicationController #has_scope :open, :type => boolean before_filter :require_login, :only => [:index] - before_filter :fetch_ticket, :only => [:show, :update, :destroy] - before_filter :require_ticket_access, :only => [:show, :update, :destroy] + before_filter :fetch_ticket, except: [:new, :create, :index] + before_filter :require_ticket_access, except: [:new, :create, :index] before_filter :fetch_user before_filter :set_title @@ -37,33 +37,32 @@ class TicketsController < ApplicationController end end - def update - if params[:button] == 'close' - @ticket.is_open = false - @ticket.save - redirect_to_tickets - elsif params[:button] == 'open' - @ticket.is_open = true - @ticket.save - redirect_to auto_ticket_path(@ticket) - else - @ticket.attributes = cleanup_ticket_params(params[:ticket]) + def close + @ticket.close + @ticket.save + redirect_to redirection_path + end - if params[:button] == 'reply_and_close' - @ticket.close - end + def open + @ticket.reopen + @ticket.save + redirect_to redirection_path + end - if @ticket.comments_changed? - @ticket.comments.last.posted_by = current_user.id - @ticket.comments.last.private = false unless admin? - end + def update + @ticket.attributes = cleanup_ticket_params(params[:ticket]) - if @ticket.changed? and @ticket.save - redirect_to_tickets - else - redirect_to auto_ticket_path(@ticket) - end + if params[:button] == 'reply_and_close' + @ticket.close + end + + if @ticket.comments_changed? + @ticket.comments.last.posted_by = current_user.id + @ticket.comments.last.private = false unless admin? end + + @ticket.save + respond_with @ticket, location: redirection_path end def index @@ -90,19 +89,14 @@ class TicketsController < ApplicationController private # - # redirects to ticket index, if appropriate. - # otherwise, just redirects to @ticket + # ticket index, if appropriate. + # otherwise, just @ticket # - def redirect_to_tickets - if logged_in? - if params[:button] == t(:reply_and_close) - redirect_to auto_tickets_path - else - redirect_to auto_ticket_path(@ticket) - end + def redirection_path + if logged_in? && params[:button] == t(:reply_and_close) + auto_tickets_path else - # if we are not logged in, there is no index to view - redirect_to auto_ticket_path(@ticket) + auto_ticket_path(@ticket) end end diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb index bf5df53..161507c 100644 --- a/engines/support/app/models/ticket.rb +++ b/engines/support/app/models/ticket.rb @@ -39,6 +39,8 @@ class Ticket < CouchRest::Model::Base # * valid email address validates :email, :allow_blank => true, :format => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/ + # validates :comments, presence: true + def self.search(options = {}) @selection = TicketSelection.new(options) @selection.tickets diff --git a/engines/support/app/views/tickets/_comments.html.haml b/engines/support/app/views/tickets/_comments.html.haml new file mode 100644 index 0000000..0a3b345 --- /dev/null +++ b/engines/support/app/views/tickets/_comments.html.haml @@ -0,0 +1,8 @@ +%table.table.table-striped.table-bordered + %tbody + = render :partial => 'tickets/comment', :collection => @ticket.comments + %tr + %td.user + = current_user.login || t(:anonymous) + %td.comment + = render 'tickets/new_comment_form' diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index b8da779..22815f2 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -17,17 +17,18 @@ regarding_user_link = '' end -= simple_form_for @ticket do |f| +- url = url_for([@ticket.is_open? ? :close : :open, @ticket]) += simple_form_for @ticket, url: url do |f| = hidden_ticket_fields %p.first - if @ticket.is_open? %span.label.label-info %b{style: 'padding:10px'}= t(:open) - = f.button :loading, t(:close), value: 'close', class: 'btn-mini' + = f.button :loading, t(:close), class: 'btn-mini' - else %span.label.label-success %b{style: 'padding:10px'}= t(:closed) - = f.button :loading, t(:open), value: 'open', class: 'btn-mini' + = f.button :loading, t(:open), class: 'btn-mini' %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe = simple_form_for @ticket do |f| = hidden_ticket_fields diff --git a/engines/support/app/views/tickets/edit.html.haml b/engines/support/app/views/tickets/edit.html.haml new file mode 100644 index 0000000..99afa2a --- /dev/null +++ b/engines/support/app/views/tickets/edit.html.haml @@ -0,0 +1,5 @@ +- @show_navigation = params[:user_id].present? + +.ticket + = render 'tickets/edit_form' + = render 'tickets/comments' diff --git a/engines/support/app/views/tickets/show.html.haml b/engines/support/app/views/tickets/show.html.haml index 4f3c127..99afa2a 100644 --- a/engines/support/app/views/tickets/show.html.haml +++ b/engines/support/app/views/tickets/show.html.haml @@ -2,11 +2,4 @@ .ticket = render 'tickets/edit_form' - %table.table.table-striped.table-bordered - %tbody - = render :partial => 'tickets/comment', :collection => @ticket.comments - %tr - %td.user - = current_user.login || t(:anonymous) - %td.comment - = render 'tickets/new_comment_form' + = render 'tickets/comments' diff --git a/engines/support/config/routes.rb b/engines/support/config/routes.rb index 23e0c11..ca471b3 100644 --- a/engines/support/config/routes.rb +++ b/engines/support/config/routes.rb @@ -1,8 +1,16 @@ Rails.application.routes.draw do - scope "(:locale)", :locale => MATCH_LOCALE do - resources :tickets, :except => :edit + scope "(:locale)", locale: MATCH_LOCALE do + + resources :tickets, except: :edit do + member do + put 'open' + put 'close' + end + end + resources :users do - resources :tickets, :except => :edit + resources :tickets, except: :edit end + end end diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index e103d01..1d074cc 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -112,5 +112,22 @@ class TicketsControllerTest < ActionController::TestCase assert_not_nil assigns(:ticket).comments.first.posted_by assert_equal assigns(:ticket).comments.first.posted_by, @current_user.id end + + test "close ticket" do + login + open_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id + post :close, id: open_ticket.id + assert !open_ticket.reload.is_open + end + + test "reopen ticket" do + login + open_ticket = FactoryGirl.create :ticket_with_comment, + created_by: @current_user.id, is_open: false + post :open, id: open_ticket.id + assert open_ticket.reload.is_open + end + end -- cgit v1.2.3 From c10f9311678ff2183443bc03e153b30d3b68ff74 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 13:09:59 +0200 Subject: Controller#flash_for instead of FlashResponder FlashResponder added a flash before responding. However at the point of responding objects have already been saved. So there is no way to test if they were changed. Now instead we can call flash_for resource before resource.save and it will add the flash messages only if the resource was actually changed. --- app/controllers/controller_extension/flash.rb | 33 ++++++++++++++++++++++ config/initializers/add_controller_methods.rb | 1 + .../support/app/controllers/tickets_controller.rb | 8 ++---- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 app/controllers/controller_extension/flash.rb diff --git a/app/controllers/controller_extension/flash.rb b/app/controllers/controller_extension/flash.rb new file mode 100644 index 0000000..6a62351 --- /dev/null +++ b/app/controllers/controller_extension/flash.rb @@ -0,0 +1,33 @@ +module ControllerExtension::Flash + extend ActiveSupport::Concern + + protected + + def flash_for(resource, options = {}) + return unless resource.changed? + message = flash_message_for(resource) + type = flash_type(resource) + if message.present? + flash[type] = [message, flash[type]].join(' ') + end + end + + def flash_message_for(resource) + I18n.t flash_i18n_key(resource), + scope: :flash, + cascade: true, + resource: resource.class.model_name.human + end + + def flash_i18n_key(resource) + namespace = [action_name] + namespace += controller_path.split('/') + namespace << flash_type(resource) + namespace.join(".") + end + + def flash_type(resource) + resource.valid? ? :success : :error + end + +end diff --git a/config/initializers/add_controller_methods.rb b/config/initializers/add_controller_methods.rb index f572ecb..03e8393 100644 --- a/config/initializers/add_controller_methods.rb +++ b/config/initializers/add_controller_methods.rb @@ -1,4 +1,5 @@ ActiveSupport.on_load(:application_controller) do include ControllerExtension::Authentication include ControllerExtension::TokenAuthentication + include ControllerExtension::Flash end diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index bb98277..7b6a7a0 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -23,10 +23,11 @@ class TicketsController < ApplicationController @ticket.comments.last.posted_by = current_user.id @ticket.comments.last.private = false unless admin? @ticket.created_by = current_user.id + flash_for @ticket if @ticket.save && !logged_in? flash[:success] = t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) end - respond_with(@ticket, :location => auto_ticket_path(@ticket)) + respond_with @ticket, :location => auto_ticket_path(@ticket) end def show @@ -61,6 +62,7 @@ class TicketsController < ApplicationController @ticket.comments.last.private = false unless admin? end + flash_for @ticket @ticket.save respond_with @ticket, location: redirection_path end @@ -82,10 +84,6 @@ class TicketsController < ApplicationController @title = t(:tickets) end - def self.responder - Responders::FlashResponder - end - private # -- cgit v1.2.3 From a337088f4d6d12d1ea26f494f4ca078cff4b4070 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 13:20:25 +0200 Subject: remove unused bold helper and instead sanitize flash --- app/controllers/application_controller.rb | 10 ---------- app/helpers/application_helper.rb | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 35d6cb4..a4560e2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -23,16 +23,6 @@ class ApplicationController < ActionController::Base json: {error: "The server failed to process your request. We'll look into it."} end - # - # Allows us to pass through bold text to flash messages. See format_flash() for where this is reversed. - # - # TODO: move to core - # - def bold(str) - "[b]#{str}[/b]" - end - helper_method :bold - ## ## LOCALE ## diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 90e649a..6de5e1b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -40,8 +40,9 @@ module ApplicationHelper end end + # fairly strict sanitation for flash messages def format_flash(msg) - html_escape(msg).gsub('[b]', '').gsub('[/b]', '').html_safe + sanitize(msg, tags: %w(em strong b br), attributes: []) end end -- cgit v1.2.3 From 560eb039f4778257559395583e1233d052d44127 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 20 May 2014 13:50:32 +0200 Subject: flash_for with_errors option displays error messages --- app/controllers/controller_extension/flash.rb | 18 ++++++++++++++---- engines/support/app/controllers/tickets_controller.rb | 2 +- engines/support/app/views/tickets/edit.html.haml | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/controllers/controller_extension/flash.rb b/app/controllers/controller_extension/flash.rb index 6a62351..8bc9ee7 100644 --- a/app/controllers/controller_extension/flash.rb +++ b/app/controllers/controller_extension/flash.rb @@ -5,10 +5,15 @@ module ControllerExtension::Flash def flash_for(resource, options = {}) return unless resource.changed? + add_flash_message_for resource + add_flash_errors_for resource if options[:with_errors] + end + + def add_flash_message_for(resource) message = flash_message_for(resource) - type = flash_type(resource) + type = flash_type_for(resource) if message.present? - flash[type] = [message, flash[type]].join(' ') + flash[type] = message end end @@ -22,12 +27,17 @@ module ControllerExtension::Flash def flash_i18n_key(resource) namespace = [action_name] namespace += controller_path.split('/') - namespace << flash_type(resource) + namespace << flash_type_for(resource) namespace.join(".") end - def flash_type(resource) + def flash_type_for(resource) resource.valid? ? :success : :error end + def add_flash_errors_for(resource) + return if resource.valid? + flash[:error] += "
" + flash[:error] += resource.errors.full_messages.join(".
") + end end diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 7b6a7a0..9c1a741 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -62,7 +62,7 @@ class TicketsController < ApplicationController @ticket.comments.last.private = false unless admin? end - flash_for @ticket + flash_for @ticket, with_errors: true @ticket.save respond_with @ticket, location: redirection_path end diff --git a/engines/support/app/views/tickets/edit.html.haml b/engines/support/app/views/tickets/edit.html.haml index 99afa2a..03bda7d 100644 --- a/engines/support/app/views/tickets/edit.html.haml +++ b/engines/support/app/views/tickets/edit.html.haml @@ -1,4 +1,5 @@ - @show_navigation = params[:user_id].present? +- @comment = TicketComment.new .ticket = render 'tickets/edit_form' -- cgit v1.2.3 From 19bce0f114180f355f0df367cf6d21bd957734a6 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 22 May 2014 14:57:29 +0200 Subject: tickets: structure i18n --- app/views/layouts/_header.html.haml | 2 +- app/views/layouts/_navigation.html.haml | 2 +- config/locales/flash.en.yml | 10 ++ .../support/app/controllers/tickets_controller.rb | 2 +- engines/support/app/helpers/tickets_helper.rb | 14 +-- engines/support/app/models/ticket_comment.rb | 5 + .../support/app/views/tickets/_comment.html.haml | 5 +- .../support/app/views/tickets/_edit_form.html.haml | 29 ++--- .../app/views/tickets/_new_comment_form.html.haml | 6 +- engines/support/app/views/tickets/_tabs.html.haml | 18 ++- engines/support/app/views/tickets/index.html.haml | 10 +- engines/support/app/views/tickets/new.html.haml | 2 +- engines/support/config/locales/en.yml | 124 +++++++++++++++++---- lib/extensions/couchrest.rb | 1 + 14 files changed, 155 insertions(+), 75 deletions(-) create mode 100644 config/locales/flash.en.yml diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index a1dd47a..fbc46b3 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -4,7 +4,7 @@ %li{:class => ("active" if controller?('users', 'overviews') || params[:user_id])} = link_to t(:users), users_path %li{:class => ("active" if controller?('tickets') && !params[:user_id])} - = link_to t(:tickets), tickets_path + = link_to t(".tickets", cascade: true), tickets_path %li = link_to t(:logout), logout_path, :method => :delete - if @user && @show_navigation diff --git a/app/views/layouts/_navigation.html.haml b/app/views/layouts/_navigation.html.haml index b81c43d..94f71f7 100644 --- a/app/views/layouts/_navigation.html.haml +++ b/app/views/layouts/_navigation.html.haml @@ -2,6 +2,6 @@ = link_to_navigation t(:overview), @user, :active => (controller?(:users) && action?(:show)) = link_to_navigation t(:account_settings), edit_user_path(@user), :active => (controller?(:users) && !action?(:show)) - # will want link for identity settings - = link_to_navigation t(:support_tickets), auto_tickets_path, :active => controller?(:tickets) + = link_to_navigation t(".tickets"), auto_tickets_path, :active => controller?(:tickets) = link_to_navigation t(:billing_settings), billing_top_link(@user), :active => controller?(:customer, :payments, :subscriptions, :credit_card_info) if APP_CONFIG[:billing] = link_to_navigation t(:logout), logout_path, :method => :delete diff --git a/config/locales/flash.en.yml b/config/locales/flash.en.yml new file mode 100644 index 0000000..7ad28f8 --- /dev/null +++ b/config/locales/flash.en.yml @@ -0,0 +1,10 @@ +en: + flash: + success: "%{resource} was successfully saved." + error: "%{resource} could not be saved." + create: + success: "%{resource} was successfully created." + error: "%{resource} could not be created." + update: + success: "%{resource} was successfully updated." + error: "%{resource} could not be updated." diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 9c1a741..857d071 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -81,7 +81,7 @@ class TicketsController < ApplicationController protected def set_title - @title = t(:tickets) + @title = t("layouts.title.tickets") end private diff --git a/engines/support/app/helpers/tickets_helper.rb b/engines/support/app/helpers/tickets_helper.rb index 7af50d6..11b02e4 100644 --- a/engines/support/app/helpers/tickets_helper.rb +++ b/engines/support/app/helpers/tickets_helper.rb @@ -35,13 +35,7 @@ module TicketsHelper # def link_to_status(new_status) - if new_status == "open" - label = t(:open_tickets) - elsif new_status == "closed" - label = t(:closed_tickets) - elsif new_status == "all" - label = t(:all_tickets) - end + label = t(:".#{new_status}", cascade: true) link_to label, auto_tickets_path(:open_status => new_status, :sort_order => search_order) end @@ -62,11 +56,7 @@ module TicketsHelper direction = 'desc' end - if order_field == 'updated' - label = t(:updated) - elsif order_field == 'created' - label = t(:created) - end + label = t(:".#{order_field}", cascade: true) link_to auto_tickets_path(:sort_order => order_field + '_at_' + direction, :open_status => search_status) do arrow + label diff --git a/engines/support/app/models/ticket_comment.rb b/engines/support/app/models/ticket_comment.rb index bed5237..2c5df41 100644 --- a/engines/support/app/models/ticket_comment.rb +++ b/engines/support/app/models/ticket_comment.rb @@ -18,6 +18,11 @@ class TicketComment # view :by_body #end + # translations are in the same scope as those of a "proper" couchrest model + def self.i18n_scope + "couchrest" + end + def is_comment_validated? !!posted_by end diff --git a/engines/support/app/views/tickets/_comment.html.haml b/engines/support/app/views/tickets/_comment.html.haml index 778ca13..65ec394 100644 --- a/engines/support/app/views/tickets/_comment.html.haml +++ b/engines/support/app/views/tickets/_comment.html.haml @@ -1,4 +1,5 @@ -- if admin? or !comment.private # only show comment if user is admin or comment is not private +- # only show comment if user is admin or comment is not private +- if admin? or !comment.private %tr %td.user %div @@ -17,4 +18,4 @@ %span.label.label-important = t(:private) %td.comment - = simple_format(comment.body) \ No newline at end of file + = simple_format(comment.body) diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index 22815f2..522489e 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -23,29 +23,24 @@ %p.first - if @ticket.is_open? %span.label.label-info - %b{style: 'padding:10px'}= t(:open) - = f.button :loading, t(:close), class: 'btn-mini' + %b{style: 'padding:10px'}= t("tickets.status.open") + = f.button :loading, t("tickets.action.close"), class: 'btn-mini' - else %span.label.label-success - %b{style: 'padding:10px'}= t(:closed) - = f.button :loading, t(:open), class: 'btn-mini' - %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe + %b{style: 'padding:10px'}= t("tickets.status.closed") + = f.button :loading, t("tickets.action.open"), class: 'btn-mini' + %span.label.label-clear + = t("tickets.created_by_on", user: created_by, time: @ticket.created_at.to_s(:short), cascade: true).html_safe = simple_form_for @ticket do |f| = hidden_ticket_fields - %div= t(:subject) - = f.text_field :subject, :class => 'large full-width' + = f.input :subject, input_html: {:class => 'large full-width'} .row-fluid .span4 - %div= t(:status) - = f.select :is_open, [[t(:open), "true"], [t(:closed), "false"]] + = f.input :is_open, as: :select, collection: [:true, :false], include_blank: false .span4 - %div= t(:email) - = f.text_field :email + = f.input :email .span4 - %div - = t(:regarding_account) - = regarding_user_link - = f.text_field :regarding_user - = f.button :loading, t(:save), :value => 'save' + = f.input :regarding_user, label: Ticket.human_attribute_name(:regarding_user) + regarding_user_link + = f.button :loading - if admin? - = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn' + = link_to t(".destroy", cascade: true), auto_ticket_path(@ticket), :confirm => t("tickets.confirm.destroy.are_you_sure", cascade: true), :method => :delete, :class => 'btn' diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml index 40c737f..b829b6b 100644 --- a/engines/support/app/views/tickets/_new_comment_form.html.haml +++ b/engines/support/app/views/tickets/_new_comment_form.html.haml @@ -7,7 +7,7 @@ = c.input :body, :label => false, :as => :text, :input_html => {:class => "full-width", :rows=> 5} - if admin? = c.input :private, :as => :boolean, :label => false, :inline_label => true - = f.button :loading, t(:post_reply), class: 'btn-primary', value: 'post_reply' + = f.button :loading, t(".post_reply"), class: 'btn-primary', value: 'post_reply' - if logged_in? && @ticket.is_open - = f.button :loading, t(:reply_and_close), value: 'reply_and_close' - = link_to t(:cancel), auto_tickets_path, :class => :btn + = f.button :loading, t(".reply_and_close"), value: 'reply_and_close' + = link_to t(".cancel"), auto_tickets_path, :class => :btn diff --git a/engines/support/app/views/tickets/_tabs.html.haml b/engines/support/app/views/tickets/_tabs.html.haml index 445a909..7872bb5 100644 --- a/engines/support/app/views/tickets/_tabs.html.haml +++ b/engines/support/app/views/tickets/_tabs.html.haml @@ -3,21 +3,17 @@ -# - unless action?(:new) or action?(:create) %ul.nav.nav-pills.pull-right.slim - %li{:class=> ("active" if search_order.start_with? 'created_at')} - = link_to_order('created') - %li{:class=> ("active" if search_order.start_with? 'updated_at')} - = link_to_order('updated') + - %w(created updated).each do |order| + %li{:class=> ("active" if search_order.start_with? order)} + = link_to_order(order) -# -# STATUS FILTER TABS -# %ul.nav.nav-tabs - if logged_in? - %li{:class => ("active" if search_status == 'open')} - = link_to_status 'open' - %li{:class => ("active" if search_status == 'closed')} - = link_to_status 'closed' - %li{:class => ("active" if search_status == 'all')} - = link_to_status 'all' + - %w(open closed all).each do |status| + %li{:class => ("active" if search_status == status)} + = link_to_status status %li{:class => ("active" if action?(:new) || action?(:create))} - = link_to icon(:plus, :black) + t(:new_ticket), auto_new_ticket_path + = link_to icon(:plus, :black) + t(".new", cascade: true), auto_new_ticket_path diff --git a/engines/support/app/views/tickets/index.html.haml b/engines/support/app/views/tickets/index.html.haml index a4df6e3..526cd6d 100644 --- a/engines/support/app/views/tickets/index.html.haml +++ b/engines/support/app/views/tickets/index.html.haml @@ -5,15 +5,15 @@ %table.table.table-striped.table-bordered %thead %tr - %th= t(:subject) - %th= t(:created) - %th= t(:updated) - %th= t(:voices) + %th= t(".subject") + %th= t(".created") + %th= t(".updated") + %th= t(".voices") %tbody - if @tickets.any? = render @tickets.all - else %tr - %td{:colspan=>4}= t(:none) + %td{:colspan=>4}= t(".none") = paginate @tickets diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml index 3de5fe9..d3580f9 100644 --- a/engines/support/app/views/tickets/new.html.haml +++ b/engines/support/app/views/tickets/new.html.haml @@ -11,7 +11,7 @@ = f.input :email = f.input :regarding_user = f.simple_fields_for :comments, @comment do |c| - = c.input :body, :label => t(:description), :as => :text, :input_html => {:class => "full-width", :rows=> 5} + = c.input :body, :as => :text, :input_html => {:class => "full-width", :rows=> 5} - if admin? = c.input :private, :as => :boolean, :label => false, :inline_label => true = f.button :wrapped, cancel: (logged_in? ? auto_tickets_path : home_path) diff --git a/engines/support/config/locales/en.yml b/engines/support/config/locales/en.yml index 342adea..f2caecc 100644 --- a/engines/support/config/locales/en.yml +++ b/engines/support/config/locales/en.yml @@ -1,22 +1,104 @@ en: - access_ticket_text: > - You can later access this ticket at the URL %{full_url}. You might want to bookmark this page to find it again. - Anybody with this URL will be able to access this ticket, so if you are on a shared computer you might want to - remove it from the browser history. - support_tickets: "Support Tickets" - all_tickets: "All Tickets" - my_tickets: "My Tickets" - open_tickets: "Open Tickets" - closed_tickets: "Closed Tickets" - new_ticket: "New Ticket" - tickets: "Tickets" - subject: "Subject" - destroy: "Destroy" - open: "Open" - closed: "Closed" - close: "Close" - post_reply: "Post Reply" - reply_and_close: "Reply and Close" - description: "Description" - ticket: "Ticket" - regarding_account: "Regarding Account" \ No newline at end of file + # translations used in the layout views or @title + layouts: + # fallback for all translations of "tickets" nested below: + tickets: "Tickets" + title: + tickets: "Tickets" + header: + tickets: "Tickets" + navigation: + tickets: "Support Tickets" + # Translations used in the views inside the tickets directory + tickets: + # If these do not exist they will be looked up in the global scope. + all: "All Tickets" + open: "Open Tickets" + closed: "Closed Tickets" + new: "New Ticket" + created: "Created at" + updated: "Updated at" + subject: "couchrest.models.tickets.attributes.subject" + status: + open: "Open" + closed: "Closed" + action: + open: "Open" + close: "Close" + confirm: + destroy: + are_you_sure: "Are you sure you want to destroy this ticket?" + # If you want to be more specific you can use the partial as a scope: + tabs: + all: "All Tickets" + open: "Open Tickets" + closed: "Closed Tickets" + index: + none: "No tickets have been found." + voices: "Voices" + destroy: "Destroy" + post_reply: "Post Reply" + reply_and_close: "Reply and Close" + access_ticket_text: > + You can later access this ticket at the URL %{full_url}. You might want to bookmark this page to find it again. + Anybody with this URL will be able to access this ticket, so if you are on a shared computer you might want to + remove it from the browser history. + # rails i18n + helpers: + # translations used for submit buttons. simple form picks these up + submit: + ticket: + create: "Submit Ticket" + update: "Update Ticket" + # translations for the model and its attributes + # serve as fallback for simpleform labels + couchrest: + models: + ticket: "Ticket" + ticket_comment: "Comment" + attributes: + ticket: + # these will fallback to translations in the "attributes" scope + subject: "Subject" + email: "Email" + regarding_user: "Regarding User" + regarding_account: "Regarding Account" + is_open: "Status" + ticket_comment: + body: "Description" + private: "private" + simple_form: + # labels next to the field + labels: + # these will fallback to the human_name translations of the model + ticket: + # these will fallback to translations in "simple_form.labels.defaults" + regarding_: + # you can be specific about translations for a given action: + #edit: + # regaring_user: + email: "Email" + comments: + # these will fall back to "simple_form.labels.comments" + body: "Description" + # comments: ~ + options: + ticket: + is_open: + "true": "Open" + "false": "Closed" + # mouse over hints for the given fields + hints: + ticket: + email: "Please provide an email address so we can get back to you." + # these will fallback to translations in "simple_form.hints.defaults" + # placeholders inside the fields before anything was typed + #placeholders: + # ticket: ~ + # these will fallback to translations in "simple_form.placeholders.defaults" + + # these are generic defaults. They should be moved into the toplevel + # attributes: + #simple_form: + #labels: + # defaults: diff --git a/lib/extensions/couchrest.rb b/lib/extensions/couchrest.rb index 0e5051a..df83c9f 100644 --- a/lib/extensions/couchrest.rb +++ b/lib/extensions/couchrest.rb @@ -2,6 +2,7 @@ module CouchRest module Model class Base extend ActiveModel::Naming + extend ActiveModel::Translation end module Designs -- cgit v1.2.3 From 4085e3fabef6427cd3f8be9b61c209bd82eaa595 Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 24 May 2014 10:33:31 +0200 Subject: navigation works with empty locale selected Just in case some translation keys are not present things should still work and make sense. So translation keys should be picked in a meaningful way and scoped rather than prefixed. For example overview.account will turn into "Account" if no translation is present while "overview_account" will turn into "Overview Account". We usually want the former. --- app/views/common/_action_buttons.html.haml | 8 +++---- app/views/common/_download_button.html.haml | 2 +- app/views/users/_overview.html.haml | 24 +++++++++++++++++++ app/views/users/new.html.haml | 2 +- app/views/users/show.html.haml | 37 ++++++----------------------- config/locales/en.yml | 33 +------------------------ config/locales/footer.en.yml | 7 ++++++ config/locales/generic.en.yml | 4 ++++ config/locales/home.en.yml | 7 ++++++ config/locales/users.en.yml | 15 ++++++------ engines/support/config/locales/en.yml | 1 + 11 files changed, 64 insertions(+), 76 deletions(-) create mode 100644 app/views/users/_overview.html.haml create mode 100644 config/locales/footer.en.yml create mode 100644 config/locales/generic.en.yml create mode 100644 config/locales/home.en.yml diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml index c74fcd1..d00cf74 100644 --- a/app/views/common/_action_buttons.html.haml +++ b/app/views/common/_action_buttons.html.haml @@ -2,10 +2,10 @@ .row-fluid.second .login.span4 %span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn') - %span.info= t(:login_info) + %span.info= t(:login_info, default: "") .signup.span4 %span.link= link_to(icon('user', icon_color) + t(:signup), signup_path, :class => 'btn') - %span.info= t(:signup_info) + %span.info= t(:signup_info, default: "") .help.span4 - %span.link= link_to(icon('question-sign', icon_color) + t(:get_help), new_ticket_path, :class => 'btn') - %span.info= t(:help_info) + %span.link= link_to(icon('question-sign', icon_color) + t(:support_tickets), new_ticket_path, :class => 'btn') + %span.info= t(:support_info, default: "") diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml index e57c56b..d6d7bde 100644 --- a/app/views/common/_download_button.html.haml +++ b/app/views/common/_download_button.html.haml @@ -4,5 +4,5 @@ .download.span8 = link_to client_download_url, class: "btn btn-large btn-primary" do = big_icon('download') - = t(:download_client) + = t(:download_bitmask) .span2 diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml new file mode 100644 index 0000000..e38fdc8 --- /dev/null +++ b/app/views/users/_overview.html.haml @@ -0,0 +1,24 @@ +.overview + + %h2.first= t(".welcome", username: @user.login, cascade: true) + + - if admin? + %p + = t(:created) + = @user.created_at + %br + = t(:updated) + = @user.updated_at + %br + = t(:enabled) + = @user.enabled? + + %p= t(:overview_intro, default: "") + + %ul.unstyled + %li= icon('user') + link_to(t(".account"), edit_user_path(@user)) + - # %li= icon('envelope') + link_to(t(:overview_email), {insert path for user identities, presuambly} + %li= icon('question-sign') + link_to(t(".tickets"), user_tickets_path(@user)) + %li= icon('shopping-cart') + link_to(t(".billing"), billing_top_link(@user)) if APP_CONFIG[:billing] + + diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index bc36068..41a9d55 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -17,5 +17,5 @@ = f.input :login, :label => t(:username), :required => false, :input_html => { :id => :srp_username } = f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password } = f.input :password_confirmation, :required => false, :validate => true, :input_html => { :id => :srp_password_confirmation } - = f.button :wrapped, value: t(:signup), cancel: home_path + = f.button :wrapped, cancel: home_path diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index ddc33ab..6760099 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,30 +1,7 @@ -.overview - - %h2.first= t(:overview_welcome, :username => @user.login) - - - if admin? - %p - = t(:created) - = @user.created_at - %br - = t(:updated) - = @user.updated_at - %br - = t(:enabled) - = @user.enabled? - - %p= t(:overview_intro) - - %ul.unstyled - %li= icon('user') + link_to(t(:overview_account), edit_user_path(@user)) - - # %li= icon('envelope') + link_to(t(:overview_email), {insert path for user identities, presuambly} - %li= icon('question-sign') + link_to(t(:overview_tickets), user_tickets_path(@user)) - %li= icon('shopping-cart') + link_to(t(:overview_billing), billing_top_link(@user)) if APP_CONFIG[:billing] - - - .container-fluid - .row-fluid - %h4 To use bitmask services: - = link_to client_download_url, class: "btn btn-primary" do - %i.icon-arrow-down.icon-white - = t(:download_client) += render 'overview' +.container-fluid + .row-fluid + %h4 To use bitmask services: + = link_to client_download_url, class: "btn btn-primary" do + %i.icon-arrow-down.icon-white + = t(:download_bitmask) diff --git a/config/locales/en.yml b/config/locales/en.yml index a7a76a8..feac53b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,17 +1,7 @@ en: - flash: - success: "%{resource} was successfully saved." - create: - success: "%{resource} was successfully created." - update: - success: "%{resource} was successfully updated." home: Home - privacy_policy: Privacy Policy - terms_of_service: Terms of Service - pricing: Pricing - about: About Us - contact: Contact + example_email: 'user@domain.org' not_found_title: Page not found. not_found_subtitle: "The page you were looking for doesn't exist." not_found_lead: "You may have mistyped the address or the page may have moved." @@ -19,8 +9,6 @@ en: server_error_subtitle: We ran into a server error. server_error_lead: The problem has been logged and we will look into it. no_such_thing: "No such %{thing}." - - thing_was_successfully_created: "%{thing} was successfully created." create_thing: "Create %{thing}" overview: "Overview" @@ -40,22 +28,3 @@ en: changes_saved: "Changes saved successfully." are_you_sure: "Are you sure? This change cannot be undone." - download_client: "Download Bitmask" - client_info: "The Bitmask application allows you to use %{provider} services." - all_downloads_info: "It is available for %{clients}." - other_downloads_info: "Bitmask is also available for %{clients}." - login_info: "Log in to change your account settings, create support tickets, and manage payments." - signup_info: "Get a user account via this website. We recommend registering via the Bitmask application instead unless you are only using Bitmask for Android." - welcome: "Welcome to %{provider}." - get_help: "Get Help" - help_info: "Can't login? Create a new support ticket anonymously." - example_email: 'user@domain.org' - os: - linux32: "Linux (32 bit)" - linux64: "Linux (64 bit)" - linux: "GNU/Linux" - windows: "Windows" - android: "Android" - osx: "Mac OS" - other: "(not available for your OS)" - diff --git a/config/locales/footer.en.yml b/config/locales/footer.en.yml new file mode 100644 index 0000000..65f8ab2 --- /dev/null +++ b/config/locales/footer.en.yml @@ -0,0 +1,7 @@ +en: + # layout/footer + privacy_policy: Privacy Policy + terms_of_service: Terms of Service + pricing: Pricing + about: About Us + contact: Contact diff --git a/config/locales/generic.en.yml b/config/locales/generic.en.yml new file mode 100644 index 0000000..dce6d93 --- /dev/null +++ b/config/locales/generic.en.yml @@ -0,0 +1,4 @@ +en: + signup: "Sign Up" + login: "Log In" + cancel: "Cancel" diff --git a/config/locales/home.en.yml b/config/locales/home.en.yml new file mode 100644 index 0000000..4fdbfc2 --- /dev/null +++ b/config/locales/home.en.yml @@ -0,0 +1,7 @@ +en: + welcome: "Welcome to %{provider}." + download_bitmask: "Download Bitmask" + + login_info: "Log in to change your account settings, create support tickets, and manage payments." + signup_info: "Get a user account via this website. We recommend registering via the Bitmask application instead unless you are only using Bitmask for Android." + support_info: "Can't login? Create a new support ticket anonymously." diff --git a/config/locales/users.en.yml b/config/locales/users.en.yml index 0ca5a73..3f4fbed 100644 --- a/config/locales/users.en.yml +++ b/config/locales/users.en.yml @@ -2,9 +2,6 @@ en: account_settings: "Account Settings" logout: "Logout" none: "None" - signup: "Sign Up" - cancel: "Cancel" - login: "Log In" username: "Username" password: "Password" change_password: "Change Password" @@ -42,11 +39,13 @@ en: # # overview # - overview_welcome: "Welcome %{username}." - overview_intro: "From this user control panel, you can:" - overview_tickets: "Create and check support tickets." - overview_email: "Modify email settings." - overview_account: "Destroy your account." + users: + overview: + welcome: "Welcome %{username}." + intro: "From this user control panel, you can:" + tickets: "Create and check support tickets." + email: "Modify email settings." + account: "Destroy your account." # # rails diff --git a/engines/support/config/locales/en.yml b/engines/support/config/locales/en.yml index f2caecc..8d2af67 100644 --- a/engines/support/config/locales/en.yml +++ b/engines/support/config/locales/en.yml @@ -1,4 +1,5 @@ en: + support_tickets: "Support" # translations used in the layout views or @title layouts: # fallback for all translations of "tickets" nested below: -- cgit v1.2.3 From bbeb4b629dc38d82b3b3200706dd25b8def8892e Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 24 May 2014 13:39:10 +0200 Subject: sorting translation keys some --- app/views/errors/not_found.html.haml | 6 +++--- app/views/errors/server_error.html.haml | 6 +++--- config/locales/en.yml | 30 ------------------------------ config/locales/errors.en.yml | 11 +++++++++++ config/locales/generic.en.yml | 21 +++++++++++++++++++++ config/locales/home.en.yml | 1 + config/locales/simple_form.en.yml | 4 ++-- config/locales/users.en.yml | 4 ++-- test/integration/browser/account_test.rb | 6 +++--- test/integration/browser/session_test.rb | 2 +- 10 files changed, 47 insertions(+), 44 deletions(-) delete mode 100644 config/locales/en.yml create mode 100644 config/locales/errors.en.yml diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 75cb889..c7fec22 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,7 +1,7 @@ .hero-unit - %h1=t :not_found_title - %h2=t :not_found_subtitle - %p.lead=t :not_found_lead + %h1=t 'errors.title.page_not_found' + %h2=t 'errors.subtitle.page_not_found', default: '' + %p.lead=t 'errors.text.page_not_found', default: '' %a.btn.btn-primary.btn-large{href:'/'} %i.icon-home.icon-white =t :home diff --git a/app/views/errors/server_error.html.haml b/app/views/errors/server_error.html.haml index 68baf20..a4133da 100644 --- a/app/views/errors/server_error.html.haml +++ b/app/views/errors/server_error.html.haml @@ -1,7 +1,7 @@ .hero-unit - %h1=t :server_error_title - %h2=t :server_error_subtitle - %p.lead=t :server_error_lead + %h1=t 'errors.title.server_error' + %h2=t 'errors.subtitle.server_error', default: '' + %p.lead=t 'errors.text.server_error', default: '' %a.btn.btn-primary.btn-large{href:'/'} %i.icon-home.icon-white =t :home diff --git a/config/locales/en.yml b/config/locales/en.yml deleted file mode 100644 index feac53b..0000000 --- a/config/locales/en.yml +++ /dev/null @@ -1,30 +0,0 @@ -en: - home: Home - - example_email: 'user@domain.org' - not_found_title: Page not found. - not_found_subtitle: "The page you were looking for doesn't exist." - not_found_lead: "You may have mistyped the address or the page may have moved." - server_error_title: Ouch! - server_error_subtitle: We ran into a server error. - server_error_lead: The problem has been logged and we will look into it. - no_such_thing: "No such %{thing}." - create_thing: "Create %{thing}" - - overview: "Overview" - user_control_panel: "user control panel" - - created: "Created" - created_by_on: "Created by %{user} on %{time}" - updated: "Updated" - - none: "None" - unknown: "Unknown" - admin: "Admin" - anonymous: "Anonymous" - save: "Save" - add: "Add" - remove: "Remove" - changes_saved: "Changes saved successfully." - are_you_sure: "Are you sure? This change cannot be undone." - diff --git a/config/locales/errors.en.yml b/config/locales/errors.en.yml new file mode 100644 index 0000000..93feab1 --- /dev/null +++ b/config/locales/errors.en.yml @@ -0,0 +1,11 @@ +en: + errors: + title: + not_found: Page not found. + server_error: Ouch! + subtitle: + not_found: "The page you were looking for doesn't exist." + server_error: We ran into a server error. + text: + not_found: "You may have mistyped the address or the page may have moved." + server_error: The problem has been logged and we will look into it. diff --git a/config/locales/generic.en.yml b/config/locales/generic.en.yml index dce6d93..14dafac 100644 --- a/config/locales/generic.en.yml +++ b/config/locales/generic.en.yml @@ -1,4 +1,25 @@ en: signup: "Sign Up" login: "Log In" + logout: "Log Out" + cancel: "Cancel" + + created: "Created" + created_by_on: "Created by %{user} on %{time}" + updated: "Updated" + + none: "None" + unknown: "Unknown" + admin: "Admin" + anonymous: "Anonymous" + save: "Save" + add: "Add" + remove: "Remove" + changes_saved: "Changes saved successfully." + are_you_sure: "Are you sure? This change cannot be undone." + + example_email: 'user@domain.org' + + no_such_thing: "No such %{thing}." + create_thing: "Create %{thing}" diff --git a/config/locales/home.en.yml b/config/locales/home.en.yml index 4fdbfc2..c3cdfb1 100644 --- a/config/locales/home.en.yml +++ b/config/locales/home.en.yml @@ -1,4 +1,5 @@ en: + home: Home welcome: "Welcome to %{provider}." download_bitmask: "Download Bitmask" diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 5d0c675..4b35883 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -29,8 +29,8 @@ en: helpers: submit: user: - create: "Sign up" + create: "Sign Up" update: "Save" session: - create: "Log in" + create: "Log In" diff --git a/config/locales/users.en.yml b/config/locales/users.en.yml index 3f4fbed..0375174 100644 --- a/config/locales/users.en.yml +++ b/config/locales/users.en.yml @@ -1,7 +1,7 @@ en: + overview: "Overview" + user_control_panel: "user control panel" account_settings: "Account Settings" - logout: "Logout" - none: "None" username: "Username" password: "Password" change_password: "Change Password" diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_test.rb index 491a9e1..8dc3043 100644 --- a/test/integration/browser/account_test.rb +++ b/test/integration/browser/account_test.rb @@ -9,7 +9,7 @@ class AccountTest < BrowserIntegrationTest test "signup successfully" do username, password = submit_signup assert page.has_content?("Welcome #{username}") - click_on 'Logout' + click_on 'Log Out' assert page.has_content?("Log In") assert_equal '/', current_path assert user = User.find_by_login(username) @@ -24,7 +24,7 @@ class AccountTest < BrowserIntegrationTest test "successful login" do username, password = submit_signup - click_on 'Logout' + click_on 'Log Out' attempt_login(username, password) assert page.has_content?("Welcome #{username}") within('.sidenav li.active') do @@ -83,7 +83,7 @@ class AccountTest < BrowserIntegrationTest fill_in 'Password confirmation', with: "other password" click_on 'Save' end - click_on 'Logout' + click_on 'Log Out' attempt_login(@user.login, "other password") assert page.has_content?("Welcome #{@user.login}") end diff --git a/test/integration/browser/session_test.rb b/test/integration/browser/session_test.rb index fb20847..d52508a 100644 --- a/test/integration/browser/session_test.rb +++ b/test/integration/browser/session_test.rb @@ -4,7 +4,7 @@ class SessionTest < BrowserIntegrationTest test "valid session" do login - assert page.has_content?("Logout") + assert page.has_content?("Log Out") end test "expired session" do -- cgit v1.2.3 From 863863ff1fb6c9ab13b44248417ae1c5e57987af Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 27 May 2014 10:14:40 +0200 Subject: remove icon_color variable - yagni --- app/views/common/_action_buttons.html.haml | 6 +++--- app/views/common/_home_page_buttons.html.haml | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml index d00cf74..398d384 100644 --- a/app/views/common/_action_buttons.html.haml +++ b/app/views/common/_action_buttons.html.haml @@ -1,11 +1,11 @@ .home-buttons .row-fluid.second .login.span4 - %span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn') + %span.link= link_to icon('ok-sign') + t(:login), login_path, :class => 'btn' %span.info= t(:login_info, default: "") .signup.span4 - %span.link= link_to(icon('user', icon_color) + t(:signup), signup_path, :class => 'btn') + %span.link= link_to icon('user') + t(:signup), signup_path, :class => 'btn' %span.info= t(:signup_info, default: "") .help.span4 - %span.link= link_to(icon('question-sign', icon_color) + t(:support_tickets), new_ticket_path, :class => 'btn') + %span.link= link_to icon('question-sign') + t(:get_help), new_ticket_path, :class => 'btn' %span.info= t(:support_info, default: "") diff --git a/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml index 8c47983..fc6348e 100644 --- a/app/views/common/_home_page_buttons.html.haml +++ b/app/views/common/_home_page_buttons.html.haml @@ -1,8 +1,6 @@ -- icon_color = :black - = render 'common/download_button' - if local_assigns[:divider] .row-fluid .span12 = render local_assigns[:divider] -= render 'common/action_buttons', icon_color: icon_color += render 'common/action_buttons' -- cgit v1.2.3 From cc59ce53e52bf48d97de16d66012e8309bf98fe8 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 27 May 2014 16:32:50 +0200 Subject: add btn helper for link_to with .btn Also translates the first arg if it's a symbol and adds more btn- classes if given as html_options[:type] --- app/views/common/_action_buttons.html.haml | 6 +++--- app/views/common/_download_button.html.haml | 2 +- app/views/pages/pricing.html.haml | 2 +- app/views/users/_destroy_account.html.haml | 6 +++--- app/views/users/show.html.haml | 2 +- engines/billing/app/views/credit_card_info/edit.html.haml | 3 +-- engines/billing/app/views/customer/_customer_data.html.haml | 2 +- engines/billing/app/views/customer/confirm.html.haml | 2 +- engines/billing/app/views/customer/edit.html.haml | 4 ++-- engines/billing/app/views/customer/show.html.haml | 6 +++--- engines/billing/app/views/subscriptions/destroy.html.haml | 2 +- engines/billing/app/views/subscriptions/index.html.haml | 2 +- engines/billing/app/views/subscriptions/show.html.haml | 3 ++- engines/support/app/views/tickets/_edit_form.html.haml | 2 +- engines/support/app/views/tickets/_new_comment_form.html.haml | 2 +- 15 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml index 398d384..266abe1 100644 --- a/app/views/common/_action_buttons.html.haml +++ b/app/views/common/_action_buttons.html.haml @@ -1,11 +1,11 @@ .home-buttons .row-fluid.second .login.span4 - %span.link= link_to icon('ok-sign') + t(:login), login_path, :class => 'btn' + %span.link= btn icon('ok-sign') + t(:login), login_path %span.info= t(:login_info, default: "") .signup.span4 - %span.link= link_to icon('user') + t(:signup), signup_path, :class => 'btn' + %span.link= btn icon('user') + t(:signup), signup_path %span.info= t(:signup_info, default: "") .help.span4 - %span.link= link_to icon('question-sign') + t(:get_help), new_ticket_path, :class => 'btn' + %span.link= btn icon('question-sign') + t(:get_help), new_ticket_path %span.info= t(:support_info, default: "") diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml index d6d7bde..9c26860 100644 --- a/app/views/common/_download_button.html.haml +++ b/app/views/common/_download_button.html.haml @@ -2,7 +2,7 @@ .row-fluid.first .span2 .download.span8 - = link_to client_download_url, class: "btn btn-large btn-primary" do + = btn client_download_url, type: [:large, :primary] do = big_icon('download') = t(:download_bitmask) .span2 diff --git a/app/views/pages/pricing.html.haml b/app/views/pages/pricing.html.haml index e339d27..983501e 100644 --- a/app/views/pages/pricing.html.haml +++ b/app/views/pages/pricing.html.haml @@ -1,5 +1,5 @@ %h1= t(:pricing) -%p= link_to(icon('user') + t(:signup), signup_path, :class => 'btn') +%p= btn icon('user') + t(:signup), signup_path - levels = APP_CONFIG[:service_levels] - if levels diff --git a/app/views/users/_destroy_account.html.haml b/app/views/users/_destroy_account.html.haml index 445f3c4..be003ce 100644 --- a/app/views/users/_destroy_account.html.haml +++ b/app/views/users/_destroy_account.html.haml @@ -8,20 +8,20 @@ - else = t(:admin_destroy_account, :username => @user.login) %p= t(:destroy_account_info) -= link_to user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :class => "btn btn-danger" do += btn user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :type => "danger" do %i.icon-remove.icon-white = t(:destroy_my_account) - if @user != current_user and @user.enabled? %legend = t(:deactivate_account, :username => @user.login) %p= t(:deactivate_description) - = link_to deactivate_user_path(@user), :method => :post, :class => "btn btn-warning" do + = btn deactivate_user_path(@user), :method => :post, :type => "warning" do %i.icon-pause.icon-white = t(:deactivate) - elsif @user != current_user and !@user.enabled? %legend = t(:enable_account, :username => @user.login) %p= t(:enable_description) - = link_to enable_user_path(@user), :method => :post, :class => "btn btn-warning" do + = btn enable_user_path(@user), :method => :post, :type => "warning" do %i.icon-ok.icon-white = t(:enable) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6760099..da8e467 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -2,6 +2,6 @@ .container-fluid .row-fluid %h4 To use bitmask services: - = link_to client_download_url, class: "btn btn-primary" do + = btn client_download_url, type: "primary" do %i.icon-arrow-down.icon-white = t(:download_bitmask) diff --git a/engines/billing/app/views/credit_card_info/edit.html.haml b/engines/billing/app/views/credit_card_info/edit.html.haml index bd86a4c..9e44344 100644 --- a/engines/billing/app/views/credit_card_info/edit.html.haml +++ b/engines/billing/app/views/credit_card_info/edit.html.haml @@ -13,5 +13,4 @@ %dt= f.label :cvv, 'CVV' %dd= f.text_field :cvv = hidden_field_tag :tr_data, @tr_data - = f.submit 'Save Payment Info', :class => :btn - = link_to t(:cancel), edit_customer_path(@user.id), :class => :btn + = f.button :wrapped, 'Save Payment Info', cancel: edit_customer_path(@user.id) diff --git a/engines/billing/app/views/customer/_customer_data.html.haml b/engines/billing/app/views/customer/_customer_data.html.haml index e9df040..439ae5c 100644 --- a/engines/billing/app/views/customer/_customer_data.html.haml +++ b/engines/billing/app/views/customer/_customer_data.html.haml @@ -13,4 +13,4 @@ %dt Expiration Date %dd= @default_cc.expiration_date - if current_user == @user - = link_to t(:edit_saved_data), edit_customer_path(@user.id), :class => :btn + = btn t(:edit_saved_data), edit_customer_path(@user.id) diff --git a/engines/billing/app/views/customer/confirm.html.haml b/engines/billing/app/views/customer/confirm.html.haml index 877a8ac..eab9616 100644 --- a/engines/billing/app/views/customer/confirm.html.haml +++ b/engines/billing/app/views/customer/confirm.html.haml @@ -11,4 +11,4 @@ - @result.customer.credit_cards.each do |cc| %dd= cc.masked_number - customer = Customer.find_by_user_id(@user.id) -= link_to 'View Customer Info', show_customer_path(@user.id), :class=> :btn \ No newline at end of file += btn 'View Customer Info', show_customer_path(@user.id) diff --git a/engines/billing/app/views/customer/edit.html.haml b/engines/billing/app/views/customer/edit.html.haml index e882d53..f461fcc 100644 --- a/engines/billing/app/views/customer/edit.html.haml +++ b/engines/billing/app/views/customer/edit.html.haml @@ -16,8 +16,8 @@ %dt= t(:stored_credit_card) %dd = @default_cc.masked_number - = link_to t(:change_credit_card), edit_credit_card_info_path(:id => @default_cc.token), :class => :btn + = btn t(:change_credit_card), edit_credit_card_info_path(:id => @default_cc.token) = hidden_field_tag :tr_data, @tr_data .form-actions = f.submit t(:save_customer_info), :class => 'btn btn-primary' - = link_to t(:cancel), show_customer_path(@user), :class=> :btn + = btn t(:cancel), show_customer_path(@user) diff --git a/engines/billing/app/views/customer/show.html.haml b/engines/billing/app/views/customer/show.html.haml index ec1779c..ce4d01a 100644 --- a/engines/billing/app/views/customer/show.html.haml +++ b/engines/billing/app/views/customer/show.html.haml @@ -9,7 +9,7 @@ - break if counter > 2 # not ruby-like, but object is a Braintree::ResourceCollection so limited methods available = render :partial => "payments/transaction_details", :locals => {:transaction => t} - counter += 1 - = link_to t(:transaction_history), user_payments_path(@user) + = btn :transaction_history, user_payments_path(@user) %legend= t(:subscriptions) - if @active_subscription = render :partial => "subscriptions/subscription_details", :locals => {:subscription => @active_subscription} @@ -19,9 +19,9 @@ - if current_user == @user %p .form-actions - = link_to t(:subscribe_to_plan), new_subscription_path, :class => :btn + = btn :subscribe_to_plan, new_subscription_path %p = link_to t(:all_subscriptions), user_subscriptions_path(@user) .form-actions - = link_to t(:make_donation), new_payment_path, :class => 'btn btn-primary' + = btn :make_donation, new_payment_path, :type => 'primary' diff --git a/engines/billing/app/views/subscriptions/destroy.html.haml b/engines/billing/app/views/subscriptions/destroy.html.haml index 44b4333..e6e8578 100644 --- a/engines/billing/app/views/subscriptions/destroy.html.haml +++ b/engines/billing/app/views/subscriptions/destroy.html.haml @@ -4,4 +4,4 @@ Error: = @result.message %p - = link_to 'Customer Information', show_customer_path(@user), :class=> :btn \ No newline at end of file + = btn 'Customer Information', show_customer_path(@user) diff --git a/engines/billing/app/views/subscriptions/index.html.haml b/engines/billing/app/views/subscriptions/index.html.haml index 3d4e8fd..1e3fb25 100644 --- a/engines/billing/app/views/subscriptions/index.html.haml +++ b/engines/billing/app/views/subscriptions/index.html.haml @@ -5,4 +5,4 @@ - pending_active_pastdue = true = render :partial => "subscription_details", :locals => {:subscription => s} - if !pending_active_pastdue and @user == current_user - = link_to 'subscribe to plan', new_subscription_path, :class => :btn \ No newline at end of file + = btn 'subscribe to plan', new_subscription_path diff --git a/engines/billing/app/views/subscriptions/show.html.haml b/engines/billing/app/views/subscriptions/show.html.haml index 2699db9..af1a5a5 100644 --- a/engines/billing/app/views/subscriptions/show.html.haml +++ b/engines/billing/app/views/subscriptions/show.html.haml @@ -3,4 +3,5 @@ Current Subscription = render :partial => "subscription_details", :locals => {:subscription => @subscription} -= link_to t(:cancel_subscription), user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn btn-danger' if allow_cancel_subscription(@subscription) +- if allow_cancel_subscription(@subscription) + = btn :cancel_subscription, user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :type => 'danger' diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index 522489e..9adc2cc 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -43,4 +43,4 @@ = f.input :regarding_user, label: Ticket.human_attribute_name(:regarding_user) + regarding_user_link = f.button :loading - if admin? - = link_to t(".destroy", cascade: true), auto_ticket_path(@ticket), :confirm => t("tickets.confirm.destroy.are_you_sure", cascade: true), :method => :delete, :class => 'btn' + = btn t(".destroy", cascade: true), auto_ticket_path(@ticket), confirm: t("tickets.confirm.destroy.are_you_sure", cascade: true), method: :delete diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml index b829b6b..711421d 100644 --- a/engines/support/app/views/tickets/_new_comment_form.html.haml +++ b/engines/support/app/views/tickets/_new_comment_form.html.haml @@ -10,4 +10,4 @@ = f.button :loading, t(".post_reply"), class: 'btn-primary', value: 'post_reply' - if logged_in? && @ticket.is_open = f.button :loading, t(".reply_and_close"), value: 'reply_and_close' - = link_to t(".cancel"), auto_tickets_path, :class => :btn + = btn t(".cancel"), auto_tickets_path -- cgit v1.2.3 From 8ca32588c969ee9eca986da8cf1de92b39ce3576 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 27 May 2014 17:52:26 +0200 Subject: move users key into layouts scope so it does not conflict with users scope --- app/views/layouts/_header.html.haml | 2 +- config/locales/users.en.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index fbc46b3..e827f60 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -2,7 +2,7 @@ %ul.nav.nav-tabs = # this navigation isn't quite right. also, we will want to active for an identity controller once it is added. %li{:class => ("active" if controller?('users', 'overviews') || params[:user_id])} - = link_to t(:users), users_path + = link_to t(".users"), users_path %li{:class => ("active" if controller?('tickets') && !params[:user_id])} = link_to t(".tickets", cascade: true), tickets_path %li diff --git a/config/locales/users.en.yml b/config/locales/users.en.yml index 0375174..f0fcb3d 100644 --- a/config/locales/users.en.yml +++ b/config/locales/users.en.yml @@ -1,5 +1,6 @@ en: - overview: "Overview" + layouts: + users: "Users" user_control_panel: "user control panel" account_settings: "Account Settings" username: "Username" -- cgit v1.2.3 From df1c2438fcfe39edfb46546be8fcee5021f95fc3 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 09:26:17 +0200 Subject: destroy_btn helper method --- app/helpers/link_helper.rb | 49 ++++++++++++++++++++++ app/views/users/_destroy_account.html.haml | 2 +- .../billing/app/views/subscriptions/show.html.haml | 2 +- .../support/app/views/tickets/_edit_form.html.haml | 2 +- 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 app/helpers/link_helper.rb diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb new file mode 100644 index 0000000..55e392b --- /dev/null +++ b/app/helpers/link_helper.rb @@ -0,0 +1,49 @@ +module LinkHelper + + # + # markup for bootstrap button + # + # takes same arguments as link_to and adds a 'btn' class. + # In addition: + # * the name will be translated if it is a symbol + # * html_options[:type] will be converted into a btn-type class + # + # example: + # btn :home, home_path, type: [:large, :primary] + # + def btn(*args, &block) + html_options = extract_html_options!(args, &block) + type = Array(html_options.delete(:type)) + type.map! {|t| "btn-#{t}"} + html_options[:class] = concat_classes(html_options[:class], 'btn', type) + args[0] = t(args[0]) if args[0].is_a?(Symbol) + link_to *args, html_options, &block + end + + def destroy_btn(*args, &block) + html_options = extract_html_options!(args, &block) + confirmation = t "#{controller_symbol}.confirm.destroy.are_you_sure", + cascade: true + html_options.merge! method: :delete, confirm: confirmation + btn *args, html_options, &block + end + + # + # concat_classes will combine classes in a fairly flexible way. + # it can handle nil, arrays, space separated strings + # it returns a space separated string of classes. + def concat_classes(*classes) + classes.compact! + classes.map {|c| c.respond_to?(:split) ? c.split(' ') : c } + classes.flatten! + classes.join ' ' + end + + def extract_html_options!(args) + if args.count > 2 or args.count > 1 && block_given? + args.extract_options! + else + {} + end + end +end diff --git a/app/views/users/_destroy_account.html.haml b/app/views/users/_destroy_account.html.haml index be003ce..a2c4ddd 100644 --- a/app/views/users/_destroy_account.html.haml +++ b/app/views/users/_destroy_account.html.haml @@ -8,7 +8,7 @@ - else = t(:admin_destroy_account, :username => @user.login) %p= t(:destroy_account_info) -= btn user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :type => "danger" do += destroy_btn user_path(@user), :type => "danger" do %i.icon-remove.icon-white = t(:destroy_my_account) - if @user != current_user and @user.enabled? diff --git a/engines/billing/app/views/subscriptions/show.html.haml b/engines/billing/app/views/subscriptions/show.html.haml index af1a5a5..246ebf0 100644 --- a/engines/billing/app/views/subscriptions/show.html.haml +++ b/engines/billing/app/views/subscriptions/show.html.haml @@ -4,4 +4,4 @@ Subscription = render :partial => "subscription_details", :locals => {:subscription => @subscription} - if allow_cancel_subscription(@subscription) - = btn :cancel_subscription, user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :type => 'danger' + = destroy_btn :cancel_subscription, user_subscription_path(@user, @subscription.id), type: 'danger' diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml index 9adc2cc..889dac2 100644 --- a/engines/support/app/views/tickets/_edit_form.html.haml +++ b/engines/support/app/views/tickets/_edit_form.html.haml @@ -43,4 +43,4 @@ = f.input :regarding_user, label: Ticket.human_attribute_name(:regarding_user) + regarding_user_link = f.button :loading - if admin? - = btn t(".destroy", cascade: true), auto_ticket_path(@ticket), confirm: t("tickets.confirm.destroy.are_you_sure", cascade: true), method: :delete + = destroy_btn t(".destroy", cascade: true), auto_ticket_path(@ticket) -- cgit v1.2.3 From 154d32bbc7cfe21d83141ff2c9a3d805165231b8 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 10:45:14 +0200 Subject: use Identity for testing login availability We create an identity alongside each user. Make sure the identity is valid when creating the user. This also ensures that the login picked is available because otherwise the identities address would not be available anymore. --- app/models/identity.rb | 30 ++++++++++++------------------ app/models/user.rb | 13 ++++++------- test/integration/browser/account_test.rb | 7 +++++++ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/models/identity.rb b/app/models/identity.rb index a4225e7..2be396c 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -10,8 +10,9 @@ class Identity < CouchRest::Model::Base property :keys, HashWithIndifferentAccess property :cert_fingerprints, Hash - validate :unique_forward validate :alias_available + validates :destination, presence: true, + uniqueness: {scope: :address} validate :address_local_email validate :destination_email @@ -44,13 +45,12 @@ class Identity < CouchRest::Model::Base end - def self.for(user, attributes = {}) - find_for(user, attributes) || build_for(user, attributes) + def self.for(user) + find_for(user) || build_for(user) end - def self.find_for(user, attributes = {}) - attributes.reverse_merge! attributes_from_user(user) - find_by_address_and_destination [attributes[:address], attributes[:destination]] + def self.find_for(user) + find_by_user_id(user.id) if user && user.persisted? end def self.build_for(user, attributes = {}) @@ -125,23 +125,17 @@ class Identity < CouchRest::Model::Base protected - def unique_forward - same = Identity.find_by_address_and_destination([address, destination]) - if same && same != self - errors.add :base, "This alias already exists" - end - end - def alias_available - same = Identity.find_by_address(address) - if same && same.user != self.user - errors.add :base, "This email has already been taken" + same_address = Identity.by_address.key(address) + if same_address.detect { |other| other.user !=self.user } + errors.add :address, :taken end end def address_local_email - return if address.valid? #this ensures it is LocalEmail - self.errors.add(:address, address.errors.messages[:email].first) #assumes only one error + return if address.valid? + # we only hand on the first error for now. + self.errors.add(:address, address.errors.messages.values.first) end def destination_email diff --git a/app/models/user.rb b/app/models/user.rb index 6678de6..6b4d1a9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,7 +24,7 @@ class User < CouchRest::Model::Base :uniqueness => true, :if => :serverside? - validate :login_is_unique_alias + validate :identity_is_valid validates :password_salt, :password_verifier, :format => { :with => /\A[\dA-Fa-f]+\z/, :message => "Only hex numbers allowed" } @@ -161,12 +161,11 @@ class User < CouchRest::Model::Base # Validation Functions ## - def login_is_unique_alias - alias_identity = Identity.find_by_address(self.email_address) - return if alias_identity.blank? - if alias_identity.user != self - errors.add(:login, "has already been taken") - end + def identity_is_valid + refresh_identity + return if identity.valid? + # hand on the first error only for now + self.errors.add(:login, identity.errors.messages.values.first) end def password diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_test.rb index 491a9e1..82bb043 100644 --- a/test/integration/browser/account_test.rb +++ b/test/integration/browser/account_test.rb @@ -22,6 +22,12 @@ class AccountTest < BrowserIntegrationTest assert page.has_content?("Welcome #{username}") end + test "signup with reserved username" do + username = 'certmaster' + submit_signup username + assert page.has_content?("is reserved.") + end + test "successful login" do username, password = submit_signup click_on 'Logout' @@ -44,6 +50,7 @@ class AccountTest < BrowserIntegrationTest click_on I18n.t('account_settings') click_on I18n.t('destroy_my_account') assert page.has_content?(I18n.t('account_destroyed')) + assert_equal 1, Identity.by_address.key("#{username}@test.me").count attempt_login(username, password) assert_invalid_login(page) end -- cgit v1.2.3 From 5c8ab9298cc4705de508a3f3f9d9d6370a01ff5e Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 11:43:50 +0200 Subject: minor: beautify handle lookup in etc/passwd some --- app/models/local_email.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/models/local_email.rb b/app/models/local_email.rb index 2b4c65e..ded7baf 100644 --- a/app/models/local_email.rb +++ b/app/models/local_email.rb @@ -58,11 +58,9 @@ class LocalEmail < Email end def handle_in_passwd? - begin - !!Etc.getpwnam(handle) - rescue ArgumentError - # handle was not found - return false - end + Etc.getpwnam(handle).present? + rescue ArgumentError + # handle was not found + return false end end -- cgit v1.2.3 From 682b4060cb86c52ffda638f4f9a837f107540610 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 11:44:12 +0200 Subject: ensure identity is cleared on user.reload - fixes test --- app/models/pgp_key.rb | 3 ++- app/models/user.rb | 5 +++++ test/integration/browser/account_test.rb | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/pgp_key.rb b/app/models/pgp_key.rb index 66f8660..3384f4c 100644 --- a/app/models/pgp_key.rb +++ b/app/models/pgp_key.rb @@ -25,9 +25,10 @@ class PgpKey # allow comparison with plain keyblock strings. def ==(other) + return false if (self.present? != other.present?) self.equal?(other) or # relax the comparison on line ends. - self.to_s.tr_s("\n\r", '') == other.tr_s("\r\n", '') + self.to_s.tr_s("\n\r", '') == other.tr_s("\n\r", '') end protected diff --git a/app/models/user.rb b/app/models/user.rb index 6b4d1a9..33508b5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,6 +42,11 @@ class User < CouchRest::Model::Base view :by_created_at end # end of design + def reload + super + @identity = nil + end + def to_json(options={}) { :login => login, diff --git a/test/integration/browser/account_test.rb b/test/integration/browser/account_test.rb index 82bb043..8e6d433 100644 --- a/test/integration/browser/account_test.rb +++ b/test/integration/browser/account_test.rb @@ -109,7 +109,8 @@ class AccountTest < BrowserIntegrationTest # at some point we're done: page.assert_no_selector 'input[value="Saving..."]' assert page.has_field? 'Public key', with: pgp_key.to_s - assert_equal pgp_key, @user.reload.public_key + @user.reload + assert_equal pgp_key, @user.public_key end end -- cgit v1.2.3 From 6fea83763f07add7d3bd07e3843b75aaf61e19b4 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 12:20:49 +0200 Subject: bring back the alias functionality in Identities --- app/models/account.rb | 1 + app/models/identity.rb | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index cf998e4..bffa288 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -18,6 +18,7 @@ class Account def self.create(attrs) @user = User.create(attrs).tap do |user| Identity.create_for user + user.refresh_identity end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 2be396c..0d25bae 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -11,8 +11,7 @@ class Identity < CouchRest::Model::Base property :cert_fingerprints, Hash validate :alias_available - validates :destination, presence: true, - uniqueness: {scope: :address} + validates :destination, uniqueness: {scope: :address} validate :address_local_email validate :destination_email @@ -45,12 +44,14 @@ class Identity < CouchRest::Model::Base end - def self.for(user) - find_for(user) || build_for(user) + def self.for(user, attributes = {}) + find_for(user, attributes) || build_for(user, attributes) end - def self.find_for(user) - find_by_user_id(user.id) if user && user.persisted? + def self.find_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + id = find_by_address_and_destination attributes.values_at(:address, :destination) + return id if id && id.user == user end def self.build_for(user, attributes = {}) @@ -67,7 +68,9 @@ class Identity < CouchRest::Model::Base def self.disable_all_for(user) Identity.by_user_id.key(user.id).each do |identity| identity.disable - identity.save + # if the identity is not unique anymore because the destination + # was reset to nil we destroy it. + identity.save || identity.destroy end end @@ -127,15 +130,15 @@ class Identity < CouchRest::Model::Base def alias_available same_address = Identity.by_address.key(address) - if same_address.detect { |other| other.user !=self.user } + if same_address.detect { |other| other.user != self.user } errors.add :address, :taken end end def address_local_email - return if address.valid? + return if address.valid? #this ensures it is a valid local email address # we only hand on the first error for now. - self.errors.add(:address, address.errors.messages.values.first) + self.errors.add(:address, address.errors.messages[:email].first) end def destination_email -- cgit v1.2.3 From 09dfa583eca69a3925c384c67c3d98cd8c69b360 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 12:28:07 +0200 Subject: allow changing the user_id on an identity we set it to nil when we disable it --- app/models/identity.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/identity.rb b/app/models/identity.rb index 0d25bae..a8eaba6 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -130,7 +130,7 @@ class Identity < CouchRest::Model::Base def alias_available same_address = Identity.by_address.key(address) - if same_address.detect { |other| other.user != self.user } + if same_address.detect { |other| other != self && other.user != self.user } errors.add :address, :taken end end -- cgit v1.2.3 From 5b601707c8af8454dacf2edd846bc3386e148253 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 12:29:50 +0200 Subject: adopt tests to new error messages for identities --- test/unit/identity_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb index eca104f..9c938f8 100644 --- a/test/unit/identity_test.rb +++ b/test/unit/identity_test.rb @@ -39,7 +39,7 @@ class IdentityTest < ActiveSupport::TestCase id = Identity.create_for @user, address: alias_name, destination: forward_address dup = Identity.build_for @user, address: alias_name, destination: forward_address assert !dup.valid? - assert_equal ["This alias already exists"], dup.errors[:base] + assert_equal ["has already been taken"], dup.errors[:destination] id.destroy end @@ -48,7 +48,7 @@ class IdentityTest < ActiveSupport::TestCase id = Identity.create_for @user, address: alias_name, destination: forward_address taken = Identity.build_for other_user, address: alias_name assert !taken.valid? - assert_equal ["This email has already been taken"], taken.errors[:base] + assert_equal ["has already been taken"], taken.errors[:address] id.destroy end -- cgit v1.2.3 From 016e61ce9ab44cf58355e843b0c0d0085d373fc7 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 09:38:53 +0200 Subject: catch corner cases of account creation Users now always check if their identity is valid. We need to make sure this works if the user is a new record and once it has been persisted. While the user is a new record the identity will have no user_id. Old identities that are left to block the login of a user who canceled their account also have a blank user_id. They still should render the new identity invalid so the user can't be saved with a login that has been reserved. Once the user has been persisted we set the user_id on the identity and save it too when creating an Account. This allows us to create a plain user and save it and it will still have an in memory identity only. But the default is to create the user by means of creating an account so an identity will be created as well. --- app/models/account.rb | 9 ++++++--- app/models/identity.rb | 13 +++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index bffa288..32ed445 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -16,10 +16,13 @@ class Account # Returns the user record so it can be used in views. def self.create(attrs) - @user = User.create(attrs).tap do |user| - Identity.create_for user - user.refresh_identity + @user = User.create(attrs) + if @user.persisted? + identity = @user.identity + identity.user_id = @user.id + identity.save end + return @user end def update(attrs) diff --git a/app/models/identity.rb b/app/models/identity.rb index a8eaba6..25be971 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -129,8 +129,12 @@ class Identity < CouchRest::Model::Base protected def alias_available - same_address = Identity.by_address.key(address) - if same_address.detect { |other| other != self && other.user != self.user } + blocking_identities = Identity.by_address.key(address).all + blocking_identities.delete self + if self.user + blocking_identities.reject! { |other| other.user == self.user } + end + if blocking_identities.any? errors.add :address, :taken end end @@ -138,13 +142,14 @@ class Identity < CouchRest::Model::Base def address_local_email return if address.valid? #this ensures it is a valid local email address # we only hand on the first error for now. - self.errors.add(:address, address.errors.messages[:email].first) + self.errors.add(:address, address.errors.messages.values.first) end def destination_email return if destination.nil? # this identity is disabled return if destination.valid? # this ensures it is Email - self.errors.add(:destination, destination.errors.messages[:email].first) #assumes only one error #TODO + # we only hand on the first error for now. + self.errors.add(:destination, destination.errors.messages.values.first) end end -- cgit v1.2.3 From e0d31118d6e4110d2c280afa9415cfe9def29deb Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 10:04:07 +0200 Subject: hand on errors from Email to Identity to User errors.each iterates through all errors for all attrbibutes nicely. --- app/models/identity.rb | 10 ++++++---- app/models/user.rb | 6 +++--- test/unit/identity_test.rb | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/models/identity.rb b/app/models/identity.rb index 25be971..f2727c8 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -141,15 +141,17 @@ class Identity < CouchRest::Model::Base def address_local_email return if address.valid? #this ensures it is a valid local email address - # we only hand on the first error for now. - self.errors.add(:address, address.errors.messages.values.first) + address.errors.each do |attribute, error| + self.errors.add(:address, error) + end end def destination_email return if destination.nil? # this identity is disabled return if destination.valid? # this ensures it is Email - # we only hand on the first error for now. - self.errors.add(:destination, destination.errors.messages.values.first) + destination.errors.each do |attribute, error| + self.errors.add(:destination, error) + end end end diff --git a/app/models/user.rb b/app/models/user.rb index 33508b5..84a795e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -167,10 +167,10 @@ class User < CouchRest::Model::Base ## def identity_is_valid - refresh_identity return if identity.valid? - # hand on the first error only for now - self.errors.add(:login, identity.errors.messages.values.first) + identity.errors.each do |attribute, error| + self.errors.add(:login, error) + end end def password diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb index 9c938f8..54c0657 100644 --- a/test/unit/identity_test.rb +++ b/test/unit/identity_test.rb @@ -107,6 +107,7 @@ class IdentityTest < ActiveSupport::TestCase other_user = find_record :user taken = Identity.build_for other_user, address: id.address assert !taken.valid? + assert_equal ["has already been taken"], taken.errors[:address] Identity.destroy_all_disabled end -- cgit v1.2.3 From 85e066920568c19b788b8789c4659092224bb517 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 10:37:31 +0200 Subject: ensure User#reload returns self --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 84a795e..f8b9ddc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -43,8 +43,8 @@ class User < CouchRest::Model::Base end # end of design def reload - super @identity = nil + super end def to_json(options={}) -- cgit v1.2.3 From bbe7b3b7deb2b44d34f7c39dda2c3db284e2bf10 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 11:19:21 +0200 Subject: clearify identity validations Identity.new.valid? should not crash. So validate presence where needed and skip the other validations if the value is absent. --- app/models/identity.rb | 23 ++++++++++++++++------- test/integration/api/smtp_cert_test.rb | 2 +- test/unit/identity_test.rb | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/app/models/identity.rb b/app/models/identity.rb index f2727c8..2f6241c 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -10,7 +10,9 @@ class Identity < CouchRest::Model::Base property :keys, HashWithIndifferentAccess property :cert_fingerprints, Hash - validate :alias_available + validates :address, presence: true + validate :address_available + validates :destination, presence: true, if: :enabled? validates :destination, uniqueness: {scope: :address} validate :address_local_email validate :destination_email @@ -94,7 +96,11 @@ class Identity < CouchRest::Model::Base end def enabled? - self.destination && self.user_id + self.user_id + end + + def disabled? + !enabled? end def disable @@ -123,12 +129,12 @@ class Identity < CouchRest::Model::Base # for LoginFormatValidation def login - self.address.handle + address.handle if address.present? end protected - def alias_available + def address_available blocking_identities = Identity.by_address.key(address).all blocking_identities.delete self if self.user @@ -140,15 +146,18 @@ class Identity < CouchRest::Model::Base end def address_local_email - return if address.valid? #this ensures it is a valid local email address + # caught by presence validation + return if address.blank? + return if address.valid? address.errors.each do |attribute, error| self.errors.add(:address, error) end end def destination_email - return if destination.nil? # this identity is disabled - return if destination.valid? # this ensures it is Email + # caught by presence validation or this identity is disabled + return if destination.blank? + return if destination.valid? destination.errors.each do |attribute, error| self.errors.add(:destination, error) end diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb index 04e6f31..f72362d 100644 --- a/test/integration/api/smtp_cert_test.rb +++ b/test/integration/api/smtp_cert_test.rb @@ -34,7 +34,7 @@ class SmtpCertTest < ApiIntegrationTest cert = OpenSSL::X509::Certificate.new(get_response.body) fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':') today = DateTime.now.to_date.to_s - assert_equal({fingerprint => today}, @user.identity.cert_fingerprints) + assert_equal({fingerprint => today}, @user.reload.identity.cert_fingerprints) end test "fetching smtp certs requires email account" do diff --git a/test/unit/identity_test.rb b/test/unit/identity_test.rb index 54c0657..49b2075 100644 --- a/test/unit/identity_test.rb +++ b/test/unit/identity_test.rb @@ -7,6 +7,22 @@ class IdentityTest < ActiveSupport::TestCase @user = find_record :user end + test "blank identity does not crash on valid?" do + id = Identity.new + assert !id.valid? + end + + test "enabled identity requires destination" do + id = Identity.new user: @user, address: @user.email_address + assert !id.valid? + assert_equal ["can't be blank"], id.errors[:destination] + end + + test "disabled identity requires no destination" do + id = Identity.new address: @user.email_address + assert id.valid? + end + test "initial identity for a user" do id = Identity.for(@user) assert_equal @user.email_address, id.address -- cgit v1.2.3 From ab49a72b52575f3b9fdf13fee47e99dfb82e2a3d Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 14:57:23 +0200 Subject: html5:
instead of
--- app/controllers/controller_extension/flash.rb | 4 ++-- app/views/pages/terms-of-service.en.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/controller_extension/flash.rb b/app/controllers/controller_extension/flash.rb index 8bc9ee7..1642141 100644 --- a/app/controllers/controller_extension/flash.rb +++ b/app/controllers/controller_extension/flash.rb @@ -37,7 +37,7 @@ module ControllerExtension::Flash def add_flash_errors_for(resource) return if resource.valid? - flash[:error] += "
" - flash[:error] += resource.errors.full_messages.join(".
") + flash[:error] += "
" + flash[:error] += resource.errors.full_messages.join(".
") end end diff --git a/app/views/pages/terms-of-service.en.md b/app/views/pages/terms-of-service.en.md index 7b57027..93490b7 100644 --- a/app/views/pages/terms-of-service.en.md +++ b/app/views/pages/terms-of-service.en.md @@ -3,8 +3,8 @@ This document is our Terms of Service, which describes what activities are allowed, under what conditions we may terminate your account, and asserts our limited liability. It applies to all interactions with **<%=APP_CONFIG[:domain]%>**. Your use of **<%=APP_CONFIG[:domain]%>** services will constitute your agreement to these Terms of Service.

- Summary:
- (1) If you do anything truly evil, we will terminate your account.
+ Summary:
+ (1) If you do anything truly evil, we will terminate your account.
(2) We are not liable for any damages related to the use of this service.

-- cgit v1.2.3 From 6a5e5edf54c76bea3de93475d949c3372d376a81 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 20:10:11 +0200 Subject: adopt tests to new translations --- engines/support/test/integration/create_ticket_test.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/engines/support/test/integration/create_ticket_test.rb b/engines/support/test/integration/create_ticket_test.rb index 0f8453c..90e9a8a 100644 --- a/engines/support/test/integration/create_ticket_test.rb +++ b/engines/support/test/integration/create_ticket_test.rb @@ -7,7 +7,7 @@ class CreateTicketTest < BrowserIntegrationTest click_on 'Get Help' fill_in 'Subject', with: 'test ticket' fill_in 'Description', with: 'description of the problem goes here' - click_on 'Create Ticket' + click_on 'Submit Ticket' assert page.has_content?("Ticket was successfully created.") assert page.has_content?("You can later access this ticket at the URL") assert page.has_content?(current_url) @@ -20,12 +20,12 @@ class CreateTicketTest < BrowserIntegrationTest click_on 'Get Help' fill_in 'Subject', with: 'test ticket' fill_in 'Email', with: 'invalid data' - fill_in 'Regarding user', with: 'some user' + fill_in 'Regarding User', with: 'some user' fill_in 'Description', with: 'description of the problem goes here' - click_on 'Create Ticket' + click_on 'Submit Ticket' assert page.has_content?("is invalid") assert_equal 'invalid data', find_field('Email').value - assert_equal 'some user', find_field('Regarding user').value + assert_equal 'some user', find_field('Regarding User').value end test "prefills fields" do @@ -35,7 +35,7 @@ class CreateTicketTest < BrowserIntegrationTest click_on "New Ticket" email = "#{@user.login}@#{APP_CONFIG[:domain]}" assert_equal email, find_field('Email').value - assert_equal @user.login, find_field('Regarding user').value + assert_equal @user.login, find_field('Regarding User').value end test "no prefill of email without email service" do @@ -44,7 +44,7 @@ class CreateTicketTest < BrowserIntegrationTest click_on "Support Tickets" click_on "New Ticket" assert_equal "", find_field('Email').value - assert_equal @user.login, find_field('Regarding user').value + assert_equal @user.login, find_field('Regarding User').value end test "cleared email field should remain clear" do @@ -55,7 +55,7 @@ class CreateTicketTest < BrowserIntegrationTest fill_in 'Subject', with: 'test ticket' fill_in 'Email', with: '' fill_in 'Description', with: 'description of the problem goes here' - click_on 'Create Ticket' + click_on 'Submit Ticket' ticket = Ticket.last assert_equal "", ticket.email ticket.destroy -- cgit v1.2.3 From 9e3be686ff2751707369894382293924420830d0 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 20:11:29 +0200 Subject: fix flash for creating anonymous tickets --- engines/support/app/controllers/tickets_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index 857d071..fab26f3 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -25,7 +25,9 @@ class TicketsController < ApplicationController @ticket.created_by = current_user.id flash_for @ticket if @ticket.save && !logged_in? - flash[:success] = t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) + flash[:success] += t 'tickets.access_ticket_text', + full_url: ticket_url(@ticket.id), + default: "" end respond_with @ticket, :location => auto_ticket_path(@ticket) end -- cgit v1.2.3 From 366ff2e7f5ecd44aab1cddfd0a7b73ab7b213e85 Mon Sep 17 00:00:00 2001 From: elijah Date: Tue, 3 Jun 2014 01:12:17 -0700 Subject: tickets: fix bug that allow index of other users --- .../support/app/controllers/tickets_controller.rb | 22 ++++++++++++++++------ .../test/functional/tickets_controller_test.rb | 15 ++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb index fab26f3..1ccbd16 100644 --- a/engines/support/app/controllers/tickets_controller.rb +++ b/engines/support/app/controllers/tickets_controller.rb @@ -4,10 +4,10 @@ class TicketsController < ApplicationController respond_to :html, :json #has_scope :open, :type => boolean + before_filter :fetch_user before_filter :require_login, :only => [:index] before_filter :fetch_ticket, except: [:new, :create, :index] - before_filter :require_ticket_access, except: [:new, :create, :index] - before_filter :fetch_user + before_filter :require_ticket_access, except: [:new, :create] before_filter :set_title def new @@ -129,14 +129,24 @@ class TicketsController < ApplicationController end def ticket_access? - admin? or - @ticket.created_by.blank? or - current_user.id == @ticket.created_by + admin? or ( + @ticket && + @ticket.created_by.blank? + ) or ( + @ticket && + @ticket.created_by == current_user.id + ) or ( + @ticket.nil? && + @user && + @user.id == current_user.id + ) end def fetch_user if params[:user_id] @user = User.find(params[:user_id]) + else + @user = current_user end end @@ -146,7 +156,7 @@ class TicketsController < ApplicationController def search_options(params) params.merge( :admin_status => params[:user_id] ? 'mine' : 'all', - :user_id => @user ? @user.id : current_user.id, + :user_id => @user.id, :is_admin => admin? ) end diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb index 1d074cc..ebaa3a4 100644 --- a/engines/support/test/functional/tickets_controller_test.rb +++ b/engines/support/test/functional/tickets_controller_test.rb @@ -45,8 +45,7 @@ class TicketsControllerTest < ActionController::TestCase user = find_record :user ticket = find_record :ticket, :created_by => user.id get :show, :id => ticket.id - assert_response :redirect - assert_redirected_to login_url + assert_login_required end test "user tickets are visible to creator" do @@ -57,13 +56,19 @@ class TicketsControllerTest < ActionController::TestCase assert_response :success end - test "other users tickets are not visible" do + test "ticket of other user is not visible" do other_user = find_record :user ticket = find_record :ticket, :created_by => other_user.id login get :show, :id => ticket.id - assert_response :redirect - assert_redirected_to home_url + assert_access_denied + end + + test "ticket list of other user is not visible" do + other_user = find_record :user + login + get :index, :user_id => other_user.id + assert_access_denied end test "should create unauthenticated ticket" do -- cgit v1.2.3 From 9fa52ed80d71ec56ed5acf18dfd63bd03b201cc5 Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 9 Jun 2014 10:16:22 +0200 Subject: Version 0.5.2 Hotfix since 0.5.2 release candiate: * tickets: fix bug that allow index of other users Pull request #167 from azul/feature/i18n-for-ticket-system: * fix flash for creating anonymous tickets * adopt tests to new translations * destroy_btn helper method * move users key into layouts scope so it does not conflict with users scope * add btn helper for link_to with .btn * remove icon_color variable - yagni * sorting translation keys some * navigation works with empty locale selected * tickets: structure i18n * flash_for with_errors option displays error messages * remove unused bold helper and instead sanitize flash * Controller#flash_for instead of FlashResponder * split up and refactor TicketController#update * separate tests for the ticket list from main controller test * splitting up long functional test case * move comment related tests out of TicketControllerTest * use i18n.missing_translations Pull request #168 from azul/bugfix/fix-login-validations: * clearify identity validations * ensure User#reload returns self * hand on errors from Email to Identity to User * catch corner cases of account creation * adopt tests to new error messages for identities * allow changing the user_id on an identity * ensure identity is cleared on user.reload - fixes test * use Identity for testing login availability Pull request #163 from azul/feature/3398-save-hashed-token * hash token with sha512 against timing attacs #3398 Pull request #165 from azul/feature/cert-fingerprints * change from GET to POST for certs * store fingerprints with timestamp * store cert fingerprint with main user identity * SmtpCertsController, routes and tests * fix Email so User.new.valid? does not crash * basic integration test for cert API * calculate cert fingerprints to store for leap_mx Pull request #166 from elijh/feature/footer * better detection if price link should be shown in the footer Pull request #162 from azul/feature/3295-custom-error-pages * little bit of documentation * i18n for error pages * custom error pages for 404 and 500 errors --- Gemfile.lock | 4 ++-- lib/leap_web/version.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2c229f9..d189022 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,13 +8,13 @@ GIT PATH remote: engines/billing specs: - leap_web_billing (0.5.1) + leap_web_billing (0.5.2) braintree PATH remote: engines/support specs: - leap_web_help (0.5.1) + leap_web_help (0.5.2) GEM remote: https://rubygems.org/ diff --git a/lib/leap_web/version.rb b/lib/leap_web/version.rb index b92d0a3..56df918 100644 --- a/lib/leap_web/version.rb +++ b/lib/leap_web/version.rb @@ -1,3 +1,3 @@ module LeapWeb - VERSION = "0.5.1" unless defined?(LeapWeb::VERSION) + VERSION = "0.5.2" unless defined?(LeapWeb::VERSION) end -- cgit v1.2.3