diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a2cf5666b3..05e40c53af 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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
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
To ensure a smooth workflow for everyone, please take note of the following:
diff --git a/Gemfile.lock b/Gemfile.lock
index 51ba52333f..6314276df2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -45,7 +45,7 @@ GIT
revision: c42bac8f2bca7850a89b67e753368f5f52d13c25
branch: dev
specs:
- openproject-translations (4.3.0.pre.alpha)
+ openproject-translations (4.3.0)
crowdin-api (~> 0.2.4)
mixlib-shellout (~> 2.1.0)
rails (~> 3.2.14)
diff --git a/app/controllers/api/v2/authentication_controller.rb b/app/controllers/api/v2/authentication_controller.rb
index af643e1ca2..68b33b05df 100644
--- a/app/controllers/api/v2/authentication_controller.rb
+++ b/app/controllers/api/v2/authentication_controller.rb
@@ -35,7 +35,7 @@ module Api
unloadable
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
def index
diff --git a/app/models/work_package.rb b/app/models/work_package.rb
index 1071b80171..d63357109e 100644
--- a/app/models/work_package.rb
+++ b/app/models/work_package.rb
@@ -444,7 +444,7 @@ class WorkPackage < ActiveRecord::Base
end
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))
attributes.delete(:attachments)
diff --git a/app/views/settings/_authentication.html.erb b/app/views/settings/_authentication.html.erb
index 91be35f76a..c91883fc8b 100644
--- a/app/views/settings/_authentication.html.erb
+++ b/app/views/settings/_authentication.html.erb
@@ -50,7 +50,11 @@ See doc/COPYRIGHT.rdoc for more details.
[l("label_password_rule_#{r}"), r]
end %>
<%= setting_text_field :password_min_adhered_rules, :size => 6 %>
- <%= setting_text_field :password_days_valid, :size => 6 %>
+ <%= setting_text_field :password_days_valid, :size => 6 %>
+
+ <%= l(:text_hint_disable_with_0) %>
+
+
<%= setting_text_field :password_count_former_banned, :size => 6 %>
<%= setting_check_box :lost_password, :label => :label_password_lost %>
<% else %>
@@ -71,8 +75,12 @@ See doc/COPYRIGHT.rdoc for more details.
<% unless OpenProject::Configuration.disable_password_login? %>
<%= I18n.t(:brute_force_prevention, :scope => [:settings]) %>
- <%= setting_text_field :brute_force_block_after_failed_logins %>
- <%= setting_text_field :brute_force_block_minutes %>
+ <%= setting_text_field :brute_force_block_after_failed_logins %>
+
+ <%= l(:text_hint_disable_with_0) %>
+
+
+ <%= setting_text_field :brute_force_block_minutes, unit: l(:label_minute_plural) %>
<% end %>
diff --git a/app/views/settings/_display.html.erb b/app/views/settings/_display.html.erb
index ac08df2b5b..bc632d4d19 100644
--- a/app/views/settings/_display.html.erb
+++ b/app/views/settings/_display.html.erb
@@ -50,6 +50,13 @@ See doc/COPYRIGHT.rdoc for more details.
<%= setting_check_box :gravatar_enabled %>
<%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %>
+
+ <%= setting_text_field :journal_aggregation_time_minutes, unit: l(:label_minute_plural) %>
+
+ <%= l(:text_journal_aggregation_time_explanation) %>
+ <%= l(:text_hint_disable_with_0) %>
+
+
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3807f2d26f..deeb6c2e9b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -578,8 +578,8 @@ en:
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_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_default_status: "Unable to delete default work package status. Please select another default work package status before deleting the current one."
+ 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 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_workflow_copy_source: "Please select a source type or role"
error_workflow_copy_target: "Please select target type(s) and role(s)"
@@ -816,6 +816,7 @@ en:
label_message_plural: "Messages"
label_message_posted: "Message added"
label_min_max_length: "Min - Max length"
+ label_minute_plural: "minutes"
label_missing_api_access_key: "Missing an API access key"
label_missing_feeds_access_key: "Missing a RSS access key"
label_modification: "%{count} change"
@@ -1234,6 +1235,7 @@ en:
project_module_news: "News"
project_module_repository: "Repository"
project_module_time_tracking: "Time tracking"
+ project_module_timelines: "Timelines"
project_module_wiki: "Wiki"
# possible query parameters (e.g. issue queries),
@@ -1253,8 +1255,8 @@ en:
setting_autologin: "Autologin"
setting_available_languages: "Available languages"
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_minutes: "Time the user is blocked for (minutes)"
+ 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"
setting_cache_formatted_text: "Cache formatted text"
setting_column_options: "Customize the appearance of the work package lists"
setting_commit_fix_keywords: "Fixing keywords"
@@ -1288,6 +1290,7 @@ en:
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_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_login_required: "Authentication required"
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_password_active_rules: "Active character classes"
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_adhered_rules: "Minimum number of required classes"
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_file_repository_writable: "Attachments directory writable"
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_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?"
@@ -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_ref_in_commit_messages: "Referencing and fixing work packages in commit messages"
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_no_detail: "%{label} updated"
text_journal_changed_with_diff: "%{label} changed (%{link})"
diff --git a/config/settings.yml b/config/settings.yml
index 416d3da626..3caf87a946 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -261,3 +261,6 @@ users_deletable_by_admins:
default: 0
users_deletable_by_self:
default: 0
+journal_aggregation_time_minutes:
+ default: 5
+ format: int
diff --git a/frontend/app/helpers/path-helper.js b/frontend/app/helpers/path-helper.js
index a719b71bc0..e7ff080fa1 100644
--- a/frontend/app/helpers/path-helper.js
+++ b/frontend/app/helpers/path-helper.js
@@ -217,26 +217,6 @@ module.exports = function() {
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
apiQueryStarPath: function(queryId) {
return PathHelper.apiV3QueryPath(queryId) + '/star';
@@ -250,6 +230,9 @@ module.exports = function() {
apiV3WorkPackagePath: function(workPackageId) {
return PathHelper.apiV3 + '/work_packages/' + workPackageId;
},
+ apiPrioritiesPath: function() {
+ return PathHelper.apiV3 + '/priorities';
+ },
apiV3ProjectsPath: function(projectIdentifier) {
return PathHelper.apiV3 + PathHelper.projectsPath() + '/' + projectIdentifier;
},
@@ -259,6 +242,15 @@ module.exports = function() {
apiV3TypePath: function(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
staticUserPath: function(userId) {
return PathHelper.userPath(userId);
diff --git a/frontend/app/services/priority-service.js b/frontend/app/services/priority-service.js
index 41831c8e3f..5be4235364 100644
--- a/frontend/app/services/priority-service.js
+++ b/frontend/app/services/priority-service.js
@@ -38,7 +38,7 @@ module.exports = function($http, PathHelper) {
doQuery: function(url, params) {
return $http.get(url, { params: params })
.then(function(response){
- return response.data.planning_element_priorities;
+ return response.data._embedded.elements;
});
}
};
diff --git a/frontend/app/services/status-service.js b/frontend/app/services/status-service.js
index ee68429923..4d7506f0e9 100644
--- a/frontend/app/services/status-service.js
+++ b/frontend/app/services/status-service.js
@@ -29,22 +29,14 @@
module.exports = function($http, PathHelper) {
var StatusService = {
- getStatuses: function(projectIdentifier) {
- var url;
-
- if(projectIdentifier) {
- url = PathHelper.apiProjectStatusesPath(projectIdentifier);
- } else {
- url = PathHelper.apiStatusesPath();
- }
-
- return StatusService.doQuery(url);
+ getStatuses: function() {
+ return StatusService.doQuery(PathHelper.apiStatusesPath());
},
doQuery: function(url, params) {
return $http.get(url, { params: params })
.then(function(response){
- return response.data.statuses;
+ return response.data._embedded.elements;
});
}
};
diff --git a/frontend/app/services/type-service.js b/frontend/app/services/type-service.js
index e8a3ef7ce9..736a5d3e13 100644
--- a/frontend/app/services/type-service.js
+++ b/frontend/app/services/type-service.js
@@ -45,7 +45,7 @@ module.exports = function($http, PathHelper) {
doQuery: function(url, params) {
return $http.get(url, { params: params })
.then(function(response){
- return response.data.planning_element_types;
+ return response.data._embedded.elements;
});
}
};
diff --git a/frontend/package.json b/frontend/package.json
index 3287164e2e..cb76f8af3c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,7 +13,7 @@
"gulp-filter": "^2.0.2",
"gulp-jshint": "^1.8.5",
"gulp-livingstyleguide": "0.1.5",
- "gulp-protractor": "0.0.11",
+ "gulp-protractor": "1.0.0",
"gulp-replace": "^0.5.3",
"gulp-ruby-sass": "^0.7.1",
"gulp-util": "^3.0.4",
@@ -34,7 +34,7 @@
"mocha": "~1.18.2",
"mocha-jenkins-reporter": "^0.1.2",
"phantomjs": "~1.9.2",
- "protractor": "^2.0.0",
+ "protractor": "^2.1.0",
"sinon": "~1.9.1",
"sinon-chai": "~2.5.0",
"sorted-object": "^1.0.0",
diff --git a/lib/api/v3/work_packages/schema/work_package_schema_representer.rb b/lib/api/v3/work_packages/schema/work_package_schema_representer.rb
index 311819cd94..8368a4a1df 100644
--- a/lib/api/v3/work_packages/schema/work_package_schema_representer.rb
+++ b/lib/api/v3/work_packages/schema/work_package_schema_representer.rb
@@ -52,9 +52,8 @@ module API
end
link :self do
- path = api_v3_paths.work_package_schema(represented.project.id, represented.type.id)
-
unless form_embedded
+ path = api_v3_paths.work_package_schema(represented.project.id, represented.type.id)
{ href: path }
end
end
diff --git a/lib/open_project/authentication.rb b/lib/open_project/authentication.rb
index 9b6e4e9ff8..013835b614 100644
--- a/lib/open_project/authentication.rb
+++ b/lib/open_project/authentication.rb
@@ -83,7 +83,7 @@ module OpenProject
module_function
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
req_scheme
diff --git a/spec/controllers/api/v2/authentication_spec.rb b/spec/controllers/api/v2/authentication_spec.rb
index 25573ac1db..cbec06ff8a 100644
--- a/spec/controllers/api/v2/authentication_spec.rb
+++ b/spec/controllers/api/v2/authentication_spec.rb
@@ -39,14 +39,23 @@ describe Api::V2::AuthenticationController, type: :controller do
it_should_behave_like 'a controller action with require_login'
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
- 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
describe 'authorization data' do
@@ -120,7 +129,7 @@ describe Api::V2::AuthenticationController, type: :controller do
context 'with Session auth scheme requested' do
before do
- request.env['X-Authentication-Scheme'] = 'Session'
+ request.env['HTTP_X_AUTHENTICATION_SCHEME'] = 'Session'
end
it 'has Session auth scheme' do
diff --git a/spec/features/work_packages/details/activity_comments_spec.rb b/spec/features/work_packages/details/activity_comments_spec.rb
index 04e2f5d1fe..cb35a2bf93 100644
--- a/spec/features/work_packages/details/activity_comments_spec.rb
+++ b/spec/features/work_packages/details/activity_comments_spec.rb
@@ -1,16 +1,23 @@
require 'spec_helper'
+require 'features/work_packages/details/inplace_editor/shared_contexts'
+
describe 'activity comments', js: true do
let(:project) { FactoryGirl.create :project_with_types, is_public: true }
let!(:work_package) { FactoryGirl.create(:work_package, project: project) }
let(:user) { FactoryGirl.create :admin }
+ include_context 'maximized window'
+
before do
allow(User).to receive(:current).and_return(user)
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.double_click
+
expect(find('#add-comment-text')).to be_present
end
diff --git a/spec/legacy/support/legacy_assertions.rb b/spec/legacy/support/legacy_assertions.rb
index cc79e3edcb..f91c10e37a 100644
--- a/spec/legacy/support/legacy_assertions.rb
+++ b/spec/legacy/support/legacy_assertions.rb
@@ -294,7 +294,7 @@ module LegacyAssertionsAndHelpers
context "should not send www authenticate when header accept auth is session #{http_method} #{url}" do
context 'without credentials' do
before do
- send(http_method, url, parameters, 'X-Authentication-Scheme' => 'Session')
+ send(http_method, url, parameters, 'HTTP_X_AUTHENTICATION_SCHEME' => 'Session')
end
it { should respond_with failure_code }
it { should_respond_with_content_type_based_on_url(url) }
diff --git a/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
index d158e01e39..e5bf508f61 100644
--- a/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
+++ b/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
@@ -81,6 +81,26 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
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
it 'is indicated as Schema' do
is_expected.to be_json_eql('Schema'.to_json).at_path('_type')
diff --git a/spec/models/work_package_spec.rb b/spec/models/work_package_spec.rb
index c9879b17ac..8da37f6269 100644
--- a/spec/models/work_package_spec.rb
+++ b/spec/models/work_package_spec.rb
@@ -1245,10 +1245,28 @@ describe WorkPackage, type: :model do
expect(instance.subject).to eq('New subject')
end
- it "should create a journal with the journal's 'notes' attribute set to the supplied" do
- instance.update_by!(user, notes: 'blubs')
+ describe 'creates a journal entry' do
+ 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
it 'should attach an attachment' do
diff --git a/spec/requests/api/v3/authentication_spec.rb b/spec/requests/api/v3/authentication_spec.rb
index 8ec345e61d..ba16883d8e 100644
--- a/spec/requests/api/v3/authentication_spec.rb
+++ b/spec/requests/api/v3/authentication_spec.rb
@@ -101,7 +101,7 @@ describe API::V3, type: :request do
let(:headers) do
auth = basic_auth(username, password.reverse)
- auth.merge('X-Authentication-Scheme' => 'Session')
+ auth.merge('HTTP_X_AUTHENTICATION_SCHEME' => 'Session')
end
before do