Merge branch 'dev' into feature/rails4

Signed-off-by: Alex Coles <alex@alexbcoles.com>
This commit is contained in:
Alex Coles
2015-08-08 19:24:14 +01:00
319 changed files with 12651 additions and 5116 deletions
+3 -1
View File
@@ -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
View File
@@ -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
+41 -31
View File
@@ -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');
}
});
});
-5
View File
@@ -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
-16
View File
@@ -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
+7 -5
View File
@@ -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?
+5 -3
View File
@@ -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
+79 -32
View File
@@ -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
+38 -2
View File
@@ -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
+1 -1
View File
@@ -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])
+1 -1
View File
@@ -98,6 +98,6 @@ module PlanningElementsHelper
formatter.send(:format_details,
attribute,
journal.changed_data[attribute])
journal.details[attribute])
end
end
+1 -1
View File
@@ -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 }
+77 -112
View File
@@ -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
+3 -3
View File
@@ -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
+3 -1
View File
@@ -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'
+2
View File
@@ -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
+2 -3
View File
@@ -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
View File
@@ -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)
+128 -16
View File
@@ -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
+42 -16
View File
@@ -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
+86
View File
@@ -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
-67
View File
@@ -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
+4 -3
View File
@@ -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
+1
View File
@@ -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)
+1 -2
View File
@@ -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
+1 -2
View File
@@ -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
-19
View File
@@ -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
View File
@@ -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
View File
@@ -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
-100
View File
@@ -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
+39 -11
View File
@@ -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?
+28 -4
View File
@@ -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?
+8
View File
@@ -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
+1 -55
View File
@@ -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
+5 -5
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+184
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
+28 -5
View File
@@ -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 %>
&nbsp;
<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
+1 -1
View File
@@ -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 %>
+1 -1
View File
@@ -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
+1
View File
@@ -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 %>
+2 -1
View File
@@ -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">
+2 -2
View File
@@ -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,
&#x00BB; <%= link_to h(dir), :action => 'show', :project_id => @project,
:path => to_path_param(link_path), :rev => @rev %>
<% end %>
<% if filename %>
/ <%= link_to h(filename),
&#x00BB; <%= 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}')"%>">&nbsp</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>
+14 -26
View File
@@ -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>
+67
View File
@@ -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 %>
+7 -1
View File
@@ -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>
@@ -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>
+56 -1
View File
@@ -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 }%>
+19 -10
View File
@@ -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' %>
+1 -1
View File
@@ -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) %>
&nbsp;
<% 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 } %>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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">
@@ -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
+91
View File
@@ -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
+50
View File
@@ -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
+59
View File
@@ -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
+4 -3
View File
@@ -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.
+41 -23
View File
@@ -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.
+3
View File
@@ -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']
+2 -1
View File
@@ -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,
+3 -4
View File
@@ -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
View File
@@ -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