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