mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge branch 'dev' into feature/rails4
Signed-off-by: Alex Coles <alex@alexbcoles.com>
This commit is contained in:
@@ -49,6 +49,7 @@ gem 'warden-basic_auth', '~> 0.2.1'
|
||||
gem 'rails_autolink', '~> 1.1.6'
|
||||
gem 'will_paginate', '~> 3.0'
|
||||
gem 'acts_as_list', '~> 0.3.0'
|
||||
gem 'acts_as_countable', git: "https://github.com/finnlabs/acts_as_countable.git", ref: '2471265'
|
||||
|
||||
gem 'friendly_id', '~> 5.1.0'
|
||||
|
||||
@@ -93,7 +94,8 @@ gem 'syck', platforms: [:mri, :mingw, :x64_mingw], require: false
|
||||
gem 'gon', '~> 4.0'
|
||||
|
||||
# catch exceptions and send them to any airbrake compatible backend
|
||||
gem 'airbrake', '~> 4.1.0'
|
||||
# don't require by default, instead load on-demand when actually configured
|
||||
gem 'airbrake', '~> 4.1.0', require: false
|
||||
|
||||
group :production do
|
||||
# we use dalli as standard memcache client
|
||||
|
||||
+15
-7
@@ -14,6 +14,13 @@ GIT
|
||||
capybara
|
||||
rspec
|
||||
|
||||
GIT
|
||||
remote: https://github.com/finnlabs/acts_as_countable.git
|
||||
revision: 2471265a14b011d566d3865c1c52516f970c02e1
|
||||
ref: 2471265
|
||||
specs:
|
||||
acts_as_countable (0.1.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/finnlabs/rack-protection.git
|
||||
revision: 5a7d1bd2f05ca75faf7909c8cc978732a0080898
|
||||
@@ -30,7 +37,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/opf/openproject-translations.git
|
||||
revision: fa34ac5106d864158dc74231e460c5ec5055428b
|
||||
revision: 89f7a195cc3b2bc71e6fc35586b61951da4f7e8c
|
||||
branch: feature/rails4
|
||||
specs:
|
||||
openproject-translations (4.3.0)
|
||||
@@ -234,7 +241,7 @@ GEM
|
||||
hashie (3.4.1)
|
||||
hike (1.2.3)
|
||||
htmldiff (0.0.1)
|
||||
i18n (0.6.11)
|
||||
i18n (0.7.0)
|
||||
ice_nine (0.11.1)
|
||||
inflecto (0.0.2)
|
||||
interception (0.5)
|
||||
@@ -256,7 +263,7 @@ GEM
|
||||
mini_portile (0.6.2)
|
||||
minitest (4.7.5)
|
||||
mixlib-shellout (2.1.0)
|
||||
multi_json (1.11.0)
|
||||
multi_json (1.11.2)
|
||||
multi_test (0.1.2)
|
||||
multi_xml (0.5.5)
|
||||
mysql2 (0.3.18)
|
||||
@@ -302,7 +309,7 @@ GEM
|
||||
railties (>= 3.1, < 5.0)
|
||||
rabl (0.9.3)
|
||||
activesupport (>= 2.3.14)
|
||||
rack (1.5.4)
|
||||
rack (1.5.5)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (4.2.0)
|
||||
@@ -415,12 +422,12 @@ GEM
|
||||
simplecov-html (~> 0.7.1)
|
||||
simplecov-html (0.7.1)
|
||||
slop (3.6.0)
|
||||
sprockets (2.12.3)
|
||||
sprockets (2.12.4)
|
||||
hike (~> 1.2)
|
||||
multi_json (~> 1.0)
|
||||
rack (~> 1.0)
|
||||
tilt (~> 1.1, != 1.3.0)
|
||||
sprockets-rails (2.3.1)
|
||||
sprockets-rails (2.3.2)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
sprockets (>= 2.8, < 4.0)
|
||||
@@ -466,6 +473,7 @@ DEPENDENCIES
|
||||
activerecord-jdbcpostgresql-adapter
|
||||
activerecord-session_store
|
||||
activerecord-tableless (~> 1.0)
|
||||
acts_as_countable!
|
||||
acts_as_list (~> 0.3.0)
|
||||
airbrake (~> 4.1.0)
|
||||
autoprefixer-rails
|
||||
@@ -561,4 +569,4 @@ DEPENDENCIES
|
||||
will_paginate (~> 3.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.10.5
|
||||
1.10.6
|
||||
|
||||
@@ -26,38 +26,48 @@
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
Event.observe(window,'load',function() {
|
||||
/*
|
||||
If we're viewing a tag or branch, don't display it in the
|
||||
revision box
|
||||
*/
|
||||
var branch_selected = $('branch') && $('rev').getValue() == $('branch').getValue();
|
||||
var tag_selected = $('tag') && $('rev').getValue() == $('tag').getValue();
|
||||
if (branch_selected || tag_selected) {
|
||||
$('rev').setValue('');
|
||||
}
|
||||
(function($) {
|
||||
$(function() {
|
||||
var revision = $('#revision-identifier-input'),
|
||||
form = revision.closest('form'),
|
||||
tag = $('#revision-tag-select'),
|
||||
branch = $('#revision-branch-select'),
|
||||
selects = tag.add(branch),
|
||||
branch_selected = branch.length > 0 && revision.val() == branch.val(),
|
||||
tag_selected = tag.length > 0 && revision.val() == tag.val();
|
||||
|
||||
/*
|
||||
Copy the branch/tag value into the revision box, then disable
|
||||
the dropdowns before submitting the form
|
||||
*/
|
||||
$$('#branch,#tag').each(function(e) {
|
||||
e.observe('change',function(e) {
|
||||
$('rev').setValue(e.element().getValue());
|
||||
$$('#branch,#tag').invoke('disable');
|
||||
e.element().parentNode.submit();
|
||||
$$('#branch,#tag').invoke('enable');
|
||||
var sendForm = function() {
|
||||
selects.prop('disable', true);
|
||||
form.submit();
|
||||
selects.prop('disable', false);
|
||||
}
|
||||
|
||||
/*
|
||||
If we're viewing a tag or branch, don't display it in the
|
||||
revision box
|
||||
*/
|
||||
if (branch_selected || tag_selected) {
|
||||
revision.val('');
|
||||
}
|
||||
|
||||
/*
|
||||
Copy the branch/tag value into the revision box, then disable
|
||||
the dropdowns before submitting the form
|
||||
*/
|
||||
selects.on('change', function() {
|
||||
var select = $(this);
|
||||
revision.val(select.val());
|
||||
sendForm();
|
||||
});
|
||||
|
||||
/*
|
||||
Disable the branch/tag dropdowns before submitting the revision form
|
||||
*/
|
||||
revision.on('keydown', function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
sendForm();
|
||||
}
|
||||
});
|
||||
});
|
||||
}(jQuery));
|
||||
|
||||
/*
|
||||
Disable the branch/tag dropdowns before submitting the revision form
|
||||
*/
|
||||
$('rev').observe('keydown', function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
$$('#branch,#tag').invoke('disable');
|
||||
e.element().parentNode.submit();
|
||||
$$('#branch,#tag').invoke('enable');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -507,11 +507,6 @@ div.issue hr
|
||||
tr.time-entry
|
||||
white-space: normal
|
||||
|
||||
/* comments */
|
||||
|
||||
.wiki ol li
|
||||
list-style: decimal inside
|
||||
|
||||
/* scm */
|
||||
|
||||
#content table .changeset td.id a:hover
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
@include grid-content
|
||||
padding: 0 1rem 0.4rem 0
|
||||
|
||||
// Exclusive toggleable attribute groups
|
||||
// include a radio input to toggle them,
|
||||
// but the positioning is off.
|
||||
.attributes-group.-toggleable &
|
||||
cursor: pointer
|
||||
padding-left: 5px
|
||||
|
||||
.attributes-group--header-control
|
||||
@include grid-content(shrink)
|
||||
padding: 0 0 0.4rem 0
|
||||
|
||||
@@ -45,3 +45,4 @@ ins
|
||||
background-color: #f6f6f6
|
||||
color: #505050
|
||||
border: 1px solid #e4e4e4
|
||||
line-height: normal
|
||||
|
||||
@@ -169,6 +169,12 @@ $inplace-edit--selected-date-border-color: $primary-color-dark
|
||||
&.-default
|
||||
font-style: italic
|
||||
|
||||
p
|
||||
font-size: inherit
|
||||
|
||||
&:last-of-type
|
||||
margin-bottom: 0
|
||||
|
||||
.inplace-edit--preview
|
||||
border: 1px solid $inplace-edit--border-color
|
||||
padding: 0.375rem
|
||||
@@ -216,6 +222,7 @@ a.inplace-editing--trigger-link,
|
||||
.inplace-edit--icon-wrapper
|
||||
visibility: visible
|
||||
|
||||
|
||||
.inplace-editing--container
|
||||
@include grid-block
|
||||
border-color: #fff
|
||||
|
||||
@@ -112,3 +112,121 @@
|
||||
|
||||
})(jQuery);
|
||||
```
|
||||
|
||||
## Upload notifications
|
||||
|
||||
### Upload progress
|
||||
|
||||
```
|
||||
<div class="notification-box -upload">
|
||||
<div class="notification-box--content">
|
||||
<p class="notification-box--upload-status">Uploading files ...</p>
|
||||
<div>
|
||||
<div>
|
||||
<div class="notification-box--toggle-message">
|
||||
<a href="#"><i class="icon-arrow-right5-2"></i></a> 1 of 3 files finished
|
||||
</div>
|
||||
<div class="notification-box--uploads-wrapper">
|
||||
<ul class="notification-box--uploads">
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape1.png</span>
|
||||
<progress max="100" value="20">20%</progress>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape2.png</span>
|
||||
<progress max="100" value="50">50%</progress>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape3.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Upload progress done
|
||||
|
||||
```
|
||||
<div class="notification-box -upload">
|
||||
<div class="notification-box--content">
|
||||
<p class="notification-box--upload-status">Uploading files ...</p>
|
||||
<div>
|
||||
<div>
|
||||
<div class="notification-box--toggle-message">
|
||||
<a href="#"><i class="icon-arrow-right5-2"></i></a> 5 of 5 files finished
|
||||
</div>
|
||||
<div class="notification-box--uploads-wrapper">
|
||||
<ul class="notification-box--uploads">
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape1.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape2.png</span>
|
||||
<span class="upload-completed"><i class="icon-close -error"></i></span>
|
||||
<p class="notification-box--notice -error">Something went wrong!...</p>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape3.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape3.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape3.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Upload progress more then five files simultaneously
|
||||
|
||||
```
|
||||
<div class="notification-box -upload">
|
||||
<div class="notification-box--content">
|
||||
<p class="notification-box--upload-status">Uploading files ...</p>
|
||||
<div>
|
||||
<div>
|
||||
<div class="notification-box--toggle-message">
|
||||
<a href="#"><i class="icon-arrow-right5"></i></a> 3 of 5 files finished
|
||||
</div>
|
||||
<div class="notification-box--uploads-wrapper hide">
|
||||
<ul class="notification-box--uploads">
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape1.png</span>
|
||||
<progress max="100" value="20">20%</progress>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape2.png</span>
|
||||
<progress max="100" value="50">50%</progress>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape3.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape4.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
<li class="notification-box--uploads-element">
|
||||
<span class="filename">Awesome_Landscape5.png</span>
|
||||
<span class="upload-completed"><i class="icon-yes -success"></i></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
// NOTE: these are demo ski
|
||||
|
||||
//nm - notification messages
|
||||
//pb - progress bar
|
||||
|
||||
$nm-font-size: rem-calc(13px)
|
||||
$nm-border-radius: rem-calc(2px)
|
||||
$nm-box-padding: rem-calc(10px 35px 10px 35px)
|
||||
@@ -51,6 +53,17 @@ $nm-color-border: #8fc0db
|
||||
$nm-color-background: #e3f5ff
|
||||
$nm-padding-box: rem-calc(10px)
|
||||
|
||||
$nm-pb-border-color: $nm-color-border
|
||||
$nm-pb-color: $primary-color-dark
|
||||
$nm-pb-regress-color: whiteSmoke
|
||||
$nm-pb-border-radius: rem-calc(6)
|
||||
$nm-pb-height: rem-calc(20)
|
||||
|
||||
$nm-upload-status-font-size: rem-calc(18)
|
||||
$nm-upload-file-status-icon-size: rem-calc(12)
|
||||
|
||||
$nm-upload-box-padding: rem-calc(15) rem-calc(25)
|
||||
|
||||
%nm-icon-error
|
||||
@extend .icon-attention2:before
|
||||
color: $nm-color-error-icon
|
||||
@@ -63,6 +76,17 @@ $nm-padding-box: rem-calc(10px)
|
||||
@extend .icon-attention1:before
|
||||
color: $nm-color-warning-icon
|
||||
|
||||
%error-placeholder
|
||||
background-color: $nm-color-error-background
|
||||
border-color: $nm-color-error-border
|
||||
|
||||
%success-placeholder
|
||||
background-color: $nm-color-success-background
|
||||
border-color: $nm-color-success-border
|
||||
|
||||
%warning-placeholder
|
||||
background-color: $nm-color-warning-background
|
||||
border-color: $nm-color-warning-border
|
||||
|
||||
@mixin messages-icon($top:rem-calc(11), $left:rem-calc(10), $size:rem-calc(18))
|
||||
@include icon-common
|
||||
@@ -104,30 +128,29 @@ $nm-padding-box: rem-calc(10px)
|
||||
padding: $nm-box-padding
|
||||
|
||||
&.-error
|
||||
background-color: $nm-color-error-background
|
||||
border-color: $nm-color-error-border
|
||||
@extend %error-placeholder
|
||||
&::before
|
||||
@include messages-icon
|
||||
@extend %nm-icon-error
|
||||
|
||||
&.-success
|
||||
background-color: $nm-color-success-background
|
||||
border-color: $nm-color-success-border
|
||||
@extend %success-placeholder
|
||||
&::before
|
||||
@include messages-icon
|
||||
@extend %nm-icon-success
|
||||
|
||||
&.-warning
|
||||
background-color: $nm-color-warning-background
|
||||
border-color: $nm-color-warning-border
|
||||
@extend %warning-placeholder
|
||||
&::before
|
||||
@include messages-icon
|
||||
@extend %nm-icon-warning
|
||||
|
||||
&.-severe
|
||||
background-color: $nm-color-error-background
|
||||
border-color: $nm-color-error-border
|
||||
|
||||
&.-upload
|
||||
padding: $nm-upload-box-padding
|
||||
|
||||
//@hack adapting to provided styles
|
||||
//messy I dont have all the details for a magic resolve, this might change
|
||||
& p, & ul
|
||||
@@ -135,6 +158,15 @@ $nm-padding-box: rem-calc(10px)
|
||||
& p, & ul>li, & .button
|
||||
font-size: $nm-font-size
|
||||
|
||||
|
||||
[class^="icon-"]
|
||||
&.-error
|
||||
color: $nm-color-error-icon
|
||||
&.-success
|
||||
color: $nm-color-success-icon
|
||||
&.-warning
|
||||
color: $nm-color-warning-icon
|
||||
|
||||
.notification-box--wrapper
|
||||
position: absolute
|
||||
top: rem-calc(52px) //this value probably will change
|
||||
@@ -145,3 +177,72 @@ $nm-padding-box: rem-calc(10px)
|
||||
position: relative
|
||||
.notification-box
|
||||
margin-bottom: rem-calc(3px)
|
||||
|
||||
// awesome animations
|
||||
.notification-box
|
||||
&.ng-enter
|
||||
+transition(opacity 0.5s ease)
|
||||
opacity: 0
|
||||
&.ng-enter.ng-enter-active
|
||||
opacity: 1
|
||||
&.ng-leave
|
||||
+transition(opacity 0.5s ease)
|
||||
opacity: 1
|
||||
&.ng-leave.ng-leave-active
|
||||
opacity: 0
|
||||
|
||||
// upload notification styles
|
||||
%progress-styles
|
||||
appearance: none
|
||||
border-radius: $nm-pb-border-radius
|
||||
height: $nm-pb-height
|
||||
animation: animate-stripes 5s linear infinite
|
||||
background-color: $nm-pb-regress-color
|
||||
border: rem-calc(1) solid $nm-pb-border-color
|
||||
color: $nm-pb-color
|
||||
margin: 0
|
||||
|
||||
.notification-box--toggle-message
|
||||
font-size: rem-calc(16)
|
||||
margin: rem-calc(5 0 15 0)
|
||||
|
||||
.notification-box--uploads
|
||||
list-style: none
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
.notification-box--uploads-element
|
||||
margin: rem-calc(5 0)
|
||||
.filename
|
||||
font-weight: bold
|
||||
[class^="icon-"]
|
||||
font-size: $nm-upload-file-status-icon-size
|
||||
|
||||
.notification-box--upload-status
|
||||
font-size: $nm-upload-status-font-size !important
|
||||
line-height: $nm-upload-status-font-size !important
|
||||
|
||||
.notification-box--content
|
||||
width: 100%
|
||||
progress[value]
|
||||
@extend %progress-styles
|
||||
&::-moz-progress-bar
|
||||
border-radius: $nm-pb-border-radius
|
||||
&::-webkit-progress-bar
|
||||
background-color: $nm-pb-regress-color
|
||||
border-radius: $nm-pb-border-radius
|
||||
&::-webkit-progress-value
|
||||
border-radius: $nm-pb-border-radius
|
||||
|
||||
.notification-box--notice
|
||||
border-radius: $nm-border-radius
|
||||
border-style: solid
|
||||
border-width: rem-calc(1)
|
||||
padding: rem-calc(5)
|
||||
margin: rem-calc(5 0)
|
||||
&.-error
|
||||
@extend %error-placeholder
|
||||
&.-success
|
||||
@extend %success-placeholder
|
||||
&.-warning
|
||||
@extend %warning-placeholder
|
||||
|
||||
@@ -118,9 +118,11 @@ div.wiki
|
||||
|
||||
ol li
|
||||
list-style-type: decimal
|
||||
list-style-position: inherit
|
||||
|
||||
ul li
|
||||
list-style-type: disc
|
||||
list-style-position: inherit
|
||||
|
||||
li li
|
||||
margin-left: 1.5em
|
||||
|
||||
@@ -112,7 +112,7 @@ $work-package-details--tab-height: 40px
|
||||
margin: 0
|
||||
padding: 0
|
||||
text-align: center
|
||||
width: 18.6%
|
||||
width: 23.25%
|
||||
cursor: pointer
|
||||
a
|
||||
color: #333
|
||||
|
||||
@@ -30,14 +30,32 @@
|
||||
@extend %absolute-layout-mode
|
||||
|
||||
#content
|
||||
$flash-margins-padding: 10px + (2 * 4px)
|
||||
$flash-margins-padding: 10px
|
||||
|
||||
// HACK: workaround to ensure correct height applied to child elements
|
||||
// the dom looks like this
|
||||
// flash (generated from rails - if it was generated when rendering the page)
|
||||
// flash (generated from angular - exists always but is hidden with ng-hide if not needed)
|
||||
// div[ui-view] (main content we want to adjust the height for)
|
||||
// div style="clear:both;" (is pushed to overflow - of no relevance)
|
||||
//
|
||||
// There can be at most two flash messages visible on the page.
|
||||
// We need to adjust the hight of the div[ui-view] depending on the amount of flash messages.
|
||||
//
|
||||
// This makes use of more specific rules overwriting less specific ones.
|
||||
// Per default, the height is always 100%
|
||||
.flash + div[ui-view]
|
||||
height: 100%
|
||||
|
||||
// If there is only one flash message shown:
|
||||
// Subtract the height of the flash message
|
||||
.flash:not(.ng-hide) ~ div[ui-view]
|
||||
height: calc(100% - #{($content-flash-height + $flash-margins-padding)})
|
||||
|
||||
.flash.ng-hide + div[ui-view]
|
||||
height: 100%
|
||||
// If there are two flash messages shown:
|
||||
// Subtract the height of the two flash messages
|
||||
.flash:not(.ng-hide) ~ .flash:not(.ng-hide) ~ div[ui-view]
|
||||
height: calc(100% - #{2 * ($content-flash-height + $flash-margins-padding)})
|
||||
|
||||
// HACK: workaround to ensure correct height applied to child elements
|
||||
#work-packages-index
|
||||
@@ -125,6 +143,26 @@
|
||||
& > .button
|
||||
margin: .5rem .5rem 0 0
|
||||
|
||||
.work-package--attachments--files
|
||||
> h4
|
||||
margin-top: 5px
|
||||
|
||||
.work-package--attachments--drop-box
|
||||
border: 2px dashed $light-gray
|
||||
background-color: lighten($gray, 5)
|
||||
text-align: center
|
||||
padding: 20px
|
||||
|
||||
> p
|
||||
margin-bottom: 8px
|
||||
|
||||
.work-package--attachments--label
|
||||
font-size: 1rem
|
||||
font-weight: bold
|
||||
|
||||
.work-package--attachments--hint
|
||||
font-size: 85%
|
||||
|
||||
.controller-work_packages
|
||||
#work-packages-query-selection
|
||||
.select2-container
|
||||
@@ -154,3 +192,39 @@
|
||||
#attributes
|
||||
.form--field-container
|
||||
max-width: 400px
|
||||
|
||||
|
||||
%flex-grow-shrink-zero
|
||||
flex-grow: 0
|
||||
flex-shrink: 0
|
||||
|
||||
.work-package--attachments--files
|
||||
ul
|
||||
list-style: none
|
||||
margin: 0
|
||||
li
|
||||
margin: rem-calc(5 0)
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
.filename
|
||||
margin: rem-calc(0 5)
|
||||
flex-grow: 1
|
||||
flex-basis: auto
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
.filesize
|
||||
@extend %flex-grow-shrink-zero
|
||||
margin: rem-calc(0 10)
|
||||
.button
|
||||
@extend %flex-grow-shrink-zero
|
||||
margin: 0
|
||||
flex-basis: rem-calc(65)
|
||||
|
||||
//an workaround for ie10
|
||||
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none)
|
||||
//IE10+ specific styles go here
|
||||
.work-package--attachments--files ul li .filename
|
||||
width: rem-calc(354)
|
||||
display: block
|
||||
|
||||
@@ -43,8 +43,6 @@ class AdminController < ApplicationController
|
||||
end
|
||||
|
||||
def projects
|
||||
@no_configuration_data = Redmine::DefaultData::Loader::no_data?
|
||||
|
||||
@status = params[:status] ? params[:status].to_i : 1
|
||||
c = ARCondition.new(@status == 0 ? 'status <> 0' : ['status = ?', @status])
|
||||
|
||||
@@ -62,20 +60,6 @@ class AdminController < ApplicationController
|
||||
@plugins = Redmine::Plugin.all.sort
|
||||
end
|
||||
|
||||
# Loads the default configuration
|
||||
# (roles, types, statuses, workflow, enumerations)
|
||||
def default_configuration
|
||||
if request.post?
|
||||
begin
|
||||
Redmine::DefaultData::Loader::load(params[:lang])
|
||||
flash[:notice] = l(:notice_default_data_loaded)
|
||||
rescue => e
|
||||
flash[:error] = l(:error_can_t_load_default_data, ERB::Util.h(e.message))
|
||||
end
|
||||
end
|
||||
redirect_to action: 'index'
|
||||
end
|
||||
|
||||
def test_email
|
||||
raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
|
||||
# Force ActionMailer to raise delivery errors so we can catch it
|
||||
|
||||
@@ -110,11 +110,6 @@ class ApplicationController < ActionController::Base
|
||||
include Redmine::MenuManager::MenuController
|
||||
helper Redmine::MenuManager::MenuHelper
|
||||
|
||||
# TODO: needed? redmine doesn't
|
||||
Redmine::Scm::Base.all.each do |scm|
|
||||
require "repository/#{scm.underscore}"
|
||||
end
|
||||
|
||||
def default_url_options(_options = {})
|
||||
{ layout: params['layout'] }
|
||||
end
|
||||
@@ -219,6 +214,13 @@ class ApplicationController < ActionController::Base
|
||||
I18n.fallbacks.defaults = fallbacks
|
||||
end
|
||||
|
||||
##
|
||||
# Sets the language for the current request.
|
||||
# The language is determined with the following priority:
|
||||
#
|
||||
# 1. The language as configured by the user.
|
||||
# 2. The first language defined in the Accept-Language header sent by the browser.
|
||||
# 3. OpenProject's default language defined in the settings.
|
||||
def set_localization
|
||||
lang = nil
|
||||
lang = find_language(User.current.language) if User.current.logged?
|
||||
|
||||
@@ -90,13 +90,15 @@ class JournalsController < ApplicationController
|
||||
end
|
||||
|
||||
def diff
|
||||
journal = Journal::AggregatedJournal.for_journal(@journal)
|
||||
|
||||
if valid_diff?
|
||||
field = params[:field].parameterize.underscore.to_sym
|
||||
from = @journal.changed_data[field][0]
|
||||
to = @journal.changed_data[field][1]
|
||||
from = journal.details[field][0]
|
||||
to = journal.details[field][1]
|
||||
|
||||
@diff = Redmine::Helpers::Diff.new(to, from)
|
||||
@journable = @journal.journable
|
||||
@journable = journal.journable
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.js do
|
||||
|
||||
@@ -30,45 +30,61 @@
|
||||
require 'SVG/Graph/Bar'
|
||||
require 'SVG/Graph/BarHorizontal'
|
||||
require 'digest/sha1'
|
||||
require_dependency 'open_project/scm/adapters'
|
||||
|
||||
class ChangesetNotFound < Exception; end
|
||||
class InvalidRevisionParam < Exception; end
|
||||
class ChangesetNotFound < Exception
|
||||
end
|
||||
class InvalidRevisionParam < Exception
|
||||
end
|
||||
|
||||
class RepositoriesController < ApplicationController
|
||||
include PaginationHelper
|
||||
|
||||
menu_item :repository
|
||||
menu_item :settings, only: :edit
|
||||
menu_item :settings, only: [:edit, :destroy_info]
|
||||
default_search_scope :changesets
|
||||
|
||||
before_filter :find_repository, except: :edit
|
||||
before_filter :find_project, only: :edit
|
||||
before_filter :find_project_by_project_id
|
||||
before_filter :find_repository, except: [:edit, :update, :create, :destroy, :destroy_info]
|
||||
before_filter :authorize
|
||||
accept_key_auth :revisions
|
||||
|
||||
rescue_from Redmine::Scm::Adapters::CommandFailed, with: :show_error_command_failed
|
||||
rescue_from OpenProject::Scm::Exceptions::ScmError, with: :show_error_command_failed
|
||||
|
||||
def edit
|
||||
@repository = @project.repository
|
||||
if !@repository
|
||||
@repository = Repository.factory(params[:repository_scm])
|
||||
@repository.project = @project if @repository
|
||||
service = Scm::RepositoryFactoryService.new(@project, params)
|
||||
if service.build_temporary
|
||||
@repository = service.repository
|
||||
else
|
||||
logger.error("Cannot create repository for #{params[:scm_vendor]}")
|
||||
flash.now[:error] = service.build_error
|
||||
end
|
||||
if request.post? && @repository
|
||||
@repository.attributes = params[:repository]
|
||||
@repository.save
|
||||
end
|
||||
|
||||
menu_reload_required = if @repository.persisted? && !@project.repository
|
||||
@project.reload # needed to reload association
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
render template: '/projects/settings/repository',
|
||||
locals: { project: @project,
|
||||
reload_menu: menu_reload_required }
|
||||
end
|
||||
format.js { render 'repositories/settings/repository_form' }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@repository = @project.repository
|
||||
update_repository(params.fetch(:repository, {}))
|
||||
respond_to do |format|
|
||||
format.js { render 'repositories/settings/repository_form' }
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
service = Scm::RepositoryFactoryService.new(@project, params)
|
||||
if service.build_and_save
|
||||
@repository = service.repository
|
||||
flash[:notice] = l('repositories.create_successful')
|
||||
flash[:notice] << (' ' + l('repositories.create_managed_delay')) if @repository.managed?
|
||||
else
|
||||
flash[:error] = service.build_error
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.js { render js: "window.location = '#{settings_repository_tab_path}'" }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,9 +103,15 @@ class RepositoriesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_info
|
||||
@repository = @project.repository
|
||||
@back_link = settings_repository_tab_path
|
||||
end
|
||||
|
||||
def destroy
|
||||
@repository.destroy
|
||||
redirect_to controller: '/projects', action: 'settings', id: @project, tab: 'repository'
|
||||
@project.repository.destroy
|
||||
flash[:notice] = I18n.t('repositories.delete_sucessful')
|
||||
redirect_to settings_repository_tab_path
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -123,7 +145,8 @@ class RepositoriesController < ApplicationController
|
||||
end
|
||||
|
||||
def revisions
|
||||
@changesets = @repository.changesets.includes(:user, :repository)
|
||||
@changesets = @repository.changesets
|
||||
.includes(:user, :repository)
|
||||
.page(params[:page])
|
||||
.per_page(per_page_param)
|
||||
|
||||
@@ -138,13 +161,17 @@ class RepositoriesController < ApplicationController
|
||||
(show_error_not_found; return) unless @entry
|
||||
|
||||
# If the entry is a dir, show the browser
|
||||
(show; return) if @entry.is_dir?
|
||||
if @entry.dir?
|
||||
show
|
||||
return
|
||||
end
|
||||
|
||||
@content = @repository.cat(@path, @rev)
|
||||
(show_error_not_found; return) unless @content
|
||||
if 'raw' == params[:format] ||
|
||||
(@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
|
||||
!is_entry_text_data?(@content, @path)
|
||||
|
||||
# Force the download
|
||||
send_opt = { filename: filename_for_content_disposition(@path.split('/').last) }
|
||||
send_type = Redmine::MimeType.of(@path)
|
||||
@@ -174,6 +201,7 @@ class RepositoriesController < ApplicationController
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
private :is_entry_text_data?
|
||||
|
||||
def annotate
|
||||
@@ -204,9 +232,10 @@ class RepositoriesController < ApplicationController
|
||||
(show_error_not_found; return) unless @diff
|
||||
filename = "changeset_r#{@rev}"
|
||||
filename << "_r#{@rev_to}" if @rev_to
|
||||
send_data @diff.join, filename: "#{filename}.diff",
|
||||
type: 'text/x-patch',
|
||||
disposition: 'attachment'
|
||||
send_data @diff.join,
|
||||
filename: "#{filename}.diff",
|
||||
type: 'text/x-patch',
|
||||
disposition: 'attachment'
|
||||
else
|
||||
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
|
||||
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
|
||||
@@ -245,6 +274,7 @@ class RepositoriesController < ApplicationController
|
||||
end
|
||||
data = graph_commits_per_author(@repository)
|
||||
end
|
||||
|
||||
if data
|
||||
headers['Content-Type'] = 'image/svg+xml'
|
||||
send_data(data, type: 'image/svg+xml', disposition: 'inline')
|
||||
@@ -257,10 +287,26 @@ class RepositoriesController < ApplicationController
|
||||
|
||||
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
|
||||
|
||||
def update_repository(repo_params)
|
||||
@repository.attributes = @repository.class.permitted_params(repo_params)
|
||||
|
||||
if @repository.save
|
||||
flash.now[:notice] = l('repositories.update_settings_successful')
|
||||
else
|
||||
flash.now[:error] = @repository.errors.full_messages.join('\n')
|
||||
end
|
||||
end
|
||||
|
||||
def settings_repository_tab_path
|
||||
settings_project_path(@project, tab: 'repository')
|
||||
end
|
||||
|
||||
def find_repository
|
||||
@project = Project.find(params[:project_id])
|
||||
@repository = @project.repository
|
||||
(render_404; return false) unless @repository
|
||||
|
||||
@repository.scm.check_availability!
|
||||
|
||||
@path = params[:path] || ''
|
||||
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
|
||||
@rev_to = params[:rev_to]
|
||||
@@ -270,6 +316,8 @@ class RepositoriesController < ApplicationController
|
||||
raise InvalidRevisionParam
|
||||
end
|
||||
end
|
||||
rescue OpenProject::Scm::Exceptions::ScmEmpty
|
||||
render 'empty'
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
rescue InvalidRevisionParam
|
||||
@@ -280,7 +328,6 @@ class RepositoriesController < ApplicationController
|
||||
render_error message: l(:error_scm_not_found), status: 404
|
||||
end
|
||||
|
||||
# Handler for Redmine::Scm::Adapters::CommandFailed exception
|
||||
def show_error_command_failed(exception)
|
||||
render_error l(:error_scm_command_failed, exception.message)
|
||||
end
|
||||
|
||||
@@ -32,6 +32,8 @@ require 'open_project/repository_authentication'
|
||||
class SysController < ActionController::Base
|
||||
before_filter :check_enabled
|
||||
before_filter :require_basic_auth, only: [:repo_auth]
|
||||
before_filter :find_project, only: [:update_required_storage]
|
||||
before_filter :find_repository_with_storage, only: [:update_required_storage]
|
||||
|
||||
def projects
|
||||
p = Project.active.has_module(:repository)
|
||||
@@ -54,8 +56,10 @@ class SysController < ActionController::Base
|
||||
render nothing: true, status: 409
|
||||
else
|
||||
logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
|
||||
project.repository = Repository.factory(params[:vendor], params[:repository])
|
||||
if project.repository && project.repository.save
|
||||
service = Scm::RepositoryFactoryService.new(project, params)
|
||||
|
||||
if service.build_and_save
|
||||
project.repository = service.repository
|
||||
render xml: project.repository, status: 201
|
||||
else
|
||||
render nothing: true, status: 422
|
||||
@@ -63,6 +67,11 @@ class SysController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
def update_required_storage
|
||||
update_storage_information(@repository, params[:force] == '1')
|
||||
render nothing: true, status: 200
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
projects = []
|
||||
if params[:id]
|
||||
@@ -106,6 +115,33 @@ class SysController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def update_storage_information(repository, force = false)
|
||||
if force
|
||||
Delayed::Job.enqueue ::Scm::StorageUpdaterJob.new(repository)
|
||||
else
|
||||
repository.required_disk_storage
|
||||
end
|
||||
end
|
||||
|
||||
def find_project
|
||||
@project = Project.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render text: "Could not find project ##{params[:id]}.", status: 404
|
||||
end
|
||||
|
||||
def find_repository_with_storage
|
||||
@repository = @project.repository
|
||||
|
||||
if @repository.nil?
|
||||
render text: "Project ##{@project.id} does not have a repository.", status: 404
|
||||
else
|
||||
return true if @repository.scm.storage_available?
|
||||
render text: 'repositories.storage.not_available', status: 400
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def require_basic_auth
|
||||
authenticate_with_http_basic do |username, password|
|
||||
@authenticated_user = cached_user_login(username, password)
|
||||
|
||||
@@ -61,7 +61,7 @@ class WorkPackages::BulkController < ApplicationController
|
||||
work_package.assign_attributes attributes
|
||||
|
||||
call_hook(:controller_work_package_bulk_before_save, params: params, work_package: work_package)
|
||||
JournalObserver.instance.send_notification = params[:send_notification] == '0' ? false : true
|
||||
JournalManager.send_notification = params[:send_notification] == '0' ? false : true
|
||||
unless work_package.save
|
||||
unsaved_work_package_ids << work_package.id
|
||||
end
|
||||
|
||||
@@ -60,9 +60,10 @@ class WorkPackages::MovesController < ApplicationController
|
||||
:new_project_id,
|
||||
ids: [])
|
||||
|
||||
if r = work_package.move_to_project(@target_project, new_type, copy: @copy,
|
||||
attributes: permitted_params,
|
||||
journal_note: @notes)
|
||||
move_service = MoveWorkPackageService.new(work_package, current_user)
|
||||
if r = move_service.call(@target_project, new_type, copy: @copy,
|
||||
attributes: permitted_params,
|
||||
journal_note: @notes)
|
||||
moved_work_packages << r
|
||||
else
|
||||
unsaved_work_package_ids << work_package.id
|
||||
@@ -79,7 +80,7 @@ class WorkPackages::MovesController < ApplicationController
|
||||
else
|
||||
redirect_to project_work_packages_path(@project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_flash_from_bulk_work_package_save(work_packages, unsaved_work_package_ids)
|
||||
if unsaved_work_package_ids.empty? and not work_packages.empty?
|
||||
@@ -101,7 +102,7 @@ class WorkPackages::MovesController < ApplicationController
|
||||
def prepare_for_work_package_move
|
||||
@work_packages.sort!
|
||||
@copy = params.has_key? :copy
|
||||
@allowed_projects = WorkPackage.allowed_target_projects_on_move
|
||||
@allowed_projects = WorkPackage.allowed_target_projects_on_move(current_user)
|
||||
@target_project = @allowed_projects.detect { |p| p.id.to_s == params[:new_project_id].to_s } if params[:new_project_id]
|
||||
@target_project ||= @project
|
||||
@types = @target_project.types
|
||||
|
||||
@@ -131,7 +131,7 @@ class WorkPackagesController < ApplicationController
|
||||
def create
|
||||
call_hook(:controller_work_package_new_before_save, params: params, work_package: work_package)
|
||||
|
||||
WorkPackageObserver.instance.send_notification = send_notifications?
|
||||
JournalManager.send_notification = send_notifications?
|
||||
|
||||
work_package.attach_files(params[:attachments])
|
||||
|
||||
|
||||
@@ -98,6 +98,6 @@ module PlanningElementsHelper
|
||||
|
||||
formatter.send(:format_details,
|
||||
attribute,
|
||||
journal.changed_data[attribute])
|
||||
journal.details[attribute])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ module ProjectsHelper
|
||||
{ name: 'members', action: :manage_members, partial: 'projects/settings/members', label: :label_member_plural },
|
||||
{ name: 'versions', action: :manage_versions, partial: 'projects/settings/versions', label: :label_version_plural },
|
||||
{ name: 'categories', action: :manage_categories, partial: 'projects/settings/categories', label: :label_work_package_category_plural },
|
||||
{ name: 'repository', action: :manage_repository, partial: 'projects/settings/repository', label: :label_repository },
|
||||
{ name: 'repository', action: :manage_repository, partial: 'repositories/settings', label: :label_repository },
|
||||
{ name: 'boards', action: :manage_boards, partial: 'projects/settings/boards', label: :label_board_plural },
|
||||
{ name: 'activities', action: :manage_project_activities, partial: 'projects/settings/activities', label: :enumeration_activities },
|
||||
{ name: 'types', action: :manage_types, partial: 'projects/settings/types', label: :label_type_plural }
|
||||
|
||||
@@ -58,7 +58,8 @@ module RepositoriesHelper
|
||||
when 'A'
|
||||
# Detects moved/copied files
|
||||
if !change.from_path.blank?
|
||||
change.action = @changeset.changes.detect { |c| c.action == 'D' && c.path == change.from_path } ? 'R' : 'C'
|
||||
action = @changeset.changes.detect { |c| c.action == 'D' && c.path == change.from_path }
|
||||
change.action = action ? 'R' : 'C'
|
||||
end
|
||||
change
|
||||
when 'D'
|
||||
@@ -97,27 +98,30 @@ module RepositoriesHelper
|
||||
if s = tree[file][:s]
|
||||
style << ' folder'
|
||||
path_param = without_leading_slash(to_path_param(@repository.relative_path(file)))
|
||||
text = link_to(h(text), controller: '/repositories',
|
||||
action: 'show',
|
||||
project_id: @project,
|
||||
path: path_param,
|
||||
rev: @changeset.identifier)
|
||||
text = link_to(h(text),
|
||||
controller: '/repositories',
|
||||
action: 'show',
|
||||
project_id: @project,
|
||||
path: path_param,
|
||||
rev: @changeset.identifier)
|
||||
output << "<li class='#{style}'>#{text}</li>"
|
||||
output << render_changes_tree(s)
|
||||
elsif c = tree[file][:c]
|
||||
style << " change-#{c.action}"
|
||||
path_param = without_leading_slash(to_path_param(@repository.relative_path(c.path)))
|
||||
text = link_to(h(text), controller: '/repositories',
|
||||
action: 'entry',
|
||||
project_id: @project,
|
||||
path: path_param,
|
||||
rev: @changeset.identifier) unless c.action == 'D'
|
||||
text = link_to(h(text),
|
||||
controller: '/repositories',
|
||||
action: 'entry',
|
||||
project_id: @project,
|
||||
path: path_param,
|
||||
rev: @changeset.identifier) unless c.action == 'D'
|
||||
text << raw(" - #{h(c.revision)}") unless c.revision.blank?
|
||||
text << raw(' (' + link_to(l(:label_diff), controller: '/repositories',
|
||||
action: 'diff',
|
||||
project_id: @project,
|
||||
path: path_param,
|
||||
rev: @changeset.identifier) + ') ') if c.action == 'M'
|
||||
text << raw(' (' + link_to(l(:label_diff),
|
||||
controller: '/repositories',
|
||||
action: 'diff',
|
||||
project_id: @project,
|
||||
path: path_param,
|
||||
rev: @changeset.identifier) + ') ') if c.action == 'M'
|
||||
text << raw(' ' + content_tag('span', h(c.from_path), class: 'copied-from')) unless c.from_path.blank?
|
||||
output << "<li class='#{style}'>#{text}</li>"
|
||||
end
|
||||
@@ -155,6 +159,7 @@ module RepositoriesHelper
|
||||
end
|
||||
str = replace_invalid_utf8(str)
|
||||
end
|
||||
|
||||
private :to_utf8_internal
|
||||
|
||||
def replace_invalid_utf8(str)
|
||||
@@ -162,8 +167,8 @@ module RepositoriesHelper
|
||||
if str.respond_to?(:force_encoding)
|
||||
str.force_encoding('UTF-8')
|
||||
if !str.valid_encoding?
|
||||
str = str.encode('US-ASCII', invalid: :replace,
|
||||
undef: :replace, replace: '?').encode('UTF-8')
|
||||
str = str.encode("US-ASCII", invalid: :replace,
|
||||
undef: :replace, replace: '?').encode("UTF-8")
|
||||
end
|
||||
else
|
||||
# removes invalid UTF8 sequences
|
||||
@@ -175,36 +180,66 @@ module RepositoriesHelper
|
||||
str
|
||||
end
|
||||
|
||||
def repository_field_tags(form, repository)
|
||||
method = repository.class.name.demodulize.underscore + '_field_tags'
|
||||
if repository.is_a?(Repository) &&
|
||||
respond_to?(method) && method != 'repository_field_tags'
|
||||
send(method, form, repository)
|
||||
##
|
||||
# Retrieves all valid SCM vendors from the Manager
|
||||
# and injects an already persisted repository for correctly
|
||||
# displaying an existing repository.
|
||||
def scm_options(repository = nil)
|
||||
scms = OpenProject::Scm::Manager.enabled
|
||||
vendor = repository.nil? ? nil : repository.vendor
|
||||
|
||||
## Set selected vendor
|
||||
if vendor && !repository.new_record?
|
||||
scms[vendor] = vendor
|
||||
end
|
||||
|
||||
# Remove repositories that were configured to have no
|
||||
# available types left.
|
||||
scms.reject! { |_, klass| klass.available_types.empty? }
|
||||
|
||||
scms = [default_selected_option] + scms.keys
|
||||
options_for_select(scms, vendor)
|
||||
end
|
||||
|
||||
def scm_select_tag(repository)
|
||||
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
|
||||
Redmine::Scm::Base.configured.each do |scm|
|
||||
if Setting.enabled_scm.include?(scm) ||
|
||||
(repository && repository.class.name.demodulize == scm)
|
||||
scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
|
||||
end
|
||||
end
|
||||
select_tag('repository_scm',
|
||||
options_for_select(scm_options, repository.class.name.demodulize),
|
||||
disabled: (repository && !repository.new_record?),
|
||||
onchange: remote_function(
|
||||
url: {
|
||||
controller: '/repositories',
|
||||
action: 'edit',
|
||||
id: @project
|
||||
},
|
||||
method: :get,
|
||||
with: 'Form.serialize(this.form)')
|
||||
def default_selected_option
|
||||
[
|
||||
"--- #{l(:actionview_instancetag_blank_option)} ---",
|
||||
'',
|
||||
{ disabled: true, selected: true }
|
||||
]
|
||||
end
|
||||
|
||||
def vendor_name(repository)
|
||||
repository.vendor.underscore
|
||||
end
|
||||
|
||||
def scm_vendor_tag(repository)
|
||||
select_tag('scm_vendor',
|
||||
scm_options(repository),
|
||||
class: 'form--select repositories--remote-select',
|
||||
data: {
|
||||
remote: true,
|
||||
url: url_for(controller: '/repositories',
|
||||
action: 'edit', project_id: @project.id),
|
||||
},
|
||||
disabled: (repository && !repository.new_record?)
|
||||
)
|
||||
end
|
||||
|
||||
def git_path_encoding_options(repository)
|
||||
default = repository.new_record? ? 'UTF-8' : repository.path_encoding
|
||||
options_for_select(Setting::ENCODINGS, default)
|
||||
end
|
||||
|
||||
##
|
||||
# Determines whether the repository settings save button should be shown.
|
||||
# By default, it is not shown when repository exists and is managed.
|
||||
def show_settings_save_button?(repository)
|
||||
@repository.nil? ||
|
||||
@repository.new_record? ||
|
||||
!@repository.managed?
|
||||
end
|
||||
|
||||
def with_leading_slash(path)
|
||||
path.to_s.starts_with?('/') ? path : "/#{path}"
|
||||
end
|
||||
@@ -212,74 +247,4 @@ module RepositoriesHelper
|
||||
def without_leading_slash(path)
|
||||
path.gsub(%r{\A/+}, '')
|
||||
end
|
||||
|
||||
def subversion_field_tags(form, repository)
|
||||
url = content_tag('div', class: 'form--field') {
|
||||
form.text_field(:url,
|
||||
size: 60,
|
||||
required: true,
|
||||
disabled: (repository && !repository.root_url.blank?)) +
|
||||
content_tag('div',
|
||||
'file:///, http://, https://, svn://, svn+[tunnelscheme]://',
|
||||
class: 'form--field-instructions')
|
||||
}
|
||||
|
||||
login = content_tag('div', class: 'form--field') {
|
||||
form.text_field(:login, size: 30)
|
||||
}
|
||||
|
||||
pwd = content_tag('div', class: 'form--field') {
|
||||
form.password_field(:password,
|
||||
size: 30,
|
||||
name: 'ignore',
|
||||
value: ((repository.new_record? || repository.password.blank?) ? '' : ('x' * 15)),
|
||||
onfocus: "this.value=''; this.name='repository[password]';",
|
||||
onchange: "this.name='repository[password]';")
|
||||
}
|
||||
|
||||
url + login + pwd
|
||||
end
|
||||
|
||||
def git_field_tags(form, repository)
|
||||
url = content_tag('div', class: 'form--field -required') {
|
||||
form.text_field(:url,
|
||||
label: :label_git_path,
|
||||
size: 60,
|
||||
disabled: (repository && !repository.root_url.blank?)) +
|
||||
content_tag('div',
|
||||
l(:text_git_repo_example),
|
||||
class: 'form--field-instructions')
|
||||
}
|
||||
|
||||
encoding = content_tag('div', class: 'form--field') {
|
||||
form.select(:path_encoding,
|
||||
[nil] + Setting::ENCODINGS,
|
||||
label: l(:label_path_encoding)) +
|
||||
content_tag('div',
|
||||
l(:text_default_encoding),
|
||||
class: 'form--field-instructions')
|
||||
}
|
||||
|
||||
url + encoding
|
||||
end
|
||||
|
||||
def filesystem_field_tags(form, repository)
|
||||
url = content_tag('div', class: 'form--field -required') {
|
||||
form.text_field(:url,
|
||||
label: :label_filesystem_path,
|
||||
size: 60,
|
||||
disabled: (repository && !repository.root_url.blank?))
|
||||
}
|
||||
|
||||
encoding = content_tag('div', class: 'form--field') {
|
||||
form.select(:path_encoding,
|
||||
[nil] + Setting::ENCODINGS,
|
||||
label: l(:label_path_encoding)) +
|
||||
content_tag('div',
|
||||
l(:text_default_encoding),
|
||||
class: 'form--field-instructions')
|
||||
}
|
||||
|
||||
url + encoding
|
||||
end
|
||||
end
|
||||
|
||||
@@ -173,9 +173,9 @@ module WorkPackagesHelper
|
||||
|
||||
['start_date', 'due_date'].each do |date|
|
||||
if changed_dates[date].nil? &&
|
||||
journal.changed_data[date] &&
|
||||
journal.changed_data[date].first
|
||||
changed_dates[date] = " (<del>#{journal.changed_data[date].first}</del>)".html_safe
|
||||
journal.details[date] &&
|
||||
journal.details[date].first
|
||||
changed_dates[date] = " (<del>#{journal.details[date].first}</del>)".html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,8 +47,10 @@ class UserMailer < BaseMailer
|
||||
end
|
||||
end
|
||||
|
||||
def work_package_added(user, work_package, author)
|
||||
def work_package_added(user, journal, author)
|
||||
work_package = journal.journable.reload
|
||||
@issue = work_package # instance variable is used in the view
|
||||
@journal = journal
|
||||
|
||||
set_work_package_headers(work_package)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class Activity::WorkPackageActivityProvider < Activity::BaseActivityProvider
|
||||
state = ''
|
||||
journal = Journal.find(event['event_id'])
|
||||
|
||||
if journal.changed_data.empty? && !journal.initial?
|
||||
if journal.details.empty? && !journal.initial?
|
||||
state = '-note'
|
||||
else
|
||||
state = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(event['status_closed']) ? '-closed' : '-edit'
|
||||
|
||||
@@ -40,7 +40,9 @@ class AuthSource < ActiveRecord::Base
|
||||
def authenticate(_login, _password)
|
||||
end
|
||||
|
||||
# implemented by a subclass, should raise when no connection is possible and not raise on success
|
||||
def test_connection
|
||||
raise I18n.t('auth_source.using_abstract_auth_source')
|
||||
end
|
||||
|
||||
def auth_method_name
|
||||
|
||||
@@ -29,13 +29,12 @@
|
||||
|
||||
class CommentObserver < ActiveRecord::Observer
|
||||
def after_create(comment)
|
||||
return unless Notifier.notify?(:news_comment_added)
|
||||
return unless Setting.notified_events.include?('news_comment_added')
|
||||
|
||||
if comment.commented.is_a?(News)
|
||||
news = comment.commented
|
||||
recipients = news.recipients + news.watcher_recipients
|
||||
users = User.find_all_by_mails(recipients)
|
||||
users.each do |user|
|
||||
recipients.uniq.each do |user|
|
||||
UserMailer.news_comment_added(user, comment, User.current).deliver
|
||||
end
|
||||
end
|
||||
|
||||
+2
-51
@@ -30,6 +30,7 @@
|
||||
class Journal < ActiveRecord::Base
|
||||
self.table_name = 'journals'
|
||||
|
||||
include JournalChanges
|
||||
include JournalFormatter
|
||||
include FormatHooks
|
||||
|
||||
@@ -99,6 +100,7 @@ class Journal < ActiveRecord::Base
|
||||
get_changes
|
||||
end
|
||||
|
||||
# TODO Evaluate whether this can be removed without disturbing any migrations
|
||||
alias_method :changed_data, :details
|
||||
|
||||
def new_value_for(prop)
|
||||
@@ -134,57 +136,6 @@ class Journal < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def get_changes
|
||||
return {} if data.nil?
|
||||
|
||||
if @changes.nil?
|
||||
@changes = HashWithIndifferentAccess.new
|
||||
|
||||
if predecessor.nil?
|
||||
@changes = data.journaled_attributes.select { |_, v| !v.nil? }
|
||||
.inject({}) { |h, (k, v)| h[k] = [nil, v]; h }
|
||||
else
|
||||
normalized_data = JournalManager.normalize_newlines(data.journaled_attributes)
|
||||
normalized_predecessor_data = JournalManager.normalize_newlines(predecessor.data.journaled_attributes)
|
||||
|
||||
normalized_data.select { |k, v|
|
||||
# we dont record changes for changes from nil to empty strings and vice versa
|
||||
pred = normalized_predecessor_data[k]
|
||||
v != pred && (v.present? || pred.present?)
|
||||
}.each do |k, v|
|
||||
@changes[k] = [normalized_predecessor_data[k], v]
|
||||
end
|
||||
end
|
||||
|
||||
@changes.merge!(get_association_changes predecessor, 'attachable', 'attachments', :attachment_id, :filename)
|
||||
@changes.merge!(get_association_changes predecessor, 'customizable', 'custom_fields', :custom_field_id, :value)
|
||||
end
|
||||
|
||||
@changes
|
||||
end
|
||||
|
||||
def get_association_changes(predecessor, journal_association, association, key, value)
|
||||
changes = {}
|
||||
journal_assoc_name = "#{journal_association}_journals"
|
||||
|
||||
if predecessor.nil?
|
||||
send(journal_assoc_name).each_with_object(changes) { |a, h| h["#{association}_#{a.send(key)}"] = [nil, a.send(value)] }
|
||||
else
|
||||
current = send(journal_assoc_name).map(&:attributes)
|
||||
predecessor_attachable_journals = predecessor.send(journal_assoc_name).map(&:attributes)
|
||||
|
||||
merged_journals = JournalManager.merge_reference_journals_by_id current,
|
||||
predecessor_attachable_journals,
|
||||
key.to_s
|
||||
|
||||
changes.merge! JournalManager.added_references(merged_journals, association, value.to_s)
|
||||
changes.merge! JournalManager.removed_references(merged_journals, association, value.to_s)
|
||||
changes.merge! JournalManager.changed_references(merged_journals, association, value.to_s)
|
||||
end
|
||||
|
||||
changes
|
||||
end
|
||||
|
||||
def predecessor
|
||||
@predecessor ||= self.class
|
||||
.where(journable_type: journable_type, journable_id: journable_id)
|
||||
|
||||
@@ -41,13 +41,35 @@
|
||||
# be dropped
|
||||
class Journal::AggregatedJournal
|
||||
class << self
|
||||
def aggregated_journals(journable: nil)
|
||||
query_aggregated_journals(journable: journable).map { |journal|
|
||||
# Returns the aggregated journal that contains the specified (vanilla/pure) journal.
|
||||
def for_journal(pure_journal)
|
||||
raw = Journal::AggregatedJournal.query_aggregated_journals(journable: pure_journal.journable)
|
||||
.where("#{version_projection} >= ?", pure_journal.version)
|
||||
.first
|
||||
|
||||
raw ? Journal::AggregatedJournal.new(raw) : nil
|
||||
end
|
||||
|
||||
def with_notes_id(notes_id)
|
||||
raw_journal = query_aggregated_journals
|
||||
.where("#{table_name}.id = ?", notes_id)
|
||||
.first
|
||||
|
||||
raw_journal ? Journal::AggregatedJournal.new(raw_journal) : nil
|
||||
end
|
||||
|
||||
##
|
||||
# The +journable+ parameter allows to filter for aggregated journals of a given journable.
|
||||
#
|
||||
# The +until_version+ parameter can be used in conjunction with the +journable+ parameter
|
||||
# to see the aggregated journals as if no versions were known after the specified version.
|
||||
def aggregated_journals(journable: nil, until_version: nil)
|
||||
query_aggregated_journals(journable: journable, until_version: until_version).map { |journal|
|
||||
Journal::AggregatedJournal.new(journal)
|
||||
}
|
||||
end
|
||||
|
||||
def query_aggregated_journals(journable: nil)
|
||||
def query_aggregated_journals(journable: nil, until_version: nil)
|
||||
# Using the roughly aggregated groups from :sql_rough_group we need to merge journals
|
||||
# where an entry with empty notes follows an entry containing notes, so that the notes
|
||||
# from the main entry are taken, while the remaining information is taken from the
|
||||
@@ -60,21 +82,58 @@ class Journal::AggregatedJournal
|
||||
# that our own row (master) would not already have been merged by its predecessor. If it is
|
||||
# (that means if we can find a valid predecessor), we drop our current row, because it will
|
||||
# already be present (in a merged form) in the row of our predecessor.
|
||||
Journal.from("(#{sql_rough_group(journable, 1)}) #{table_name}")
|
||||
.joins("LEFT OUTER JOIN (#{sql_rough_group(journable, 2)}) addition
|
||||
Journal.from("(#{sql_rough_group(journable, until_version, 1)}) #{table_name}")
|
||||
.joins("LEFT OUTER JOIN (#{sql_rough_group(journable, until_version, 2)}) addition
|
||||
ON #{sql_on_groups_belong_condition(table_name, 'addition')}")
|
||||
.joins("LEFT OUTER JOIN (#{sql_rough_group(journable, 3)}) predecessor
|
||||
.joins("LEFT OUTER JOIN (#{sql_rough_group(journable, until_version, 3)}) predecessor
|
||||
ON #{sql_on_groups_belong_condition('predecessor', table_name)}")
|
||||
.where('predecessor.id IS NULL')
|
||||
.order("COALESCE(addition.created_at, #{table_name}.created_at) ASC")
|
||||
.select("#{table_name}.journable_id,
|
||||
#{table_name}.journable_type,
|
||||
#{table_name}.user_id,
|
||||
#{table_name}.notes,
|
||||
#{table_name}.id \"notes_id\",
|
||||
#{table_name}.version \"notes_version\",
|
||||
#{table_name}.activity_type,
|
||||
COALESCE(addition.created_at, #{table_name}.created_at) \"created_at\",
|
||||
COALESCE(addition.id, #{table_name}.id) \"id\",
|
||||
COALESCE(addition.version, #{table_name}.version) \"version\"")
|
||||
#{version_projection} \"version\"")
|
||||
end
|
||||
|
||||
# Returns whether "notification-hiding" should be assumed for the given journal pair.
|
||||
# This leads to an aggregated journal effectively blocking notifications of an earlier journal,
|
||||
# because it "steals" the addition from its predecessor. See the specs section under
|
||||
# "mail suppressing aggregation" (for EnqueueWorkPackageNotificationJob) for more details
|
||||
def hides_notifications?(successor, predecessor)
|
||||
return false unless successor && predecessor
|
||||
|
||||
timeout = Setting.journal_aggregation_time_minutes.to_i.minutes
|
||||
|
||||
if successor.journable_type != predecessor.journable_type ||
|
||||
successor.journable_id != predecessor.journable_id ||
|
||||
successor.user_id != predecessor.user_id ||
|
||||
(successor.created_at - predecessor.created_at) <= timeout
|
||||
return false
|
||||
end
|
||||
|
||||
# imaginary state in which the successor never existed
|
||||
# if this makes the predecessor disappear, the successor must have taken journals
|
||||
# from it (that now became part of the predecessor again).
|
||||
!Journal::AggregatedJournal
|
||||
.query_aggregated_journals(
|
||||
journable: successor.journable,
|
||||
until_version: successor.version - 1)
|
||||
.where("#{version_projection} = #{predecessor.version}")
|
||||
.exists?
|
||||
end
|
||||
|
||||
def table_name
|
||||
Journal.table_name
|
||||
end
|
||||
|
||||
def version_projection
|
||||
"COALESCE(addition.version, #{table_name}.version)"
|
||||
end
|
||||
|
||||
private
|
||||
@@ -88,21 +147,31 @@ class Journal::AggregatedJournal
|
||||
# To be able to self-join results of this statement, we add an additional column called
|
||||
# "group_number" to the result. This allows to compare a group resulting from this query with
|
||||
# its predecessor and successor.
|
||||
def sql_rough_group(journable, uid)
|
||||
def sql_rough_group(journable, until_version, uid)
|
||||
if until_version && !journable
|
||||
raise 'need to provide a journable, when specifying a version limit'
|
||||
end
|
||||
|
||||
sql = "SELECT predecessor.*, #{sql_group_counter(uid)} AS group_number
|
||||
FROM #{sql_rough_group_from_clause(uid)}
|
||||
LEFT OUTER JOIN journals successor
|
||||
ON predecessor.version + 1 = successor.version AND
|
||||
predecessor.journable_type = successor.journable_type AND
|
||||
predecessor.journable_id = successor.journable_id
|
||||
#{until_version ? " AND successor.version <= #{until_version}" : ''}
|
||||
WHERE (predecessor.user_id != successor.user_id OR
|
||||
(predecessor.notes != '' AND predecessor.notes IS NOT NULL) OR
|
||||
#{sql_beyond_aggregation_time?('predecessor', 'successor')} OR
|
||||
successor.id IS NULL)"
|
||||
|
||||
if journable
|
||||
raise 'journable has no id' if journable.id.nil?
|
||||
sql += " AND predecessor.journable_type = '#{journable.class.name}' AND
|
||||
predecessor.journable_id = #{journable.id}"
|
||||
|
||||
if until_version
|
||||
sql += " AND predecessor.version <= #{until_version}"
|
||||
end
|
||||
end
|
||||
|
||||
sql
|
||||
@@ -117,7 +186,7 @@ class Journal::AggregatedJournal
|
||||
group_counter = mysql_group_count_variable(uid)
|
||||
"(#{group_counter} := #{group_counter} + 1)"
|
||||
else
|
||||
'row_number() OVER ()'
|
||||
'row_number() OVER (ORDER BY predecessor.version ASC)'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -153,7 +222,12 @@ class Journal::AggregatedJournal
|
||||
# to be considered for aggregation. This takes the current instance settings for temporal
|
||||
# proximity into account.
|
||||
def sql_beyond_aggregation_time?(predecessor, successor)
|
||||
aggregation_time_seconds = Setting.journal_aggregation_time_minutes.to_i * 60
|
||||
aggregation_time_seconds = Setting.journal_aggregation_time_minutes.to_i.minutes
|
||||
if aggregation_time_seconds == 0
|
||||
# if aggregation is disabled, we consider everything to be beyond aggregation time
|
||||
# even if creation dates are exactly equal
|
||||
return '(true = true)'
|
||||
end
|
||||
|
||||
if OpenProject::Database.mysql?
|
||||
difference = "TIMESTAMPDIFF(second, #{predecessor}.created_at, #{successor}.created_at)"
|
||||
@@ -165,29 +239,44 @@ class Journal::AggregatedJournal
|
||||
|
||||
"(#{difference} > #{threshold})"
|
||||
end
|
||||
|
||||
def table_name
|
||||
Journal.table_name
|
||||
end
|
||||
end
|
||||
|
||||
include JournalChanges
|
||||
include JournalFormatter
|
||||
include Redmine::Acts::Journalized::FormatHooks
|
||||
|
||||
register_journal_formatter :diff, OpenProject::JournalFormatter::Diff
|
||||
register_journal_formatter :attachment, OpenProject::JournalFormatter::Attachment
|
||||
register_journal_formatter :custom_field, OpenProject::JournalFormatter::CustomField
|
||||
|
||||
alias_method :details, :get_changes
|
||||
|
||||
delegate :journable_type,
|
||||
:journable_id,
|
||||
:journable,
|
||||
:user_id,
|
||||
:user,
|
||||
:notes,
|
||||
:notes?,
|
||||
:activity_type,
|
||||
:created_at,
|
||||
:id,
|
||||
:version,
|
||||
:attributes,
|
||||
:attachable_journals,
|
||||
:customizable_journals,
|
||||
:editable_by?,
|
||||
to: :journal
|
||||
|
||||
def initialize(journal)
|
||||
@journal = journal
|
||||
end
|
||||
|
||||
# returns an instance of this class that is reloaded from the database
|
||||
def reloaded
|
||||
self.class.with_notes_id(notes_id)
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= User.find(user_id)
|
||||
end
|
||||
@@ -195,8 +284,9 @@ class Journal::AggregatedJournal
|
||||
def predecessor
|
||||
unless defined? @predecessor
|
||||
raw_journal = self.class.query_aggregated_journals(journable: journable)
|
||||
.where("#{Journal.table_name}.version < ?", version)
|
||||
.order("#{Journal.table_name}.version DESC")
|
||||
.where("#{self.class.version_projection} < ?", version)
|
||||
.except(:order)
|
||||
.order("#{self.class.version_projection} DESC")
|
||||
.first
|
||||
|
||||
@predecessor = raw_journal ? Journal::AggregatedJournal.new(raw_journal) : nil
|
||||
@@ -205,10 +295,28 @@ class Journal::AggregatedJournal
|
||||
@predecessor
|
||||
end
|
||||
|
||||
def successor
|
||||
unless defined? @successor
|
||||
raw_journal = self.class.query_aggregated_journals(journable: journable)
|
||||
.where("#{self.class.version_projection} > ?", version)
|
||||
.except(:order)
|
||||
.order("#{self.class.version_projection} ASC")
|
||||
.first
|
||||
|
||||
@successor = raw_journal ? Journal::AggregatedJournal.new(raw_journal) : nil
|
||||
end
|
||||
|
||||
@successor
|
||||
end
|
||||
|
||||
def initial?
|
||||
predecessor.nil?
|
||||
end
|
||||
|
||||
def data
|
||||
@data ||= "Journal::#{journable_type}Journal".constantize.find_by_journal_id(id)
|
||||
end
|
||||
|
||||
# ARs automagic addition of dynamic columns (those not present in the physical table) seems
|
||||
# not to work with PostgreSQL and simply return a string for unknown columns.
|
||||
# Thus we need to ensure manually that this column is correctly casted.
|
||||
@@ -216,6 +324,10 @@ class Journal::AggregatedJournal
|
||||
ActiveRecord::ConnectionAdapters::Column.value_to_integer(journal.notes_id)
|
||||
end
|
||||
|
||||
def notes_version
|
||||
ActiveRecord::ConnectionAdapters::Column.value_to_integer(journal.notes_version)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :journal
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
#++
|
||||
|
||||
class JournalManager
|
||||
class << self
|
||||
attr_accessor :send_notification
|
||||
end
|
||||
|
||||
self.send_notification = true
|
||||
|
||||
def self.is_journalized?(obj)
|
||||
not obj.nil? and obj.respond_to? :journals
|
||||
end
|
||||
@@ -85,31 +91,45 @@ class JournalManager
|
||||
# This would lead to false change information, otherwise.
|
||||
# We need to be careful though, because we want to accept false (and false.blank? == true)
|
||||
def self.remove_empty_associations(associations, value)
|
||||
associations.reject { |h| h.has_key?(value) && h[value].blank? && h[value] != false }
|
||||
associations.reject { |association|
|
||||
association.has_key?(value) &&
|
||||
association[value].blank? &&
|
||||
association[value] != false
|
||||
}
|
||||
end
|
||||
|
||||
def self.merge_reference_journals_by_id(current, predecessor, key)
|
||||
all_attachable_journal_ids = current.map { |j| j[key] } | predecessor.map { |j| j[key] }
|
||||
def self.merge_reference_journals_by_id(new_journals, old_journals, id_key)
|
||||
all_associated_journal_ids = new_journals.map { |j| j[id_key] } |
|
||||
old_journals.map { |j| j[id_key] }
|
||||
|
||||
all_attachable_journal_ids.each_with_object({}) { |i, h|
|
||||
h[i] = [predecessor.detect { |j| j[key] == i },
|
||||
current.detect { |j| j[key] == i }]
|
||||
all_associated_journal_ids.each_with_object({}) { |id, result|
|
||||
result[id] = [old_journals.detect { |j| j[id_key] == id },
|
||||
new_journals.detect { |j| j[id_key] == id }]
|
||||
}
|
||||
end
|
||||
|
||||
def self.added_references(merged_references, key, value)
|
||||
merged_references.select { |_, v| v[0].nil? and not v[1].nil? }
|
||||
.each_with_object({}) { |k, h| h["#{key}_#{k[0]}"] = [nil, k[1][1][value]] }
|
||||
merged_references.select { |_, (old_attributes, new_attributes)|
|
||||
old_attributes.nil? && !new_attributes.nil?
|
||||
}.each_with_object({}) { |(id, (_, new_attributes)), result|
|
||||
result["#{key}_#{id}"] = [nil, new_attributes[value]]
|
||||
}
|
||||
end
|
||||
|
||||
def self.removed_references(merged_references, key, value)
|
||||
merged_references.select { |_, v| not v[0].nil? and v[1].nil? }
|
||||
.each_with_object({}) { |k, h| h["#{key}_#{k[0]}"] = [k[1][0][value], nil] }
|
||||
merged_references.select { |_, (old_attributes, new_attributes)|
|
||||
!old_attributes.nil? && new_attributes.nil?
|
||||
}.each_with_object({}) { |(id, (old_attributes, _)), result|
|
||||
result["#{key}_#{id}"] = [old_attributes[value], nil]
|
||||
}
|
||||
end
|
||||
|
||||
def self.changed_references(merged_references, key, value)
|
||||
merged_references.select { |_, v| not v[0].nil? and not v[1].nil? and v[0][value] != v[1][value] }
|
||||
.each_with_object({}) { |k, h| h["#{key}_#{k[0]}"] = [k[1][0][value], k[1][1][value]] }
|
||||
merged_references.select { |_, (old_attributes, new_attributes)|
|
||||
!old_attributes.nil? && !new_attributes.nil? && old_attributes[value] != new_attributes[value]
|
||||
}.each_with_object({}) { |(id, (old_attributes, new_attributes)), result|
|
||||
result["#{key}_#{id}"] = [old_attributes[value], new_attributes[value]]
|
||||
}
|
||||
end
|
||||
|
||||
def self.recreate_initial_journal(type, journal, changed_data)
|
||||
@@ -136,7 +156,7 @@ class JournalManager
|
||||
journable_type: journal_class_name(journable.class),
|
||||
version: version,
|
||||
activity_type: journable.send(:activity_type),
|
||||
changed_data: journable.attributes.symbolize_keys }
|
||||
details: journable.attributes.symbolize_keys }
|
||||
|
||||
create_journal journable, journal_attributes, user, notes
|
||||
end
|
||||
@@ -146,17 +166,19 @@ class JournalManager
|
||||
type = base_class(journable.class)
|
||||
extended_journal_attributes = journal_attributes.merge(journable_type: type.to_s)
|
||||
.merge(notes: notes)
|
||||
.except(:changed_data)
|
||||
.except(:details)
|
||||
.except(:id)
|
||||
|
||||
unless extended_journal_attributes.has_key? :user_id
|
||||
extended_journal_attributes[:user_id] = user.id
|
||||
end
|
||||
|
||||
journal_attributes[:changed_data] = normalize_newlines(journal_attributes[:changed_data])
|
||||
journal_attributes[:details] = normalize_newlines(journal_attributes[:details])
|
||||
|
||||
journal = journable.journals.build extended_journal_attributes
|
||||
journal.data = create_journal_data journal.id, type, valid_journal_attributes(type, journal_attributes[:changed_data])
|
||||
journal.data = create_journal_data journal.id,
|
||||
type,
|
||||
valid_journal_attributes(type, journal_attributes[:details])
|
||||
|
||||
create_association_data journable, journal
|
||||
|
||||
@@ -253,4 +275,8 @@ class JournalManager
|
||||
journal.customizable_journals.build custom_field_id: cv.custom_field_id, value: cv.value
|
||||
end
|
||||
end
|
||||
|
||||
def self.reset_notification
|
||||
@send_notification = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
class JournalNotificationMailer
|
||||
class << self
|
||||
def distinguish_journals(journal, send_notification)
|
||||
if send_notification
|
||||
if journal.journable_type == 'WorkPackage'
|
||||
handle_work_package_journal(journal)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_work_package_journal(journal)
|
||||
return nil unless send_notification? journal
|
||||
|
||||
aggregated = find_aggregated_journal_for(journal)
|
||||
|
||||
# Send the notification on behalf of the predecessor in case it could not send it on its own
|
||||
if Journal::AggregatedJournal.hides_notifications?(aggregated, aggregated.predecessor)
|
||||
job = DeliverWorkPackageNotificationJob.new(aggregated.predecessor.id, User.current.id)
|
||||
Delayed::Job.enqueue job
|
||||
end
|
||||
|
||||
job = EnqueueWorkPackageNotificationJob.new(journal.id, User.current.id)
|
||||
Delayed::Job.enqueue job, run_at: delivery_time
|
||||
end
|
||||
|
||||
def send_notification?(journal)
|
||||
(Setting.notified_events.include?('work_package_added') && journal.initial?) ||
|
||||
(Setting.notified_events.include?('work_package_updated') && !journal.initial?) ||
|
||||
notify_for_notes?(journal) ||
|
||||
notify_for_status?(journal) ||
|
||||
notify_for_priority(journal)
|
||||
end
|
||||
|
||||
def notify_for_notes?(journal)
|
||||
Setting.notified_events.include?('work_package_note_added') && journal.notes.present?
|
||||
end
|
||||
|
||||
def notify_for_status?(journal)
|
||||
Setting.notified_events.include?('status_updated') &&
|
||||
journal.details.has_key?(:status_id)
|
||||
end
|
||||
|
||||
def notify_for_priority(journal)
|
||||
Setting.notified_events.include?('work_package_priority_updated') &&
|
||||
journal.details.has_key?(:priority_id)
|
||||
end
|
||||
|
||||
def delivery_time
|
||||
Setting.journal_aggregation_time_minutes.to_i.minutes.from_now
|
||||
end
|
||||
|
||||
def find_aggregated_journal_for(raw_journal)
|
||||
wp_journals = Journal::AggregatedJournal.aggregated_journals(journable: raw_journal.journable)
|
||||
wp_journals.detect { |journal| journal.version == raw_journal.version }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,67 +0,0 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
class JournalObserver < ActiveRecord::Observer
|
||||
attr_accessor :send_notification
|
||||
|
||||
def after_create(journal)
|
||||
if journal.journable_type == 'WorkPackage' and !journal.initial? and send_notification
|
||||
after_create_issue_journal(journal)
|
||||
end
|
||||
clear_notification
|
||||
end
|
||||
|
||||
def after_create_issue_journal(journal)
|
||||
if Setting.notified_events.include?('work_package_updated') ||
|
||||
(Setting.notified_events.include?('work_package_note_added') && journal.notes.present?) ||
|
||||
(Setting.notified_events.include?('status_updated') && journal.changed_data.has_key?(:status_id)) ||
|
||||
(Setting.notified_events.include?('work_package_priority_updated') && journal.changed_data.has_key?(:priority_id))
|
||||
issue = journal.journable
|
||||
recipients = issue.recipients + issue.watcher_recipients
|
||||
users = User.find_all_by_mails(recipients.uniq)
|
||||
users.each do |user|
|
||||
job = DeliverWorkPackageUpdatedJob.new(user.id, journal.id, User.current.id)
|
||||
Delayed::Job.enqueue job
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Wrap send_notification so it defaults to true, when it's nil
|
||||
def send_notification
|
||||
return true if @send_notification.nil?
|
||||
@send_notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Need to clear the notification setting after each usage otherwise it might be cached
|
||||
def clear_notification
|
||||
@send_notification = true
|
||||
end
|
||||
end
|
||||
@@ -53,10 +53,11 @@ class LdapAuthSource < AuthSource
|
||||
|
||||
# test the connection to the LDAP
|
||||
def test_connection
|
||||
ldap_con = initialize_ldap_con(account, account_password)
|
||||
ldap_con.open {}
|
||||
unless authenticate_dn(account, account_password)
|
||||
raise I18n.t('auth_source.ldap_error', error_message: I18n.t('auth_source.ldap_auth_failed'))
|
||||
end
|
||||
rescue Net::LDAP::LdapError => text
|
||||
raise 'LdapError: ' + text.to_s
|
||||
raise I18n.t('auth_source.ldap_error', error_message: text.to_s)
|
||||
end
|
||||
|
||||
def auth_method_name
|
||||
|
||||
@@ -115,6 +115,7 @@ class LegacyJournal < ActiveRecord::Base
|
||||
attributes['changed_data'] || {}
|
||||
end
|
||||
|
||||
# TODO Evaluate whether this can be removed without disturbing any migrations
|
||||
alias_method :changed_data, :details
|
||||
|
||||
def new_value_for(prop)
|
||||
|
||||
@@ -33,8 +33,7 @@ class MessageObserver < ActiveRecord::Observer
|
||||
recipients = message.recipients
|
||||
recipients += message.root.watcher_recipients
|
||||
recipients += message.board.watcher_recipients
|
||||
users = User.find_all_by_mails(recipients.uniq)
|
||||
users.each do |user|
|
||||
recipients.uniq.each do |user|
|
||||
UserMailer.message_posted(user, message, User.current).deliver
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
class NewsObserver < ActiveRecord::Observer
|
||||
def after_create(news)
|
||||
if Setting.notified_events.include?('news_added')
|
||||
users = User.find_all_by_mails(news.recipients)
|
||||
users.each do |user|
|
||||
news.recipients.uniq.each do |user|
|
||||
UserMailer.news_added(user, news, User.current).deliver
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,25 +46,6 @@ class PlanningElementTypeColor < ActiveRecord::Base
|
||||
validates_length_of :name, maximum: 255, unless: lambda { |e| e.name.blank? }
|
||||
validates_format_of :hexcode, with: /\A#[0-9A-F]{6}\z/, unless: lambda { |e| e.hexcode.blank? }
|
||||
|
||||
def self.colors
|
||||
[
|
||||
find_or_initialize_by(name: 'Black', hexcode: '#000000'),
|
||||
find_or_initialize_by(name: 'White', hexcode: '#FFFFFF'),
|
||||
find_or_initialize_by(name: 'Blue', hexcode: '#3399CC'),
|
||||
find_or_initialize_by(name: 'Mint', hexcode: '#66CCCC'),
|
||||
find_or_initialize_by(name: 'Lime', hexcode: '#66CC99'),
|
||||
find_or_initialize_by(name: 'Green-neon', hexcode: '#00CC33'),
|
||||
find_or_initialize_by(name: 'Green', hexcode: '#339933'),
|
||||
find_or_initialize_by(name: 'Orange', hexcode: '#FFCC00'),
|
||||
find_or_initialize_by(name: 'Red', hexcode: '#CC3333'),
|
||||
find_or_initialize_by(name: 'Red-bright', hexcode: '#FF3300'),
|
||||
find_or_initialize_by(name: 'Yellow', hexcode: '#FFFF00'),
|
||||
find_or_initialize_by(name: 'Purple', hexcode: '#CC0066'),
|
||||
find_or_initialize_by(name: 'Grey-dark', hexcode: '#666666'),
|
||||
find_or_initialize_by(name: 'Grey-light', hexcode: '#DDDDDD')
|
||||
]
|
||||
end
|
||||
|
||||
def text_hexcode
|
||||
# 0.63 - Optimal threshold to switch between white and black text color
|
||||
# determined by intensive user tests and expensive research
|
||||
|
||||
+23
-2
@@ -108,6 +108,9 @@ class Project < ActiveRecord::Base
|
||||
author: nil,
|
||||
datetime: :created_on
|
||||
|
||||
acts_as_countable :required_project_storage,
|
||||
countable: [:work_packages, :repository]
|
||||
|
||||
attr_protected :status
|
||||
|
||||
validates_presence_of :name, :identifier
|
||||
@@ -632,9 +635,9 @@ class Project < ActiveRecord::Base
|
||||
possible_responsible_members.map(&:principal).compact.sort
|
||||
end
|
||||
|
||||
# Returns the mail adresses of users that should be always notified on project events
|
||||
# Returns users that should be always notified on project events
|
||||
def recipients
|
||||
notified_users.map(&:mail)
|
||||
notified_users
|
||||
end
|
||||
|
||||
# Returns the users that should be notified on project events
|
||||
@@ -943,6 +946,24 @@ class Project < ActiveRecord::Base
|
||||
update_attribute :status, STATUS_ARCHIVED
|
||||
end
|
||||
|
||||
def required_storage
|
||||
Rails.cache.fetch("project##{id}/project_storage",
|
||||
expires_in: self.class.project_storage_expires_in) do
|
||||
count_for(:required_project_storage).first
|
||||
end
|
||||
end
|
||||
|
||||
def self.total_projects_size
|
||||
Rails.cache.fetch('projects/total_projects_size',
|
||||
expires_in: project_storage_expires_in) do
|
||||
Project.all.map { |p| p.required_storage[:total] }.sum
|
||||
end
|
||||
end
|
||||
|
||||
def self.project_storage_expires_in
|
||||
Setting.project_storage_cache_minutes.to_i.minutes
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def self.possible_principles_condition
|
||||
|
||||
@@ -99,7 +99,7 @@ module Queries::WorkPackages::AvailableFilterOptions
|
||||
@available_work_package_filters = {
|
||||
status_id: { type: :list_status, order: 1, values: Status.all.map { |s| [s.name, s.id.to_s] } },
|
||||
type_id: { type: :list, order: 2, values: types.map { |s| [s.name, s.id.to_s] } },
|
||||
priority_id: { type: :list, order: 3, values: IssuePriority.all.map { |s| [s.name, s.id.to_s] } },
|
||||
priority_id: { type: :list, order: 3, values: IssuePriority.active.map { |s| [s.name, s.id.to_s] } },
|
||||
subject: { type: :text, order: 8 },
|
||||
created_at: { type: :date_past, order: 9 },
|
||||
updated_at: { type: :date_past, order: 10 },
|
||||
|
||||
+139
-47
@@ -29,6 +29,7 @@
|
||||
|
||||
class Repository < ActiveRecord::Base
|
||||
include Redmine::Ciphering
|
||||
include OpenProject::Scm::ManageableRepository
|
||||
|
||||
belongs_to :project
|
||||
has_many :changesets, -> {
|
||||
@@ -37,6 +38,10 @@ class Repository < ActiveRecord::Base
|
||||
|
||||
before_save :sanitize_urls
|
||||
|
||||
# Managed repository lifetime
|
||||
after_create :create_managed_repository, if: Proc.new { |repo| repo.managed? }
|
||||
after_destroy :delete_managed_repository, if: Proc.new { |repo| repo.managed? }
|
||||
|
||||
# Raw SQL to delete changesets and changes in the database
|
||||
# has_many :changesets, :dependent => :destroy is too slow for big repositories
|
||||
before_destroy :clear_changesets
|
||||
@@ -46,6 +51,10 @@ class Repository < ActiveRecord::Base
|
||||
validates_length_of :password, maximum: 255, allow_nil: true
|
||||
validate :validate_enabled_scm, on: :create
|
||||
|
||||
acts_as_countable :required_project_storage,
|
||||
label: 'label_repository',
|
||||
countable: :required_disk_storage
|
||||
|
||||
def changes
|
||||
Change.where(changeset_id: changesets).joins(:changeset)
|
||||
end
|
||||
@@ -80,12 +89,32 @@ class Repository < ActiveRecord::Base
|
||||
def scm
|
||||
@scm ||= scm_adapter.new(url, root_url,
|
||||
login, password, path_encoding)
|
||||
update_attribute(:root_url, @scm.root_url) if root_url.blank?
|
||||
|
||||
# override the adapter's root url with the full url
|
||||
# if none other was set.
|
||||
unless @scm.root_url.present?
|
||||
@scm.root_url = root_url.presence || url
|
||||
end
|
||||
|
||||
@scm
|
||||
end
|
||||
|
||||
def scm_name
|
||||
self.class.scm_name
|
||||
def self.scm_config
|
||||
scm_adapter_class.config
|
||||
end
|
||||
|
||||
def self.available_types
|
||||
supported_types - disabled_types
|
||||
end
|
||||
|
||||
##
|
||||
# Retrieves the :disabled_types setting from `configuration.yml
|
||||
def self.disabled_types
|
||||
scm_config[:disabled_types] || []
|
||||
end
|
||||
|
||||
def vendor
|
||||
self.class.vendor
|
||||
end
|
||||
|
||||
def supports_cat?
|
||||
@@ -148,6 +177,19 @@ class Repository < ActiveRecord::Base
|
||||
path
|
||||
end
|
||||
|
||||
def required_disk_storage
|
||||
if scm.storage_available?
|
||||
oldest_cachable_time = Setting.repository_storage_cache_minutes.to_i.minutes.ago
|
||||
if storage_updated_at.nil? ||
|
||||
storage_updated_at < oldest_cachable_time
|
||||
|
||||
Delayed::Job.enqueue ::Scm::StorageUpdaterJob.new(self)
|
||||
end
|
||||
|
||||
required_storage_bytes
|
||||
end
|
||||
end
|
||||
|
||||
# Finds and returns a revision with a number or the beginning of a hash
|
||||
def find_changeset_by_name(name)
|
||||
name = name.to_s
|
||||
@@ -240,7 +282,7 @@ class Repository < ActiveRecord::Base
|
||||
if project.repository
|
||||
begin
|
||||
project.repository.fetch_changesets
|
||||
rescue Redmine::Scm::Adapters::CommandFailed => e
|
||||
rescue OpenProject::Scm::Exceptions::CommandFailed => e
|
||||
logger.error "scm: error during fetching changesets: #{e.message}"
|
||||
end
|
||||
end
|
||||
@@ -252,61 +294,83 @@ class Repository < ActiveRecord::Base
|
||||
all.each(&:scan_changesets_for_work_package_ids)
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Abstract'
|
||||
|
||||
##
|
||||
# Builds a model instance of type +Repository::#{vendor}+ with the given parameters.
|
||||
#
|
||||
# @param [Project] project The project this repository belongs to.
|
||||
# @param [String] vendor The SCM vendor name (e.g., Git, Subversion)
|
||||
# @param [Hash] params Custom parameters for this SCM as delivered from the repository
|
||||
# field.
|
||||
#
|
||||
# @param [Symbol] type SCM tag to determine the type this repository should be built as
|
||||
#
|
||||
# @raise [OpenProject::Scm::RepositoryBuildError]
|
||||
# Raised when the instance could not be built
|
||||
# given the parameters.
|
||||
# @raise [::NameError] Raised when the given +vendor+ could not be resolved to a class.
|
||||
def self.build(project, vendor, params, type)
|
||||
klass = build_scm_class(vendor)
|
||||
|
||||
# We can't possibly know the form fields this particular vendor
|
||||
# desires, so we allow it to filter them from raw params
|
||||
# before building the instance with it.
|
||||
args = klass.permitted_params(params)
|
||||
|
||||
repository = klass.new(args)
|
||||
repository.attributes = args
|
||||
repository.project = project
|
||||
|
||||
set_verified_type!(repository, type) unless type.nil?
|
||||
|
||||
repository.configure(type, args)
|
||||
|
||||
repository
|
||||
end
|
||||
|
||||
def self.available_scm
|
||||
subclasses.map { |klass| [klass.scm_name, klass.name] }
|
||||
##
|
||||
# Build a temporary model instance of the given vendor for temporary use in forms.
|
||||
# Will not receive any args.
|
||||
def self.build_scm_class(vendor)
|
||||
klass = OpenProject::Scm::Manager.registered[vendor]
|
||||
|
||||
if klass.nil?
|
||||
raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
|
||||
I18n.t('repositories.errors.disabled_or_unknown_vendor', vendor: vendor)
|
||||
)
|
||||
else
|
||||
klass
|
||||
end
|
||||
end
|
||||
|
||||
def self.factory(klass_name, *args)
|
||||
klass = "Repository::#{klass_name}".constantize
|
||||
klass.new(*args)
|
||||
rescue
|
||||
nil
|
||||
##
|
||||
# Verifies that the chosen scm type can be selected
|
||||
def self.set_verified_type!(repository, type)
|
||||
if repository.class.available_types.include? type
|
||||
repository.scm_type = type
|
||||
else
|
||||
raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
|
||||
I18n.t('repositories.errors.disabled_or_unknown_type',
|
||||
type: type,
|
||||
vendor: repository.vendor)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Allow global permittible params. May be overridden by plugins
|
||||
def self.permitted_params(params)
|
||||
params.permit(:url)
|
||||
end
|
||||
|
||||
def self.scm_adapter_class
|
||||
nil
|
||||
end
|
||||
|
||||
def self.scm_command
|
||||
ret = ''
|
||||
begin
|
||||
ret = scm_adapter_class.client_command if scm_adapter_class
|
||||
rescue Redmine::Scm::Adapters::CommandFailed => e
|
||||
logger.error "scm: error during get command: #{e.message}"
|
||||
end
|
||||
ret
|
||||
def self.vendor
|
||||
name.demodulize
|
||||
end
|
||||
|
||||
def self.scm_version_string
|
||||
ret = ''
|
||||
begin
|
||||
ret = scm_adapter_class.client_version_string if scm_adapter_class
|
||||
rescue Redmine::Scm::Adapters::CommandFailed => e
|
||||
logger.error "scm: error during get version string: #{e.message}"
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
def self.scm_available
|
||||
ret = false
|
||||
begin
|
||||
ret = scm_adapter_class.client_available if scm_adapter_class
|
||||
rescue Redmine::Scm::Adapters::CommandFailed => e
|
||||
logger.error "scm: error during get scm available: #{e.message}"
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
def self.configured?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Strips url and root_url
|
||||
def sanitize_urls
|
||||
url.strip! if url.present?
|
||||
@@ -322,4 +386,32 @@ class Repository < ActiveRecord::Base
|
||||
self.class.connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
|
||||
self.class.connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Create local managed repository request when the built instance
|
||||
# is managed by OpenProject
|
||||
def create_managed_repository
|
||||
service = Scm::CreateManagedRepositoryService.new(self)
|
||||
if service.call
|
||||
true
|
||||
else
|
||||
raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
|
||||
service.localized_rejected_reason
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Destroy local managed repository request when the built instance
|
||||
# is managed by OpenProject
|
||||
def delete_managed_repository
|
||||
service = Scm::DeleteManagedRepositoryService.new(self)
|
||||
# Even if the service can't remove the physical repository,
|
||||
# we should continue removing the associated instance.
|
||||
service.call
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'redmine/scm/adapters/filesystem_adapter'
|
||||
|
||||
class Repository::Filesystem < Repository
|
||||
attr_protected :root_url
|
||||
validates_presence_of :url
|
||||
|
||||
validate :validate_whitelisted_url,
|
||||
:validate_url_is_dir
|
||||
|
||||
ATTRIBUTE_KEY_NAMES = {
|
||||
'url' => 'Root directory',
|
||||
}
|
||||
def self.human_attribute_name(attribute_key_name, options = {})
|
||||
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
|
||||
end
|
||||
|
||||
def self.scm_adapter_class
|
||||
Redmine::Scm::Adapters::FilesystemAdapter
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Filesystem'
|
||||
end
|
||||
|
||||
def self.configured?
|
||||
!whitelisted_paths.empty?
|
||||
end
|
||||
|
||||
def supports_all_revisions?
|
||||
false
|
||||
end
|
||||
|
||||
def entries(path = nil, identifier = nil)
|
||||
scm.entries(path, identifier)
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.whitelisted_paths
|
||||
OpenProject::Configuration['scm_filesystem_path_whitelist']
|
||||
end
|
||||
|
||||
# validates that the url is a directory
|
||||
def validate_url_is_dir
|
||||
errors.add :url, :no_directory unless Dir.exists?(url)
|
||||
end
|
||||
|
||||
# validate url against whitelisted urls as provided by the
|
||||
# scm_filesystem_path_whitelist configuration parameter.
|
||||
#
|
||||
# The url needs to exist and needs to match one of the directories
|
||||
# returned when globbing the configuration setting.
|
||||
def validate_whitelisted_url
|
||||
globbed_url = Dir.glob(url).first
|
||||
|
||||
unless globbed_url
|
||||
errors.add :url, :not_whitelisted
|
||||
return
|
||||
end
|
||||
|
||||
globbed_whitelisted = Dir.glob(self.class.whitelisted_paths)
|
||||
|
||||
unless globbed_whitelisted.include?(globbed_url)
|
||||
errors.add :url, :not_whitelisted
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -27,25 +27,53 @@
|
||||
# See doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'redmine/scm/adapters/git_adapter'
|
||||
require 'open_project/scm/adapters/git'
|
||||
|
||||
class Repository::Git < Repository
|
||||
attr_protected :root_url
|
||||
validates_presence_of :url
|
||||
|
||||
ATTRIBUTE_KEY_NAMES = {
|
||||
'url' => 'Path to repository',
|
||||
}
|
||||
def self.human_attribute_name(attribute_key_name, options = {})
|
||||
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
|
||||
end
|
||||
|
||||
def self.scm_adapter_class
|
||||
Redmine::Scm::Adapters::GitAdapter
|
||||
OpenProject::Scm::Adapters::Git
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Git'
|
||||
def configure(scm_type, _args)
|
||||
if scm_type == self.class.managed_type
|
||||
unless manageable?
|
||||
raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
|
||||
I18n.t('repositories.managed.error_not_manageable')
|
||||
)
|
||||
end
|
||||
|
||||
self.root_url = managed_repository_path
|
||||
self.url = managed_repository_url
|
||||
end
|
||||
end
|
||||
|
||||
def self.permitted_params(params)
|
||||
super(params).merge(params.permit(:path_encoding))
|
||||
end
|
||||
|
||||
def self.supported_types
|
||||
types = [:local]
|
||||
types << managed_type if manageable?
|
||||
|
||||
types
|
||||
end
|
||||
|
||||
def managed_repo_created
|
||||
scm.initialize_bare_git
|
||||
end
|
||||
|
||||
def repository_identifier
|
||||
"#{super}.git"
|
||||
end
|
||||
|
||||
##
|
||||
# Git doesn't like local urls when visiting
|
||||
# the repository, thus always use the path.
|
||||
def managed_repository_url
|
||||
managed_repository_path
|
||||
end
|
||||
|
||||
def supports_directory_revisions?
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
# See doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'redmine/scm/adapters/subversion_adapter'
|
||||
require 'open_project/scm/adapters/subversion'
|
||||
|
||||
class Repository::Subversion < Repository
|
||||
attr_protected :root_url
|
||||
@@ -35,11 +35,35 @@ class Repository::Subversion < Repository
|
||||
validates_format_of :url, with: /\A(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+\z/i
|
||||
|
||||
def self.scm_adapter_class
|
||||
Redmine::Scm::Adapters::SubversionAdapter
|
||||
OpenProject::Scm::Adapters::Subversion
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Subversion'
|
||||
def configure(scm_type, _args)
|
||||
if scm_type == self.class.managed_type
|
||||
unless manageable?
|
||||
raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
|
||||
I18n.t('repositories.managed.error_not_manageable')
|
||||
)
|
||||
end
|
||||
|
||||
self.root_url = managed_repository_path
|
||||
self.url = managed_repository_url
|
||||
end
|
||||
end
|
||||
|
||||
def self.permitted_params(params)
|
||||
super(params).merge(params.permit(:login, :password))
|
||||
end
|
||||
|
||||
def self.supported_types
|
||||
types = [:existing]
|
||||
types << managed_type if manageable?
|
||||
|
||||
types
|
||||
end
|
||||
|
||||
def managed_repo_created
|
||||
scm.create_empty_svn
|
||||
end
|
||||
|
||||
def supports_directory_revisions?
|
||||
|
||||
@@ -710,6 +710,14 @@ class User < Principal
|
||||
@current_user ||= User.anonymous
|
||||
end
|
||||
|
||||
def self.execute_as(user)
|
||||
previous_user = User.current
|
||||
User.current = user
|
||||
yield
|
||||
ensure
|
||||
User.current = previous_user
|
||||
end
|
||||
|
||||
def roles(project)
|
||||
User.current.admin? ? Role.all : User.current.roles_for_project(project)
|
||||
end
|
||||
|
||||
@@ -68,8 +68,7 @@ class WikiContent < ActiveRecord::Base
|
||||
# Returns the mail adresses of users that should be notified
|
||||
def recipients
|
||||
notified = project.notified_users
|
||||
notified.reject! do |user| !visible?(user) end
|
||||
notified.map(&:mail)
|
||||
notified.select { |user| visible?(user) }
|
||||
end
|
||||
|
||||
# FIXME: Deprecate
|
||||
@@ -87,57 +86,4 @@ class WikiContent < ActiveRecord::Base
|
||||
def comments_to_journal_notes
|
||||
add_journal author, comments
|
||||
end
|
||||
|
||||
# FIXME: This is for backwards compatibility only. Remove once we decide it is not needed anymore
|
||||
# WikiContentJournal.class_eval do
|
||||
# attr_protected :data
|
||||
# after_save :compress_version_text
|
||||
#
|
||||
# # Wiki Content might be large and the data should possibly be compressed
|
||||
# def compress_version_text
|
||||
# self.text = changed_data["text"].last if changed_data["text"]
|
||||
# self.text ||= self.journaled.text
|
||||
# end
|
||||
#
|
||||
# def text=(plain)
|
||||
# case Setting.wiki_compression
|
||||
# when "gzip"
|
||||
# begin
|
||||
# text_hash :text => Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION), :compression => Setting.wiki_compression
|
||||
# rescue
|
||||
# text_hash :text => plain, :compression => ''
|
||||
# end
|
||||
# else
|
||||
# text_hash :text => plain, :compression => ''
|
||||
# end
|
||||
# plain
|
||||
# end
|
||||
#
|
||||
# def text_hash(hash)
|
||||
# changed_data.delete("text")
|
||||
# changed_data["data"] = hash[:text]
|
||||
# changed_data["compression"] = hash[:compression]
|
||||
# update_attribute(:changed_data, changed_data)
|
||||
# end
|
||||
#
|
||||
# def text
|
||||
# @text ||= case changed_data["compression"]
|
||||
# when "gzip"
|
||||
# Zlib::Inflate.inflate(changed_data["data"])
|
||||
# else
|
||||
# # uncompressed data
|
||||
# changed_data["data"]
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # Returns the previous version or nil
|
||||
# def previous
|
||||
# @previous ||= journaled.journals.at(version - 1)
|
||||
# end
|
||||
#
|
||||
# # FIXME: Deprecate
|
||||
# def versioned
|
||||
# journaled
|
||||
# end
|
||||
# end
|
||||
end
|
||||
|
||||
@@ -31,8 +31,7 @@ class WikiContentObserver < ActiveRecord::Observer
|
||||
def after_create(wiki_content)
|
||||
if Setting.notified_events.include?('wiki_content_added')
|
||||
recipients = wiki_content.recipients + wiki_content.page.wiki.watcher_recipients
|
||||
users = User.find_all_by_mails(recipients.uniq)
|
||||
users.each do |user|
|
||||
recipients.uniq.each do |user|
|
||||
UserMailer.wiki_content_added(user, wiki_content, User.current).deliver
|
||||
end
|
||||
end
|
||||
@@ -40,9 +39,10 @@ class WikiContentObserver < ActiveRecord::Observer
|
||||
|
||||
def after_update(wiki_content)
|
||||
if wiki_content.text_changed? && Setting.notified_events.include?('wiki_content_updated')
|
||||
recipients = wiki_content.recipients + wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients
|
||||
users = User.find_all_by_mails(recipients.uniq)
|
||||
users.each do |user|
|
||||
recipients = wiki_content.recipients
|
||||
recipients += wiki_content.page.wiki.watcher_recipients
|
||||
recipients += wiki_content.page.watcher_recipients
|
||||
recipients.uniq.each do |user|
|
||||
UserMailer.wiki_content_updated(user, wiki_content, User.current).deliver
|
||||
end
|
||||
end
|
||||
|
||||
+13
-114
@@ -180,6 +180,11 @@ class WorkPackage < ActiveRecord::Base
|
||||
|
||||
acts_as_journalized except: ['root_id']
|
||||
|
||||
acts_as_countable :required_project_storage,
|
||||
label: 'attributes.attachments',
|
||||
collection: true,
|
||||
countable: Proc.new { joins(:attachments).sum(:filesize).to_i }
|
||||
|
||||
# This one is here only to ease reading
|
||||
module JournalizedProcs
|
||||
def self.event_title
|
||||
@@ -202,7 +207,7 @@ class WorkPackage < ActiveRecord::Base
|
||||
journal = o.last_journal
|
||||
t = 'work_package'
|
||||
|
||||
t << if journal && journal.changed_data.empty? && !journal.initial?
|
||||
t << if journal && journal.details.empty? && !journal.initial?
|
||||
'-note'
|
||||
else
|
||||
status = Status.find_by(id: o.status_id)
|
||||
@@ -432,6 +437,7 @@ class WorkPackage < ActiveRecord::Base
|
||||
# TODO: move into Business Object and rename to update
|
||||
# update for now is a private method defined by AR
|
||||
def update_by!(user, attributes)
|
||||
attributes = attributes.dup
|
||||
raw_attachments = attributes.delete(:attachments)
|
||||
|
||||
update_by(user, attributes)
|
||||
@@ -479,16 +485,8 @@ class WorkPackage < ActiveRecord::Base
|
||||
@spent_hours ||= compute_spent_hours(usr)
|
||||
end
|
||||
|
||||
# Moves/copies an work_package to a new project and type
|
||||
# Returns the moved/copied work_package on success, false on failure
|
||||
def move_to_project(*args)
|
||||
WorkPackage.transaction do
|
||||
move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
|
||||
end || false
|
||||
end
|
||||
|
||||
# >>> issues.rb >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
# Returns the mail addresses of users that should be notified
|
||||
# Returns users that should be notified
|
||||
def recipients
|
||||
notified = project.notified_users
|
||||
# Author and assignee are always notified unless they have been
|
||||
@@ -503,8 +501,7 @@ class WorkPackage < ActiveRecord::Base
|
||||
end
|
||||
notified.uniq!
|
||||
# Remove users that can not view the issue
|
||||
notified.reject! do |user| !visible?(user) end
|
||||
notified.map(&:mail)
|
||||
notified.select { |user| visible?(user) }
|
||||
end
|
||||
|
||||
def done_ratio
|
||||
@@ -667,77 +664,6 @@ class WorkPackage < ActiveRecord::Base
|
||||
done_date <= Date.today
|
||||
end
|
||||
|
||||
def move_to_project_without_transaction(new_project, new_type = nil, options = {})
|
||||
options ||= {}
|
||||
work_package = options[:copy] ? self.class.new.copy_from(self) : self
|
||||
|
||||
if new_project && work_package.project_id != new_project.id
|
||||
delete_relations(work_package)
|
||||
# work_package is moved to another project
|
||||
# reassign to the category with same name if any
|
||||
new_category = if work_package.category.nil?
|
||||
nil
|
||||
else
|
||||
new_project.categories.find_by(name: work_package.category.name)
|
||||
end
|
||||
work_package.category = new_category
|
||||
# Keep the fixed_version if it's still valid in the new_project
|
||||
unless new_project.shared_versions.include?(work_package.fixed_version)
|
||||
work_package.fixed_version = nil
|
||||
end
|
||||
|
||||
work_package.project = new_project
|
||||
|
||||
enforce_cross_project_settings(work_package)
|
||||
end
|
||||
if new_type
|
||||
work_package.type = new_type
|
||||
work_package.reset_custom_values!
|
||||
end
|
||||
# Allow bulk setting of attributes on the work_package
|
||||
if options[:attributes]
|
||||
# before setting the attributes, we need to remove the move-related fields
|
||||
work_package.attributes =
|
||||
options[:attributes].except(:copy, :new_project_id, :new_type_id, :follow, :ids)
|
||||
.reject { |_key, value| value.blank? }
|
||||
end # FIXME this eliminates the case, where values shall be bulk-assigned to null,
|
||||
# but this needs to work together with the permit
|
||||
if options[:copy]
|
||||
work_package.author = User.current
|
||||
work_package.custom_field_values =
|
||||
custom_field_values.inject({}) do |h, v|
|
||||
h[v.custom_field_id] = v.value
|
||||
h
|
||||
end
|
||||
work_package.status = if options[:attributes] && options[:attributes][:status_id].present?
|
||||
Status.find_by(id: options[:attributes][:status_id])
|
||||
else
|
||||
status
|
||||
end
|
||||
else
|
||||
work_package.add_journal User.current, options[:journal_note] if options[:journal_note]
|
||||
end
|
||||
|
||||
if work_package.save
|
||||
if options[:copy]
|
||||
create_and_save_journal_note work_package, options[:journal_note]
|
||||
else
|
||||
# Manually update project_id on related time entries
|
||||
TimeEntry.where(work_package_id: id).update_all("project_id = #{new_project.id}")
|
||||
|
||||
work_package.children.each do |child|
|
||||
unless child.move_to_project_without_transaction(new_project)
|
||||
# Move failed and transaction was rollback'd
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
work_package
|
||||
end
|
||||
|
||||
# check if user is allowed to edit WorkPackage Journals.
|
||||
# see Redmine::Acts::Journalized::Permissions#journal_editable_by
|
||||
def editable_by?(user)
|
||||
@@ -842,22 +768,10 @@ class WorkPackage < ActiveRecord::Base
|
||||
reload(select: [:lock_version, :created_at, :updated_at])
|
||||
end
|
||||
|
||||
# Returns an array of projects that current user can move issues to
|
||||
def self.allowed_target_projects_on_move
|
||||
projects = []
|
||||
if User.current.admin?
|
||||
# admin is allowed to move issues to any active (visible) project
|
||||
projects = Project.visible
|
||||
elsif User.current.logged?
|
||||
if Role.non_member.allowed_to?(:move_work_packages)
|
||||
projects = Project.visible
|
||||
else
|
||||
User.current.memberships.each do |m|
|
||||
projects << m.project if m.roles.detect { |r| r.allowed_to?(:move_work_packages) }
|
||||
end
|
||||
end
|
||||
end
|
||||
projects
|
||||
# Returns a scope for the projects
|
||||
# the user is allowed to move a work package to
|
||||
def self.allowed_target_projects_on_move(user)
|
||||
Project.where(Project.allowed_to_condition(user, :move_work_packages))
|
||||
end
|
||||
|
||||
# Do not redefine alias chain on reload (see #4838)
|
||||
@@ -1105,21 +1019,6 @@ class WorkPackage < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def create_and_save_journal_note(work_package, journal_note)
|
||||
if work_package && journal_note
|
||||
work_package.add_journal User.current, journal_note
|
||||
work_package.save!
|
||||
end
|
||||
end
|
||||
|
||||
def enforce_cross_project_settings(work_package)
|
||||
parent_in_project =
|
||||
work_package.parent.nil? || work_package.parent.project == work_package.project
|
||||
|
||||
work_package.parent_id =
|
||||
nil unless Setting.cross_project_work_package_relations? || parent_in_project
|
||||
end
|
||||
|
||||
def compute_spent_hours(usr = User.current)
|
||||
spent_time = TimeEntry.visible(usr)
|
||||
.on_work_packages(self_and_descendants.visible(usr))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
@@ -27,39 +26,38 @@
|
||||
# See doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
class WorkPackageObserver < ActiveRecord::Observer
|
||||
attr_accessor :send_notification
|
||||
class AddAttachmentService
|
||||
attr_reader :container, :author
|
||||
|
||||
def after_create(work_package)
|
||||
if send_notification
|
||||
recipients = work_package.recipients + work_package.watcher_recipients
|
||||
users = User.find_all_by_mails(recipients.uniq)
|
||||
|
||||
users.each do |user|
|
||||
notify(user, work_package)
|
||||
end
|
||||
end
|
||||
clear_notification
|
||||
def initialize(container, author:)
|
||||
@container = container
|
||||
@author = author
|
||||
end
|
||||
|
||||
##
|
||||
# Notifies the user of the created work package.
|
||||
def notify(user, work_package)
|
||||
job = DeliverWorkPackageCreatedJob.new(user.id, work_package.id, User.current.id)
|
||||
# Adds and saves the uploaded file as attachment of the given container.
|
||||
# In case the container supports it, a journal will be written.
|
||||
#
|
||||
# An ActiveRecord::RecordInvalid error is raised if any record can't be saved.
|
||||
def add_attachment(uploaded_file:, description:)
|
||||
attachment = Attachment.new(file: uploaded_file,
|
||||
container: container,
|
||||
description: description,
|
||||
author: author)
|
||||
save attachment
|
||||
|
||||
Delayed::Job.enqueue job
|
||||
end
|
||||
|
||||
# Wrap send_notification so it defaults to true, when it's nil
|
||||
def send_notification
|
||||
return true if @send_notification.nil?
|
||||
@send_notification
|
||||
attachment
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Need to clear the notification setting after each usage otherwise it might be cached
|
||||
def clear_notification
|
||||
@send_notification = true
|
||||
def save(attachment)
|
||||
ActiveRecord::Base.transaction do
|
||||
attachment.save!
|
||||
if container.respond_to? :add_journal
|
||||
container.add_journal author
|
||||
container.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -34,7 +34,7 @@ class CreateWorkPackageService
|
||||
self.user = user
|
||||
self.project = project
|
||||
|
||||
WorkPackageObserver.instance.send_notification = send_notifications
|
||||
JournalManager.send_notification = send_notifications
|
||||
end
|
||||
|
||||
def create
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
|
||||
# Moves/copies an work_package to a new project and type
|
||||
# Returns the moved/copied work_package on success, false on failure
|
||||
|
||||
class MoveWorkPackageService
|
||||
attr_accessor :work_package,
|
||||
:user
|
||||
|
||||
def initialize(work_package, user)
|
||||
self.work_package = work_package
|
||||
self.user = user
|
||||
end
|
||||
|
||||
def call(new_project, new_type = nil, options = {})
|
||||
if options[:no_transaction]
|
||||
move_without_transaction(new_project, new_type, options)
|
||||
else
|
||||
WorkPackage.transaction do
|
||||
move_without_transaction(new_project, new_type, options) ||
|
||||
raise(ActiveRecord::Rollback)
|
||||
end || false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_without_transaction(new_project, new_type = nil, options = {})
|
||||
attributes = options[:attributes] || {}
|
||||
|
||||
modified_work_package = copy_or_move(options[:copy], new_project, new_type, attributes)
|
||||
|
||||
if options[:copy]
|
||||
return false unless copy(modified_work_package, attributes, options)
|
||||
else
|
||||
return false unless move(modified_work_package, new_project, options)
|
||||
end
|
||||
|
||||
modified_work_package
|
||||
end
|
||||
|
||||
def copy_or_move(make_copy, new_project, new_type, attributes)
|
||||
modified_work_package = if make_copy
|
||||
WorkPackage.new.copy_from(work_package)
|
||||
else
|
||||
work_package
|
||||
end
|
||||
|
||||
move_to_project(modified_work_package, new_project)
|
||||
|
||||
move_to_type(modified_work_package, new_type)
|
||||
|
||||
bulk_assign_attributes(modified_work_package, attributes)
|
||||
|
||||
modified_work_package
|
||||
end
|
||||
|
||||
def copy(modified_work_package, attributes, options)
|
||||
set_default_values_on_copy(modified_work_package, attributes)
|
||||
|
||||
return false unless modified_work_package.save
|
||||
|
||||
create_and_save_journal_note modified_work_package, options[:journal_note]
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def move(modified_work_package, new_project, options)
|
||||
if options[:journal_note]
|
||||
modified_work_package.add_journal user, options[:journal_note]
|
||||
end
|
||||
|
||||
return false unless modified_work_package.save
|
||||
|
||||
move_time_entries(modified_work_package, new_project)
|
||||
|
||||
return false unless move_children(modified_work_package, new_project, options)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def move_to_project(work_package, new_project)
|
||||
if new_project &&
|
||||
work_package.project_id != new_project.id &&
|
||||
allowed_to_move_to_project?(new_project)
|
||||
|
||||
work_package.delete_relations(work_package)
|
||||
|
||||
reassign_category(work_package, new_project)
|
||||
|
||||
# Keep the fixed_version if it's still valid in the new_project
|
||||
unless new_project.shared_versions.include?(work_package.fixed_version)
|
||||
work_package.fixed_version = nil
|
||||
end
|
||||
|
||||
work_package.project = new_project
|
||||
|
||||
enforce_cross_project_settings(work_package)
|
||||
end
|
||||
end
|
||||
|
||||
def move_to_type(work_package, new_type)
|
||||
if new_type
|
||||
work_package.type = new_type
|
||||
work_package.reset_custom_values!
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_assign_attributes(work_package, attributes)
|
||||
# Allow bulk setting of attributes on the work_package
|
||||
if attributes
|
||||
# before setting the attributes, we need to remove the move-related fields
|
||||
work_package.attributes =
|
||||
attributes.except(:copy, :new_project_id, :new_type_id, :follow, :ids)
|
||||
.reject { |_key, value| value.blank? }
|
||||
end # FIXME this eliminates the case, where values shall be bulk-assigned to null,
|
||||
# but this needs to work together with the permit
|
||||
end
|
||||
|
||||
def set_default_values_on_copy(work_package, attributes)
|
||||
work_package.author = user
|
||||
|
||||
assign_status_or_default(work_package, attributes[:status_id])
|
||||
end
|
||||
|
||||
def move_children(work_package, new_project, options)
|
||||
work_package.children.each do |child|
|
||||
child_service = self.class.new(child, user)
|
||||
unless child_service.call(new_project, nil, options.merge(no_transaction: true))
|
||||
# Move failed and transaction was rollback'd
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def move_time_entries(work_package, new_project)
|
||||
# Manually update project_id on related time entries
|
||||
TimeEntry.update_all("project_id = #{new_project.id}", work_package_id: work_package.id)
|
||||
end
|
||||
|
||||
def enforce_cross_project_settings(work_package)
|
||||
parent_in_project =
|
||||
work_package.parent.nil? || work_package.parent.project == work_package.project
|
||||
|
||||
work_package.parent_id =
|
||||
nil unless Setting.cross_project_work_package_relations? || parent_in_project
|
||||
end
|
||||
|
||||
def create_and_save_journal_note(work_package, journal_note)
|
||||
if journal_note
|
||||
work_package.add_journal user, journal_note
|
||||
work_package.save!
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_to_move_to_project?(new_project)
|
||||
WorkPackage
|
||||
.allowed_target_projects_on_move(user)
|
||||
.where(id: new_project.id)
|
||||
.exists?
|
||||
end
|
||||
|
||||
def reassign_category(work_package, new_project)
|
||||
# work_package is moved to another project
|
||||
# reassign to the category with same name if any
|
||||
new_category = if work_package.category.nil?
|
||||
nil
|
||||
else
|
||||
new_project.categories.find_by_name(work_package.category.name)
|
||||
end
|
||||
work_package.category = new_category
|
||||
end
|
||||
|
||||
def assign_status_or_default(work_package, status_id)
|
||||
status = if status_id.present?
|
||||
Status.find_by_id(status_id)
|
||||
else
|
||||
self.work_package.status
|
||||
end
|
||||
|
||||
work_package.status = status
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
##
|
||||
# Implements the creation of a local repository.
|
||||
Scm::CreateManagedRepositoryService = Struct.new :repository do
|
||||
##
|
||||
# Checks if a given repository may be created and managed locally.
|
||||
# Registers an job to create the repository on disk.
|
||||
#
|
||||
# @return True if the repository creation request has been initiated, false otherwise.
|
||||
def call
|
||||
if repository.managed? && repository.manageable?
|
||||
|
||||
# Cowardly refusing to override existing local repository
|
||||
return false if repository_exists?
|
||||
|
||||
##
|
||||
# We want to move this functionality in a Delayed Job,
|
||||
# but this heavily interferes with the error handling of the whole
|
||||
# repository management process.
|
||||
# Instead, this will be refactored into a single service wrapper for
|
||||
# creating and deleting repositories, which provides transactional DB access
|
||||
# as well as filesystem access.
|
||||
Scm::CreateRepositoryJob.new(repository).perform
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
rescue Errno::EACCES
|
||||
@rejected = I18n.t('repositories.errors.path_permission_failed',
|
||||
path: repository.root_url)
|
||||
false
|
||||
rescue SystemCallError => e
|
||||
@rejected = I18n.t('repositories.errors.filesystem_access_failed',
|
||||
message: e.message)
|
||||
false
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the error symbol
|
||||
def localized_rejected_reason
|
||||
@rejected ||= I18n.t('repositories.errors.not_manageable')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Test if the repository exists already on filesystem.
|
||||
def repository_exists?
|
||||
if File.directory?(repository.root_url)
|
||||
@rejected = I18n.t('repositories.errors.exists_on_filesystem')
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
+25
-33
@@ -26,43 +26,35 @@
|
||||
#
|
||||
# See doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
require 'legacy_spec_helper'
|
||||
|
||||
describe Redmine::DefaultData do
|
||||
include Redmine::I18n
|
||||
##
|
||||
# Implements the asynchronous deletion of a local repository.
|
||||
Scm::DeleteManagedRepositoryService = Struct.new :repository do
|
||||
##
|
||||
# Checks if a given repository may be deleted
|
||||
# Registers an asynchronous job to delete the repository on disk.
|
||||
#
|
||||
def call
|
||||
if repository.managed?
|
||||
|
||||
before do
|
||||
delete_loaded_data!
|
||||
assert Redmine::DefaultData::Loader::no_data?
|
||||
end
|
||||
# Create necessary changes to repository to mark
|
||||
# it as managed by OP, but delete asynchronously.
|
||||
managed_path = repository.root_url
|
||||
|
||||
it 'should no_data' do
|
||||
Redmine::DefaultData::Loader::load
|
||||
assert !Redmine::DefaultData::Loader::no_data?
|
||||
|
||||
delete_loaded_data!
|
||||
assert Redmine::DefaultData::Loader::no_data?
|
||||
end
|
||||
|
||||
it 'should load' do
|
||||
valid_languages.each do |lang|
|
||||
begin
|
||||
delete_loaded_data!
|
||||
assert Redmine::DefaultData::Loader::load(lang)
|
||||
assert_not_nil IssuePriority.first
|
||||
assert_not_nil TimeEntryActivity.first
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
assert false, ":#{lang} default data is invalid (#{e.message})."
|
||||
if File.directory?(managed_path)
|
||||
##
|
||||
# We want to move this functionality in a Delayed Job,
|
||||
# but this heavily interferes with the error handling of the whole
|
||||
# repository management process.
|
||||
# Instead, this will be refactored into a single service wrapper for
|
||||
# creating and deleting repositories, which provides transactional DB access
|
||||
# as well as filesystem access.
|
||||
Scm::DeleteRepositoryJob.new(managed_path).perform
|
||||
end
|
||||
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_loaded_data!
|
||||
Role.delete_all('builtin = 0')
|
||||
::Type.delete_all(is_standard: false)
|
||||
Status.delete_all
|
||||
Enumeration.delete_all
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
##
|
||||
# Implements a repository factory for building temporary and permanent repositories.
|
||||
Scm::RepositoryFactoryService = Struct.new :project, :params do
|
||||
attr_reader :repository
|
||||
|
||||
##
|
||||
# Build a full repository from a given scm_type
|
||||
# and persists it.
|
||||
#
|
||||
# @return [Boolean] true iff the repository was built
|
||||
def build_and_save
|
||||
build_guarded do
|
||||
repository = build_with_type(params.fetch(:scm_type).to_sym)
|
||||
if repository.save
|
||||
repository
|
||||
else
|
||||
raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
|
||||
repository.errors.full_messages.join("\n")
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Build a temporary repository used only for determining availabe settings and types
|
||||
# of that particular vendor.
|
||||
#
|
||||
# @return [Boolean] true iff the repository was built
|
||||
def build_temporary
|
||||
build_guarded do
|
||||
build_with_type(nil)
|
||||
end
|
||||
end
|
||||
|
||||
def build_error
|
||||
I18n.t('repositories.errors.build_failed', reason: @build_failed_msg)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Helper to actually build the repository and return it.
|
||||
# May raise +OpenProject::Scm::Exceptions::RepositoryBuildError+ internally.
|
||||
#
|
||||
# @param [Symbol] scm_type Type to build the repository with. May be nil
|
||||
# during temporary build
|
||||
def build_with_type(scm_type)
|
||||
Repository.build(
|
||||
project,
|
||||
params[:scm_vendor],
|
||||
params.fetch(:repository, {}),
|
||||
scm_type
|
||||
)
|
||||
end
|
||||
|
||||
def build_guarded
|
||||
@repository = yield
|
||||
@repository.present?
|
||||
rescue OpenProject::Scm::Exceptions::RepositoryBuildError => e
|
||||
@build_failed_msg = e.message
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -35,7 +35,7 @@ class UpdateWorkPackageService
|
||||
self.work_package = work_package
|
||||
self.permitted_params = permitted_params
|
||||
|
||||
JournalObserver.instance.send_notification = send_notifications
|
||||
JournalManager.send_notification = send_notifications
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@@ -27,10 +27,6 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<div id="admin-index">
|
||||
<%= render :partial => 'no_data' if @no_configuration_data %>
|
||||
</div>
|
||||
|
||||
<% html_title(l(:label_administration), l(:label_project_plural)) -%>
|
||||
<%= toolbar title: l(:label_project_plural) do %>
|
||||
<li class="toolbar-item">
|
||||
@@ -40,6 +36,31 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<div class="attributes-group">
|
||||
<div class="attributes-group--header">
|
||||
<div class="attributes-group--header-container">
|
||||
<h3 class="attributes-group--header-text"><%= l(:label_information) %></h3>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="attributes-key-value">
|
||||
<dt class="attributes-key-value--key"><%= l(:label_project_count) %></dt>
|
||||
<dd class="attributes-key-value--value-container">
|
||||
<div class="attributes-key-value--value -text">
|
||||
<span title="<%= l(:label_project_count) %>">
|
||||
<%= l(:label_x_projects, :count => Project.count) %>
|
||||
</span>
|
||||
</div>
|
||||
</dd>
|
||||
<dt class="attributes-key-value--key"><%= l(:label_required_disk_storage) %></dt>
|
||||
<dd class="attributes-key-value--value-container">
|
||||
<div class="attributes-key-value--value -text">
|
||||
<span title="0">
|
||||
<%= number_to_human_size(Project.total_projects_size, precision: 2) %>
|
||||
</span>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<%= form_tag({}, :method => :get) do %>
|
||||
<fieldset class="simple-filters--container">
|
||||
<legend><%= l(:label_filter_plural) %></legend>
|
||||
@@ -60,13 +81,13 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
</fieldset>
|
||||
<% end %>
|
||||
|
||||
|
||||
<div class="autoscroll">
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= Project.model_name.human %></th>
|
||||
<th><%= Project.human_attribute_name(:is_public) %></th>
|
||||
<th><%= l(:label_required_disk_storage) %></th>
|
||||
<th><%= Project.human_attribute_name(:created_on) %></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -76,6 +97,8 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
|
||||
<td class="name"><span><%= link_to project, settings_project_path(project), :title => project.short_description %></span></td>
|
||||
<td align="center"><%= checked_image project.is_public? %></td>
|
||||
<td align="center"><%= render partial: '/storage/required_storage',
|
||||
locals: { storage: project.required_storage} %></td>
|
||||
<td align="center"><%= format_date(project.created_on) %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to(l(:button_archive),
|
||||
|
||||
@@ -37,13 +37,13 @@ node :user do |journal|
|
||||
end
|
||||
|
||||
node :changes do |journal|
|
||||
journal.changed_data.map do |attribute, changes|
|
||||
journal.details.map do |attribute, details|
|
||||
user_friendly_attribute, old, new = user_friendly_change(journal, attribute)
|
||||
{
|
||||
technical: {
|
||||
name: attribute.to_s,
|
||||
old: changes.first,
|
||||
new: changes.last
|
||||
old: details.first,
|
||||
new: details.last
|
||||
},
|
||||
user_friendly: {
|
||||
name: user_friendly_attribute,
|
||||
@@ -57,4 +57,4 @@ end
|
||||
|
||||
node :created_on do |journal|
|
||||
journal.created_at.utc.iso8601
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<% html_title l(:label_administration), l(:label_auth_source_plural) %>
|
||||
<%= toolbar title: l(:label_auth_source_plural) do %>
|
||||
<li class="toolbar-item">
|
||||
<%= link_to new_auth_source_path, class: 'button -alt-highlight' do %>
|
||||
<%= link_to({ action: 'new' }, class: 'button -alt-highlight') do %>
|
||||
<i class="button--icon icon-add"></i>
|
||||
<span class="button--text"><%= l(:label_auth_source_new) %></span>
|
||||
<% end %>
|
||||
|
||||
@@ -47,7 +47,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
|
||||
end
|
||||
xml.content "type" => "html" do
|
||||
xml.text! '<ul>'
|
||||
change.changed_data.each do |detail|
|
||||
change.details.each do |detail|
|
||||
change_content = change.render_detail(detail, :no_html => false)
|
||||
xml.text!(content_tag(:li, change_content)) if change_content.present?
|
||||
end
|
||||
|
||||
@@ -66,6 +66,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
</h2>
|
||||
</div>
|
||||
</noscript>
|
||||
<notifications></notifications>
|
||||
<% main_menu = render_main_menu(@project) %>
|
||||
<% side_displayed = content_for?(:sidebar) || content_for?(:main_menu) || !main_menu.blank? %>
|
||||
<div id="wrapper" class="<%= (side_displayed) ? '' : "nosidebar" %><%= (show_decoration) ? '' : 'nomenus' %>"
|
||||
|
||||
@@ -47,3 +47,5 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
locals: { form: form } %>
|
||||
<%= render partial: "projects/form/attributes/responsible_id",
|
||||
locals: { form: form } %>
|
||||
<%= render partial: "projects/form/attributes/storage",
|
||||
locals: { form: form } %>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<%
|
||||
project = form.object
|
||||
storage = project.required_storage
|
||||
%>
|
||||
|
||||
<% if storage.key?(:total) && storage[:total] > 0 %>
|
||||
<div class="form--field">
|
||||
<label class="form--label" for="project_project_type_id" title="<%= l(:label_required_disk_storage) %>"><%= l('repositories.storage.title') %></label>
|
||||
<span class="form--field-container">
|
||||
<td align="center"><%= render partial: '/storage/required_storage',
|
||||
locals: { storage: storage } %></td>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,64 +0,0 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= remote_form_for :repository, @repository,
|
||||
:url => { :controller => '/repositories', :action => 'edit', :id => @project },
|
||||
:builder => TabularFormBuilder,
|
||||
:lang => current_language,
|
||||
html: { class: 'form' } do |f| %>
|
||||
|
||||
<%= error_messages_for 'repository' %>
|
||||
|
||||
<div class="form--field -required">
|
||||
<%= label_tag('repository_scm', l(:label_scm), class: "form--label") %>
|
||||
<div class="form--field-container">
|
||||
<div class="form--select-container">
|
||||
<%= scm_select_tag(@repository) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= repository_field_tags(f, @repository) if @repository %>
|
||||
|
||||
<div class="contextual">
|
||||
<% if @repository && !@repository.new_record? %>
|
||||
<%= link_to(l(:label_user_plural), {:controller => '/repositories', :action => 'committers', :project_id => @project}, :class => 'icon icon-user1') %>
|
||||
<%= link_to(l(:button_delete), project_repository_path(@project),
|
||||
data: { confirm: l(:text_are_you_sure) },
|
||||
:method => :delete,
|
||||
:class => 'icon icon-delete') %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<hr class="form--separator">
|
||||
|
||||
<%= f.button((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save),
|
||||
:disabled => @repository.nil?,
|
||||
class: 'button -highlight -with-icon icon-yes') %>
|
||||
<% end %>
|
||||
@@ -33,6 +33,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
select = $('add_filter_select');
|
||||
field = select.value
|
||||
Element.show('tr_' + field);
|
||||
Element.show('filter_spacer');
|
||||
check_box = $('cb_' + field);
|
||||
check_box.checked = true;
|
||||
toggle_filter(field);
|
||||
@@ -160,7 +161,7 @@ when :list, :list_optional, :list_status, :list_subprojects %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<li class="advanced-filters--spacer"></li>
|
||||
<li style="display:none" id="filter_spacer" class="advanced-filters--spacer"></li>
|
||||
|
||||
<li class="advanced-filters--add-filter">
|
||||
<label for="add_filter_select" class="advanced-filters--add-filter-label">
|
||||
|
||||
@@ -39,11 +39,11 @@ dirs.each do |dir|
|
||||
link_path << '/' unless link_path.empty?
|
||||
link_path << "#{dir}"
|
||||
%>
|
||||
/ <%= link_to h(dir), :action => 'show', :project_id => @project,
|
||||
» <%= link_to h(dir), :action => 'show', :project_id => @project,
|
||||
:path => to_path_param(link_path), :rev => @rev %>
|
||||
<% end %>
|
||||
<% if filename %>
|
||||
/ <%= link_to h(filename),
|
||||
» <%= link_to h(filename),
|
||||
:action => 'changes', :project_id => @project,
|
||||
:path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
|
||||
<% end %>
|
||||
|
||||
@@ -33,7 +33,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<% ent_name = replace_invalid_utf8(entry.name) %>
|
||||
<tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= h(entry.kind) %>">
|
||||
<td style="padding-left: <%=18 * depth%>px;" class="filename">
|
||||
<% if entry.is_dir? %>
|
||||
<% if entry.dir? %>
|
||||
<span class="icon-context dir-expander" onclick="<%= remote_function :url => {:action => 'show', :project_id => @project, :path => to_path_param(ent_path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
|
||||
:method => :get,
|
||||
:update => { :success => tr_id },
|
||||
@@ -42,10 +42,10 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
:condition => "scmEntryClick('#{tr_id}')"%>"> </span>
|
||||
<% end %>
|
||||
<%= link_to h(ent_name),
|
||||
{:action => (entry.is_dir? ? 'show' : 'changes'), :project_id => @project, :path => to_path_param(ent_path), :rev => @rev},
|
||||
:class => (entry.is_dir? ? 'icon-context icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
|
||||
{:action => (entry.dir? ? 'show' : 'changes'), :project_id => @project, :path => to_path_param(ent_path), :rev => @rev},
|
||||
:class => (entry.dir? ? 'icon-context icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
|
||||
</td>
|
||||
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
|
||||
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.dir? %></td>
|
||||
<% changeset = @project.repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
|
||||
<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
|
||||
<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
|
||||
|
||||
@@ -31,38 +31,26 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<%= javascript_include_tag 'repository_navigation' %>
|
||||
<% end %>
|
||||
<% merge ||= {} %>
|
||||
<% crumbs = render :partial => 'breadcrumbs', :locals => { :path => @path, :revision => @rev }.merge(merge) %>
|
||||
<%= toolbar title: crumbs do %>
|
||||
<%= toolbar title: l(:label_repository) do %>
|
||||
<% if User.current.allowed_to?(:browse_repository, @project) %>
|
||||
<li class="toolbar-item">
|
||||
<%= link_to stats_project_repository_path(@project), class: 'button' do %>
|
||||
<i class="button--icon icon-stats1"></i>
|
||||
<span class="button--text"><%= l(:label_statistics) %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<%# rev => nil prevents overwriting the rev parameter queried for in the form with the parameter from the request %>
|
||||
<%= form_tag({:action => controller.action_name, :project_id => @project, :path => to_path_param(@path), :rev => nil}, {:method => :get, :id => 'revision_selector'}) do -%>
|
||||
<!-- Branches Dropdown -->
|
||||
<% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
|
||||
<li class="toolbar-item">
|
||||
<%= label_tag :branch, l(:label_branch), class: 'hidden-for-sighted' %>
|
||||
<%= select_tag :branch, options_for_select([''] + @repository.branches, @rev), id: 'branch' %>
|
||||
</li>
|
||||
<% end -%>
|
||||
|
||||
<% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
|
||||
<li class="toolbar-item">
|
||||
<%= label_tag :tag, l(:label_tag), class: 'hidden-for-sighted' %>
|
||||
<%= select_tag :tag, options_for_select([''] + @repository.tags, @rev), id: 'tag' %>
|
||||
</li>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
<% if User.current.allowed_to?(:manage_repository, @project) %>
|
||||
<li class="toolbar-item">
|
||||
<%= label_tag :rev, l(:label_revision), class: 'hidden-for-sighted' %>
|
||||
<%= text_field_tag :rev, @rev, placeholder: l(:label_revision) %>
|
||||
<%= link_to settings_project_path(@project, tab: 'repository') , class: 'button' do %>
|
||||
<i class="button--icon icon-settings"></i>
|
||||
<span class="button--text"><%= l(:label_settings) %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% if User.current.impaired? %>
|
||||
<li class="toolbar-item">
|
||||
<%= button_tag 'OK', type: :submit, class: 'button -highlight' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end -%>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div>
|
||||
<h3>
|
||||
<%= render :partial => 'breadcrumbs', :locals => { :path => @path, :revision => @rev }.merge(merge) %>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
|
||||
<% is_creation_form = @repository.nil? || @repository.new_record? %>
|
||||
<%= form_for :repository,
|
||||
remote: true,
|
||||
url: { controller: '/repositories',
|
||||
action: is_creation_form ? 'create' : 'update',
|
||||
project_id: @project },
|
||||
method: is_creation_form ? 'POST' : 'PUT',
|
||||
builder: TabularFormBuilder,
|
||||
html: { class: 'form' } do |f| %>
|
||||
<%= error_messages_for 'repository' %>
|
||||
<%# Hide the select for existing repositories %>
|
||||
<% if is_creation_form %>
|
||||
<div class="form--field -required">
|
||||
<%= label_tag('scm_vendor', l('repositories.scm_vendor'), class: "form--label") %>
|
||||
<div class="form--field-container">
|
||||
<div class="form--select-container -middle">
|
||||
<%= scm_vendor_tag(@repository) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Show (selected) type options %>
|
||||
<% unless @repository.nil? %>
|
||||
<%= render partial: "/repositories/settings/vendor_form",
|
||||
locals: { form: f, repository: @repository, vendor: vendor_name(@repository) } %>
|
||||
<% end %>
|
||||
|
||||
<%# Allow plugins to add additional information %>
|
||||
<%= call_hook :repository_settings_fields, repository: @repository, form: f %>
|
||||
|
||||
<%# Hide the save button for existing managed repositories %>
|
||||
<% if show_settings_save_button?(@repository) %>
|
||||
<%= render partial: "/repositories/settings/submit",
|
||||
locals: { form: f, is_creation_form: is_creation_form, project: @project } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -53,6 +53,12 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<% end -%>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><%= submit_tag l(:button_update), class: 'button -highlight' %></p>
|
||||
<hr class="form--separator">
|
||||
<p>
|
||||
<%= submit_tag l(:button_update), class: 'button -highlight' %>
|
||||
<%= link_to settings_project_path(id: @project.id, tab: 'repository'), class: 'button' do %>
|
||||
<span class="button--text"><%= l(:button_cancel) %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<%= toolbar title: l(:label_confirmation) %>
|
||||
<div class="notification-box -warning <%= @repository.managed? ? '-severe' : '' %>">
|
||||
<div class="notification-box--content">
|
||||
<p><strong><%= l('repositories.destroy.title') %></strong><br /></p>
|
||||
<p>
|
||||
<% if @repository.managed? %>
|
||||
<%= l('repositories.destroy.managed_text', project_name: @project.identifier) %>
|
||||
<br/>
|
||||
<%= l('repositories.destroy.managed_path_note', path: @repository.root_url) %>
|
||||
<% else %>
|
||||
<%= simple_format l('repositories.destroy.linked_text',
|
||||
project_name: @project.identifier, url: @repository.url) %>
|
||||
<% end %>
|
||||
</p>
|
||||
<p>
|
||||
<%= link_to project_repository_path(@project),
|
||||
:method => :delete,
|
||||
:class => 'button' do %>
|
||||
<i class="button--icon icon-delete"></i>
|
||||
<span class="button--text"><%= l(:button_delete) %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-8
@@ -27,11 +27,10 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<% partial = escape_javascript render(partial: 'projects/settings/repository') %>
|
||||
jQuery('#tab-content-repository').html("<%= partial %>");
|
||||
|
||||
<% if reload_menu %>
|
||||
jQuery('#main-menu').html("<%= escape_javascript(render_main_menu(project)) %>");
|
||||
<% end %>
|
||||
|
||||
activateFlashError();
|
||||
<div class="notification-box -warning">
|
||||
<div class="notification-box--content">
|
||||
<p><%= l('repositories.errors.exception_title',
|
||||
message: l('repositories.errors.empty_repository')) %></p>
|
||||
<%# TODO: Add checkout instructions when included in core %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<button type="submit" class="button -highlight">
|
||||
<i class="button--icon icon-yes"></i>
|
||||
<span class="button--text">
|
||||
<%= is_creation_form ? l(:button_create) : l(:button_save) %>
|
||||
</span>
|
||||
</button>
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="description attributes-group">
|
||||
<div class="attributes-group--header">
|
||||
<div class="attributes-group--header-container">
|
||||
<h3 class="attributes-group--header-text">
|
||||
<input type="radio"
|
||||
value="<%= type %>"
|
||||
name="scm_type"
|
||||
<%= existing ? 'disabled' : '' %>
|
||||
<%= alone ? 'checked' : '' %>>
|
||||
<%= l("repositories.#{vendor}.#{type}_title") %>
|
||||
</h3>
|
||||
</div>
|
||||
<% unless repository.new_record? %>
|
||||
<div class="attributes-group--header-control">
|
||||
<%= link_to l(:label_user_plural), committers_project_repository_path(repository.project),
|
||||
class: 'icon icon-user1' %>
|
||||
<%= link_to l(:button_delete), destroy_info_project_repository_path(repository.project),
|
||||
method: :get,
|
||||
class: 'icon icon-delete' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p><%= l("repositories.#{vendor}.#{type}_introduction") %></p>
|
||||
<div id="toggleable-attributes-group--content-<%= type %>"
|
||||
class="attributes-group--content -toggleable single-attribute"
|
||||
<%= alone ? '' : 'hidden' %>>
|
||||
|
||||
<%= render partial: "/repositories/settings/#{vendor}/#{type}",
|
||||
locals: { form: form, repository: repository } %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<% if repository.new_record? %>
|
||||
<% scm_types = repository.class.available_types %>
|
||||
<div class="attributes-group -toggleable" data-switch="scm_type">
|
||||
<% scm_types.each do |type|%>
|
||||
<%= render partial: "/repositories/settings/vendor_attribute_groups",
|
||||
locals: { existing: false, alone: scm_types.length == 1,
|
||||
vendor: vendor, type: type,
|
||||
form: form, repository: repository } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render partial: "/repositories/settings/vendor_attribute_groups",
|
||||
locals: { existing: true, alone: true, vendor: repository.vendor.underscore,
|
||||
type: repository.scm_type,
|
||||
form: form, repository: repository } %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,45 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<div class="form--field -required">
|
||||
<%= form.text_field(:url,
|
||||
label: l('repositories.git.path'),
|
||||
size: 60,
|
||||
required: true) %>
|
||||
<div class="form--field-instructions">
|
||||
<%= raw l('repositories.git.instructions.path', example_path: '<code>/path/to/repository/.git</code>') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field -required">
|
||||
<%= form.select(:path_encoding,
|
||||
git_path_encoding_options(repository),
|
||||
label: l('repositories.git.path_encoding')) %>
|
||||
<div class="form--field-instructions">
|
||||
<%= l('repositories.git.instructions.path_encoding') %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<div class="form--field">
|
||||
<%= form.text_field(:url,
|
||||
label: l('repositories.git.managed_path'),
|
||||
value: repository.new_record? ? repository.managed_repository_path :
|
||||
repository.root_url,
|
||||
size: 60,
|
||||
disabled: true,
|
||||
'aria-disabled' => true
|
||||
|
||||
) %>
|
||||
<div class="form--field-instructions">
|
||||
<%= l('repositories.git.instructions.managed_url') %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
(function($) {
|
||||
|
||||
<% content = render partial: 'repositories/settings' %>
|
||||
$('#tab-content-repository').html('<%= escape_javascript content %>');
|
||||
|
||||
<% unless flash.empty? %>
|
||||
<%# TODO: Double flash from regular flash %>
|
||||
var div_content = $('#content');
|
||||
div_content.find('.flash').remove();
|
||||
div_content.prepend('<%= render_flash_messages %>');
|
||||
<% end %>
|
||||
|
||||
var toggleContent = function(content, selected) {
|
||||
var targetName = '#toggleable-attributes-group--content-' + selected,
|
||||
oldTargets = content.not(targetName),
|
||||
newTarget = $(targetName);
|
||||
|
||||
// would work with fieldset#disabled, but that's bugged up unto IE11
|
||||
// https://connect.microsoft.com/IE/feedbackdetail/view/962368/
|
||||
//
|
||||
// Ugly workaround: disable all inputs manually, but
|
||||
// spare enabling inputs marked with `aria-disabled`
|
||||
oldTargets
|
||||
.slideUp(500)
|
||||
.prop('hidden', true)
|
||||
.find('input,select')
|
||||
.prop('disabled', true);
|
||||
|
||||
newTarget
|
||||
.slideDown(500)
|
||||
.prop('hidden', false)
|
||||
.find('input,select')
|
||||
.not('[aria-disabled="true"]')
|
||||
.prop('disabled', false);
|
||||
};
|
||||
|
||||
$('#tab-content-repository')
|
||||
.find('.attributes-group.-toggleable')
|
||||
.each(function(_i, el) {
|
||||
|
||||
var fs = $(el),
|
||||
name = fs.attr('data-switch'),
|
||||
switches = fs.find('[name="' + name + '"]'),
|
||||
headers = fs.find('.attributes-group--header-text'),
|
||||
content = fs.find('.attributes-group--content.-toggleable');
|
||||
|
||||
// If only one choice, skip setup
|
||||
if (switches.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clickable headers
|
||||
headers.click(function() {
|
||||
var input = $(this).find('input');
|
||||
if (!input.prop('checked')) {
|
||||
input
|
||||
.prop('checked', true)
|
||||
.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle content
|
||||
switches.on('change', function() {
|
||||
toggleContent(content, this.value);
|
||||
});
|
||||
});
|
||||
|
||||
}(jQuery));
|
||||
@@ -0,0 +1,63 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<div class="form--field -required">
|
||||
<%= form.text_field(:url,
|
||||
label: l('repositories.url'),
|
||||
required: true,
|
||||
size: 60,
|
||||
disabled: !repository.new_record?
|
||||
) %>
|
||||
<div class="form--field-instructions">
|
||||
<%= raw simple_format l('repositories.subversion.instructions.url', local_proto: "<code>file://</code>") %>
|
||||
<code>file://</code>
|
||||
<code>http://</code>
|
||||
<code>https://</code>
|
||||
<code>svn://</code>
|
||||
<code>svn+[tunnelscheme]://</code>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="form--field">
|
||||
<%= form.text_field(:login,
|
||||
size: 30,
|
||||
label: l('repositories.subversion.username')) %>
|
||||
<div class="form--field-instructions">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
<%= form.password_field(:password,
|
||||
size: 30,
|
||||
label: l('repositories.subversion.password'),
|
||||
name: 'ignore',
|
||||
value: ((repository.new_record? || repository.password.blank?) ? '' : ('x' * 15)),
|
||||
onfocus: "this.value=''; this.name='repository[password]';",
|
||||
onchange: "this.name='repository[password]';")
|
||||
%>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<div class="form--field">
|
||||
<%= form.text_field(:url,
|
||||
label: l('repositories.subversion.managed_url'),
|
||||
value: repository.new_record? ? repository.managed_repository_url :
|
||||
repository.url,
|
||||
size: 60,
|
||||
disabled: true,
|
||||
'aria-disabled' => true
|
||||
) %>
|
||||
<div class="form--field-instructions">
|
||||
<%= l('repositories.subversion.instructions.managed_url') %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,6 +29,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
|
||||
|
||||
|
||||
<%= render :partial => 'navigation', locals: { merge: { kind: 'dir' } } %>
|
||||
|
||||
<% if !@entries.nil? && authorize_for('repositories', 'browse') %>
|
||||
@@ -37,9 +38,63 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
<%= render_properties(@properties) %>
|
||||
|
||||
<%# rev => nil prevents overwriting the rev parameter queried for in the form with the parameter from the request %>
|
||||
<%= form_tag({ action: controller.action_name,
|
||||
project_id: @project,
|
||||
path: to_path_param(@path),
|
||||
rev: nil
|
||||
},
|
||||
method: :get,
|
||||
id: 'revision_selector',
|
||||
class: 'form -vertical'
|
||||
) do %>
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= l(:label_revision_plural) %></legend>
|
||||
<div class="grid-block wrap">
|
||||
<div class="form--column medium-3">
|
||||
<div class="form--field -wide-label -break-words ">
|
||||
<label class="form--label"><%= l('repositories.go_to_revision') %></label>
|
||||
<div class="form--field-container">
|
||||
<div class="form--text-field-container">
|
||||
<%= text_field_tag :rev, @rev, id: 'revision-identifier-input', placeholder: l(:label_revision) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if !@repository.branches.nil? && @repository.branches.length > 0 %>
|
||||
<div class="form--column medium-3">
|
||||
<div class="form--field -wide-label -break-words ">
|
||||
<label class="form--label"><%= l(:label_branch) %></label>
|
||||
<div class="form--field-container">
|
||||
<div class="form--select-container">
|
||||
<%= select_tag :branch, options_for_select([''] + @repository.branches, @rev), id: 'revision-branch-select' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if !@repository.tags.nil? && @repository.tags.length > 0 %>
|
||||
<div class="form--column medium-3">
|
||||
<div class="form--field -wide-label -break-words ">
|
||||
<label class="form--label"><%= l(:label_tag) %></label>
|
||||
<div class="form--field-container">
|
||||
<div class="form--select-container">
|
||||
<%= select_tag :tag, options_for_select([''] + @repository.tags, @rev), id: 'revision-tag-select' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if User.current.impaired? %>
|
||||
<div>
|
||||
<%= button_tag 'OK', type: :submit, class: 'button -highlight' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
<% if authorize_for('repositories', 'revisions') %>
|
||||
<% if @changesets && !@changesets.empty? %>
|
||||
<h3><%= l(:label_latest_revision_plural) %></h3>
|
||||
<%= render :partial => 'revisions',
|
||||
:locals => {:project => @project, :path => @path,
|
||||
:revisions => @changesets, :entry => nil }%>
|
||||
|
||||
@@ -47,20 +47,29 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
</div>
|
||||
</li>
|
||||
<li class="simple-filters--filter">
|
||||
<%= styled_label_tag :all_words, class: 'form--label-with-check-box' do %>
|
||||
<%= styled_check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %>
|
||||
<% end %>
|
||||
<%= styled_label_tag :titles_only, class: 'form--label-with-check-box' do %>
|
||||
<%= styled_check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %>
|
||||
<% end %>
|
||||
<span class="form--field -trailing-label">
|
||||
<%= styled_label_tag :all_words, l(:label_all_words) %>
|
||||
<span class="form--field-container">
|
||||
<%= styled_check_box_tag 'all_words', 1, @all_words %>
|
||||
</span>
|
||||
</span>
|
||||
<span class="form--field -trailing-label">
|
||||
<%= styled_label_tag :titles_only, l(:label_search_titles_only) %>
|
||||
<span class="form--field-container">
|
||||
<%= styled_check_box_tag 'titles_only', 1, @titles_only %>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</li>
|
||||
<li class="simple-filters--filter">
|
||||
<% @object_types.each do |t| %>
|
||||
<%= styled_label_tag t, class: 'form--label-with-check-box' do %>
|
||||
<%= styled_check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %>
|
||||
<% @object_types.each do |t| %>
|
||||
<span class="form--field -trailing-label">
|
||||
<%= styled_label_tag t, type_label(t) %>
|
||||
<span class="form--field-container">
|
||||
<%= styled_check_box_tag t, 1, @scope.include?(t) %>
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
<%= styled_submit_tag l(:button_submit), :name => 'submit', class: 'button -highlight -small' %>
|
||||
|
||||
@@ -43,7 +43,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<%= link_to_function l(:label_generate_key), "if ($('settings_sys_api_key').disabled == false) { $('settings_sys_api_key').value = randomKey(20) }" %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form--field"><%= setting_multiselect(:enabled_scm, Redmine::Scm::Base.configured) %></div>
|
||||
<div class="form--field"><%= setting_multiselect(:enabled_scm, OpenProject::Scm::Manager.vendors) %></div>
|
||||
<div class="form--field">
|
||||
<%= setting_text_field :repositories_encodings, :size => 60 %>
|
||||
<div class="form--field-instructions"><%= l(:text_comma_separated) %></div>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<%#-- copyright
|
||||
OpenProject is a project management system.
|
||||
Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
|
||||
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 doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
<% if storage.key?(:total) && storage[:total] > 0 %>
|
||||
<span class="tooltip--right"
|
||||
data-tooltip="
|
||||
<% (storage.keys - [:total]).each do |type| %>
|
||||
<% if storage[type] > 0 %>
|
||||
<%= l(type) %>: <%= number_to_human_size(storage[type], precision: 2) %>
|
||||
|
||||
<% end %>
|
||||
<% end %>
|
||||
">
|
||||
<%= number_to_human_size(storage[:total], precision: 2) %>
|
||||
</span>
|
||||
<% end %>
|
||||
@@ -28,5 +28,6 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
++#%>
|
||||
|
||||
<%= t(:text_work_package_added, :id => "##{@issue.id}", :author => @issue.author) %>
|
||||
<%= format_text(@journal.notes, :only_path => false) %>
|
||||
<hr />
|
||||
<%= render :partial => 'issue_details', :locals => { :issue => @issue } %>
|
||||
|
||||
@@ -29,5 +29,6 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
|
||||
<%= t(:text_work_package_added, :id => "##{@issue.id}", :author => @issue.author) %>
|
||||
|
||||
<%= @journal.notes if @journal.notes? %>
|
||||
----------------------------------------
|
||||
<%= render :partial => 'issue_details', :locals => { :issue => @issue } %>
|
||||
|
||||
@@ -53,7 +53,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<div class="form--field">
|
||||
<label class="form--label" for='work_package_priority_id'><%= WorkPackage.human_attribute_name(:priority) %></label>
|
||||
<div class="form--field-container">
|
||||
<%= styled_select_tag('work_package[priority_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>".html_safe + options_from_collection_for_select(IssuePriority.all, :id, :name)) %>
|
||||
<%= styled_select_tag('work_package[priority_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>".html_safe + options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
|
||||
@@ -29,9 +29,9 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<%= toolbar title: (@query.new_record? ? l(:label_calendar) : h(@query.name)) %>
|
||||
<%= form_tag(work_packages_calendar_index_path, :method => :get, :id => 'query_form') do %>
|
||||
<%= hidden_field_tag('project_id', @project.to_param) if @project%>
|
||||
<fieldset id="filters" class="form--fieldset -collapsible <%= @query.new_record? ? "" : "collapsed" %>">
|
||||
<fieldset id="filters" class="form--fieldset -collapsible collapsed" >
|
||||
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_filter_plural) %></a></legend>
|
||||
<div class=""<%= " style=\"display: none;\"" if !@query.new_record? %>>
|
||||
<div class="" style="display: none;">
|
||||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -72,7 +72,7 @@ See doc/COPYRIGHT.rdoc for more details.
|
||||
<div class="form--field-container">
|
||||
<%= styled_select_tag('priority_id',
|
||||
content_tag('option', l(:label_no_change_option), :value => '') +
|
||||
options_from_collection_for_select(IssuePriority.all, :id, :name)) %>
|
||||
options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form--field">
|
||||
|
||||
+39
-38
@@ -27,53 +27,54 @@
|
||||
# See doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
##
|
||||
# Requires including class to implement #notification_mail.
|
||||
class MailNotificationJob
|
||||
mattr_accessor :raise_exceptions
|
||||
|
||||
def initialize(recipient_id, author_id)
|
||||
@recipient_id = recipient_id
|
||||
@author_id = author_id
|
||||
class DeliverWorkPackageNotificationJob
|
||||
def initialize(journal_id, author_id)
|
||||
@journal_id = journal_id
|
||||
@author_id = author_id
|
||||
end
|
||||
|
||||
def perform
|
||||
execute_as recipient do
|
||||
notify
|
||||
return unless raw_journal # abort, assuming that the underlying WP was deleted
|
||||
|
||||
journal = find_aggregated_journal
|
||||
|
||||
# The caller should have ensured that the journal can't outdate anymore
|
||||
# before queuing a notification
|
||||
raise 'aggregated journal got outdated' unless journal
|
||||
|
||||
notification_receivers(work_package).uniq.each do |recipient|
|
||||
mail = User.execute_as(recipient) {
|
||||
if journal.initial?
|
||||
UserMailer.work_package_added(recipient, journal, author)
|
||||
else
|
||||
UserMailer.work_package_updated(recipient, journal, author)
|
||||
end
|
||||
}
|
||||
|
||||
mail.deliver
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
# Expecting this error if recipient user was deleted intermittently.
|
||||
# Since we cannot recover from this error we catch it and move on.
|
||||
Rails.logger.error "Cannot deliver notification (#{inspect})
|
||||
as required record was not found: #{e}".squish
|
||||
raise e if raise_exceptions
|
||||
end
|
||||
|
||||
def error(_job, e)
|
||||
Rails.logger.error "notification failed (#{inspect}): #{e}"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def recipient
|
||||
@recipient ||= Principal.find(@recipient_id)
|
||||
end
|
||||
|
||||
def author
|
||||
@author ||= Principal.find(@author_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify
|
||||
notification_mail.deliver
|
||||
def find_aggregated_journal
|
||||
wp_journals = Journal::AggregatedJournal.aggregated_journals(journable: work_package)
|
||||
wp_journals.detect { |journal| journal.version == raw_journal.version }
|
||||
end
|
||||
|
||||
def execute_as(user)
|
||||
previous_user = User.current
|
||||
User.current = user
|
||||
yield
|
||||
ensure
|
||||
User.current = previous_user
|
||||
def notification_receivers(work_package)
|
||||
work_package.recipients + work_package.watcher_recipients
|
||||
end
|
||||
|
||||
def raw_journal
|
||||
@raw_journal ||= Journal.find_by_id(@journal_id)
|
||||
end
|
||||
|
||||
def work_package
|
||||
@work_package ||= raw_journal.journable
|
||||
end
|
||||
|
||||
def author
|
||||
@author ||= User.find_by_id(@author_id) || DeletedUser.first
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
# Enqueues
|
||||
class EnqueueWorkPackageNotificationJob
|
||||
def initialize(journal_id, author_id)
|
||||
@journal_id = journal_id
|
||||
@author_id = author_id
|
||||
end
|
||||
|
||||
def perform
|
||||
# if the WP has been deleted the unaggregated journal will have been deleted too
|
||||
# and our job here is done
|
||||
return nil unless raw_journal
|
||||
|
||||
journal = find_aggregated_journal
|
||||
|
||||
# If we can't find the aggregated journal, it was superseded by a journal that aggregated ours.
|
||||
# In that case a job for the new journal will have been enqueued that is now responsible for
|
||||
# sending the notification. Our job here is done.
|
||||
return nil unless journal
|
||||
|
||||
# Do not deliver notifications if a follow-up journal will already have sent a notification
|
||||
# on behalf of this job.
|
||||
unless Journal::AggregatedJournal.hides_notifications?(journal.successor, journal)
|
||||
deliver_notifications_for(journal)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_aggregated_journal
|
||||
wp_journals = Journal::AggregatedJournal.aggregated_journals(journable: work_package)
|
||||
wp_journals.detect { |journal| journal.version == raw_journal.version }
|
||||
end
|
||||
|
||||
def deliver_notifications_for(journal)
|
||||
job = DeliverWorkPackageNotificationJob.new(journal.id, @author_id)
|
||||
Delayed::Job.enqueue job
|
||||
end
|
||||
|
||||
def raw_journal
|
||||
@raw_journal ||= Journal.find_by_id(@journal_id)
|
||||
end
|
||||
|
||||
def work_package
|
||||
@work_package ||= raw_journal.journable
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
##
|
||||
# Provides an asynchronous job to create a managed repository on the filesystem.
|
||||
# Currently, this is run synchronously due to potential issues
|
||||
# with error handling.
|
||||
# We envision a repository management wrapper that covers transactional
|
||||
# creation and deletion of repositories BOTH on the database and filesystem.
|
||||
# Until then, a synchronous process is more failsafe.
|
||||
class Scm::CreateRepositoryJob
|
||||
def initialize(repository)
|
||||
@repository = repository
|
||||
end
|
||||
|
||||
def perform
|
||||
# Create the repository locally.
|
||||
mode = config[:mode] || default_mode
|
||||
create(mode)
|
||||
|
||||
# Allow adapter to act upon the created repository
|
||||
# e.g., initializing an empty scm repository within it
|
||||
repository.managed_repo_created
|
||||
|
||||
# Ensure group ownership after creation
|
||||
ensure_group(mode, config[:group])
|
||||
end
|
||||
|
||||
def destroy_failed_jobs?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Creates the repository at the +root_url+.
|
||||
# Accepts an overriden permission mode mask from the scm config,
|
||||
# or sets a sensible default of 0700.
|
||||
def create(mode)
|
||||
FileUtils.mkdir_p(repository.root_url, mode: mode)
|
||||
end
|
||||
|
||||
##
|
||||
# Overrides the group permission of the created repository
|
||||
# after the adapter was able to work in the directory.
|
||||
def ensure_group(mode, group)
|
||||
FileUtils.chmod_R(mode, repository.root_url)
|
||||
|
||||
# Note that this is effectively a noop when group is nil,
|
||||
# and then permissions remain OPs runuser/-group
|
||||
FileUtils.chown_R(nil, group, repository.root_url)
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= repository.class.scm_config
|
||||
end
|
||||
|
||||
def repository
|
||||
@repository
|
||||
end
|
||||
|
||||
def default_mode
|
||||
0700
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
##
|
||||
# Provides an asynchronous job to delete a managed repository on the filesystem.
|
||||
# Currently, this is run synchronously due to potential issues
|
||||
# with error handling.
|
||||
# We envision a repository management wrapper that covers transactional
|
||||
# creation and deletion of repositories BOTH on the database and filesystem.
|
||||
# Until then, a synchronous process is more failsafe.
|
||||
class Scm::DeleteRepositoryJob
|
||||
def initialize(managed_path)
|
||||
@managed_path = managed_path
|
||||
end
|
||||
|
||||
def perform
|
||||
# Delete the repository project itself.
|
||||
FileUtils.remove_dir(@managed_path)
|
||||
end
|
||||
|
||||
def destroy_failed_jobs?
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
class Scm::StorageUpdaterJob
|
||||
def initialize(repository)
|
||||
@id = repository.id
|
||||
|
||||
unless repository.scm.storage_available?
|
||||
raise OpenProject::Scm::Exceptions::ScmError.new(
|
||||
I18n.t('repositories.storage.not_available')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def perform
|
||||
bytes = repository.scm.count_repository!
|
||||
|
||||
repository.update_attributes!(
|
||||
required_storage_bytes: bytes,
|
||||
storage_updated_at: Time.now,
|
||||
)
|
||||
end
|
||||
|
||||
def destroy_failed_jobs?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def repository
|
||||
@repository ||= Repository.find @id
|
||||
end
|
||||
end
|
||||
@@ -96,9 +96,10 @@ module OpenProject
|
||||
|
||||
# Activate observers that should always be running.
|
||||
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
|
||||
config.active_record.observers = :journal_observer, :message_observer,
|
||||
:news_observer, :wiki_content_observer,
|
||||
:comment_observer, :work_package_observer
|
||||
config.active_record.observers = :message_observer,
|
||||
:news_observer,
|
||||
:wiki_content_observer,
|
||||
:comment_observer
|
||||
|
||||
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
||||
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
||||
|
||||
@@ -210,34 +210,52 @@ default:
|
||||
# airbrake:
|
||||
# api_key: <your_api_key>
|
||||
# host: errbit.example.org
|
||||
# post: 443
|
||||
# port: 443 (default)
|
||||
|
||||
# Configuration of SCM executable command.
|
||||
# Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
|
||||
# On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
|
||||
# Examples:
|
||||
# scm_subversion_command: svn # (default: svn)
|
||||
# scm_git_command: /usr/local/bin/git # (default: git)
|
||||
# scm_subversion_command:
|
||||
# scm_git_command:
|
||||
# Configuration of Source control vendors
|
||||
# client_command:
|
||||
# Use this command to the default SCM vendor command (taken from path).
|
||||
# Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
|
||||
# On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
|
||||
# manages:
|
||||
# Enable managed repositories for this vendor. This allows OpenProject to
|
||||
# take control over the given path to create and delete repositories directly
|
||||
# when created in the frontend.
|
||||
# NOTE: Disabling :managed repositories using disabled_types takes precedence over this setting.
|
||||
# mode:
|
||||
# The file mode to set the repository folder to (defaults to 0700).
|
||||
#
|
||||
# Enable filesystem base SCM by specifying the directories that are available
|
||||
# to serve as an SCM directory. The format in which the available directories
|
||||
# can be specified is close to a shell glob. Please see
|
||||
# http://ruby-doc.org/core-2.2.0/Dir.html#method-c-glob for details.
|
||||
# group:
|
||||
# The group that should own the repository folder.
|
||||
# Important: The user running OpenProject must be a member of this group in order to have the
|
||||
# privilege to set the group ownership.
|
||||
# Note: The owner is always the user that runs OpenProject!
|
||||
# disabled_types:
|
||||
# Disable specific repository types for this particular vendor. This allows
|
||||
# to restrict the available choices a project administrator has for creating repositories
|
||||
# See the example below for available types
|
||||
#
|
||||
# Multiple patterns can be configured.
|
||||
# Available types for git:
|
||||
# - :local (Local repositories, registered using a local path)
|
||||
# - :managed (Managed repositores, available IF :manages path is set below)
|
||||
# Available types for subversion:
|
||||
# - :existing (Existing subversion repositories by URL - local using file:/// or remote
|
||||
# using one of the supported URL schemes (e.g., https://, svn+ssh:// )
|
||||
# - :managed (Managed repositores, available IF :manages path is set below)
|
||||
#
|
||||
# If no directory is specified, there will be no option to configure a
|
||||
# filesystem based SCM within OpenProject.
|
||||
# Examplary configuration
|
||||
#
|
||||
# Please note that this allows users to browse the filesystem which could
|
||||
# lead to sensitive information (e.g. db credentials) being made accessible
|
||||
# to them.
|
||||
#
|
||||
# scm_filesystem_path_whitelist:
|
||||
# - "/opt/app/openproject/repositories/*"
|
||||
# - "/tmp/**/*"
|
||||
# scm:
|
||||
# Git:
|
||||
# client_command: /usr/local/bin/git
|
||||
# disabled_types:
|
||||
# - :local
|
||||
# manages: /opt/repositories/git
|
||||
# Subversion:
|
||||
# client_command: /usr/local/bin/svn
|
||||
# disabled_types:
|
||||
# - :existing
|
||||
# manages: /opt/repositories/svn
|
||||
|
||||
# Key used to encrypt sensitive data in the database (SCM and LDAP passwords).
|
||||
# If you don't want to enable data encryption, just leave it blank.
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
airbrake = OpenProject::Configuration['airbrake']
|
||||
|
||||
if airbrake && airbrake['api_key']
|
||||
# airbrake isn't loaded by default, so let's do that now
|
||||
require 'airbrake'
|
||||
|
||||
Airbrake.configure do |config|
|
||||
config.api_key = airbrake['api_key']
|
||||
config.host = airbrake['host'] if airbrake['host']
|
||||
|
||||
@@ -268,7 +268,8 @@ Redmine::AccessControl.map do |map|
|
||||
|
||||
map.project_module :repository do |repo|
|
||||
repo.permission :manage_repository,
|
||||
{ repositories: [:edit, :committers, :destroy] },
|
||||
{ repositories: [:edit, :create, :update, :committers,
|
||||
:destroy_info, :destroy] },
|
||||
require: :member
|
||||
|
||||
repo.permission :browse_repository,
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
# See doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'redmine/scm/base'
|
||||
require 'open_project/scm/manager'
|
||||
|
||||
Redmine::Scm::Base.add 'Subversion'
|
||||
Redmine::Scm::Base.add 'Git'
|
||||
Redmine::Scm::Base.add 'Filesystem'
|
||||
OpenProject::Scm::Manager.add 'Subversion'
|
||||
OpenProject::Scm::Manager.add 'Git'
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#-- encoding: UTF-8
|
||||
#-- copyright
|
||||
# OpenProject is a project management system.
|
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
#
|
||||
# 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 doc/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
OpenProject::Notifications.subscribe('journal_created') do |payload|
|
||||
JournalNotificationMailer.distinguish_journals(payload[:journal], payload[:send_notification])
|
||||
end
|
||||
+107
-23
@@ -72,6 +72,8 @@ en:
|
||||
host: "Host"
|
||||
onthefly: "On-the-fly user creation"
|
||||
port: "Port"
|
||||
changeset:
|
||||
repository: "Repository"
|
||||
comment:
|
||||
commented: "Commented" # an object that this comment belongs to
|
||||
custom_field:
|
||||
@@ -479,44 +481,65 @@ en:
|
||||
one: "hour"
|
||||
other: "hours"
|
||||
|
||||
default_activity_management: "Management"
|
||||
default_activity_design: "Design"
|
||||
default_activity_development: "Development"
|
||||
default_activity_management: "Management"
|
||||
default_activity_other: "Other"
|
||||
default_activity_specification: "Specification"
|
||||
default_activity_support: "Support"
|
||||
default_activity_testing: "Testing"
|
||||
default_activity_Support: "Support"
|
||||
default_activity_Other: "Other"
|
||||
default_status_new: "new"
|
||||
default_status_specified: "specified"
|
||||
default_status_confirmed: "confirmed"
|
||||
default_status_to_be_scheduled: "to be scheduled"
|
||||
default_status_scheduled: "scheduled"
|
||||
default_status_in_progress: "in progress"
|
||||
default_status_tested: "tested"
|
||||
default_status_on_hold: "on hold"
|
||||
default_status_rejected: "rejected"
|
||||
default_status_closed: "closed"
|
||||
default_color_black: "Black"
|
||||
default_color_blue: "Blue"
|
||||
default_color_blue_dark: "Blue (dark)"
|
||||
default_color_blue_light: "Blue (light)"
|
||||
default_color_green_dark: "Green (dark)"
|
||||
default_color_green_light: "Green (light)"
|
||||
default_color_grey_dark: "Grey (dark)"
|
||||
default_color_grey_light: "Grey (light)"
|
||||
default_color_grey: "Grey"
|
||||
default_color_magenta: "Magenta"
|
||||
default_color_orange: "Orange"
|
||||
default_color_red: "Red"
|
||||
default_color_white: "White"
|
||||
default_color_yellow: "Yellow"
|
||||
default_status_closed: "Closed"
|
||||
default_status_confirmed: "Confirmed"
|
||||
default_status_developed: "Developed"
|
||||
default_status_in_development: "In development"
|
||||
default_status_in_progress: "In progress"
|
||||
default_status_in_specification: "In specification"
|
||||
default_status_in_testing: "In testing"
|
||||
default_status_new: "New"
|
||||
default_status_on_hold: "On hold"
|
||||
default_status_rejected: "Rejected"
|
||||
default_status_scheduled: "Scheduled"
|
||||
default_status_specified: "Specified"
|
||||
default_status_tested: "Tested"
|
||||
default_status_test_failed: "Test failed"
|
||||
default_status_to_be_scheduled: "To be scheduled"
|
||||
default_priority_low: "Low"
|
||||
default_priority_normal: "Normal"
|
||||
default_priority_high: "High"
|
||||
default_priority_immediate: "Immediate"
|
||||
default_project_type_customer: "Customer Project"
|
||||
default_project_type_internal: "Internal Project"
|
||||
default_project_type_scrum: "Scrum team"
|
||||
default_project_type_standard: "Standard project"
|
||||
default_reported_project_status_green: "Green"
|
||||
default_reported_project_status_amber: "Amber"
|
||||
default_reported_project_status_red: "Red"
|
||||
default_role_anonymous: "Anonymous"
|
||||
default_role_developer: "Developer"
|
||||
default_role_manager: "Manager"
|
||||
default_role_project_admin: "Project admin"
|
||||
default_role_non_member: "Non member"
|
||||
default_role_reader: "Reader"
|
||||
default_role_member: "Member"
|
||||
default_type: "Work Package"
|
||||
default_type_task: "Task"
|
||||
default_type_bug: "Bug"
|
||||
default_type_deliverable: "Deliverable"
|
||||
default_type_epic: "Epic"
|
||||
default_type_feature: "Feature"
|
||||
default_type_milestone: "Milestone"
|
||||
default_type_phase: "Phase"
|
||||
default_type_bug: "Bug"
|
||||
default_type_feature: "Feature"
|
||||
default_type_task: "Task"
|
||||
default_type_user_story: "User story"
|
||||
|
||||
description_active: "Active?"
|
||||
description_attachment_toggle: "Show/Hide attachments"
|
||||
@@ -568,7 +591,6 @@ en:
|
||||
error_can_not_delete_standard_type: "Standard types cannot be deleted."
|
||||
error_can_not_remove_role: "This role is in use and cannot be deleted."
|
||||
error_can_not_reopen_work_package_on_closed_version: "A work package assigned to a closed version cannot be reopened"
|
||||
error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
|
||||
error_check_user_and_role: "Please choose a user and a role."
|
||||
error_work_package_done_ratios_not_updated: "Work package done ratios not updated."
|
||||
error_work_package_not_found_in_project: "The work package was not found or does not belong to this project"
|
||||
@@ -668,6 +690,7 @@ en:
|
||||
label_category: "Category"
|
||||
label_wiki_menu_item: Wiki menu item
|
||||
label_select_main_menu_item: Select new main menu item
|
||||
label_required_disk_storage: "Required disk storage"
|
||||
label_change_plural: "Changes"
|
||||
label_change_properties: "Change properties"
|
||||
label_change_status: "Change status"
|
||||
@@ -742,7 +765,6 @@ en:
|
||||
label_feeds_access_key_created_on: "RSS access key created %{value} ago"
|
||||
label_file_added: "File added"
|
||||
label_file_plural: "Files"
|
||||
label_filesystem_path: "Root directory"
|
||||
label_filter_add: "Add filter"
|
||||
label_filter_plural: "Filters"
|
||||
label_float: "Float"
|
||||
@@ -885,6 +907,7 @@ en:
|
||||
label_principal_search: "Search for users or groups"
|
||||
label_profile: "Profile"
|
||||
label_project_all: "All Projects"
|
||||
label_project_count: "Total number of projects"
|
||||
label_project_copy_notifications: "Send email notifications during the project copy"
|
||||
label_project_latest: "Latest projects"
|
||||
label_project_default_type: "Allow empty type"
|
||||
@@ -1073,6 +1096,11 @@ en:
|
||||
label_keyboard_shortcut_focus_next_item: "Focus next list element (on some lists only)"
|
||||
label_visible_elements: Visible elements
|
||||
|
||||
auth_source:
|
||||
using_abstract_auth_source: "Can't use an abstract authentication source."
|
||||
ldap_error: "LDAP-Error: %{error_message}"
|
||||
ldap_auth_failed: "Could not authenticate at the LDAP-Server."
|
||||
|
||||
macro_execution_error: "Error executing the macro %{macro_name}"
|
||||
macro_unavailable: "Macro %{macro_name} cannot be displayed."
|
||||
|
||||
@@ -1113,7 +1141,6 @@ en:
|
||||
notice_account_registered_and_logged_in: "Welcome, your account has been activated. You are logged in now."
|
||||
notice_api_access_key_reseted: "Your API access key was reset."
|
||||
notice_can_t_change_password: "This account uses an external authentication source. Impossible to change the password."
|
||||
notice_default_data_loaded: "Default configuration successfully loaded."
|
||||
notice_email_error: "An error occurred while sending mail (%{value})"
|
||||
notice_email_sent: "An email was sent to %{value}"
|
||||
notice_failed_to_save_work_packages: "Failed to save %{count} work package(s) on %{total} selected: %{ids}."
|
||||
@@ -1247,6 +1274,63 @@ en:
|
||||
member_of_group: "Assignee's group"
|
||||
subproject_id: "Subproject"
|
||||
|
||||
repositories:
|
||||
checkout_instructions: "Checkout instructions"
|
||||
create_managed_delay: "Please note: The repository is managed, it is created asynchronously on the disk and will be available shortly."
|
||||
create_successful: "The repository has been registered."
|
||||
delete_sucessful: "The repository has been deleted."
|
||||
destroy:
|
||||
title: "Are you absolutely sure?"
|
||||
managed_text: "If you continue, this will permanently delete the managed repository of %{project_name}. This action cannot be reverted."
|
||||
managed_path_note: "The following directory will be erased: %{path}"
|
||||
linked_text: "You're about to remove the linked repository %{url} from the project %{project_name}.\nNote: This will NOT delete the contents of this repository, as it is not managed by OpenProject."
|
||||
errors:
|
||||
build_failed: "Unable to create the repository with the selected configuration. %{reason}"
|
||||
empty_repository: "The repository exists, but is empty. It does not contain any revisions yet."
|
||||
exists_on_filesystem: "The repository directory exists already on filesystem."
|
||||
filesystem_access_failed: "An error occurred while creating the repository on filesystem: %{message}."
|
||||
not_manageable: "This repository vendor cannot be managed by OpenProject."
|
||||
path_permission_failed: "An error occurred trying to create the following path: %{path}. Please ensure that OpenProject may write to that folder."
|
||||
unauthorized: "You're not authorized to access the repository or the credentials are invalid."
|
||||
unavailable: "The repository is unavailable."
|
||||
exception_title: "Cannot access the repository: %{message}"
|
||||
disabled_or_unknown_type: "The selected type %{type} is disabled or no longer available for the SCM vendor %{vendor}."
|
||||
disabled_or_unknown_vendor: "The SCM vendor %{vendor} is disabled or no longer available."
|
||||
git:
|
||||
instructions:
|
||||
managed_url: "This is the URL of the managed (local) Git repository."
|
||||
path: "Specify the path to your local Git repository ( e.g., %{example_path} )."
|
||||
path_encoding: "Override Git path encoding (Default: UTF-8)"
|
||||
local_title: "Link existing local Git repository"
|
||||
local_introduction: "If you have an existing local Git repository, you can link it with OpenProject to access it from within the application."
|
||||
managed_path: "Managed Git path (preview)"
|
||||
managed_introduction: "Let OpenProject create and integrate a local Git repository automatically."
|
||||
managed_title: "Git repository integrated into OpenProject"
|
||||
path: "Path to Git repository"
|
||||
path_encoding: "Path encoding"
|
||||
go_to_revision: "Go to revision"
|
||||
scm_vendor: "Source control management system"
|
||||
scm_type: "Repository type"
|
||||
scm_types:
|
||||
local: "Link existing local repository"
|
||||
existing: "Link existing repository"
|
||||
managed: "Create new repository in OpenProject"
|
||||
storage:
|
||||
not_available: "Disk storage consumption is not available for this repository."
|
||||
subversion:
|
||||
existing_title: "Existing Subversion repository"
|
||||
existing_introduction: "If you have an existing Subversion repository, you can link it with OpenProject to access it from within the application."
|
||||
instructions:
|
||||
managed_url: "This is the URL of the managed (local) Subversion repository."
|
||||
url: "Enter the repository URL. This may either target a local repository (starting with %{local_proto} ), or a remote repository.\nThe following URL schemes are supported:"
|
||||
managed_title: "Subversion repository integrated into OpenProject"
|
||||
managed_introduction: "Let OpenProject create and integrate a local Subversion repository automatically."
|
||||
managed_url: "Managed URL"
|
||||
password: "Repository Password"
|
||||
username: "Repository username"
|
||||
update_settings_successful: "The settings have been sucessfully saved."
|
||||
url: "URL to repository"
|
||||
|
||||
search_input_placeholder: "search ..."
|
||||
|
||||
setting_activity_days_default: "Days displayed on project activity"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user