@ -0,0 +1,39 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module PlaceholderUsersHelper |
||||
## |
||||
# Determine whether the given actor can delete the placeholder user |
||||
def can_delete_placeholder_user?(placeholder, actor = User.current) |
||||
PlaceholderUsers::DeleteContract.deletion_allowed? placeholder, |
||||
actor, |
||||
Authorization::UserAllowedService.new(actor) |
||||
end |
||||
end |
@ -0,0 +1,48 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module TooltipHelper |
||||
include OpenProject::FormTagHelper |
||||
|
||||
## |
||||
# Render a tooltip span |
||||
# |
||||
# @param text [string] Content of the tooltip |
||||
# @param placement [string] placement (top, left, right, bottom) |
||||
# @param span_classes [string] Additional classes on the span |
||||
# @param icon [string] icon class |
||||
def tooltip_tag(text, placement: 'left', icon: 'icon-help', span_classes: nil) |
||||
content_tag :span, |
||||
class: "tooltip--#{placement} #{span_classes}", |
||||
data: { tooltip: text } do |
||||
op_icon "icon #{icon}" |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,48 @@ |
||||
<%#-- copyright |
||||
OpenProject is an open source project management software. |
||||
Copyright (C) 2012-2021 the OpenProject GmbH |
||||
|
||||
This program is free software; you can redistribute it and/or |
||||
modify it under the terms of the GNU General Public License version 3. |
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
Copyright (C) 2010-2013 the ChiliProject Team |
||||
|
||||
This program is free software; you can redistribute it and/or |
||||
modify it under the terms of the GNU General Public License |
||||
as published by the Free Software Foundation; either version 2 |
||||
of the License, or (at your option) any later version. |
||||
|
||||
This program is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU General Public License |
||||
along with this program; if not, write to the Free Software |
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
|
||||
See docs/COPYRIGHT.rdoc for more details. |
||||
|
||||
++#%> |
||||
|
||||
<% if can_delete_placeholder_user?(@placeholder_user) %> |
||||
<li class="toolbar-item"> |
||||
<%= link_to deletion_info_placeholder_user_path(@placeholder_user), |
||||
class: 'button' do %> |
||||
<%= op_icon('button--icon icon-delete') %> |
||||
<span class="button--text"><%= t(:button_delete) %></span> |
||||
<% end %> |
||||
</li> |
||||
<% else %> |
||||
<li class="toolbar-item"> |
||||
<%= content_tag :span, title: I18n.t('placeholder_users.right_to_manage_members_missing') do %> |
||||
<%= link_to '#', |
||||
class: 'button -disabled' do %> |
||||
<%= op_icon('button--icon icon-delete') %> |
||||
<span class="button--text"><%= t(:button_delete) %></span> |
||||
<% end %> |
||||
<% end %> |
||||
</li> |
||||
<% end %> |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 256 KiB |
After Width: | Height: | Size: 73 KiB |
After Width: | Height: | Size: 401 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 343 KiB |
After Width: | Height: | Size: 394 KiB |
After Width: | Height: | Size: 179 KiB |
@ -1,31 +1,31 @@ |
||||
<fieldset class="form--fieldset -collapsible" |
||||
[ngClass]="{'collapsed': to.collapsibleFieldGroupsCollapsed}" |
||||
*ngIf="to?.collapsibleFieldGroups"> |
||||
<legend class="form--fieldset-legend" |
||||
title="Show/hide" |
||||
(click)="to.collapsibleFieldGroupsCollapsed = !to.collapsibleFieldGroupsCollapsed"> |
||||
<a href="#"> |
||||
<fieldset |
||||
class="op-fieldset op-fieldset_collapsible" |
||||
[ngClass]="{'op-fieldset_collapsed': to.collapsibleFieldGroupsCollapsed}" |
||||
*ngIf="to?.collapsibleFieldGroups" |
||||
> |
||||
<legend class="op-fieldset--legend"> |
||||
<button |
||||
title="Show/hide" |
||||
type="button" |
||||
class="op-fieldset--toggle" |
||||
(click)="to.collapsibleFieldGroupsCollapsed = !to.collapsibleFieldGroupsCollapsed" |
||||
> |
||||
{{ to.label }} |
||||
</a> |
||||
</button> |
||||
</legend> |
||||
|
||||
<div |
||||
[ngStyle]="{ |
||||
'height': to.collapsibleFieldGroupsCollapsed ? 0 : 'auto', |
||||
'visibility': to.collapsibleFieldGroupsCollapsed ? 'hidden' : 'visible', |
||||
'overflow': to.collapsibleFieldGroupsCollapsed ? 'hidden' : 'visible' |
||||
}" |
||||
> |
||||
<div class="op-fieldset--fields"> |
||||
<ng-container #fieldComponent></ng-container> |
||||
</div> |
||||
</fieldset> |
||||
|
||||
<ng-container *ngIf="!to?.collapsibleFieldGroups"> |
||||
<div> |
||||
{{ to.label }} |
||||
</div> |
||||
<fieldset |
||||
class="op-fieldset" |
||||
*ngIf="!to?.collapsibleFieldGroups" |
||||
> |
||||
<legend class="op-fieldset--legend">{{ to.label }}</legend> |
||||
|
||||
<div> |
||||
<div class="op-fieldset--fields"> |
||||
<ng-container #fieldComponent></ng-container> |
||||
</div> |
||||
</ng-container> |
||||
</fieldset> |
@ -1,5 +1,7 @@ |
||||
<input type="checkbox" |
||||
[attr.aria-required]="to.required" |
||||
[attr.required]="to.required" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field"> |
||||
<input |
||||
type="checkbox" |
||||
[attr.aria-required]="to.required" |
||||
[attr.required]="to.required" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field" |
||||
> |
||||
|
@ -1,5 +1,6 @@ |
||||
<op-date-picker-adapter [required]="to.required" |
||||
[disable]="to.disabled" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field"> |
||||
</op-date-picker-adapter> |
||||
<op-date-picker-adapter |
||||
[required]="to.required" |
||||
[disabled]="to.disabled" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field" |
||||
></op-date-picker-adapter> |
||||
|
@ -1,4 +1,5 @@ |
||||
<op-formattable-control [templateOptions]="to" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field"> |
||||
</op-formattable-control> |
||||
<op-formattable-control |
||||
[templateOptions]="to" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field" |
||||
></op-formattable-control> |
||||
|
@ -1,7 +1,8 @@ |
||||
<input [type]="to.type" |
||||
[attr.aria-required]="to.required" |
||||
[attr.required]="to.required" |
||||
[attr.lang]="to.locale" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field" |
||||
class="op-input"> |
||||
<input |
||||
[type]="to.type" |
||||
[attr.required]="to.required" |
||||
[attr.lang]="to.locale" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field" |
||||
class="op-input" |
||||
> |
@ -1,6 +1,8 @@ |
||||
<input [type]="to.type" |
||||
[attr.aria-required]="to.required" |
||||
[attr.required]="to.required" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field" |
||||
class="op-input"> |
||||
<input |
||||
[type]="to.type" |
||||
[attr.aria-required]="to.required" |
||||
[attr.required]="to.required" |
||||
[formControl]="formControl" |
||||
[formlyAttributes]="field" |
||||
class="op-input" |
||||
> |
||||
|
@ -1,3 +1,4 @@ |
||||
@import './form-field/form-field' |
||||
@import './form' |
||||
@import './fieldset' |
||||
@import './highlighted-input' |
||||
|
@ -1,13 +1,15 @@ |
||||
<input #dateInput |
||||
[id]="id" |
||||
[name]="name" |
||||
[value]="initialDate" |
||||
[ngClass]="classes + ' op-input'" |
||||
[size]="size" |
||||
[required]="required" |
||||
[disabled]="disabled" |
||||
(click)="openOnClick()" |
||||
(keydown.escape)="close()" |
||||
(blur)="closeOnOutsideClick($event)" |
||||
(input)="onInputChange($event)" |
||||
type="text"> |
||||
<input |
||||
#dateInput |
||||
[id]="id" |
||||
[name]="name" |
||||
[value]="initialDate" |
||||
[ngClass]="classes + ' op-input'" |
||||
[size]="size" |
||||
[required]="required" |
||||
[disabled]="disabled" |
||||
(click)="openOnClick()" |
||||
(keydown.escape)="close()" |
||||
(blur)="closeOnOutsideClick($event)" |
||||
(input)="onInputChange($event)" |
||||
type="text" |
||||
> |
||||
|
@ -0,0 +1,46 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module API |
||||
module V3 |
||||
URN_PREFIX = 'urn:openproject-org:api:v3:'.freeze |
||||
URN_ERROR_PREFIX = "#{URN_PREFIX}errors:".freeze |
||||
# For resources invisible to the user, a resource (including a payload) will contain |
||||
# an "undisclosed uri" instead of a url. This indicates the existence of a value |
||||
# without revealing anything. An example for this is the parent project which might be |
||||
# invisible to a user. |
||||
# In case a "undisclosed uri" is provided as a link, the current value is not |
||||
# to be altered and thus it is treated as if the value where never provided in |
||||
# the first place. This allows a schema/_embedded/payload -> client -> POST/PUT |
||||
# request/response round trip where the user knows of the existence of the value without revealing |
||||
# the contents. The payload remains valid in this case and the client can distinguish between |
||||
# keeping the value and unsetting the linked resource to null. |
||||
URN_UNDISCLOSED = "#{URN_PREFIX}undisclosed".freeze |
||||
end |
||||
end |
||||
|
@ -1,23 +1,22 @@ |
||||
<div class="git-actions-menu dropdown-relative dropdown -overflow-in-view dropdown-anchor-right"> |
||||
<h3 class="title"> |
||||
<op-icon icon-classes="button--icon icon-console-light"></op-icon> |
||||
{{text.title}} |
||||
</h3> |
||||
<op-scrollable-tabs |
||||
[tabs]="tabs" |
||||
[currentTabId]="selectedTab.id" |
||||
(tabSelected)="selectedTab = $event" |
||||
> |
||||
</op-scrollable-tabs> |
||||
<div class="copy-wrapper"> |
||||
<textarea class="copy-content" [textContent]="selectedTab.textToCopy()" [style.height.em]="selectedTab.lines" readonly="true"></textarea> |
||||
<button class="button copy-button" |
||||
type="button" |
||||
[attr.aria-label]="text.copyButtonHelpText" |
||||
(click)="onCopyButtonClick()"> |
||||
<op-icon icon-classes="button--icon icon-copy"></op-icon> |
||||
</button> |
||||
<div class="copy-result-message" *ngIf="showCopyResult" [textContent]="lastCopyResult"></div> |
||||
<h3 class="title">{{text.title}}</h3> |
||||
|
||||
<div class="copy-wrapper op-form-field" *ngFor="let snippet of snippets"> |
||||
<label class="op-form-field--label-wrap"> |
||||
<div class="op-form-field--label">{{ snippet.name }}</div> |
||||
<div class="op-form-field--input"> |
||||
<input type="text" class="copy-content op-input" readonly="true" [value]="snippet.textToCopy()"> |
||||
<button class="button copy-button" |
||||
type="button" |
||||
[attr.aria-label]="text.copyButtonHelpText" |
||||
(click)="onCopyButtonClick(snippet)"> |
||||
<op-icon icon-classes="button--icon icon-copy"></op-icon> |
||||
</button> |
||||
<div class="copy-result-message" *ngIf="showCopyResult && snippet.id === copiedSnippetId" [textContent]="lastCopyResult"></div> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="help-text" [textContent]="selectedTab.help"></div> |
||||
|
||||
</div> |
||||
|
||||
|
||||
|
@ -0,0 +1,62 @@ |
||||
.op-pr-check |
||||
display: grid |
||||
grid-row: span 4 |
||||
grid-template-columns: 28px 33px 1fr auto |
||||
grid-template-areas: "check-state-icon check-avatar check-info check-details" |
||||
list-style-type: none |
||||
border: 1px solid #dddddd |
||||
padding: 0.3rem 1rem |
||||
background: rgba(0, 0, 0, 0.05) |
||||
font-size: 0.9rem |
||||
|
||||
&:first-child |
||||
border-top-right-radius: 5px |
||||
border-top-left-radius: 5px |
||||
|
||||
&:last-child |
||||
border-bottom-right-radius: 5px |
||||
border-bottom-left-radius: 5px |
||||
|
||||
&--avatar img |
||||
grid-area: check-avatar |
||||
display: inline-block |
||||
width: 22px |
||||
height: 22px |
||||
margin-right: 5px |
||||
border-radius: var(--user-avatar-border-radius) |
||||
|
||||
&--info |
||||
grid-area: check-info |
||||
|
||||
&--state |
||||
color: var(--gray-dark) |
||||
font-style: italic |
||||
margin-left: 1em |
||||
|
||||
&--state-icon |
||||
grid-area: check-state-icon |
||||
|
||||
&_queued |
||||
color: cadetblue |
||||
|
||||
&_in_progress |
||||
color: orange |
||||
|
||||
&_success |
||||
color: green |
||||
|
||||
&_failure, |
||||
&_timed_out, |
||||
&_action_required, |
||||
&_stale |
||||
color: red |
||||
|
||||
&_skipped, |
||||
&_neutral, |
||||
&_cancelled |
||||
color: gray |
||||
color: gray |
||||
color: gray |
||||
|
||||
&--details |
||||
grid-area: check-details |
@ -0,0 +1,61 @@ |
||||
<a |
||||
class='op-pull-request--link' |
||||
[href]="pullRequest.htmlUrl" |
||||
target="blank" |
||||
[textContent]="pullRequest.repository + '#' + pullRequest.number" |
||||
></a> |
||||
|
||||
<div |
||||
class='op-pull-request--title' |
||||
[textContent]="pullRequest.title" |
||||
></div> |
||||
|
||||
<div class="op-pull-request--info"> |
||||
{{ text.label_created_by }} |
||||
<img |
||||
alt='PR author avatar' |
||||
class='op-pull-request--avatar op-avatar op-avatar_mini' |
||||
[src]="pullRequest.githubUser.avatarUrl" |
||||
*ngIf="pullRequest.githubUser" |
||||
/> |
||||
<span class='op-pull-request--user'> |
||||
<a |
||||
[href]="pullRequest.githubUser.htmlUrl" |
||||
[textContent]="pullRequest.githubUser.login" |
||||
*ngIf="pullRequest.githubUser" |
||||
></a>. |
||||
</span> |
||||
|
||||
<span class='op-pull-request--date'> |
||||
{{ text.label_last_updated_on }} |
||||
<op-date-time [dateTimeValue]="pullRequest.githubUpdatedAt"></op-date-time> |
||||
</span>. |
||||
</div> |
||||
|
||||
<span class='op-pull-request--state' [ngClass]="'op-pull-request--state_' + state"> |
||||
<op-icon icon-classes="button--icon icon-merge-branch"></op-icon> |
||||
{{state}} |
||||
</span> |
||||
|
||||
<span class="op-pull-request--checks-label" *ngIf="pullRequest.checkRuns?.length">{{ text.label_actions }}</span> |
||||
|
||||
<ul [attr.aria-label]="text.label_actions" class='op-pull-request--checks' *ngIf="pullRequest.checkRuns?.length"> |
||||
<li class='op-pr-check' *ngFor="let checkRun of pullRequest.checkRuns"> |
||||
<span class='op-pr-check--state-icon' [ngClass]="'op-pr-check--state-icon_' + checkRunState(checkRun)"> |
||||
<op-icon icon-classes="icon-{{ checkRunStateIcon(checkRun) }}" |
||||
[icon-title]="checkRunStateText(checkRun)"></op-icon> |
||||
</span> |
||||
<span class='op-pr-check--avatar'><img alt='app owner avatar' [src]="checkRun.appOwnerAvatarUrl" /></span> |
||||
|
||||
<span class='op-pr-check--info'> |
||||
<span class='op-pr-check--name' [textContent]="checkRun.name"></span> |
||||
<span class='op-pr-check--state' [textContent]="checkRunStateText(checkRun)"></span> |
||||
</span> |
||||
|
||||
<span class='op-pr-check--details'> |
||||
<a [href]="checkRun.detailsUrl"> |
||||
{{ text.label_details }} |
||||
</a> |
||||
</span> |
||||
</li> |
||||
</ul> |
@ -1,42 +0,0 @@ |
||||
<div class='op-pr-pull-request'> |
||||
<div class='op-pr-title' |
||||
[textContent]="pullRequest.title"> |
||||
</div> |
||||
|
||||
<img alt='PR author avatar' |
||||
class='op-avatar' |
||||
[src]="pullRequest.githubUser.avatarUrl" |
||||
*ngIf="pullRequest.githubUser" |
||||
/> |
||||
<span class='op-pr-user'> |
||||
<a [href]="pullRequest.githubUser.htmlUrl" |
||||
[textContent]="pullRequest.githubUser.login" |
||||
*ngIf="pullRequest.githubUser" |
||||
> |
||||
</a> |
||||
</span> |
||||
|
||||
<span class='op-pr-date'> |
||||
{{ text.label_updated_on }} |
||||
<op-date-time [dateTimeValue]="pullRequest.githubUpdatedAt"></op-date-time> |
||||
</span> |
||||
|
||||
<span class='op-pr-state' [ngClass]="'op-pr-state_' + state">{{state}}</span> |
||||
<a class='op-pr-link' [href]="pullRequest.htmlUrl" [textContent]="pullRequest.repository + '#' + pullRequest.number"></a> |
||||
|
||||
<ul [attr.aria-label]="text.label_actions" class='op-pr-checks' *ngIf="pullRequest.checkRuns?.length"> |
||||
<li class='op-pr-check' *ngFor="let checkRun of pullRequest.checkRuns"> |
||||
<span class='op-pr-check-avatar'><img alt='app owner avatar' [src]="checkRun.appOwnerAvatarUrl" /></span> |
||||
|
||||
<span class='op-pr-check-name' [textContent]="checkRun.name"></span> |
||||
|
||||
<span class='op-pr-check-state' [textContent]="checkRunState(checkRun)"></span> |
||||
|
||||
<div class='op-pr-check-details'> |
||||
<a [href]="checkRun.detailsUrl"> |
||||
{{ text.label_details }} |
||||
</a> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -0,0 +1,127 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'Projects', 'creation', type: :feature, js: true do |
||||
let(:name_field) { ::FormFields::InputFormField.new :name } |
||||
|
||||
current_user { FactoryBot.create(:admin) } |
||||
|
||||
shared_let(:project) { FactoryBot.create(:project, name: 'Foo project', identifier: 'foo-project') } |
||||
|
||||
before do |
||||
visit projects_path |
||||
end |
||||
|
||||
it 'can create a project' do |
||||
click_on 'New project' |
||||
|
||||
name_field.set_value 'Foo bar' |
||||
click_button 'Save' |
||||
|
||||
sleep 1 |
||||
|
||||
expect(page).to have_content 'Foo bar' |
||||
expect(page).to have_current_path /\/projects\/foo-bar\/?/ |
||||
end |
||||
|
||||
it 'does not create a project with an already existing identifier' do |
||||
click_on 'New project' |
||||
|
||||
name_field.set_value 'Foo project' |
||||
click_on 'Save' |
||||
|
||||
expect(page).to have_current_path /\/projects\/foo-project-1\/?/ |
||||
|
||||
project = Project.last |
||||
expect(project.identifier).to eq 'foo-project-1' |
||||
end |
||||
|
||||
context 'with a multi-select custom field' do |
||||
let!(:list_custom_field) { FactoryBot.create(:list_project_custom_field, name: 'List CF', multi_value: true) } |
||||
let(:list_field) { ::FormFields::SelectFormField.new list_custom_field } |
||||
|
||||
it 'can create a project' do |
||||
click_on 'New project' |
||||
|
||||
name_field.set_value 'Foo bar' |
||||
|
||||
find('.op-fieldset--toggle', text: 'ADVANCED SETTINGS').click |
||||
|
||||
list_field.select_option 'A', 'B' |
||||
|
||||
click_button 'Save' |
||||
|
||||
expect(page).to have_current_path /\/projects\/foo-bar\/?/ |
||||
expect(page).to have_content 'Foo bar' |
||||
|
||||
project = Project.last |
||||
expect(project.name).to eq 'Foo bar' |
||||
cvs = project.custom_value_for(list_custom_field) |
||||
expect(cvs.count).to eq 2 |
||||
expect(cvs.map(&:typed_value)).to contain_exactly 'A', 'B' |
||||
end |
||||
end |
||||
|
||||
it 'hides the active field and the identifier' do |
||||
visit new_project_path |
||||
|
||||
find('.op-fieldset--toggle', text: 'ADVANCED SETTINGS').click |
||||
|
||||
expect(page).to have_no_content 'Active' |
||||
expect(page).to have_no_content 'Identifier' |
||||
end |
||||
|
||||
context 'with optional and required custom fields' do |
||||
let!(:optional_custom_field) do |
||||
FactoryBot.create(:custom_field, name: 'Optional Foo', |
||||
type: ProjectCustomField, |
||||
is_for_all: true) |
||||
end |
||||
let!(:required_custom_field) do |
||||
FactoryBot.create(:custom_field, name: 'Required Foo', |
||||
type: ProjectCustomField, |
||||
is_for_all: true, |
||||
is_required: true) |
||||
end |
||||
|
||||
it 'seperates optional and required custom fields for new' do |
||||
visit new_project_path |
||||
|
||||
expect(page).to have_content 'Required Foo' |
||||
|
||||
click_on 'Advanced settings' |
||||
|
||||
within('.op-fieldset') do |
||||
expect(page).to have_text 'Optional Foo' |
||||
expect(page).to have_no_text 'Required Foo' |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,210 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'Projects', 'editing settings', type: :feature, js: true do |
||||
let(:name_field) { ::FormFields::InputFormField.new :name } |
||||
let(:parent_field) { ::FormFields::SelectFormField.new :parent } |
||||
let(:permissions) { %i(edit_project) } |
||||
|
||||
current_user do |
||||
FactoryBot.create(:user, |
||||
member_in_project: project, |
||||
member_with_permissions: permissions) |
||||
end |
||||
|
||||
shared_let(:project) do |
||||
FactoryBot.create(:project, name: 'Foo project', identifier: 'foo-project') |
||||
end |
||||
|
||||
it 'hides the field whose functionality is presented otherwise' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
expect(page).to have_no_text :all, 'Active' |
||||
expect(page).to have_no_text :all, 'Identifier' |
||||
end |
||||
|
||||
describe 'identifier edit' do |
||||
it 'updates the project identifier' do |
||||
visit projects_path |
||||
click_on project.name |
||||
SeleniumHubWaiter.wait |
||||
click_on 'Project settings' |
||||
SeleniumHubWaiter.wait |
||||
click_on 'Change identifier' |
||||
|
||||
expect(page).to have_content "CHANGE THE PROJECT'S IDENTIFIER" |
||||
expect(current_path).to eq '/projects/foo-project/identifier' |
||||
|
||||
fill_in 'project[identifier]', with: 'foo-bar' |
||||
click_on 'Update' |
||||
|
||||
expect(page).to have_content 'Successful update.' |
||||
expect(current_path).to match '/projects/foo-bar/settings/generic' |
||||
expect(Project.first.identifier).to eq 'foo-bar' |
||||
end |
||||
|
||||
it 'displays error messages on invalid input' do |
||||
visit identifier_project_path(project) |
||||
|
||||
fill_in 'project[identifier]', with: 'FOOO' |
||||
click_on 'Update' |
||||
|
||||
expect(page).to have_content 'Identifier is invalid.' |
||||
expect(current_path).to eq '/projects/foo-project/identifier' |
||||
end |
||||
end |
||||
|
||||
context 'with optional and required custom fields' do |
||||
let!(:optional_custom_field) do |
||||
FactoryBot.create(:custom_field, name: 'Optional Foo', |
||||
type: ProjectCustomField, |
||||
is_for_all: true) |
||||
end |
||||
let!(:required_custom_field) do |
||||
FactoryBot.create(:custom_field, name: 'Required Foo', |
||||
type: ProjectCustomField, |
||||
is_for_all: true, |
||||
is_required: true) |
||||
end |
||||
|
||||
it 'shows optional and required custom fields for edit without a separation' do |
||||
project.custom_field_values.last.value = 'FOO' |
||||
project.save! |
||||
|
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
expect(page).to have_text 'Optional Foo' |
||||
expect(page).to have_text 'Required Foo' |
||||
end |
||||
end |
||||
|
||||
context 'with a length restricted custom field' do |
||||
let!(:required_custom_field) do |
||||
FactoryBot.create(:string_project_custom_field, |
||||
name: 'Foo', |
||||
type: ProjectCustomField, |
||||
min_length: 1, |
||||
max_length: 2, |
||||
is_for_all: true) |
||||
end |
||||
let(:foo_field) { ::FormFields::InputFormField.new required_custom_field } |
||||
|
||||
it 'shows the errors of that field when saving (Regression #33766)' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
expect(page).to have_content 'Foo' |
||||
|
||||
# Enter something too long |
||||
foo_field.set_value '1234' |
||||
|
||||
# It should cut of that remaining value |
||||
foo_field.expect_value '12' |
||||
|
||||
click_button 'Save' |
||||
|
||||
expect(page).to have_text 'Successful update.' |
||||
end |
||||
end |
||||
|
||||
context 'with a multi-select custom field' do |
||||
include_context 'ng-select-autocomplete helpers' |
||||
|
||||
let!(:list_custom_field) { FactoryBot.create(:list_project_custom_field, name: 'List CF', multi_value: true) } |
||||
let(:form_field) { ::FormFields::SelectFormField.new list_custom_field } |
||||
|
||||
it 'can select multiple values' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
form_field.select_option 'A', 'B' |
||||
|
||||
click_on 'Save' |
||||
|
||||
expect(page).to have_content 'Successful update.' |
||||
|
||||
form_field.expect_selected 'A', 'B' |
||||
|
||||
cvs = project.reload.custom_value_for(list_custom_field) |
||||
expect(cvs.count).to eq 2 |
||||
expect(cvs.map(&:typed_value)).to contain_exactly 'A', 'B' |
||||
end |
||||
end |
||||
|
||||
context 'with a date custom field' do |
||||
let!(:date_custom_field) { FactoryBot.create(:date_project_custom_field, name: 'Date') } |
||||
let(:form_field) { ::FormFields::InputFormField.new date_custom_field } |
||||
|
||||
it 'can save and remove the date (Regression #37459)' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
form_field.set_value '2021-05-26' |
||||
form_field.send_keys :escape |
||||
|
||||
click_on 'Save' |
||||
|
||||
expect(page).to have_content 'Successful update.' |
||||
|
||||
form_field.expect_value '2021-05-26' |
||||
|
||||
cv = project.reload.custom_value_for(date_custom_field) |
||||
expect(cv.typed_value).to eq '2021-05-26'.to_date |
||||
end |
||||
end |
||||
|
||||
context 'with a user not allowed to see the parent project' do |
||||
include_context 'ng-select-autocomplete helpers' |
||||
|
||||
let(:parent_project) { FactoryBot.create(:project) } |
||||
let(:parent_field) { ::FormFields::SelectFormField.new 'parent' } |
||||
|
||||
before do |
||||
project.update_attribute(:parent, parent_project) |
||||
end |
||||
|
||||
it 'can update the project without destroying the relation to the parent' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
fill_in 'Name', with: 'New project name' |
||||
|
||||
parent_field.expect_selected I18n.t(:'api_v3.undisclosed.parent') |
||||
|
||||
click_on 'Save' |
||||
|
||||
expect(page).to have_content 'Successful update.' |
||||
|
||||
project.reload |
||||
|
||||
expect(project.name) |
||||
.to eql 'New project name' |
||||
|
||||
expect(project.parent) |
||||
.to eql parent_project |
||||
end |
||||
end |
||||
end |
@ -1,339 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'Projects', type: :feature, js: true do |
||||
let(:current_user) { FactoryBot.create(:admin) } |
||||
let(:name_field) { ::FormFields::InputFormField.new :name } |
||||
let(:parent_field) { ::FormFields::SelectFormField.new :parent } |
||||
|
||||
before do |
||||
allow(User).to receive(:current).and_return current_user |
||||
end |
||||
|
||||
describe 'creation' do |
||||
shared_let(:project) { FactoryBot.create(:project, name: 'Foo project', identifier: 'foo-project') } |
||||
|
||||
before do |
||||
visit projects_path |
||||
end |
||||
|
||||
it 'can create a project' do |
||||
click_on 'New project' |
||||
|
||||
name_field.set_value 'Foo bar' |
||||
click_button 'Save' |
||||
|
||||
sleep 1 |
||||
|
||||
expect(page).to have_content 'Foo bar' |
||||
expect(page).to have_current_path /\/projects\/foo-bar\/?/ |
||||
end |
||||
|
||||
it 'can create a subproject' do |
||||
click_on project.name |
||||
SeleniumHubWaiter.wait |
||||
click_on 'Project settings' |
||||
SeleniumHubWaiter.wait |
||||
click_on 'New subproject' |
||||
|
||||
name_field.set_value 'Foo child' |
||||
|
||||
sleep 1 |
||||
|
||||
parent_field.expect_selected project.name |
||||
|
||||
click_button 'Save' |
||||
|
||||
sleep 1 |
||||
|
||||
expect(page).to have_current_path /\/projects\/foo-child\/?/ |
||||
|
||||
child = Project.last |
||||
expect(child.identifier).to eq 'foo-child' |
||||
expect(child.parent).to eq project |
||||
end |
||||
|
||||
it 'does not create a project with an already existing identifier' do |
||||
click_on 'New project' |
||||
|
||||
name_field.set_value 'Foo project' |
||||
click_on 'Save' |
||||
|
||||
expect(page).to have_current_path /\/projects\/foo-project-1\/?/ |
||||
|
||||
project = Project.last |
||||
expect(project.identifier).to eq 'foo-project-1' |
||||
end |
||||
|
||||
context 'with a multi-select custom field' do |
||||
let!(:list_custom_field) { FactoryBot.create(:list_project_custom_field, name: 'List CF', multi_value: true) } |
||||
let(:list_field) { ::FormFields::SelectFormField.new list_custom_field } |
||||
|
||||
it 'can create a project' do |
||||
click_on 'New project' |
||||
|
||||
name_field.set_value 'Foo bar' |
||||
|
||||
find('.form--fieldset-legend a', text: 'ADVANCED SETTINGS').click |
||||
|
||||
list_field.select_option 'A', 'B' |
||||
|
||||
click_button 'Save' |
||||
|
||||
expect(page).to have_current_path /\/projects\/foo-bar\/?/ |
||||
expect(page).to have_content 'Foo bar' |
||||
|
||||
project = Project.last |
||||
expect(project.name).to eq 'Foo bar' |
||||
cvs = project.custom_value_for(list_custom_field) |
||||
expect(cvs.count).to eq 2 |
||||
expect(cvs.map(&:typed_value)).to contain_exactly 'A', 'B' |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'project types' do |
||||
let(:phase_type) { FactoryBot.create(:type, name: 'Phase', is_default: true) } |
||||
let(:milestone_type) { FactoryBot.create(:type, name: 'Milestone', is_default: false) } |
||||
let!(:project) { FactoryBot.create(:project, name: 'Foo project', types: [phase_type, milestone_type]) } |
||||
|
||||
it "have the correct types checked for the project's types" do |
||||
visit projects_path |
||||
click_on 'Foo project' |
||||
click_on 'Project settings' |
||||
click_on 'Work package types' |
||||
|
||||
field_checked = find_field('Phase', visible: false)['checked'] |
||||
expect(field_checked).to be_truthy |
||||
field_checked = find_field('Milestone', visible: false)['checked'] |
||||
expect(field_checked).to be_truthy |
||||
end |
||||
end |
||||
|
||||
describe 'deletion' do |
||||
let(:project) { FactoryBot.create(:project) } |
||||
let(:projects_page) { Pages::Projects::Destroy.new(project) } |
||||
|
||||
before do |
||||
projects_page.visit! |
||||
end |
||||
|
||||
describe 'disable delete w/o confirm' do |
||||
it { expect(page).to have_css('.danger-zone .button[disabled]') } |
||||
end |
||||
|
||||
describe 'disable delete with wrong input' do |
||||
let(:input) { find('.danger-zone input') } |
||||
it do |
||||
input.set 'Not the project name' |
||||
expect(page).to have_css('.danger-zone .button[disabled]') |
||||
end |
||||
end |
||||
|
||||
describe 'enable delete with correct input' do |
||||
let(:input) { find('.danger-zone input') } |
||||
it do |
||||
input.set project.name |
||||
expect(page).to have_css('.danger-zone .button:not([disabled])') |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'identifier edit' do |
||||
let!(:project) { FactoryBot.create(:project, identifier: 'foo') } |
||||
|
||||
it 'updates the project identifier' do |
||||
visit projects_path |
||||
click_on project.name |
||||
SeleniumHubWaiter.wait |
||||
click_on 'Project settings' |
||||
SeleniumHubWaiter.wait |
||||
click_on 'Change identifier' |
||||
|
||||
expect(page).to have_content "CHANGE THE PROJECT'S IDENTIFIER" |
||||
expect(current_path).to eq '/projects/foo/identifier' |
||||
|
||||
fill_in 'project[identifier]', with: 'foo-bar' |
||||
click_on 'Update' |
||||
|
||||
expect(page).to have_content 'Successful update.' |
||||
expect(current_path).to match '/projects/foo-bar/settings/generic' |
||||
expect(Project.first.identifier).to eq 'foo-bar' |
||||
end |
||||
|
||||
it 'displays error messages on invalid input' do |
||||
visit identifier_project_path(project) |
||||
|
||||
fill_in 'project[identifier]', with: 'FOOO' |
||||
click_on 'Update' |
||||
|
||||
expect(page).to have_content 'Identifier is invalid.' |
||||
expect(current_path).to eq '/projects/foo/identifier' |
||||
end |
||||
end |
||||
|
||||
describe 'form' do |
||||
let(:project) { FactoryBot.build(:project, name: 'Foo project', identifier: 'foo-project') } |
||||
|
||||
context 'when creating' do |
||||
it 'hides the active field and the identifier' do |
||||
visit new_project_path |
||||
|
||||
find('.form--fieldset-legend a', text: 'ADVANCED SETTINGS').click |
||||
|
||||
expect(page).to have_no_content 'Active' |
||||
expect(page).to have_no_content 'Identifier' |
||||
end |
||||
end |
||||
|
||||
context 'when editing' do |
||||
it 'hides the active field' do |
||||
project.save! |
||||
|
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
expect(page).to have_no_text :all, 'Active' |
||||
expect(page).to have_no_text :all, 'Identifier' |
||||
end |
||||
end |
||||
|
||||
context 'with optional and required custom fields' do |
||||
let!(:optional_custom_field) do |
||||
FactoryBot.create(:custom_field, name: 'Optional Foo', |
||||
type: ProjectCustomField, |
||||
is_for_all: true) |
||||
end |
||||
let!(:required_custom_field) do |
||||
FactoryBot.create(:custom_field, name: 'Required Foo', |
||||
type: ProjectCustomField, |
||||
is_for_all: true, |
||||
is_required: true) |
||||
end |
||||
|
||||
it 'seperates optional and required custom fields for new' do |
||||
visit new_project_path |
||||
|
||||
expect(page).to have_content 'Required Foo' |
||||
|
||||
click_on 'Advanced settings' |
||||
|
||||
within('.form--fieldset') do |
||||
expect(page).to have_text 'Optional Foo' |
||||
expect(page).to have_no_text 'Required Foo' |
||||
end |
||||
end |
||||
|
||||
it 'shows optional and required custom fields for edit without a separation' do |
||||
project.custom_field_values.last.value = 'FOO' |
||||
project.save! |
||||
|
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
expect(page).to have_text 'Optional Foo' |
||||
expect(page).to have_text 'Required Foo' |
||||
end |
||||
end |
||||
|
||||
context 'with a length restricted custom field' do |
||||
let(:project) { FactoryBot.create(:project, name: 'Foo project', identifier: 'foo-project') } |
||||
let!(:required_custom_field) do |
||||
FactoryBot.create(:string_project_custom_field, |
||||
name: 'Foo', |
||||
type: ProjectCustomField, |
||||
min_length: 1, |
||||
max_length: 2, |
||||
is_for_all: true) |
||||
end |
||||
let(:foo_field) { ::FormFields::InputFormField.new required_custom_field } |
||||
|
||||
it 'shows the errors of that field when saving (Regression #33766)' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
expect(page).to have_content 'Foo' |
||||
|
||||
# Enter something too long |
||||
foo_field.set_value '1234' |
||||
|
||||
# It should cut of that remaining value |
||||
foo_field.expect_value '12' |
||||
|
||||
click_button 'Save' |
||||
|
||||
expect(page).to have_text 'Successful update.' |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'with a multi-select custom field' do |
||||
include_context 'ng-select-autocomplete helpers' |
||||
|
||||
let(:project) { FactoryBot.create(:project, name: 'Foo project', identifier: 'foo-project') } |
||||
let!(:list_custom_field) { FactoryBot.create(:list_project_custom_field, name: 'List CF', multi_value: true) } |
||||
let(:form_field) { ::FormFields::SelectFormField.new list_custom_field } |
||||
|
||||
it 'can create a project' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
form_field.select_option 'A', 'B' |
||||
|
||||
click_on 'Save' |
||||
|
||||
expect(page).to have_content 'Successful update.' |
||||
|
||||
form_field.expect_selected 'A', 'B' |
||||
|
||||
cvs = project.reload.custom_value_for(list_custom_field) |
||||
expect(cvs.count).to eq 2 |
||||
expect(cvs.map(&:typed_value)).to contain_exactly 'A', 'B' |
||||
end |
||||
end |
||||
|
||||
context 'with a date custom field' do |
||||
let(:project) { FactoryBot.create(:project, name: 'Foo project', identifier: 'foo-project') } |
||||
let!(:date_custom_field) { FactoryBot.create(:date_project_custom_field, name: 'Date') } |
||||
let(:form_field) { ::FormFields::InputFormField.new date_custom_field } |
||||
|
||||
it 'can save and remove the date (Regression #37459)' do |
||||
visit settings_generic_project_path(project.id) |
||||
|
||||
form_field.set_value '2021-05-26' |
||||
form_field.send_keys :escape |
||||
|
||||
click_on 'Save' |
||||
|
||||
expect(page).to have_content 'Successful update.' |
||||
|
||||
form_field.expect_value '2021-05-26' |
||||
|
||||
cv = project.reload.custom_value_for(date_custom_field) |
||||
expect(cv.typed_value).to eq '2021-05-26'.to_date |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,49 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'Projects', 'work package type mgmt', type: :feature, js: true do |
||||
current_user { FactoryBot.create(:admin) } |
||||
|
||||
let(:phase_type) { FactoryBot.create(:type, name: 'Phase', is_default: true) } |
||||
let(:milestone_type) { FactoryBot.create(:type, name: 'Milestone', is_default: false) } |
||||
let!(:project) { FactoryBot.create(:project, name: 'Foo project', types: [phase_type, milestone_type]) } |
||||
|
||||
it "have the correct types checked for the project's types" do |
||||
visit projects_path |
||||
click_on 'Foo project' |
||||
click_on 'Project settings' |
||||
click_on 'Work package types' |
||||
|
||||
field_checked = find_field('Phase', visible: false)['checked'] |
||||
expect(field_checked).to be_truthy |
||||
field_checked = find_field('Milestone', visible: false)['checked'] |
||||
expect(field_checked).to be_truthy |
||||
end |
||||
end |
@ -0,0 +1,519 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::API::V3::Projects::ProjectRepresenter, 'rendering' do |
||||
include ::API::V3::Utilities::PathHelper |
||||
|
||||
subject(:generated) { representer.to_json } |
||||
|
||||
let(:project) do |
||||
FactoryBot.build_stubbed(:project, |
||||
parent: parent_project, |
||||
description: 'some description', |
||||
status: status).tap do |p| |
||||
allow(p) |
||||
.to receive(:available_custom_fields) |
||||
.and_return([int_custom_field, version_custom_field]) |
||||
|
||||
allow(p) |
||||
.to receive(:"custom_field_#{int_custom_field.id}") |
||||
.and_return(int_custom_value.value) |
||||
|
||||
allow(p) |
||||
.to receive(:custom_value_for) |
||||
.with(version_custom_field) |
||||
.and_return(version_custom_value) |
||||
end |
||||
end |
||||
let(:status) do |
||||
FactoryBot.build_stubbed(:project_status) |
||||
end |
||||
let(:parent_project) { FactoryBot.build_stubbed(:project) } |
||||
let(:representer) { described_class.create(project, current_user: user, embed_links: true) } |
||||
|
||||
let(:user) do |
||||
FactoryBot.build_stubbed(:user).tap do |u| |
||||
allow(u) |
||||
.to receive(:allowed_to?) do |permission, context| |
||||
permissions.include?(permission) && context == project |
||||
end |
||||
end |
||||
end |
||||
|
||||
let(:int_custom_field) { FactoryBot.build_stubbed(:int_project_custom_field, visible: false) } |
||||
let(:version_custom_field) { FactoryBot.build_stubbed(:version_project_custom_field, visible: true) } |
||||
let(:int_custom_value) do |
||||
CustomValue.new(custom_field: int_custom_field, |
||||
value: '1234', |
||||
customized: nil) |
||||
end |
||||
let(:version) { FactoryBot.build_stubbed(:version) } |
||||
let(:version_custom_value) do |
||||
CustomValue.new(custom_field: version_custom_field, |
||||
value: version.id, |
||||
customized: nil).tap do |cv| |
||||
allow(cv) |
||||
.to receive(:typed_value) |
||||
.and_return(version) |
||||
end |
||||
end |
||||
|
||||
let(:permissions) { %i[add_work_packages view_members] } |
||||
|
||||
it { is_expected.to include_json('Project'.to_json).at_path('_type') } |
||||
|
||||
describe 'properties' do |
||||
it_behaves_like 'property', :_type do |
||||
let(:value) { 'Project' } |
||||
end |
||||
|
||||
it_behaves_like 'property', :id do |
||||
let(:value) { project.id } |
||||
end |
||||
|
||||
it_behaves_like 'property', :identifier do |
||||
let(:value) { project.identifier } |
||||
end |
||||
|
||||
it_behaves_like 'property', :name do |
||||
let(:value) { project.name } |
||||
end |
||||
|
||||
it_behaves_like 'property', :active do |
||||
let(:value) { project.active } |
||||
end |
||||
|
||||
it_behaves_like 'property', :public do |
||||
let(:value) { project.public } |
||||
end |
||||
|
||||
it_behaves_like 'formattable property', :description do |
||||
let(:value) { project.description } |
||||
end |
||||
|
||||
context 'statusExplanation' do |
||||
it_behaves_like 'formattable property', 'statusExplanation' do |
||||
let(:value) { status.explanation } |
||||
end |
||||
end |
||||
|
||||
it_behaves_like 'has UTC ISO 8601 date and time' do |
||||
let(:date) { project.created_at } |
||||
let(:json_path) { 'createdAt' } |
||||
end |
||||
|
||||
it_behaves_like 'has UTC ISO 8601 date and time' do |
||||
let(:date) { project.updated_at } |
||||
let(:json_path) { 'updatedAt' } |
||||
end |
||||
|
||||
context 'int custom field' do |
||||
context 'if the user is admin' do |
||||
before do |
||||
allow(user) |
||||
.to receive(:admin?) |
||||
.and_return(true) |
||||
end |
||||
|
||||
it "has a property for the int custom field" do |
||||
is_expected |
||||
.to be_json_eql(int_custom_value.value.to_json) |
||||
.at_path("customField#{int_custom_field.id}") |
||||
end |
||||
end |
||||
|
||||
context 'if the user is no admin' do |
||||
it "has no property for the int custom field" do |
||||
is_expected |
||||
.not_to have_json_path("customField#{int_custom_field.id}") |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '_links' do |
||||
it { is_expected.to have_json_type(Object).at_path('_links') } |
||||
|
||||
it 'links to self' do |
||||
expect(subject).to have_json_path('_links/self/href') |
||||
end |
||||
it 'has a title for link to self' do |
||||
expect(subject).to have_json_path('_links/self/title') |
||||
end |
||||
|
||||
describe 'create work packages' do |
||||
context 'user allowed to create work packages' do |
||||
it 'has the correct path for a create form' do |
||||
is_expected |
||||
.to be_json_eql(api_v3_paths.create_project_work_package_form(project.id).to_json) |
||||
.at_path('_links/createWorkPackage/href') |
||||
end |
||||
|
||||
it 'has the correct path to create a work package' do |
||||
is_expected.to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json) |
||||
.at_path('_links/createWorkPackageImmediately/href') |
||||
end |
||||
end |
||||
|
||||
context 'user not allowed to create work packages' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'createWorkPackage' } |
||||
end |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'createWorkPackageImmediately' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'parent' do |
||||
before do |
||||
allow(parent_project) |
||||
.to receive(:visible?) |
||||
.and_return(visible) |
||||
end |
||||
let(:visible) { true } |
||||
|
||||
it_behaves_like 'has a titled link' do |
||||
let(:link) { 'parent' } |
||||
let(:href) { api_v3_paths.project(parent_project.id) } |
||||
let(:title) { parent_project.name } |
||||
end |
||||
|
||||
context 'if lacking the permissions to see the parent' do |
||||
let(:visible) { false } |
||||
|
||||
it_behaves_like 'has a titled link' do |
||||
let(:link) { 'parent' } |
||||
let(:href) { API::V3::URN_UNDISCLOSED } |
||||
let(:title) { I18n.t(:'api_v3.undisclosed.parent') } |
||||
end |
||||
end |
||||
|
||||
context 'without a parent' do |
||||
let(:parent_project) { nil } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'parent' } |
||||
let(:href) { nil } |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'status' do |
||||
it_behaves_like 'has a titled link' do |
||||
let(:link) { 'status' } |
||||
let(:href) { api_v3_paths.project_status(project.status.code) } |
||||
let(:title) { I18n.t(:"activerecord.attributes.projects/status.codes.#{project.status.code}") } |
||||
end |
||||
|
||||
context 'if the status is nil' do |
||||
let(:status) { nil } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'status' } |
||||
let(:href) { nil } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'categories' do |
||||
it 'has the correct link to its categories' do |
||||
is_expected.to be_json_eql(api_v3_paths.categories_by_project(project.id).to_json) |
||||
.at_path('_links/categories/href') |
||||
end |
||||
end |
||||
|
||||
describe 'versions' do |
||||
context 'with only manage_versions permission' do |
||||
let(:permissions) { [:manage_versions] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'versions' } |
||||
let(:href) { api_v3_paths.versions_by_project(project.id) } |
||||
end |
||||
end |
||||
|
||||
context 'with only view_work_packages permission' do |
||||
let(:permissions) { [:view_work_packages] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'versions' } |
||||
let(:href) { api_v3_paths.versions_by_project(project.id) } |
||||
end |
||||
end |
||||
|
||||
context 'without both permissions' do |
||||
let(:permissions) { [:add_work_packages] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'versions' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'types' do |
||||
context 'for a user having the view_work_packages permission' do |
||||
let(:permissions) { [:view_work_packages] } |
||||
|
||||
it 'links to the types active in the project' do |
||||
is_expected.to be_json_eql(api_v3_paths.types_by_project(project.id).to_json) |
||||
.at_path('_links/types/href') |
||||
end |
||||
|
||||
it 'links to the work packages in the project' do |
||||
is_expected.to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json) |
||||
.at_path('_links/workPackages/href') |
||||
end |
||||
end |
||||
|
||||
context 'for a user having the manage_types permission' do |
||||
let(:permissions) { [:manage_types] } |
||||
|
||||
it 'links to the types active in the project' do |
||||
is_expected.to be_json_eql(api_v3_paths.types_by_project(project.id).to_json) |
||||
.at_path('_links/types/href') |
||||
end |
||||
end |
||||
|
||||
context 'for a user not having the necessary permissions' do |
||||
let(:permission) { [] } |
||||
|
||||
it 'has no types link' do |
||||
is_expected.to_not have_json_path('_links/types/href') |
||||
end |
||||
|
||||
it 'has no work packages link' do |
||||
is_expected.to_not have_json_path('_links/workPackages/href') |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'memberships' do |
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'memberships' } |
||||
let(:href) { api_v3_paths.path_for(:memberships, filters: [{ project: { operator: "=", values: [project.id.to_s] } }]) } |
||||
end |
||||
|
||||
context 'without the view_members permission' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'memberships' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'link custom field' do |
||||
context 'if the user is admin and the field is invisible' do |
||||
before do |
||||
allow(user) |
||||
.to receive(:admin?) |
||||
.and_return(true) |
||||
|
||||
version_custom_field.visible = false |
||||
end |
||||
|
||||
it 'links custom fields' do |
||||
is_expected |
||||
.to be_json_eql(api_v3_paths.version(version.id).to_json) |
||||
.at_path("_links/customField#{version_custom_field.id}/href") |
||||
end |
||||
end |
||||
|
||||
context 'if the user is no admin and the field is invisible' do |
||||
before do |
||||
version_custom_field.visible = false |
||||
end |
||||
|
||||
it "has no property for the int custom field" do |
||||
is_expected |
||||
.not_to have_json_path("links/customField#{version_custom_field.id}") |
||||
end |
||||
end |
||||
|
||||
context 'if the user is no admin and the field is visible' do |
||||
it 'links custom fields' do |
||||
is_expected |
||||
.to be_json_eql(api_v3_paths.version(version.id).to_json) |
||||
.at_path("_links/customField#{version_custom_field.id}/href") |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'update' do |
||||
context 'for a user having the edit_project permission' do |
||||
let(:permissions) { [:edit_project] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'update' } |
||||
let(:href) { api_v3_paths.project_form project.id } |
||||
end |
||||
end |
||||
|
||||
context 'for a user lacking the edit_project permission' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'update' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'updateImmediately' do |
||||
context 'for a user having the edit_project permission' do |
||||
let(:permissions) { [:edit_project] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'updateImmediately' } |
||||
let(:href) { api_v3_paths.project project.id } |
||||
end |
||||
end |
||||
|
||||
context 'for a user lacking the edit_project permission' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'updateImmediately' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'delete' do |
||||
context 'for a user being admin' do |
||||
before do |
||||
allow(user) |
||||
.to receive(:admin?) |
||||
.and_return(true) |
||||
end |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'delete' } |
||||
let(:href) { api_v3_paths.project project.id } |
||||
end |
||||
end |
||||
|
||||
context 'for a non admin user' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'delete' } |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '_embedded' do |
||||
describe 'parent' do |
||||
let(:embedded_path) { '_embedded/parent' } |
||||
|
||||
before do |
||||
allow(parent_project) |
||||
.to receive(:visible?) |
||||
.and_return(parent_visible) |
||||
end |
||||
|
||||
context 'when the user is allowed to see the parent' do |
||||
let(:parent_visible) { true } |
||||
|
||||
it 'has the parent embedded' do |
||||
expect(generated) |
||||
.to be_json_eql('Project'.to_json) |
||||
.at_path("#{embedded_path}/_type") |
||||
|
||||
expect(generated) |
||||
.to be_json_eql(parent_project.name.to_json) |
||||
.at_path("#{embedded_path}/name") |
||||
end |
||||
end |
||||
|
||||
context 'when the user is forbidden to see the parent' do |
||||
let(:parent_visible) { false } |
||||
|
||||
it 'hides the parent' do |
||||
expect(generated) |
||||
.not_to have_json_path(embedded_path) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'caching' do |
||||
it 'is based on the representer\'s cache_key' do |
||||
allow(OpenProject::Cache) |
||||
.to receive(:fetch) |
||||
.and_call_original |
||||
|
||||
representer.to_json |
||||
|
||||
expect(OpenProject::Cache) |
||||
.to have_received(:fetch) |
||||
.with(representer.json_cache_key) |
||||
end |
||||
|
||||
describe '#json_cache_key' do |
||||
let!(:former_cache_key) { representer.json_cache_key } |
||||
|
||||
it 'includes the name of the representer class' do |
||||
expect(representer.json_cache_key) |
||||
.to include('API', 'V3', 'Projects', 'ProjectRepresenter') |
||||
end |
||||
|
||||
it 'changes when the locale changes' do |
||||
I18n.with_locale(:fr) do |
||||
expect(representer.json_cache_key) |
||||
.not_to eql former_cache_key |
||||
end |
||||
end |
||||
|
||||
it 'changes when the project is updated' do |
||||
project.updated_at = Time.now + 20.seconds |
||||
|
||||
expect(representer.json_cache_key) |
||||
.not_to eql former_cache_key |
||||
end |
||||
|
||||
it 'changes when the project status is updated' do |
||||
project.status.updated_at = Time.now + 20.seconds |
||||
|
||||
expect(representer.json_cache_key) |
||||
.not_to eql former_cache_key |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '.checked_permissions' do |
||||
it 'lists add_work_packages' do |
||||
expect(described_class.checked_permissions).to match_array([:add_work_packages]) |
||||
end |
||||
end |
||||
end |
@ -1,474 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::API::V3::Projects::ProjectRepresenter do |
||||
include ::API::V3::Utilities::PathHelper |
||||
|
||||
let(:project) do |
||||
FactoryBot.build_stubbed(:project, |
||||
parent: parent_project, |
||||
description: 'some description', |
||||
status: status).tap do |p| |
||||
allow(p) |
||||
.to receive(:available_custom_fields) |
||||
.and_return([int_custom_field, version_custom_field]) |
||||
|
||||
allow(p) |
||||
.to receive(:"custom_field_#{int_custom_field.id}") |
||||
.and_return(int_custom_value.value) |
||||
|
||||
allow(p) |
||||
.to receive(:custom_value_for) |
||||
.with(version_custom_field) |
||||
.and_return(version_custom_value) |
||||
end |
||||
end |
||||
let(:status) do |
||||
FactoryBot.build_stubbed(:project_status) |
||||
end |
||||
let(:parent_project) { FactoryBot.build_stubbed(:project) } |
||||
let(:representer) { described_class.create(project, current_user: user) } |
||||
|
||||
let(:user) do |
||||
FactoryBot.build_stubbed(:user).tap do |u| |
||||
allow(u) |
||||
.to receive(:allowed_to?) do |permission, context| |
||||
permissions.include?(permission) && context == project |
||||
end |
||||
end |
||||
end |
||||
|
||||
let(:int_custom_field) { FactoryBot.build_stubbed(:int_project_custom_field, visible: false) } |
||||
let(:version_custom_field) { FactoryBot.build_stubbed(:version_project_custom_field, visible: true) } |
||||
let(:int_custom_value) do |
||||
CustomValue.new(custom_field: int_custom_field, |
||||
value: '1234', |
||||
customized: nil) |
||||
end |
||||
let(:version) { FactoryBot.build_stubbed(:version) } |
||||
let(:version_custom_value) do |
||||
CustomValue.new(custom_field: version_custom_field, |
||||
value: version.id, |
||||
customized: nil).tap do |cv| |
||||
allow(cv) |
||||
.to receive(:typed_value) |
||||
.and_return(version) |
||||
end |
||||
end |
||||
|
||||
let(:permissions) { %i[add_work_packages view_members] } |
||||
|
||||
context 'generation' do |
||||
subject(:generated) { representer.to_json } |
||||
|
||||
it { is_expected.to include_json('Project'.to_json).at_path('_type') } |
||||
|
||||
describe 'properties' do |
||||
it_behaves_like 'property', :_type do |
||||
let(:value) { 'Project' } |
||||
end |
||||
|
||||
it_behaves_like 'property', :id do |
||||
let(:value) { project.id } |
||||
end |
||||
|
||||
it_behaves_like 'property', :identifier do |
||||
let(:value) { project.identifier } |
||||
end |
||||
|
||||
it_behaves_like 'property', :name do |
||||
let(:value) { project.name } |
||||
end |
||||
|
||||
it_behaves_like 'property', :active do |
||||
let(:value) { project.active } |
||||
end |
||||
|
||||
it_behaves_like 'property', :public do |
||||
let(:value) { project.public } |
||||
end |
||||
|
||||
it_behaves_like 'formattable property', :description do |
||||
let(:value) { project.description } |
||||
end |
||||
|
||||
context 'statusExplanation' do |
||||
it_behaves_like 'formattable property', 'statusExplanation' do |
||||
let(:value) { status.explanation } |
||||
end |
||||
end |
||||
|
||||
it_behaves_like 'has UTC ISO 8601 date and time' do |
||||
let(:date) { project.created_at } |
||||
let(:json_path) { 'createdAt' } |
||||
end |
||||
|
||||
it_behaves_like 'has UTC ISO 8601 date and time' do |
||||
let(:date) { project.updated_at } |
||||
let(:json_path) { 'updatedAt' } |
||||
end |
||||
|
||||
context 'int custom field' do |
||||
context 'if the user is admin' do |
||||
before do |
||||
allow(user) |
||||
.to receive(:admin?) |
||||
.and_return(true) |
||||
end |
||||
|
||||
it "has a property for the int custom field" do |
||||
is_expected |
||||
.to be_json_eql(int_custom_value.value.to_json) |
||||
.at_path("customField#{int_custom_field.id}") |
||||
end |
||||
end |
||||
|
||||
context 'if the user is no admin' do |
||||
it "has no property for the int custom field" do |
||||
is_expected |
||||
.not_to have_json_path("customField#{int_custom_field.id}") |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '_links' do |
||||
it { is_expected.to have_json_type(Object).at_path('_links') } |
||||
|
||||
it 'links to self' do |
||||
expect(subject).to have_json_path('_links/self/href') |
||||
end |
||||
it 'has a title for link to self' do |
||||
expect(subject).to have_json_path('_links/self/title') |
||||
end |
||||
|
||||
describe 'create work packages' do |
||||
context 'user allowed to create work packages' do |
||||
it 'has the correct path for a create form' do |
||||
is_expected |
||||
.to be_json_eql(api_v3_paths.create_project_work_package_form(project.id).to_json) |
||||
.at_path('_links/createWorkPackage/href') |
||||
end |
||||
|
||||
it 'has the correct path to create a work package' do |
||||
is_expected.to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json) |
||||
.at_path('_links/createWorkPackageImmediately/href') |
||||
end |
||||
end |
||||
|
||||
context 'user not allowed to create work packages' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'createWorkPackage' } |
||||
end |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'createWorkPackageImmediately' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'parent' do |
||||
before do |
||||
allow(parent_project) |
||||
.to receive(:visible?) |
||||
.and_return(visible) |
||||
end |
||||
let(:visible) { true } |
||||
|
||||
it_behaves_like 'has a titled link' do |
||||
let(:link) { 'parent' } |
||||
let(:href) { api_v3_paths.project(parent_project.id) } |
||||
let(:title) { parent_project.name } |
||||
end |
||||
|
||||
context 'if lacking the permissions to see the parent' do |
||||
let(:visible) { false } |
||||
|
||||
it_behaves_like 'has a titled link' do |
||||
let(:link) { 'parent' } |
||||
let(:href) { nil } |
||||
let(:title) { nil } |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'status' do |
||||
it_behaves_like 'has a titled link' do |
||||
let(:link) { 'status' } |
||||
let(:href) { api_v3_paths.project_status(project.status.code) } |
||||
let(:title) { I18n.t(:"activerecord.attributes.projects/status.codes.#{project.status.code}") } |
||||
end |
||||
|
||||
context 'if the status is nil' do |
||||
let(:status) { nil } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'status' } |
||||
let(:href) { nil } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'categories' do |
||||
it 'has the correct link to its categories' do |
||||
is_expected.to be_json_eql(api_v3_paths.categories_by_project(project.id).to_json) |
||||
.at_path('_links/categories/href') |
||||
end |
||||
end |
||||
|
||||
describe 'versions' do |
||||
context 'with only manage_versions permission' do |
||||
let(:permissions) { [:manage_versions] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'versions' } |
||||
let(:href) { api_v3_paths.versions_by_project(project.id) } |
||||
end |
||||
end |
||||
|
||||
context 'with only view_work_packages permission' do |
||||
let(:permissions) { [:view_work_packages] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'versions' } |
||||
let(:href) { api_v3_paths.versions_by_project(project.id) } |
||||
end |
||||
end |
||||
|
||||
context 'without both permissions' do |
||||
let(:permissions) { [:add_work_packages] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'versions' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'types' do |
||||
context 'for a user having the view_work_packages permission' do |
||||
let(:permissions) { [:view_work_packages] } |
||||
|
||||
it 'links to the types active in the project' do |
||||
is_expected.to be_json_eql(api_v3_paths.types_by_project(project.id).to_json) |
||||
.at_path('_links/types/href') |
||||
end |
||||
|
||||
it 'links to the work packages in the project' do |
||||
is_expected.to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json) |
||||
.at_path('_links/workPackages/href') |
||||
end |
||||
end |
||||
|
||||
context 'for a user having the manage_types permission' do |
||||
let(:permissions) { [:manage_types] } |
||||
|
||||
it 'links to the types active in the project' do |
||||
is_expected.to be_json_eql(api_v3_paths.types_by_project(project.id).to_json) |
||||
.at_path('_links/types/href') |
||||
end |
||||
end |
||||
|
||||
context 'for a user not having the necessary permissions' do |
||||
let(:permission) { [] } |
||||
|
||||
it 'has no types link' do |
||||
is_expected.to_not have_json_path('_links/types/href') |
||||
end |
||||
|
||||
it 'has no work packages link' do |
||||
is_expected.to_not have_json_path('_links/workPackages/href') |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'memberships' do |
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'memberships' } |
||||
let(:href) { api_v3_paths.path_for(:memberships, filters: [{ project: { operator: "=", values: [project.id.to_s] } }]) } |
||||
end |
||||
|
||||
context 'without the view_members permission' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'memberships' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'link custom field' do |
||||
context 'if the user is admin and the field is invisible' do |
||||
before do |
||||
allow(user) |
||||
.to receive(:admin?) |
||||
.and_return(true) |
||||
|
||||
version_custom_field.visible = false |
||||
end |
||||
|
||||
it 'links custom fields' do |
||||
is_expected |
||||
.to be_json_eql(api_v3_paths.version(version.id).to_json) |
||||
.at_path("_links/customField#{version_custom_field.id}/href") |
||||
end |
||||
end |
||||
|
||||
context 'if the user is no admin and the field is invisible' do |
||||
before do |
||||
version_custom_field.visible = false |
||||
end |
||||
|
||||
it "has no property for the int custom field" do |
||||
is_expected |
||||
.not_to have_json_path("links/customField#{version_custom_field.id}") |
||||
end |
||||
end |
||||
|
||||
context 'if the user is no admin and the field is visible' do |
||||
it 'links custom fields' do |
||||
is_expected |
||||
.to be_json_eql(api_v3_paths.version(version.id).to_json) |
||||
.at_path("_links/customField#{version_custom_field.id}/href") |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'update' do |
||||
context 'for a user having the edit_project permission' do |
||||
let(:permissions) { [:edit_project] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'update' } |
||||
let(:href) { api_v3_paths.project_form project.id } |
||||
end |
||||
end |
||||
|
||||
context 'for a user lacking the edit_project permission' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'update' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'updateImmediately' do |
||||
context 'for a user having the edit_project permission' do |
||||
let(:permissions) { [:edit_project] } |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'updateImmediately' } |
||||
let(:href) { api_v3_paths.project project.id } |
||||
end |
||||
end |
||||
|
||||
context 'for a user lacking the edit_project permission' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'updateImmediately' } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'delete' do |
||||
context 'for a user being admin' do |
||||
before do |
||||
allow(user) |
||||
.to receive(:admin?) |
||||
.and_return(true) |
||||
end |
||||
|
||||
it_behaves_like 'has an untitled link' do |
||||
let(:link) { 'delete' } |
||||
let(:href) { api_v3_paths.project project.id } |
||||
end |
||||
end |
||||
|
||||
context 'for a non admin user' do |
||||
let(:permissions) { [] } |
||||
|
||||
it_behaves_like 'has no link' do |
||||
let(:link) { 'delete' } |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'caching' do |
||||
it 'is based on the representer\'s cache_key' do |
||||
expect(OpenProject::Cache) |
||||
.to receive(:fetch) |
||||
.with(representer.json_cache_key) |
||||
.and_call_original |
||||
|
||||
representer.to_json |
||||
end |
||||
|
||||
describe '#json_cache_key' do |
||||
let!(:former_cache_key) { representer.json_cache_key } |
||||
|
||||
it 'includes the name of the representer class' do |
||||
expect(representer.json_cache_key) |
||||
.to include('API', 'V3', 'Projects', 'ProjectRepresenter') |
||||
end |
||||
|
||||
it 'changes when the locale changes' do |
||||
I18n.with_locale(:fr) do |
||||
expect(representer.json_cache_key) |
||||
.not_to eql former_cache_key |
||||
end |
||||
end |
||||
|
||||
it 'changes when the project is updated' do |
||||
project.updated_at = Time.now + 20.seconds |
||||
|
||||
expect(representer.json_cache_key) |
||||
.not_to eql former_cache_key |
||||
end |
||||
|
||||
it 'changes when the project status is updated' do |
||||
project.status.updated_at = Time.now + 20.seconds |
||||
|
||||
expect(representer.json_cache_key) |
||||
.not_to eql former_cache_key |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '.checked_permissions' do |
||||
it 'lists add_work_packages' do |
||||
expect(described_class.checked_permissions).to match_array([:add_work_packages]) |
||||
end |
||||
end |
||||
end |