Merge branch 'release/4.3' into dev

pull/3257/head
Jens Ulferts 9 years ago
commit 98678183c1
  1. 13
      CONTRIBUTING.md
  2. 2
      Gemfile.lock
  3. 2
      app/controllers/api/v2/authentication_controller.rb
  4. 2
      app/models/work_package.rb
  5. 14
      app/views/settings/_authentication.html.erb
  6. 7
      app/views/settings/_display.html.erb
  7. 15
      config/locales/en.yml
  8. 3
      config/settings.yml
  9. 32
      frontend/app/helpers/path-helper.js
  10. 2
      frontend/app/services/priority-service.js
  11. 14
      frontend/app/services/status-service.js
  12. 2
      frontend/app/services/type-service.js
  13. 4
      frontend/package.json
  14. 3
      lib/api/v3/work_packages/schema/work_package_schema_representer.rb
  15. 2
      lib/open_project/authentication.rb
  16. 19
      spec/controllers/api/v2/authentication_spec.rb
  17. 9
      spec/features/work_packages/details/activity_comments_spec.rb
  18. 2
      spec/legacy/support/legacy_assertions.rb
  19. 20
      spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
  20. 24
      spec/models/work_package_spec.rb
  21. 2
      spec/requests/api/v3/authentication_spec.rb

@ -59,6 +59,19 @@ We will then review your pull request.
Please note that you can add commits after the pull request has been created by pushing Please note that you can add commits after the pull request has been created by pushing
to the branch in your fork. to the branch in your fork.
## Translations
Beginning with OpenProject 4.2.0 the OpenProject core only holds the
english locales and all other locales are stored in
OpenProject-Translations. But since this plugin is hardwired in the
Gemfile, german and other locales are available again.
If you want to contribute to the localization of OpenProject and its
plugins you can do so on [Crowdin](https://crowdin.com/projects/opf).
Once a day we will fetch those locales and upload them to GitHub.
More on this topic can be found [in our blog](https://www.openproject.org/2015/07/10/help-translate-openproject-into-your-language/).
## Important notes ## Important notes
To ensure a smooth workflow for everyone, please take note of the following: To ensure a smooth workflow for everyone, please take note of the following:

@ -45,7 +45,7 @@ GIT
revision: c42bac8f2bca7850a89b67e753368f5f52d13c25 revision: c42bac8f2bca7850a89b67e753368f5f52d13c25
branch: dev branch: dev
specs: specs:
openproject-translations (4.3.0.pre.alpha) openproject-translations (4.3.0)
crowdin-api (~> 0.2.4) crowdin-api (~> 0.2.4)
mixlib-shellout (~> 2.1.0) mixlib-shellout (~> 2.1.0)
rails (~> 3.2.14) rails (~> 3.2.14)

@ -35,7 +35,7 @@ module Api
unloadable unloadable
AuthorizationData = Struct.new(:authorized, :authenticated_user_id) AuthorizationData = Struct.new(:authorized, :authenticated_user_id)
skip_before_filter :require_login skip_before_filter :require_login, :check_if_login_required
before_filter :api_allows_login, :require_login before_filter :api_allows_login, :require_login
def index def index

@ -444,7 +444,7 @@ class WorkPackage < ActiveRecord::Base
end end
def update_by(user, attributes) def update_by(user, attributes)
add_journal(user, attributes.delete(:notes)) if attributes[:notes] add_journal(user, attributes.delete(:notes) || '')
add_time_entry_for(user, attributes.delete(:time_entry)) add_time_entry_for(user, attributes.delete(:time_entry))
attributes.delete(:attachments) attributes.delete(:attachments)

@ -50,7 +50,11 @@ See doc/COPYRIGHT.rdoc for more details.
[l("label_password_rule_#{r}"), r] [l("label_password_rule_#{r}"), r]
end %></div> end %></div>
<div class="form--field"><%= setting_text_field :password_min_adhered_rules, :size => 6 %></div> <div class="form--field"><%= setting_text_field :password_min_adhered_rules, :size => 6 %></div>
<div class="form--field"><%= setting_text_field :password_days_valid, :size => 6 %></div> <div class="form--field"><%= setting_text_field :password_days_valid, :size => 6 %>
<span class="form--field-instructions">
<%= l(:text_hint_disable_with_0) %>
</span>
</div>
<div class="form--field"><%= setting_text_field :password_count_former_banned, :size => 6 %></div> <div class="form--field"><%= setting_text_field :password_count_former_banned, :size => 6 %></div>
<div class="form--field"><%= setting_check_box :lost_password, :label => :label_password_lost %></div> <div class="form--field"><%= setting_check_box :lost_password, :label => :label_password_lost %></div>
<% else %> <% else %>
@ -71,8 +75,12 @@ See doc/COPYRIGHT.rdoc for more details.
<% unless OpenProject::Configuration.disable_password_login? %> <% unless OpenProject::Configuration.disable_password_login? %>
<fieldset class="form--fieldset"> <fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= I18n.t(:brute_force_prevention, :scope => [:settings]) %></legend> <legend class="form--fieldset-legend"><%= I18n.t(:brute_force_prevention, :scope => [:settings]) %></legend>
<div class="form--field"><%= setting_text_field :brute_force_block_after_failed_logins %></div> <div class="form--field"><%= setting_text_field :brute_force_block_after_failed_logins %>
<div class="form--field"><%= setting_text_field :brute_force_block_minutes %></div> <span class="form--field-instructions">
<%= l(:text_hint_disable_with_0) %>
</span>
</div>
<div class="form--field"><%= setting_text_field :brute_force_block_minutes, unit: l(:label_minute_plural) %></div>
</fieldset> </fieldset>
<% end %> <% end %>

@ -50,6 +50,13 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field"><%= setting_check_box :gravatar_enabled %></div> <div class="form--field"><%= setting_check_box :gravatar_enabled %></div>
<div class="form--field"><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></div> <div class="form--field"><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></div>
<div class="form--field"><%= setting_text_field :journal_aggregation_time_minutes, unit: l(:label_minute_plural) %>
<span class="form--field-instructions">
<%= l(:text_journal_aggregation_time_explanation) %><br/>
<%= l(:text_hint_disable_with_0) %>
</span>
</div>
</section> </section>
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %> <%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %>

@ -578,8 +578,8 @@ en:
error_scm_annotate: "The entry does not exist or cannot be annotated." error_scm_annotate: "The entry does not exist or cannot be annotated."
error_scm_command_failed: "An error occurred when trying to access the repository: %{value}" error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
error_scm_not_found: "The entry or revision was not found in the repository." error_scm_not_found: "The entry or revision was not found in the repository."
error_unable_delete_status: "Unable to delete work package status because it contains work packages." error_unable_delete_status: "The work package status cannot be deleted since it is used by at least one work package."
error_unable_delete_default_status: "Unable to delete default work package status. Please select another default work package status before deleting the current one." error_unable_delete_default_status: "Unable to delete the default work package status. Please select another default work package status before deleting the current one."
error_unable_to_connect: "Unable to connect (%{value})" error_unable_to_connect: "Unable to connect (%{value})"
error_workflow_copy_source: "Please select a source type or role" error_workflow_copy_source: "Please select a source type or role"
error_workflow_copy_target: "Please select target type(s) and role(s)" error_workflow_copy_target: "Please select target type(s) and role(s)"
@ -816,6 +816,7 @@ en:
label_message_plural: "Messages" label_message_plural: "Messages"
label_message_posted: "Message added" label_message_posted: "Message added"
label_min_max_length: "Min - Max length" label_min_max_length: "Min - Max length"
label_minute_plural: "minutes"
label_missing_api_access_key: "Missing an API access key" label_missing_api_access_key: "Missing an API access key"
label_missing_feeds_access_key: "Missing a RSS access key" label_missing_feeds_access_key: "Missing a RSS access key"
label_modification: "%{count} change" label_modification: "%{count} change"
@ -1234,6 +1235,7 @@ en:
project_module_news: "News" project_module_news: "News"
project_module_repository: "Repository" project_module_repository: "Repository"
project_module_time_tracking: "Time tracking" project_module_time_tracking: "Time tracking"
project_module_timelines: "Timelines"
project_module_wiki: "Wiki" project_module_wiki: "Wiki"
# possible query parameters (e.g. issue queries), # possible query parameters (e.g. issue queries),
@ -1253,8 +1255,8 @@ en:
setting_autologin: "Autologin" setting_autologin: "Autologin"
setting_available_languages: "Available languages" setting_available_languages: "Available languages"
setting_bcc_recipients: "Blind carbon copy recipients (bcc)" setting_bcc_recipients: "Blind carbon copy recipients (bcc)"
setting_brute_force_block_after_failed_logins: "Block user after this number of failed login attempts (disable with 0)" setting_brute_force_block_after_failed_logins: "Block user after this number of failed login attempts"
setting_brute_force_block_minutes: "Time the user is blocked for (minutes)" setting_brute_force_block_minutes: "Time the user is blocked for"
setting_cache_formatted_text: "Cache formatted text" setting_cache_formatted_text: "Cache formatted text"
setting_column_options: "Customize the appearance of the work package lists" setting_column_options: "Customize the appearance of the work package lists"
setting_commit_fix_keywords: "Fixing keywords" setting_commit_fix_keywords: "Fixing keywords"
@ -1288,6 +1290,7 @@ en:
setting_work_package_properties: "Work package properties" setting_work_package_properties: "Work package properties"
setting_work_package_startdate_is_adddate: "Use current date as start date for new work packages" setting_work_package_startdate_is_adddate: "Use current date as start date for new work packages"
setting_work_packages_export_limit: "Work packages export limit" setting_work_packages_export_limit: "Work packages export limit"
setting_journal_aggregation_time_minutes: "Display journals as aggregated within"
setting_log_requesting_user: "Log user login, name, and mail address for all requests" setting_log_requesting_user: "Log user login, name, and mail address for all requests"
setting_login_required: "Authentication required" setting_login_required: "Authentication required"
setting_mail_from: "Emission email address" setting_mail_from: "Emission email address"
@ -1297,7 +1300,7 @@ en:
setting_new_project_user_role_id: "Role given to a non-admin user who creates a project" setting_new_project_user_role_id: "Role given to a non-admin user who creates a project"
setting_password_active_rules: "Active character classes" setting_password_active_rules: "Active character classes"
setting_password_count_former_banned: "Number of most recently used passwords banned for reuse" setting_password_count_former_banned: "Number of most recently used passwords banned for reuse"
setting_password_days_valid: "Number of days, after which to enforce a password change (disable with 0)" setting_password_days_valid: "Number of days, after which to enforce a password change"
setting_password_min_length: "Minimum length" setting_password_min_length: "Minimum length"
setting_password_min_adhered_rules: "Minimum number of required classes" setting_password_min_adhered_rules: "Minimum number of required classes"
setting_per_page_options: "Objects per page options" setting_per_page_options: "Objects per page options"
@ -1362,6 +1365,7 @@ en:
text_enumeration_destroy_question: "%{count} objects are assigned to this value." text_enumeration_destroy_question: "%{count} objects are assigned to this value."
text_file_repository_writable: "Attachments directory writable" text_file_repository_writable: "Attachments directory writable"
text_git_repo_example: "a bare and local repository (e.g. /gitrepo, c:\\gitrepo)" text_git_repo_example: "a bare and local repository (e.g. /gitrepo, c:\\gitrepo)"
text_hint_disable_with_0: "Note: Disable with 0"
text_work_package_added: "Work package %{id} has been reported by %{author}." text_work_package_added: "Work package %{id} has been reported by %{author}."
text_work_package_category_destroy_assignments: "Remove category assignments" text_work_package_category_destroy_assignments: "Remove category assignments"
text_work_package_category_destroy_question: "Some work packages (%{count}) are assigned to this category. What do you want to do?" text_work_package_category_destroy_question: "Some work packages (%{count}) are assigned to this category. What do you want to do?"
@ -1370,6 +1374,7 @@ en:
text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?"
text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages"
text_journal_added: "%{label} %{value} added" text_journal_added: "%{label} %{value} added"
text_journal_aggregation_time_explanation: "Combine journals for display if their age difference is less than the specified timespan. This will also delay mail notifications by the same amount of time."
text_journal_changed: "%{label} changed from %{old} to %{new}" text_journal_changed: "%{label} changed from %{old} to %{new}"
text_journal_changed_no_detail: "%{label} updated" text_journal_changed_no_detail: "%{label} updated"
text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_changed_with_diff: "%{label} changed (%{link})"

@ -261,3 +261,6 @@ users_deletable_by_admins:
default: 0 default: 0
users_deletable_by_self: users_deletable_by_self:
default: 0 default: 0
journal_aggregation_time_minutes:
default: 5
format: int

@ -217,26 +217,6 @@ module.exports = function() {
return PathHelper.apiWorkPackagesPath() + '/column_sums'; return PathHelper.apiWorkPackagesPath() + '/column_sums';
}, },
// API V2
apiPrioritiesPath: function() {
return PathHelper.apiV2 + '/planning_element_priorities';
},
apiProjectStatusesPath: function(projectIdentifier) {
return PathHelper.apiV2ProjectPath(projectIdentifier) + '/statuses';
},
apiProjectWorkPackageTypesPath: function(projectIdentifier) {
return PathHelper.apiV2ProjectPath(projectIdentifier) + '/planning_element_types';
},
apiStatusesPath: function() {
return PathHelper.apiV2 + '/statuses';
},
apiV2ProjectPath: function(projectIdentifier) {
return PathHelper.apiV2 + PathHelper.projectPath(projectIdentifier);
},
apiWorkPackageTypesPath: function() {
return PathHelper.apiV2 + '/planning_element_types';
},
// API V3 // API V3
apiQueryStarPath: function(queryId) { apiQueryStarPath: function(queryId) {
return PathHelper.apiV3QueryPath(queryId) + '/star'; return PathHelper.apiV3QueryPath(queryId) + '/star';
@ -250,6 +230,9 @@ module.exports = function() {
apiV3WorkPackagePath: function(workPackageId) { apiV3WorkPackagePath: function(workPackageId) {
return PathHelper.apiV3 + '/work_packages/' + workPackageId; return PathHelper.apiV3 + '/work_packages/' + workPackageId;
}, },
apiPrioritiesPath: function() {
return PathHelper.apiV3 + '/priorities';
},
apiV3ProjectsPath: function(projectIdentifier) { apiV3ProjectsPath: function(projectIdentifier) {
return PathHelper.apiV3 + PathHelper.projectsPath() + '/' + projectIdentifier; return PathHelper.apiV3 + PathHelper.projectsPath() + '/' + projectIdentifier;
}, },
@ -259,6 +242,15 @@ module.exports = function() {
apiV3TypePath: function(typeId) { apiV3TypePath: function(typeId) {
return PathHelper.apiV3 + '/types/' + typeId; return PathHelper.apiV3 + '/types/' + typeId;
}, },
apiStatusesPath: function() {
return PathHelper.apiV3 + '/statuses';
},
apiProjectWorkPackageTypesPath: function(projectIdentifier) {
return PathHelper.apiV3ProjectsPath(projectIdentifier) + '/types';
},
apiWorkPackageTypesPath: function() {
return PathHelper.apiV3 + '/types';
},
// Static // Static
staticUserPath: function(userId) { staticUserPath: function(userId) {
return PathHelper.userPath(userId); return PathHelper.userPath(userId);

@ -38,7 +38,7 @@ module.exports = function($http, PathHelper) {
doQuery: function(url, params) { doQuery: function(url, params) {
return $http.get(url, { params: params }) return $http.get(url, { params: params })
.then(function(response){ .then(function(response){
return response.data.planning_element_priorities; return response.data._embedded.elements;
}); });
} }
}; };

@ -29,22 +29,14 @@
module.exports = function($http, PathHelper) { module.exports = function($http, PathHelper) {
var StatusService = { var StatusService = {
getStatuses: function(projectIdentifier) { getStatuses: function() {
var url; return StatusService.doQuery(PathHelper.apiStatusesPath());
if(projectIdentifier) {
url = PathHelper.apiProjectStatusesPath(projectIdentifier);
} else {
url = PathHelper.apiStatusesPath();
}
return StatusService.doQuery(url);
}, },
doQuery: function(url, params) { doQuery: function(url, params) {
return $http.get(url, { params: params }) return $http.get(url, { params: params })
.then(function(response){ .then(function(response){
return response.data.statuses; return response.data._embedded.elements;
}); });
} }
}; };

@ -45,7 +45,7 @@ module.exports = function($http, PathHelper) {
doQuery: function(url, params) { doQuery: function(url, params) {
return $http.get(url, { params: params }) return $http.get(url, { params: params })
.then(function(response){ .then(function(response){
return response.data.planning_element_types; return response.data._embedded.elements;
}); });
} }
}; };

@ -13,7 +13,7 @@
"gulp-filter": "^2.0.2", "gulp-filter": "^2.0.2",
"gulp-jshint": "^1.8.5", "gulp-jshint": "^1.8.5",
"gulp-livingstyleguide": "0.1.5", "gulp-livingstyleguide": "0.1.5",
"gulp-protractor": "0.0.11", "gulp-protractor": "1.0.0",
"gulp-replace": "^0.5.3", "gulp-replace": "^0.5.3",
"gulp-ruby-sass": "^0.7.1", "gulp-ruby-sass": "^0.7.1",
"gulp-util": "^3.0.4", "gulp-util": "^3.0.4",
@ -34,7 +34,7 @@
"mocha": "~1.18.2", "mocha": "~1.18.2",
"mocha-jenkins-reporter": "^0.1.2", "mocha-jenkins-reporter": "^0.1.2",
"phantomjs": "~1.9.2", "phantomjs": "~1.9.2",
"protractor": "^2.0.0", "protractor": "^2.1.0",
"sinon": "~1.9.1", "sinon": "~1.9.1",
"sinon-chai": "~2.5.0", "sinon-chai": "~2.5.0",
"sorted-object": "^1.0.0", "sorted-object": "^1.0.0",

@ -52,9 +52,8 @@ module API
end end
link :self do link :self do
path = api_v3_paths.work_package_schema(represented.project.id, represented.type.id)
unless form_embedded unless form_embedded
path = api_v3_paths.work_package_schema(represented.project.id, represented.type.id)
{ href: path } { href: path }
end end
end end

@ -83,7 +83,7 @@ module OpenProject
module_function module_function
def pick_auth_scheme(supported_schemes, default_scheme, request_headers = {}) def pick_auth_scheme(supported_schemes, default_scheme, request_headers = {})
req_scheme = request_headers['X-Authentication-Scheme'] req_scheme = request_headers['HTTP_X_AUTHENTICATION_SCHEME']
if supported_schemes.include? req_scheme if supported_schemes.include? req_scheme
req_scheme req_scheme

@ -39,14 +39,23 @@ describe Api::V2::AuthenticationController, type: :controller do
it_should_behave_like 'a controller action with require_login' it_should_behave_like 'a controller action with require_login'
describe 'REST API disabled' do describe 'REST API disabled' do
before do before { allow(Setting).to receive(:rest_api_enabled?).and_return false }
allow(Setting).to receive(:rest_api_enabled?).and_return false context 'without login_required' do
before { fetch }
fetch it { expect(response.status).to eq(403) }
end end
it { expect(response.status).to eq(403) } context 'with login_required' do
before do
allow(Setting).to receive(:login_required?).and_return true
fetch
end
it { expect(response.status).to eq(403) }
end
end end
describe 'authorization data' do describe 'authorization data' do
@ -120,7 +129,7 @@ describe Api::V2::AuthenticationController, type: :controller do
context 'with Session auth scheme requested' do context 'with Session auth scheme requested' do
before do before do
request.env['X-Authentication-Scheme'] = 'Session' request.env['HTTP_X_AUTHENTICATION_SCHEME'] = 'Session'
end end
it 'has Session auth scheme' do it 'has Session auth scheme' do

@ -1,16 +1,23 @@
require 'spec_helper' require 'spec_helper'
require 'features/work_packages/details/inplace_editor/shared_contexts'
describe 'activity comments', js: true do describe 'activity comments', js: true do
let(:project) { FactoryGirl.create :project_with_types, is_public: true } let(:project) { FactoryGirl.create :project_with_types, is_public: true }
let!(:work_package) { FactoryGirl.create(:work_package, project: project) } let!(:work_package) { FactoryGirl.create(:work_package, project: project) }
let(:user) { FactoryGirl.create :admin } let(:user) { FactoryGirl.create :admin }
include_context 'maximized window'
before do before do
allow(User).to receive(:current).and_return(user) allow(User).to receive(:current).and_return(user)
visit project_work_packages_path(project) visit project_work_packages_path(project)
current_window.resize_to(1440, 800)
ensure_wp_table_loaded
row = page.find("#work-package-#{work_package.id}") row = page.find("#work-package-#{work_package.id}")
row.double_click row.double_click
expect(find('#add-comment-text')).to be_present expect(find('#add-comment-text')).to be_present
end end

@ -294,7 +294,7 @@ module LegacyAssertionsAndHelpers
context "should not send www authenticate when header accept auth is session #{http_method} #{url}" do context "should not send www authenticate when header accept auth is session #{http_method} #{url}" do
context 'without credentials' do context 'without credentials' do
before do before do
send(http_method, url, parameters, 'X-Authentication-Scheme' => 'Session') send(http_method, url, parameters, 'HTTP_X_AUTHENTICATION_SCHEME' => 'Session')
end end
it { should respond_with failure_code } it { should respond_with failure_code }
it { should_respond_with_content_type_based_on_url(url) } it { should_respond_with_content_type_based_on_url(url) }

@ -81,6 +81,26 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
end end
end end
describe 'self link' do
it_behaves_like 'has an untitled link' do
let(:link) { 'self' }
let(:href) {
api_v3_paths.work_package_schema(work_package.project.id, work_package.type.id)
}
end
context 'embedded in a form' do
let(:embedded) { true }
# In a form there is no guarantee that the current state contains a valid WP
let(:work_package) { FactoryGirl.build(:work_package, type: nil) }
it_behaves_like 'has no link' do
let(:link) { 'self' }
end
end
end
describe '_type' do describe '_type' do
it 'is indicated as Schema' do it 'is indicated as Schema' do
is_expected.to be_json_eql('Schema'.to_json).at_path('_type') is_expected.to be_json_eql('Schema'.to_json).at_path('_type')

@ -1245,10 +1245,28 @@ describe WorkPackage, type: :model do
expect(instance.subject).to eq('New subject') expect(instance.subject).to eq('New subject')
end end
it "should create a journal with the journal's 'notes' attribute set to the supplied" do describe 'creates a journal entry' do
instance.update_by!(user, notes: 'blubs') it 'with the supplied notes' do
instance.update_by!(user, notes: 'blubs')
expect(instance.journals.last.notes).to eq('blubs')
end
it 'by the given user' do
instance.update_by!(user, notes: 'blubs')
expect(instance.journals.last.user).to eq(user)
end
expect(instance.journals.last.notes).to eq('blubs') context 'without supplying journal notes' do
it 'creates an entry by the given user' do
instance.update_by!(user, subject: 'blubs')
expect(instance.journals.last.user).to eq(user)
end
it 'has empty journal notes' do
instance.update_by!(user, subject: 'blubs')
expect(instance.journals.last.notes).to eq('')
end
end
end end
it 'should attach an attachment' do it 'should attach an attachment' do

@ -101,7 +101,7 @@ describe API::V3, type: :request do
let(:headers) do let(:headers) do
auth = basic_auth(username, password.reverse) auth = basic_auth(username, password.reverse)
auth.merge('X-Authentication-Scheme' => 'Session') auth.merge('HTTP_X_AUTHENTICATION_SCHEME' => 'Session')
end end
before do before do

Loading…
Cancel
Save