Merge branch 'dev' into fix/32598-Style-issues-on-BIM-module-page

pull/8169/head
Oliver Günther 5 years ago committed by GitHub
commit ac9abbafa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/assets/stylesheets/layout/work_packages/_details_view.sass
  2. 3
      app/assets/stylesheets/layout/work_packages/_mobile.sass
  3. 3
      app/assets/stylesheets/layout/work_packages/_table.sass
  4. 7
      app/models/journal/aggregated_journal.rb
  5. 4
      app/uploaders/fog_file_uploader.rb
  6. 2
      config/environments/development.rb
  7. 2
      config/environments/production.rb
  8. 15
      config/initializers/suppress_routing_error_logs.rb
  9. 510
      docs/api/apiv2_1-doc.md
  10. 2
      frontend/src/app/components/wp-card-view/styles/wp-card-view-horizontal.sass
  11. 1
      frontend/src/app/modules/fields/edit/field-types/te-work-package-edit-field.component.ts
  12. 2
      frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.html
  13. 8
      frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.ts
  14. 1
      frontend/src/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass
  15. 1
      modules/bim/app/assets/stylesheets/bim/ifc_viewer/generic.sass
  16. 4
      modules/bim/config/locales/crowdin/js-tr.yml
  17. 29
      modules/bim/spec/requests/api/bcf/v2_1/shared_responses.rb
  18. 12
      modules/costs/app/controllers/cost_objects_controller.rb
  19. 11
      modules/costs/app/views/cost_objects/items/_labor_budget_item.html.erb
  20. 11
      modules/costs/app/views/cost_objects/items/_material_budget_item.html.erb
  21. 3
      modules/costs/app/views/costlog/edit.html.erb
  22. 7
      modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts
  23. 34
      modules/costs/frontend/module/augment/planned-costs-form.ts
  24. 80
      modules/costs/spec/features/budgets/update_budget_spec.rb
  25. 8
      spec/models/attachment_spec.rb

@ -32,8 +32,7 @@ body.router--work-packages-partitioned-split-view-details,
body.router--work-packages-partitioned-split-view-new
.work-packages-partitioned-page--content-right
overflow-x: hidden
overflow-y: auto
overflow: auto
position: relative
border-left: 2px solid #eee
border-top: 2px solid #eee

@ -72,9 +72,6 @@
@include breakpoint(1248px down)
.router--work-packages-base
// --------------- ALL WP VIEWS ---------------
.work-packages-tabletimeline--table-side
contain: none
.toolbar-container
padding-right: 0

@ -168,6 +168,9 @@
#content
height: 100%
.work-packages-partitioned-page--content-left
overflow: hidden
.icon-button, .sort-header, .action-icon
cursor: pointer

@ -346,7 +346,6 @@ class Journal::AggregatedJournal
:project,
:data,
:data=,
:noop?,
to: :journal
# Initializes a new AggregatedJournal. Allows to explicitly set a predecessor, if it is already
@ -422,6 +421,12 @@ class Journal::AggregatedJournal
predecessor.nil?
end
# If we where to delegate here, the wrapped journal would be compared to its predecessor which is
# not necessarily the this aggreated journal's predecessor.
def noop?
(!notes || notes&.empty?) && get_changes.empty?
end
private
attr_reader :journal

@ -104,7 +104,9 @@ class FogFileUploader < CarrierWave::Uploader::Base
def set_expires_at!(url_options, options:)
if options[:expires_in].present?
url_options[:expire_at] = ::Fog::Time.now + options[:expires_in]
# AWS allows at max < 604800 expires time
expires = [options[:expires_in], 604799].min
url_options[:expire_at] = ::Fog::Time.now + expires
end
if options[:expires_at].present?

@ -73,7 +73,7 @@ OpenProject::Application.configure do
config.assets.digest = false
# Suppress asset output
config.assets.quiet = true
config.assets.quiet = true unless config.log_level == :debug
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true

@ -93,6 +93,8 @@ OpenProject::Application.configure do
# Set to :debug to see everything in the log.
config.log_level = OpenProject::Configuration['log_level'].to_sym
config.assets.quiet = true unless config.log_level == :debug
# Prepend all log lines with the following tags.
# config.log_tags = [ :subdomain, :uuid ]

@ -0,0 +1,15 @@
module NoRoutingErrorLogging
def log_error(env, wrapper)
super unless ignore_error?(env, wrapper)
end
def ignore_error?(env, wrapper)
!Rails.logger.debug? && routing_error?(env, wrapper)
end
def routing_error?(_env, wrapper)
wrapper.exception.is_a? ActionController::RoutingError
end
end
ActionDispatch::DebugExceptions.prepend NoRoutingErrorLogging if Rails.env.production?

@ -0,0 +1,510 @@
# BCF REST API in OpenProject
![](https://raw.githubusercontent.com/BuildingSMART/BCF/master/Icons/BCFicon128.png)
The following describes the extensions and deviations of the BCF API v2.1 implementation in OpenProject.
While the intend of the implementation is to follow the specification, the API builds on the existing OpenProject data
schema and by that requires to map between the concepts required in the much broader domain of project management and BCF.
In other parts, the BCF API specification has not been completely implemented. It will be amended where requirements dictate.
OpenProject offers a second API (v3) which might be able to fill the gaps the BCF API implementation still has.
This document should be read as an extension to the [standard specification](https://github.com/buildingSMART/BCF-API/blob/release_2_1/README.md).
The user should read the standard specification first, and then take a look at this document to be informed about OpenProject specificities.
The document follows the structure of the standard specification to ease comparing the two documents.
**Table of Contents**
<!-- toc -->
- [1. Introduction](#1-introduction)
* [1.1 Paging, Sorting and Filtering](#11-paging-sorting-and-filtering)
* [1.2 Caching](#12-caching)
* [1.3 Updating Resources via HTTP PUT](#13-updating-resources-via-http-put)
* [1.4 Cross Origin Resource Sharing (CORS)](#14-cross-origin-resource-sharing-cors)
* [1.5 Http Status Codes](#15-http-status-codes)
* [1.6 Error Response Body Format](#16-error-response-body-format)
* [1.7 DateTime Format](#17-datetime-format)
* [1.8 Authorization](#18-authorization)
+ [1.8.1 Per-Entity Authorization](#181-per-entity-authorization)
+ [1.8.2 Determining Authorized Entity Actions](#182-determining-authorized-entity-actions)
* [1.9 Additional Response and Request Object Properties](#19-additional-response-and-request-object-properties)
* [1.10 Binary File Uploads](#110-binary-file-uploads)
- [2. Topologies](#2-topologies)
* [2.1 Topology 1 - BCF-Server only](#21-topology-1---bcf-server-only)
* [2.2 Topology 2 - Colocated BCF-Server and Model Server](#22-topology-2---colocated-bcf-server-and-model-server)
- [3. Public Services](#3-public-services)
* [3.1 Versions Service](#31-versions-service)
* [3.2 Authentication Services](#32-authentication-services)
+ [3.2.1 Obtaining Authentication Information](#321-obtaining-authentication-information)
+ [3.2.2 OAuth2 Example](#322-oauth2-example)
+ [3.2.3 OAuth2 Protocol Flow - Dynamic Client Registration](#323-oauth2-protocol-flow---dynamic-client-registration)
* [3.3 User Services](#33-user-services)
+ [3.3.1 Get current user](#331-get-current-user)
- [4. BCF Services](#4-bcf-services)
* [4.1 Project Services](#41-project-services)
+ [4.1.1 GET Projects Service](#411-get-projects-service)
+ [4.1.2 GET Project Service](#412-get-project-service)
+ [4.1.3 PUT Project Service](#413-put-project-service)
+ [4.1.4 GET Project Extension Service](#414-get-project-extension-service)
+ [4.1.5 Expressing User Authorization Through Project Extensions](#415-expressing-user-authorization-through-project-extensions)
- [4.1.5.1 Project](#4151-project)
- [4.1.5.2 Topic](#4152-topic)
- [4.1.5.3 Comment](#4153-comment)
* [4.2 Topic Services](#42-topic-services)
+ [4.2.1 GET Topics Service](#421-get-topics-service)
+ [4.2.2 POST Topic Service](#422-post-topic-service)
+ [4.2.3 GET Topic Service](#423-get-topic-service)
+ [4.2.4 PUT Topic Service](#424-put-topic-service)
+ [4.2.5 DELETE Topic Service](#425-delete-topic-service)
+ [4.2.6 GET Topic BIM Snippet Service](#426-get-topic-bim-snippet-service)
+ [4.2.7 PUT Topic BIM Snippet Service](#427-put-topic-bim-snippet-service)
+ [4.2.8 Determining Allowed Topic Modifications](#428-determining-allowed-topic-modifications)
* [4.3 File Services](#43-file-services)
+ [4.3.1 GET Files (Header) Service](#431-get-files-header-service)
+ [4.3.2 PUT Files (Header) Service](#432-put-files-header-service)
* [4.4 Comment Services](#44-comment-services)
+ [4.4.1 GET Comments Service](#441-get-comments-service)
+ [4.4.2 POST Comment Service](#442-post-comment-service)
+ [4.4.3 GET Comment Service](#443-get-comment-service)
+ [4.4.4 PUT Comment Service](#444-put-comment-service)
+ [4.4.5 DELETE Comment Service](#445-delete-comment-service)
+ [4.4.6 Determining allowed Comment modifications](#446-determining-allowed-comment-modifications)
* [4.5 Viewpoint Services](#45-viewpoint-services)
+ [4.5.1 GET Viewpoints Service](#451-get-viewpoints-service)
+ [4.5.2 POST Viewpoint Service](#452-post-viewpoint-service)
- [4.5.2.1 Point](#4521-point)
- [4.5.2.2 Direction](#4522-direction)
- [4.5.2.3 Orthogonal camera](#4523-orthogonal-camera)
- [4.5.2.4 Perspective camera](#4524-perspective-camera)
- [4.5.2.5 Line](#4525-line)
- [4.5.2.6 Clipping plane](#4526-clipping-plane)
- [4.5.2.7 Bitmap](#4527-bitmap)
- [4.5.2.8 Snapshot](#4528-snapshot)
- [4.5.2.9 Components](#4529-components)
- [4.5.2.10 Component](#45210-component)
* [Optimization rules](#optimization-rules)
- [4.5.2.11 Coloring](#45211-coloring)
* [Optimization rules](#optimization-rules-1)
- [4.5.2.12 Visibility](#45212-visibility)
* [Optimization rules](#optimization-rules-2)
- [4.5.2.13 View setup hints](#45213-view-setup-hints)
+ [4.5.3 GET Viewpoint Service](#453-get-viewpoint-service)
+ [4.5.4 GET Viewpoint Snapshot Service](#454-get-viewpoint-snapshot-service)
+ [4.5.5 GET Viewpoint Bitmap Service](#455-get-viewpoint-bitmap-service)
+ [4.5.6 GET selected Components Service](#456-get-selected-components-service)
+ [4.5.7 GET colored Components Service](#457-get-colored-components-service)
+ [4.5.8 GET visibility of Components Service](#458-get-visibility-of-components-service)
* [4.6 Related Topics Services](#46-related-topics-services)
+ [4.6.1 GET Related Topics Service](#461-get-related-topics-service)
+ [4.6.2 PUT Related Topics Service](#462-put-related-topics-service)
* [4.7 Document Reference Services](#47-document-reference-services)
+ [4.7.1 GET Document References Service](#471-get-document-references-service)
+ [4.7.2 POST Document Reference Service](#472-post-document-reference-service)
+ [4.7.3 PUT Document Reference Service](#473-put-document-reference-service)
* [4.8 Document Services](#48-document-services)
+ [4.8.1 GET Documents Service](#481-get-documents-service)
+ [4.8.2 POST Document Service](#482-post-document-service)
+ [4.8.3 GET Document Service](#483-get-document-service)
* [4.9 Topics Events Services](#49-topics-events-services)
+ [4.9.1 GET Topics Events Service](#491-get-topics-events-service)
+ [4.9.2 GET Topic Events Service](#492-get-topic-events-service)
* [4.10 Comments Events Services](#410-comments-events-services)
+ [4.10.1 GET Comments Events Service](#4101-get-comments-events-service)
+ [4.10.2 GET Comment Events Service](#4102-get-comment-events-service)
<!-- tocstop -->
# 1. Introduction
All end points are nested within the `/api` path. So for a server listening on `https://foo.com/` the API root will be
`https://foo.com/api/bcf/2.1`. For a server listening on `https://foo.com/bar` the API root will be
`https://foo.com/bar/api/bcf/2.1`.
## 1.1 Paging, Sorting and Filtering
_Not implemented_
## 1.2 Caching
_Implemented_
## 1.3 Updating Resources via HTTP PUT
_Implemented_
## 1.4 Cross Origin Resource Sharing (CORS)
_Not implemented_
## 1.5 Http Status Codes
_Implemented_
## 1.6 Error Response Body Format
_Implemented_
## 1.7 DateTime Format
_Implemented_
## 1.8 Authorization
_Implemented_
Authorization is granted based on the _view_linked_issues_ and the _manage_bcf_ permission. As BCFs share part of their
data structure with WorkPackages, which enables them to be worked on by the project team just like any other work package,
a user also needs to have the _view_work_packages_ permission to have _view_linked_issues_. For _manage_bcf_ the permissions
_view_work_packages_, _add_work_packages_, _edit_work_packages_ and _delete_work_packages_ are dependently required.
### 1.8.1 Per-Entity Authorization
_Implemented_
The `authorization` field is always returned, regardless of an `includeAuthorization` query parameter.
### 1.8.2 Determining Authorized Entity Actions
_Implemented_
## 1.9 Additional Response and Request Object Properties
The implementation relies on a client to particularly adhere to this.
## 1.10 Binary File Uploads
_Implemented_
# 2. Topologies
_Out of scope_
# 3. Public Services
## 3.1 Versions Service
_Not implemented_
## 3.2 Authentication Services
### 3.2.1 Obtaining Authentication Information
_Implemented_
The following OAuth2 flows are supported:
* `authorization_code_grant` - [4.1 - Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1)
* `client_credentials` - [4.4 - Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4)
The `clients_credentials` grant explicitly ruled out by the standard specification as not being user specific can be supported by OpenProject as the grant is mapped to a user account
when configuring the OAuth access.
Before a client is able to perform the flows, they need to be [configured in OpenProject](https://docs.openproject.org/system-admin-guide/authentication/oauth-applications/). `bcf_v2_1` needs
to be checked for the scope. That value also needs to be provided for the scope property in OAuth requests.
The OAuth2 flows alternatively proposed by the specification
* `implicit_grant` - [4.2 - Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2)
* `resource_owner_password_credentials_grant` - [4.3 - Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3)
are not implemented.
### 3.2.2 OAuth2 Example
_Out of scope_
### 3.2.3 OAuth2 Protocol Flow - Dynamic Client Registration
_Not implemented_
## 3.3 User Services
### 3.3.1 Get current user
_Implemented_
# 4. BCF Services
## 4.1 Project Services
The `project_id` is an integer value. However, the API also understands requests where the project identifier, e.g. `bcf_project`
is used instead of the integer within a url. So the following urls might point to the same project resource: `/api/bcf/2.1/projects/3` and `/api/bcf/2.1/projects/bcf_project`.
### 4.1.1 GET Projects Service
_Partly implemented_
The end point is implemented but lacks the `authorization` property. However, the [Project Extension Service](#414-get-project-extension-service) is completely implemented and provides the same information.
### 4.1.2 GET Project Service
_Partly implemented_
The end point is implemented but lacks the `authorization` property. However, the [Project Extension Service](#414-get-project-extension-service) is completely implemented and provides the same information.
### 4.1.3 PUT Project Service
_Implemented_
### 4.1.4 GET Project Extension Service
_Implemented and extended_
However, as some end points are not implemented, the actions indicating the ability to call those end points will also not be returned, e.g. `updateDocumentReferences`
### 4.1.5 Expressing User Authorization Through Project Extensions
_Out of scope_
#### 4.1.5.1 Project
_Implemented and extended_
* *viewTopic* - The ability to see topics (see [4.2.3 GET Topic Service](#423-get-topic-service))
#### 4.1.5.2 Topic
_Implemented_
#### 4.1.5.3 Comment
_Implemented_
## 4.2 Topic Services
BCF topics are tightly coupled to work packages in OpenProject. This coupling is denoted in the `reference_links` property
of a topic which will always have a link to the work package resource in the API v3. e.g.:
<-- other properties -->
"reference_links": [
"/api/v3/work_packages/92"
],
<-- other properties -->
### 4.2.1 GET Topics Service
_Partly implemented_
The following properties are not supported:
* `labels` (the property exists but cannot be written and is always empty)
* `stage` (the property exists but cannot be written and is always null)
* `bim_snippet.snippet_type`
* `bim_snippet.is_external`
* `bim_snippet.reference`
* `bim_snippet.reference_schema`
OData sort, filtering and pagination is not supported.
### 4.2.2 POST Topic Service
_Partly implemented_
See [4.2.3 GET Topic Service](#423-get-topic-service) for details.
Either a new work package is created or, if a work package is referenced in the `reference_links` section, a the referenced
work package is associated to the newly created topic. A work package can only be associated to one topic and vice versa.
### 4.2.3 GET Topic Service
_Partly implemented_
See [4.2.3 GET Topic Service](#423-get-topic-service) for details.
### 4.2.4 PUT Topic Service
_Partly implemented_
The reference to the work package cannot be altered.
See [4.2.3 GET Topic Service](#423-get-topic-service) for details.
### 4.2.5 DELETE Topic Service
_Implemented_
The associated work package will also be deleted.
### 4.2.6 GET Topic BIM Snippet Service
_Not implemented_
### 4.2.7 PUT Topic BIM Snippet Service
_Not implemented_
## 4.3 File Services
### 4.3.1 GET Files (Header) Service
_Not implemented_
### 4.3.2 PUT Files (Header) Service
_Not implemented_
## 4.4 Comment Services
### 4.4.1 GET Comments Service
_Not implemented_
### 4.4.2 POST Comment Service
_Not implemented_
### 4.4.3 GET Comment Service
_Not implemented_
### 4.4.4 PUT Comment Service
_Not implemented_
### 4.4.5 DELETE Comment Service
_Not implemented_
### 4.4.6 Determining allowed Comment modifications
_Not implemented_
## 4.5 Viewpoint Services
### 4.5.1 GET Viewpoints Service
_Implemented_
### 4.5.2 POST Viewpoint Service
_Implemented_
#### 4.5.2.1 Point
_Implemented_
#### 4.5.2.2 Direction
_Implemented_
#### 4.5.2.3 Orthogonal camera
_Implemented_
#### 4.5.2.4 Perspective camera
_Implemented_
#### 4.5.2.5 Line
_Implemented_
#### 4.5.2.6 Clipping plane
_Implemented_
#### 4.5.2.7 Bitmap
_Implemented_
#### 4.5.2.8 Snapshot
_Implemented_
#### 4.5.2.9 Components
_Implemented_
#### 4.5.2.10 Component
_Implemented_
#### 4.5.2.11 Coloring
_Implemented_
#### 4.5.2.12 Visibility
_Implemented_
#### 4.5.2.13 View setup hints
_Implemented_
### 4.5.3 GET Viewpoint Service
_Implemented_
### 4.5.4 GET Viewpoint Snapshot Service
_Implemented_
### 4.5.6 GET selected Components Service
_Implemented_
### 4.5.7 GET colored Components Service
_Implemented_
### 4.5.8 GET visibility of Components Service
_Implemented_
## 4.6 Related Topics Services
### 4.6.1 GET Related Topics Service
_Not implemented_
### 4.6.2 PUT Related Topics Service
_Not implemented_
## 4.7 Document Reference Services
### 4.7.1 GET Document References Service
_Not implemented_
### 4.7.2 POST Document Reference Service
_Not implemented_
### 4.7.3 PUT Document Reference Service
_Not implemented_
## 4.8 Document Services
### 4.8.1 GET Documents Service
_Not implemented_
### 4.8.2 POST Document Service
_Not implemented_
### 4.8.3 GET Document Service
_Not implemented_
## 4.9 Topics Events Services
### 4.9.1 GET Topics Events Service
_Not implemented_
### 4.9.2 GET Topic Events Service
_Not implemented_
## 4.10 Comments Events Services
_Not implemented_
### 4.10.1 GET Comments Events Service
_Not implemented_
### 4.10.2 GET Comment Events Service
_Not implemented_

@ -1,6 +1,6 @@
.wp-cards-container.-horizontal
display: grid
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr))
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))
grid-column-gap: 10px
grid-row-gap: 10px
margin-right: 5px

@ -54,6 +54,7 @@ export class TimeEntryWorkPackageEditFieldComponent extends WorkPackageEditField
// existing values.
if (this.referenceOutputs) {
this.referenceOutputs['modeSwitch'] = (mode:TimeEntryWorkPackageAutocompleterMode) => {
this.valuesLoaded = false;
let lastValue = this.requests.lastRequestedValue!;
// Hack to provide a new value to "reset" the input.

@ -4,7 +4,7 @@
model: selectedOption ? selectedOption : '',
required: required,
disabled: inFlight,
typeahead: requests.input$,
typeahead: typeahead,
id: handler.htmlId,
finishedLoading: requests.loading$,
hideSelected: true,

@ -57,6 +57,14 @@ export class WorkPackageEditFieldComponent extends SelectEditFieldComponent {
});
}
public get typeahead() {
if (this.valuesLoaded) {
return false;
} else {
return this.requests.input$;
}
}
protected allowedValuesFilter(query?:string):{} {
let filterParams = super.allowedValuesFilter(query);

@ -23,7 +23,6 @@
.work-packages-partitioned-page--content-left,
.work-packages-partitioned-page--content-right
position: relative
min-width: 580px
.work-packages-partitioned-page--content-left
flex: 1

@ -57,6 +57,7 @@
height: 100%
padding-bottom: 10px
overflow: hidden
min-width: 400px
.ifc-model-viewer--model-canvas
width: 100%

@ -10,8 +10,8 @@ tr:
show_viewpoint: 'Bakış açısını göster'
delete_viewpoint: 'Bakış açısını sil'
ifc_models:
empty_warning: "This project does not yet have any IFC models."
use_this_link_to_manage: "Use this link to upload and manage your IFC models"
empty_warning: "Bu projenin henüz IFC modeli yok."
use_this_link_to_manage: "IFC modellerinizi yüklemek ve yönetmek için bu bağlantıyı kullanın"
models:
default: 'Varsayılan IFC modelleri'
manage: 'Modelleri yönetin'

@ -27,10 +27,37 @@
#++
shared_examples_for 'bcf api successful response' do
def expect_identical_without_time(subject, expected_body)
# Remove modified date
body = Array.wrap(JSON.parse(subject.body))
Array.wrap(expected_body).each_with_index do |expected_item, index|
subject_body = body[index]
expected_item.stringify_keys!
subject_modified_date = subject_body.delete('modified_date')&.to_time
expected_modified_date = expected_item.delete('modified_date')&.to_time
if expected_modified_date
expect(subject_modified_date).to be_within(10.seconds).of(expected_modified_date)
else
expect(subject_modified_date).to eql(expected_modified_date)
end
expect(subject_body.to_json).to be_json_eql(expected_item.to_json)
end
end
it 'responds correctly with the expected body', :aggregate_failures do
expect(subject.status)
.to eql(defined?(expected_status) ? expected_status : 200)
expect(subject.body).to be_json_eql(expected_body.to_json)
if expected_body.nil?
expect("").to be_json_eql(expected_body.to_json)
else
expect_identical_without_time(subject, expected_body)
end
expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8' unless defined?(no_content)
end
end

@ -199,9 +199,13 @@ class CostObjectsController < ApplicationController
@unit = cost_type.try(:unit_plural) || ''
end
response = { "#{@element_id}_unit_name" => h(@unit) }
response = {
"#{@element_id}_unit_name" => h(@unit),
"#{@element_id}_currency" => Setting.plugin_openproject_costs['costs_currency']
}
if current_user.allowed_to?(:view_cost_rates, @project)
response["#{@element_id}_costs"] = number_to_currency(@costs)
response["#{@element_id}_cost_value"] = @costs
end
respond_to do |format|
@ -222,9 +226,13 @@ class CostObjectsController < ApplicationController
@costs = 0.0
end
response = { "#{@element_id}_unit_name" => h(@unit) }
response = {
"#{@element_id}_unit_name" => h(@unit),
"#{@element_id}_currency" => Setting.plugin_openproject_costs['costs_currency']
}
if current_user.allowed_to?(:view_hourly_rates, @project)
response["#{@element_id}_costs"] = number_to_currency(@costs)
response["#{@element_id}_cost_value"] = @costs
end
respond_to do |format|

@ -80,9 +80,16 @@ See docs/COPYRIGHT.rdoc for more details.
<% if templated == false && !labor_budget_item.new_record? && labor_budget_item.overridden_budget? %>
<%= cost_form.hidden_field :budget, value: labor_budget_item.budget %>
<% end %>
<cost-unit-subform obj-id="<%= "#{id_prefix}_costs" %>" obj-name="<%= "#{name_prefix}[budget]" %>">
<% cost_value = labor_budget_item.budget || labor_budget_item.calculated_costs(@cost_object.fixed_date, @cost_object.project_id) %>
<%= cost_form.hidden_field :currency, index: id_or_index, value: Setting.plugin_openproject_costs['costs_currency'] %>
<cost-unit-subform obj-id="<%= id_prefix %>"
obj-name="<%= "#{name_prefix}[budget]" %>">
<a id="<%= "#{id_prefix}_costs" %>" class="costs--edit-planned-costs-btn icon-context icon-edit" title="<%= t(:help_click_to_edit) %>">
<%= number_to_currency(labor_budget_item.budget || labor_budget_item.calculated_costs(@cost_object.fixed_date, @cost_object.project_id)) if labor_budget_item.costs_visible_by?(User.current) %>
<% if labor_budget_item.costs_visible_by?(User.current) %>
<%= cost_form.hidden_field :cost_value, index: id_or_index, value: cost_value %>
<%= number_to_currency(cost_value) %>
<% end %>
</a>
</cost-unit-subform>
</td>

@ -78,14 +78,17 @@ See docs/COPYRIGHT.rdoc for more details.
</td>
<% if User.current.allowed_to? :view_cost_rates, @project %>
<td class="currency budget-table--fields">
<% obj_id = "#{id_prefix}_costs" %>
<%# Keep current budget as hidden field because otherwise they will be overridden %>
<% if templated == false && !material_budget_item.new_record? && material_budget_item.overridden_budget? %>
<%= cost_form.hidden_field :budget, value: material_budget_item.budget %>
<% end %>
<cost-unit-subform obj-id="<%= obj_id %>" obj-name="<%= "#{name_prefix}[budget]" %>">
<a id="<%= obj_id %>" class="costs--edit-planned-costs-btn icon-context icon-edit" role="button" title="<%= t(:help_click_to_edit) %>">
<%= number_to_currency(material_budget_item.budget || material_budget_item.calculated_costs(@cost_object.fixed_date)) %>
<% cost_value = material_budget_item.budget || material_budget_item.calculated_costs(@cost_object.fixed_date) %>
<%= cost_form.hidden_field :currency, index: id_or_index, value: Setting.plugin_openproject_costs['costs_currency'] %>
<%= cost_form.hidden_field :cost_value, index: id_or_index, value: cost_value %>
<cost-unit-subform obj-id="<%= id_prefix %>"
obj-name="<%= "#{name_prefix}[budget]" %>">
<a id="<%= id_prefix %>_costs" class="costs--edit-planned-costs-btn icon-context icon-edit" role="button" title="<%= t(:help_click_to_edit) %>">
<%= number_to_currency(cost_value) %>
</a>
</cost-unit-subform>
</td>

@ -113,7 +113,8 @@ See docs/COPYRIGHT.rdoc for more details.
<div class="form--field">
<label for="cost_entry_costs_edit" class="form--label"><%= CostEntry.human_attribute_name(:costs) %></label>
<span class="form--field-container">
<cost-unit-subform obj-id="cost_entry_costs" obj-name="cost_entry[overridden_costs]">
<cost-unit-subform obj-id="cost_entry"
obj-name="cost_entry[overridden_costs]" %>
<% if User.current.allowed_to? :view_cost_rates, @cost_entry.project %>
<a href="#" id="cost_entry_costs" class="costs--edit-planned-costs-btn icon-context icon-edit" title="<%= t(:help_click_to_edit) %>">
<%= number_to_currency(@cost_entry.real_costs) %>

@ -90,7 +90,12 @@ export class CostBudgetSubformAugmentService {
.subscribe(
(data:any) => {
_.each(data, (val:string, selector:string) => {
jQuery('#' + selector).html(val);
let element = document.getElementById(selector) as HTMLElement|HTMLInputElement|undefined;
if (element instanceof HTMLInputElement) {
element.value = val;
} else if (element) {
element.textContent = val;
}
});
},
(error:any) => this.halNotification.handleRawError(error)

@ -42,35 +42,36 @@ export class PlannedCostsFormAugment {
constructor(public $element:JQuery) {
this.objId = this.$element.attr('obj-id')!;
this.objName = this.$element.attr('obj-name')!;
this.obj = jQuery(`#${this.objId}`) as any;
this.obj = jQuery(`#${this.objId}_costs`) as any;
this.makeEditable('#' + this.objId, this.objName);
this.makeEditable();
}
private getCurrencyValue(str:string) {
var result = str.match(/^\s*(([0-9]+[.,])+[0-9]+) (.+)\s*/);
return result ? new Array(result[1], result[3]) : new Array(str, "");
}
public makeEditable(id:string, name:string) {
public makeEditable() {
this.edit_and_focus();
}
private edit_and_focus() {
this.edit();
jQuery('#' + this.objId + '_edit').trigger('focus');
jQuery('#' + this.objId + '_edit').trigger('select');
jQuery('#' + this.objId + '_costs_edit').trigger('focus');
jQuery('#' + this.objId + '_costs_edit').trigger('select');
}
private getCurrency() {
return jQuery('#' + this.objId + '_currency').val();
}
private getValue() {
return jQuery('#' + this.objId + '_cost_value').val();
}
private edit() {
this.obj.hide();
let obj_value = this.obj[0].innerHTML;
let id = this.obj[0].id;
let parsed = this.getCurrencyValue(obj_value);
let value = parsed[0];
let currency = parsed[1];
let currency = this.getCurrency();
let value = this.getValue();
let name = this.objName;
let template = `
@ -87,12 +88,11 @@ export class PlannedCostsFormAugment {
</section>
`;
jQuery(template).insertAfter(this.obj);
let that = this;
jQuery('#' + id + '_cancel').on('click', function () {
jQuery('#' + id + '_section').remove();
jQuery('#' + id + '_costs_cancel').on('click', function () {
jQuery('#' + id + '_costs_section').remove();
that.obj.show();
return false;
});

@ -29,7 +29,7 @@
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
describe 'updating a budget', type: :feature, js: true do
let(:project) { FactoryBot.create :project_with_types }
let(:project) { FactoryBot.create :project_with_types, enabled_module_names: %i[costs_module] }
let(:user) { FactoryBot.create :admin }
let(:budget) { FactoryBot.create :cost_object, author: user, project: project }
@ -169,6 +169,45 @@ describe 'updating a budget', type: :feature, js: true do
expect(material_budget_item_2.overridden_budget?).to be_truthy
expect(material_budget_item_2.costs).to eq(543.0)
end
context 'with a reversed currency format' do
before do
allow(Setting)
.to receive(:plugin_openproject_costs)
.and_return({costs_currency_format: '%u %n', costs_currency: 'USD'}.with_indifferent_access)
end
it 'can still update budgets (Regression test #32664)' do
budget_page.visit!
click_on 'Update'
# Update first element
budget_page.edit_planned_costs! material_budget_item.id, type: :material, costs: 123
expect(budget_page).to have_content('Successful update')
expect(page).to have_selector('tbody td.currency', text: 'USD 123.00')
click_on 'Update'
# Update second element
budget_page.edit_planned_costs! material_budget_item_2.id, type: :material, costs: 543
expect(budget_page).to have_content('Successful update')
expect(page).to have_selector('tbody td.currency', text: 'USD 123.00')
expect(page).to have_selector('tbody td.currency', text: 'USD 543.00')
# Expect overridden costs on both
material_budget_item.reload
material_budget_item_2.reload
# Expect budget == costs
expect(material_budget_item.budget).to eq(123.0)
expect(material_budget_item.overridden_budget?).to be_truthy
expect(material_budget_item.costs).to eq(123.0)
expect(material_budget_item_2.budget).to eq(543.0)
expect(material_budget_item_2.overridden_budget?).to be_truthy
expect(material_budget_item_2.costs).to eq(543.0)
end
end
end
context 'with two labor budget items' do
@ -207,6 +246,45 @@ describe 'updating a budget', type: :feature, js: true do
expect(labor_budget_item_2.overridden_budget?).to be_truthy
expect(labor_budget_item_2.costs).to eq(987.0)
end
context 'with a reversed currency format' do
before do
allow(Setting)
.to receive(:plugin_openproject_costs)
.and_return({costs_currency_format: '%u %n', costs_currency: 'USD'}.with_indifferent_access)
end
it 'can still update budgets (Regression test #32664)' do
budget_page.visit!
click_on 'Update'
# Update first element
budget_page.edit_planned_costs! labor_budget_item.id, type: :labor, costs: 456
expect(budget_page).to have_content('Successful update')
expect(page).to have_selector('tbody td.currency', text: 'USD 456.00')
click_on 'Update'
# Update second element
budget_page.edit_planned_costs! labor_budget_item_2.id, type: :labor, costs: 987
expect(budget_page).to have_content('Successful update')
expect(page).to have_selector('tbody td.currency', text: 'USD 456.00')
expect(page).to have_selector('tbody td.currency', text: 'USD 987.00')
# Expect overridden costs on both
labor_budget_item.reload
labor_budget_item_2.reload
# Expect budget == costs
expect(labor_budget_item.budget).to eq(456.0)
expect(labor_budget_item.overridden_budget?).to be_truthy
expect(labor_budget_item.costs).to eq(456.0)
expect(labor_budget_item_2.budget).to eq(987.0)
expect(labor_budget_item_2.overridden_budget?).to be_truthy
expect(labor_budget_item_2.costs).to eq(987.0)
end
end
end
it 'removes existing cost items' do

@ -276,6 +276,14 @@ describe Attachment, type: :model do
expect(query).to include "X-Amz-Expires=3600"
end
end
context 'with expiry time exeeding maximum' do
let(:url_options) { { expires_in: 1.year } }
it "uses the allowed max" do
expect(query).to include "X-Amz-Expires=604799"
end
end
end
describe "for an image file" do

Loading…
Cancel
Save