Merge branch 'release/11.3' into dev

pull/9377/head
ulferts 3 years ago
commit d7246f312d
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 4
      app/models/project.rb
  2. 2
      config/locales/crowdin/ja.yml
  3. 2
      config/locales/en.yml
  4. 11
      config/locales/js-en.yml
  5. 2
      docs/release-notes/11-3-0/README.md
  6. 6
      docs/system-admin-guide/github-integration/README.md
  7. 0
      docs/user-guide/integrations/Integrations-faq/Add_OpenProject_widget.png
  8. BIN
      docs/user-guide/integrations/Nextcloud/Add_OpenPorject_widget.png
  9. BIN
      docs/user-guide/integrations/Nextcloud/Add_OpenProject_widget.png
  10. BIN
      docs/user-guide/integrations/Nextcloud/Cron_job_settings.png
  11. BIN
      docs/user-guide/integrations/Nextcloud/Navigation_link_OpenProject.png
  12. BIN
      docs/user-guide/integrations/Nextcloud/Nextcloud_app_store.png
  13. BIN
      docs/user-guide/integrations/Nextcloud/Nextcloud_connected_account-2798067.png
  14. BIN
      docs/user-guide/integrations/Nextcloud/Nextcloud_connected_account.png
  15. BIN
      docs/user-guide/integrations/Nextcloud/Nextcloud_dashboard.png
  16. BIN
      docs/user-guide/integrations/Nextcloud/OAuth.png
  17. BIN
      docs/user-guide/integrations/Nextcloud/OpenProject_API_key.png
  18. BIN
      docs/user-guide/integrations/Nextcloud/OpenProject_API_key_copy.png
  19. BIN
      docs/user-guide/integrations/Nextcloud/OpenProject_OAuth.png
  20. 24
      docs/user-guide/integrations/Nextcloud/README.md
  21. BIN
      docs/user-guide/integrations/Nextcloud/Unified_search.png
  22. 2
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.base.ts
  23. 2
      frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.ts
  24. 2
      frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb-parent.html
  25. 2
      frontend/src/app/components/work-packages/wp-breadcrumb/wp-breadcrumb.html
  26. 2
      frontend/src/app/components/wp-activity/activity-link.component.ts
  27. 3
      frontend/src/app/modules/invite-user-modal/principal/principal-search.component.html
  28. 4
      frontend/src/app/modules/invite-user-modal/principal/principal-search.component.ts
  29. 6
      frontend/src/app/modules/invite-user-modal/principal/principal.component.html
  30. 13
      frontend/src/app/modules/invite-user-modal/principal/principal.component.ts
  31. 1
      frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.html
  32. 9
      lib/open_project/acts_as_url/adapter/op_active_record.rb
  33. 2
      modules/bim/db/migrate/20210521080035_update_xkt_to_version8.rb
  34. 2
      modules/bim/lib/open_project/bim/bcf_xml/issue_writer.rb
  35. 24
      modules/bim/spec/features/bcf/export_spec.rb
  36. 22
      spec/services/projects/set_attributes_service_integration_spec.rb

@ -118,6 +118,10 @@ class Project < ApplicationRecord
only_when_blank: true, # Only generate when identifier not set only_when_blank: true, # Only generate when identifier not set
limit: IDENTIFIER_MAX_LENGTH, limit: IDENTIFIER_MAX_LENGTH,
blacklist: RESERVED_IDENTIFIERS, blacklist: RESERVED_IDENTIFIERS,
custom_rule: ->(base_url) {
# remove leading numbers and hyphens as they would clash with the identifier's validations later on.
base_url.sub(/^[-\d]*|-*$/, '')
},
adapter: OpenProject::ActsAsUrl::Adapter::OpActiveRecord # use a custom adapter able to handle edge cases adapter: OpenProject::ActsAsUrl::Adapter::OpActiveRecord # use a custom adapter able to handle edge cases
validates :identifier, validates :identifier,

@ -2326,7 +2326,7 @@ ja:
brute_force_prevention: "自動的にユーザをロック" brute_force_prevention: "自動的にユーザをロック"
display: display:
first_date_of_week_and_year_set: > first_date_of_week_and_year_set: >
If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. オプション "%{day_of_week_setting_name}" または "%{first_week_setting_name}" が設定されている場合、フロントエンドでの不整合を避けるために、もう一方も設定する必要があります。
first_week_of_year_text: > first_week_of_year_text: >
その年の最初の週に含まれる1月の日付を選択します。 この値と週の開始の日によって、1年の合計週数が決定されます。 その年の最初の週に含まれる1月の日付を選択します。 この値と週の開始の日によって、1年の合計週数が決定されます。
projects: projects:

@ -1949,7 +1949,7 @@ en:
mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}." mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
mail_subject_account_activation_request: "%{value} account activation request" mail_subject_account_activation_request: "%{value} account activation request"
mail_subject_backup_ready: "Your backup is ready" mail_subject_backup_ready: "Your backup is ready"
mail_subject_backup_token_reset: "Your backup token has been reset" mail_subject_backup_token_reset: "Backup token reset"
mail_subject_lost_password: "Your %{value} password" mail_subject_lost_password: "Your %{value} password"
mail_subject_register: "Your %{value} account activation" mail_subject_register: "Your %{value} account activation"
mail_subject_reminder: "%{count} work package(s) due in the next %{days} days" mail_subject_reminder: "%{count} work package(s) due in the next %{days} days"

@ -223,7 +223,6 @@ en:
label_company: "Company" label_company: "Company"
label_first_name: "First name" label_first_name: "First name"
label_last_name: "Last name" label_last_name: "Last name"
label_email: "Email"
label_domain: "Domain" label_domain: "Domain"
label_subscriber: "Subscriber" label_subscriber: "Subscriber"
label_maximum_users: "Maximum active users" label_maximum_users: "Maximum active users"
@ -366,6 +365,7 @@ en:
label_board_locked: "Locked" label_board_locked: "Locked"
label_board_plural: "Boards" label_board_plural: "Boards"
label_board_sticky: "Sticky" label_board_sticky: "Sticky"
label_change: "Change"
label_create: "Create" label_create: "Create"
label_create_work_package: "Create new work package" label_create_work_package: "Create new work package"
label_created_by: "Created by" label_created_by: "Created by"
@ -388,6 +388,7 @@ en:
label_created_on: "created on" label_created_on: "created on"
label_edit_comment: "Edit this comment" label_edit_comment: "Edit this comment"
label_edit_status: "Edit the status of the work package" label_edit_status: "Edit the status of the work package"
label_email: "Email"
label_equals: "is" label_equals: "is"
label_expand: "Expand" label_expand: "Expand"
label_expanded: "expanded" label_expanded: "expanded"
@ -1110,21 +1111,13 @@ en:
label: label:
name_or_email: 'Name or email address' name_or_email: 'Name or email address'
name: 'Name' name: 'Name'
already_member_message: 'Already a member of %{project}' already_member_message: 'Already a member of %{project}'
no_results_user: 'No users were found' no_results_user: 'No users were found'
invite_user: 'Invite:' invite_user: 'Invite:'
change_user_selection: 'Change e-mail-address or select an existing user'
no_results_placeholder: 'No placeholders were found' no_results_placeholder: 'No placeholders were found'
change_placeholder_selection: 'Change name or select an existing placeholder'
create_new_placeholder: 'Create new placeholder:' create_new_placeholder: 'Create new placeholder:'
no_results_group: 'No groups were found' no_results_group: 'No groups were found'
change_group_selection: 'Change name or select an existing group'
next_button: 'Next' next_button: 'Next'
required: required:
user: 'Please select a user' user: 'Please select a user'
placeholder: 'Please select a placeholder' placeholder: 'Please select a placeholder'

@ -207,7 +207,7 @@ Want to upgrade from a Community version to try out the Enterprise premium featu
## Migrating to OpenProject 11.3 ## Migrating to OpenProject 11.3
Follow the [upgrade guide for the packaged installation or Docker installation](../installation-and-operations/operation/upgrading/) to update your OpenProject installation to OpenProject 11.3. Follow the [upgrade guide for the packaged installation or Docker installation](https://docs.openproject.org/installation-and-operations/operation/upgrading/) to update your OpenProject installation to OpenProject 11.3.
We update hosted OpenProject environments (Enterprise cloud) automatically. We update hosted OpenProject environments (Enterprise cloud) automatically.

@ -123,6 +123,10 @@ You need to configure just two things in the webhook.
The **Content Type** has to be `application/json`. The **Content Type** has to be `application/json`.
The **Payload URL** must point to your OpenProject server's GitHub webhook endpoint (`/webhooks/github`). The **Payload URL** must point to your OpenProject server's GitHub webhook endpoint (`/webhooks/github`).
<div class="alert alert-info" role="alert">
**Note**: For the events that should be triggered by the webhook, please select "Send me everything".
</div>
Now you need the API key you copied earlier. Append it to the *Payload URL* as a simple GET parameter named `key`. In the end the URL should look something like this: Now you need the API key you copied earlier. Append it to the *Payload URL* as a simple GET parameter named `key`. In the end the URL should look something like this:
``` ```
@ -130,4 +134,4 @@ https://myopenproject.com/webhooks/github?key=42
``` ```
_Earlier version may have used the `api_key` parameter. In OpenProject 10.4, it is `key`._ _Earlier version may have used the `api_key` parameter. In OpenProject 10.4, it is `key`._
Now the integration is set up on both sides and you can use it. Now the integration is set up on both sides and you can use it.

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

@ -25,49 +25,49 @@ The integration is available starting with Nextcloud 20. It enables users to kee
To activate your integration to OpenProject in Nextcloud, navigate to the built in app store under your user name in Your apps. You can use the search field in the top right corner to look for the OpenProject integration. Click the button Download and enable. To activate your integration to OpenProject in Nextcloud, navigate to the built in app store under your user name in Your apps. You can use the search field in the top right corner to look for the OpenProject integration. Click the button Download and enable.
![Nextcloud_app_store](../faq/Nextcloud_app_store.png) ![Nextcloud_app_store](Nextcloud_app_store.png)
**Activate the OpenProject integration app** **Activate the OpenProject integration app**
To activate your integration, navigate to your personal settings and choose Connected accounts in the menu on the left. To activate your integration, navigate to your personal settings and choose Connected accounts in the menu on the left.
![Nextcloud_connected_account](../faq/Nextcloud_connected_account.png) ![Nextcloud_connected_account](Nextcloud_connected_account.png)
Enter the URL of your OpenProject instance and your access token which you can find in OpenProject under My Account and then Access token. Reset the API token and copy/paste it. Enter the URL of your OpenProject instance and your access token which you can find in OpenProject under My Account and then Access token. Reset the API token and copy/paste it.
![OpenProject_API_key](../faq/OpenProject_API_key.png) ![OpenProject_API_key](OpenProject_API_key.png)
![OpenProject_API_key_copy](../faq/OpenProject_API_key_copy.png) ![OpenProject_API_key_copy](OpenProject_API_key_copy.png)
**Display of OpenProject in the Nextcloud dashboard** **Display of OpenProject in the Nextcloud dashboard**
On the Nextcloud dashboard you can add an OpenProject widget. Display the latest changes to your project's work packages to keep an eye on your ongoing project activities directly from your Nextcloud instance. On the Nextcloud dashboard you can add an OpenProject widget. Display the latest changes to your project's work packages to keep an eye on your ongoing project activities directly from your Nextcloud instance.
![Add_OpenProject_widget](../faq/Add_OpenPorject_widget.png) ![Add_OpenProject_widget](Add_OpenProject_widget.png)
![Nextcloud_dashboard](../faq/Nextcloud_dashboard.png) ![Nextcloud_dashboard](Nextcloud_dashboard.png)
In your personal settings in Connected accounts, please remember to also activate the Enable navigation link to display a link to your OpenProject instance in the header navigation. In your personal settings in Connected accounts, please remember to also activate the Enable navigation link to display a link to your OpenProject instance in the header navigation.
![Nextcloud_connected_account](../faq/Nextcloud_connected_account.png) ![Nextcloud_connected_account](Nextcloud_connected_account.png)
The link will show here: The link will show here:
![Navigation_link_OpenProject](../faq/Navigation_link_OpenProject.png) ![Navigation_link_OpenProject](Navigation_link_OpenProject.png)
By activating "enable unified search for tickets" in your personal settings, the Nextcloud dashboard will include OpenProject information in the the built-in universal search: By activating "enable unified search for tickets" in your personal settings, the Nextcloud dashboard will include OpenProject information in the the built-in universal search:
![Unified_search](../faq/Unified_search.png) ![Unified_search](Unified_search.png)
**Set up of OAuth to OpenProject** **Set up of OAuth to OpenProject**
Within your Settings under Administration and then Connected Accounts you can set-up the OAuth authentication to your OpenProject instance. Within your Settings under Administration and then Connected Accounts you can set-up the OAuth authentication to your OpenProject instance.
![OAuth](../faq/OAuth.png) ![OAuth](OAuth.png)
In OpenProject, add Nextcloud as application under Administration then Authentication and OAuth and enter the information in your Nextcloud instance. In OpenProject, add Nextcloud as application under Administration then Authentication and OAuth and enter the information in your Nextcloud instance.
![OpenProject_OAuth](../faq/OpenProject_OAuth.png) ![OpenProject_OAuth](OpenProject_OAuth.png)
## Where do I find the Nextcloud integration in OpenProject? ## Where do I find the Nextcloud integration in OpenProject?
@ -77,4 +77,4 @@ Further integration efforts are under way, which will deliver a Nextcloud integr
If the notifications are not displayed in your Nextcloud dashboard, please check the following in your Nextcloud basic settings: in the background jobs, Cron must be activated. If the notifications are not displayed in your Nextcloud dashboard, please check the following in your Nextcloud basic settings: in the background jobs, Cron must be activated.
![Cron_job_settings](../faq/Cron_job_settings.png) ![Cron_job_settings](Cron_job_settings.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@ -31,7 +31,7 @@ import { I18nService } from "app/modules/common/i18n/i18n.service";
export class EEActiveTrialBase extends UntilDestroyedMixin { export class EEActiveTrialBase extends UntilDestroyedMixin {
public text = { public text = {
label_email: this.I18n.t('js.admin.enterprise.trial.form.label_email'), label_email: this.I18n.t('js.label_email'),
label_expires_at: this.I18n.t('js.admin.enterprise.trial.form.label_expires_at'), label_expires_at: this.I18n.t('js.admin.enterprise.trial.form.label_expires_at'),
label_maximum_users: this.I18n.t('js.admin.enterprise.trial.form.label_maximum_users'), label_maximum_users: this.I18n.t('js.admin.enterprise.trial.form.label_maximum_users'),
label_company: this.I18n.t('js.admin.enterprise.trial.form.label_company'), label_company: this.I18n.t('js.admin.enterprise.trial.form.label_company'),

@ -70,7 +70,7 @@ export class EETrialFormComponent {
label_company: this.I18n.t('js.admin.enterprise.trial.form.label_company'), label_company: this.I18n.t('js.admin.enterprise.trial.form.label_company'),
label_first_name: this.I18n.t('js.admin.enterprise.trial.form.label_first_name'), label_first_name: this.I18n.t('js.admin.enterprise.trial.form.label_first_name'),
label_last_name: this.I18n.t('js.admin.enterprise.trial.form.label_last_name'), label_last_name: this.I18n.t('js.admin.enterprise.trial.form.label_last_name'),
label_email: this.I18n.t('js.admin.enterprise.trial.form.label_email'), label_email: this.I18n.t('js.label_email'),
label_domain: this.I18n.t('js.admin.enterprise.trial.form.label_domain'), label_domain: this.I18n.t('js.admin.enterprise.trial.form.label_domain'),
privacy_policy: this.I18n.t('js.admin.enterprise.trial.form.privacy_policy'), privacy_policy: this.I18n.t('js.admin.enterprise.trial.form.privacy_policy'),
receive_newsletter: this.I18n.t('js.admin.enterprise.trial.form.receive_newsletter', { link: newsletterURL }), receive_newsletter: this.I18n.t('js.admin.enterprise.trial.form.receive_newsletter', { link: newsletterURL }),

@ -1,7 +1,7 @@
<ng-container *ngIf="!active"> <ng-container *ngIf="!active">
<a *ngIf="parent" <a *ngIf="parent"
[attr.title]="parent.name" [attr.title]="parent.name"
uiSref="work-packages.show.activity" uiSref="work-packages.show"
[uiParams]="{workPackageId: parent.id}" [uiParams]="{workPackageId: parent.id}"
class="wp-breadcrumb-parent breadcrumb-project-title nocut"> class="wp-breadcrumb-parent breadcrumb-project-title nocut">
<span [textContent]="parent.name"></span> <span [textContent]="parent.name"></span>

@ -12,7 +12,7 @@
[ngClass]="{ 'icon4 icon-small icon-arrow-right5': !first }"> [ngClass]="{ 'icon4 icon-small icon-arrow-right5': !first }">
<a [attr.title]="ancestor.name" <a [attr.title]="ancestor.name"
[textContent]="ancestor.name" [textContent]="ancestor.name"
uiSref="work-packages.show.activity" uiSref="work-packages.show"
[uiParams]="{workPackageId: ancestor.id}" [uiParams]="{workPackageId: ancestor.id}"
class="breadcrumb-project-title nocut"></a> class="breadcrumb-project-title nocut"></a>
</li> </li>

@ -6,7 +6,7 @@ import { WorkPackageResource } from "core-app/modules/hal/resources/work-package
template: ` template: `
<a id ="{{ activityHtmlId }}-link" <a id ="{{ activityHtmlId }}-link"
[textContent]="activityLabel" [textContent]="activityLabel"
uiSref="work-packages.show.activity" uiSref="work-packages.show"
[uiParams]="{workPackageId: workPackage.id!, '#': activityHtmlId }"> [uiParams]="{workPackageId: workPackage.id!, '#': activityHtmlId }">
</a> </a>
` `

@ -4,7 +4,6 @@
[addTag]="showAddTag ? createNewFromInput.bind(this) : false" [addTag]="showAddTag ? createNewFromInput.bind(this) : false"
[typeahead]="input$" [typeahead]="input$"
[items]="items$ | async" [items]="items$ | async"
[virtualScroll]="true"
[clearable]="true" [clearable]="true"
[clearOnBackspace]="false" [clearOnBackspace]="false"
[clearSearchOnAdd]="false" [clearSearchOnAdd]="false"
@ -51,7 +50,7 @@
<!--Create a new placeholder by name--> <!--Create a new placeholder by name-->
<div *ngIf="canCreateNewPlaceholder$ | async"> <div *ngIf="canCreateNewPlaceholder$ | async">
<op-icon icon-classes="icon-mail1 icon-context"></op-icon> <op-icon icon-classes="icon-add icon-context"></op-icon>
<b>{{ text.createNewPlaceholder }}</b> <b>{{ text.createNewPlaceholder }}</b>
{{ input }} {{ input }}
</div> </div>

@ -44,6 +44,7 @@ export class PrincipalSearchComponent extends UntilDestroyedMixin implements OnI
switchMap(this.loadPrincipalData.bind(this)), switchMap(this.loadPrincipalData.bind(this)),
share(), share(),
); );
private emailRegExp:RegExp = /^\S+@\S+\.\S+$/;
public canInviteByEmail$ = combineLatest( public canInviteByEmail$ = combineLatest(
this.items$, this.items$,
@ -53,7 +54,8 @@ export class PrincipalSearchComponent extends UntilDestroyedMixin implements OnI
map(([elements, input, canCreateUsers]) => { map(([elements, input, canCreateUsers]) => {
return canCreateUsers return canCreateUsers
&& this.type === PrincipalType.User && this.type === PrincipalType.User
&& input?.includes('@') && !!input
&& this.emailRegExp.test(input)
&& !elements.find((el) => (el.principal as UserResource).email === input); && !elements.find((el) => (el.principal as UserResource).email === input);
}), }),
); );

@ -6,7 +6,7 @@
<div class="op-modal--body op-form"> <div class="op-modal--body op-form">
<op-form-field <op-form-field
[label]="text.label[type]" [label]="textLabel"
required required
> >
<op-ium-principal-search <op-ium-principal-search
@ -27,7 +27,7 @@
type="button" type="button"
class="op-link" class="op-link"
(click)="principalControl?.setValue(null)" (click)="principalControl?.setValue(null)"
>{{ text.changeUserSelection }}</button> >{{ text.change }}</button>
</p> </p>
<p <p
@ -39,7 +39,7 @@
type="button" type="button"
class="op-link" class="op-link"
(click)="principalControl?.setValue(null)" (click)="principalControl?.setValue(null)"
>{{ text.changePlaceholderSelection }}</button> >{{ text.change }}</button>
</p> </p>
<div <div

@ -62,10 +62,9 @@ export class PrincipalComponent implements OnInit {
User: this.I18n.t('js.invite_user_modal.principal.label.name_or_email'), User: this.I18n.t('js.invite_user_modal.principal.label.name_or_email'),
PlaceholderUser: this.I18n.t('js.invite_user_modal.principal.label.name'), PlaceholderUser: this.I18n.t('js.invite_user_modal.principal.label.name'),
Group: this.I18n.t('js.invite_user_modal.principal.label.name'), Group: this.I18n.t('js.invite_user_modal.principal.label.name'),
Email: this.I18n.t('js.label_email')
}, },
changeUserSelection: this.I18n.t('js.invite_user_modal.principal.change_user_selection'), change: this.I18n.t('js.label_change'),
changePlaceholderSelection: this.I18n.t('js.invite_user_modal.principal.change_placeholder_selection'),
changeGroupSelection: this.I18n.t('js.invite_user_modal.principal.change_group_selection'),
inviteUser: this.I18n.t('js.invite_user_modal.principal.invite_user'), inviteUser: this.I18n.t('js.invite_user_modal.principal.invite_user'),
createNewPlaceholder: this.I18n.t('js.invite_user_modal.principal.create_new_placeholder'), createNewPlaceholder: this.I18n.t('js.invite_user_modal.principal.create_new_placeholder'),
required: { required: {
@ -110,6 +109,14 @@ export class PrincipalComponent implements OnInit {
return !!this.principal; return !!this.principal;
} }
get textLabel() {
if (this.type === PrincipalType.User && this.isNewPrincipal) {
return this.text.label.Email;
} else {
return this.text.label[this.type];
}
}
get isNewPrincipal() { get isNewPrincipal() {
return this.hasPrincipalSelected && !(this.principal instanceof HalResource); return this.hasPrincipalSelected && !(this.principal instanceof HalResource);
} }

@ -3,7 +3,6 @@
[formControl]="projectFormControl" [formControl]="projectFormControl"
[typeahead]="input$" [typeahead]="input$"
[items]="items$ | async" [items]="items$ | async"
[virtualScroll]="true"
[clearable]="true" [clearable]="true"
[clearOnBackspace]="false" [clearOnBackspace]="false"
[clearSearchOnAdd]="false" [clearSearchOnAdd]="false"

@ -31,7 +31,10 @@
# Improves handling of some edge cases when to_url is called. The method is provided by # Improves handling of some edge cases when to_url is called. The method is provided by
# stringex but some edge cases have not been handled properly by that gem. # stringex but some edge cases have not been handled properly by that gem.
# #
# Currently, this is limited to the string '.' which would lead to an empty string otherwise. # This includes
# * the strings '.' and '!' which would lead to an empty string otherwise
# * the ability to add a custom_rule lambda that is able to postprocess the identifier. It will run
# after the default transformation was executed.
module OpenProject module OpenProject
module ActsAsUrl module ActsAsUrl
@ -57,6 +60,10 @@ module OpenProject
def modify_base_url def modify_base_url
super super
if !base_url.empty? && settings.respond_to?(:custom_rule)
self.base_url = settings.custom_rule.call(base_url)
end
modify_base_url_custom_rules if base_url.empty? modify_base_url_custom_rules if base_url.empty?
end end

@ -65,7 +65,7 @@ class UpdateXktToVersion8 < ActiveRecord::Migration[6.1]
) )
old_attachment.destroy old_attachment.destroy
attachment.save! attachment.save! validate: false
end end
end end

@ -212,7 +212,7 @@ module OpenProject::Bim::BcfXml
def comment_node(xml, uuid, journal) def comment_node(xml, uuid, journal)
xml.Comment "Guid" => uuid do xml.Comment "Guid" => uuid do
xml.Date to_bcf_datetime(journal.created_at) xml.Date to_bcf_datetime(journal.created_at)
xml.Author journal.user.mail if journal.user_id xml.Author(journal.user.mail) if journal.user_id && journal&.user&.mail.present?
xml.Comment journal.notes xml.Comment journal.notes
end end
end end

@ -81,8 +81,8 @@ describe 'bcf export',
page.find('.export-bcf-button').click page.find('.export-bcf-button').click
# Expect to get a response regarding queuing # Expect to get a response regarding queuing
expect(page).to have_content I18n.t('js.job_status.generic_messages.in_queue'), expect(page).to have_content(I18n.t('js.job_status.generic_messages.in_queue'),
wait: 10 wait: 10)
perform_enqueued_jobs perform_enqueued_jobs
expect(page).to have_text("completed successfully") expect(page).to have_text("completed successfully")
@ -102,28 +102,28 @@ describe 'bcf export',
it 'can export the open and closed BCF issues (Regression #30953)' do it 'can export the open and closed BCF issues (Regression #30953)' do
model_page.visit! model_page.visit!
wp_cards.expect_work_package_listed open_work_package wp_cards.expect_work_package_listed(open_work_package)
wp_cards.expect_work_package_not_listed closed_work_package wp_cards.expect_work_package_not_listed(closed_work_package)
filters.expect_filter_count 1 filters.expect_filter_count(1)
# Expect only the open issue # Expect only the open issue
extractor_list = export_into_bcf_extractor extractor_list = export_into_bcf_extractor
expect(extractor_list.length).to eq 1 expect(extractor_list.length).to eq(1)
expect(extractor_list.first[:title]).to eq 'Open WP' expect(extractor_list.first[:title]).to eq('Open WP')
model_page.visit! model_page.visit!
# Change the query to show all statuses # Change the query to show all statuses
filters.open filters.open
filters.remove_filter 'status' filters.remove_filter('status')
filters.expect_filter_count 0 filters.expect_filter_count(0)
wp_cards.expect_work_package_listed open_work_package, closed_work_package wp_cards.expect_work_package_listed(open_work_package, closed_work_package)
# Download again # Download again
extractor_list = export_into_bcf_extractor extractor_list = export_into_bcf_extractor
expect(extractor_list.length).to eq 2 expect(extractor_list.length).to eq(2)
titles = extractor_list.map { |hash| hash[:title] } titles = extractor_list.map { |hash| hash[:title] }
expect(titles).to contain_exactly 'Open WP', 'Closed WP' expect(titles).to contain_exactly('Open WP', 'Closed WP')
end end
end end

@ -35,12 +35,23 @@ describe Projects::SetAttributesService, 'integration', type: :model do
let(:contract) { Projects::CreateContract } let(:contract) { Projects::CreateContract }
let(:instance) { described_class.new(user: user, model: project, contract_class: contract) } let(:instance) { described_class.new(user: user, model: project, contract_class: contract) }
let(:attributes) { {} } let(:attributes) { {} }
let(:project) { Project.new }
let(:service_result) do let(:service_result) do
instance.call(attributes) instance.call(attributes)
end end
describe 'with a project name starting with numbers' do
let(:attributes) { { name: '100 Project A' } }
it 'will create an identifier with the numbers stripped' do
expect(service_result).to be_success
expect(service_result.result.identifier).to eq 'project-a'
end
end
describe 'with an existing project' do describe 'with an existing project' do
let!(:existing) { FactoryBot.create :project, identifier: 'my-new-project' } let(:existing_identifier) { 'my-new-project' }
let!(:existing) { FactoryBot.create :project, identifier: existing_identifier }
context 'and a new project with no identifier set' do context 'and a new project with no identifier set' do
let(:project) { Project.new name: 'My new project' } let(:project) { Project.new name: 'My new project' }
@ -62,5 +73,14 @@ describe Projects::SetAttributesService, 'integration', type: :model do
expect(errors).to eq ['Identifier has already been taken.'] expect(errors).to eq ['Identifier has already been taken.']
end end
end end
context 'with an existing identifier and a project name starting with numbers' do
let(:attributes) { { name: '100 My new project' } }
it 'will auto correct the identifier with the numbers stripped' do
expect(service_result).to be_success
expect(service_result.result.identifier).to eq 'my-new-project-1'
end
end
end end
end end

Loading…
Cancel
Save