Merge branch 'dev' into fix/21411-table-header-too-big-repository-revision-table

pull/3471/head
Henriette Dinger 9 years ago
commit 06a05c115c
  1. 4
      .gitignore
  2. 3
      .travis.yml
  3. 2
      Gemfile.lock
  4. 10
      app/assets/javascripts/admin_users.js
  5. 1
      app/assets/javascripts/application.js.erb
  6. 32
      app/assets/javascripts/danger_zone_validation.js
  7. 7
      app/assets/javascripts/members_select_boxes.js
  8. 4
      app/assets/javascripts/modal.js
  9. 38
      app/assets/javascripts/new_user.js
  10. 2
      app/assets/javascripts/unsupported-browsers.js
  11. 2
      app/assets/stylesheets/_misc_legacy.sass
  12. 8
      app/assets/stylesheets/content/_accounts.sass
  13. 42
      app/assets/stylesheets/content/_forms.lsg
  14. 57
      app/assets/stylesheets/content/_forms.sass
  15. 7
      app/assets/stylesheets/content/_links.sass
  16. 8
      app/assets/stylesheets/content/_notifications.lsg
  17. 17
      app/assets/stylesheets/content/_notifications.sass
  18. 10
      app/assets/stylesheets/content/_select2.scss
  19. 124
      app/assets/stylesheets/content/_table.lsg
  20. 16
      app/assets/stylesheets/content/_table.sass
  21. 61
      app/assets/stylesheets/content/_tables.sass
  22. 51
      app/assets/stylesheets/content/_widget_box.lsg
  23. 106
      app/assets/stylesheets/content/_widget_box.sass
  24. 10
      app/assets/stylesheets/content/work_package_details/_activities_tab.sass
  25. 3
      app/assets/stylesheets/default.css.sass
  26. 6
      app/assets/stylesheets/layout/_main_menu.sass
  27. 2
      app/assets/stylesheets/layout/_toolbar.lsg
  28. 20
      app/assets/stylesheets/layout/_toolbar.sass
  29. 4
      app/assets/stylesheets/layout/_work_package.sass
  30. 9
      app/assets/stylesheets/open_project_global/_variables.sass
  31. 47
      app/assets/stylesheets/specific/homescreen.sass
  32. 170
      app/controllers/account_controller.rb
  33. 61
      app/controllers/application_controller.rb
  34. 7
      app/controllers/auth_sources_controller.rb
  35. 17
      app/controllers/concerns/omniauth_login.rb
  36. 110
      app/controllers/concerns/user_invitation.rb
  37. 6
      app/controllers/copy_projects_controller.rb
  38. 9
      app/controllers/homescreen_controller.rb
  39. 109
      app/controllers/invitations_controller.rb
  40. 189
      app/controllers/members_controller.rb
  41. 1
      app/controllers/repositories_controller.rb
  42. 44
      app/controllers/users_controller.rb
  43. 2
      app/controllers/work_packages/bulk_controller.rb
  44. 10
      app/controllers/work_packages_controller.rb
  45. 50
      app/helpers/homescreen_helper.rb
  46. 7
      app/helpers/projects_helper.rb
  47. 39
      app/models/dummy_auth_source.rb
  48. 24
      app/models/journal/aggregated_journal.rb
  49. 13
      app/models/journal_notification_mailer.rb
  50. 10
      app/models/member.rb
  51. 2
      app/models/news.rb
  52. 7
      app/models/principal.rb
  53. 19
      app/models/project.rb
  54. 3
      app/models/repository.rb
  55. 55
      app/models/user.rb
  56. 6
      app/models/watcher_notification_mailer.rb
  57. 149
      app/policies/redirect_policy.rb
  58. 5
      app/views/account/_auth_providers.html.erb
  59. 4
      app/views/account/_password_login_form.html.erb
  60. 2
      app/views/account/register.html.erb
  61. 2
      app/views/copy_projects/copy_from_admin.html.erb
  62. 2
      app/views/copy_projects/copy_from_settings.html.erb
  63. 9
      app/views/help/wiki_syntax_detailed.html.erb
  64. 47
      app/views/homescreen/blocks/_administration.html.erb
  65. 55
      app/views/homescreen/blocks/_community.html.erb
  66. 17
      app/views/homescreen/blocks/_my_account.html.erb
  67. 20
      app/views/homescreen/blocks/_news.html.erb
  68. 28
      app/views/homescreen/blocks/_projects.html.erb
  69. 26
      app/views/homescreen/blocks/_users.html.erb
  70. 8
      app/views/homescreen/blocks/_welcome.html.erb
  71. 61
      app/views/homescreen/index.html.erb
  72. 16
      app/views/members/_member_form_impaired.html.erb
  73. 21
      app/views/members/_member_form_non_impaired.html.erb
  74. 25
      app/views/members/autocomplete_for_member.json.erb
  75. 4
      app/views/members/create.js.erb
  76. 158
      app/views/members/index.html.erb
  77. 43
      app/views/members/new.html.erb
  78. 5
      app/views/projects/index.html.erb
  79. 147
      app/views/projects/settings/_members.html.erb
  80. 2
      app/views/repositories/_breadcrumbs.html.erb
  81. 2
      app/views/repositories/_checkout_instructions.html.erb
  82. 3
      app/views/repositories/_repository_header.html.erb
  83. 59
      app/views/repositories/annotate.html.erb
  84. 1
      app/views/repositories/show.html.erb
  85. 2
      app/views/search/index.html.erb
  86. 14
      app/views/settings/_general.html.erb
  87. 2
      app/views/users/_memberships.html.erb
  88. 60
      app/views/users/_simple_form.html.erb
  89. 9
      app/views/users/new.html.erb
  90. 47
      app/views/workflows/_form.html.erb
  91. 40
      app/views/workflows/edit.html.erb
  92. 73
      app/workers/deliver_notification_job.rb
  93. 25
      app/workers/deliver_watcher_notification_job.rb
  94. 41
      app/workers/deliver_work_package_notification_job.rb
  95. 10
      app/workers/enqueue_work_package_notification_job.rb
  96. 18
      app/workers/scm/storage_updater_job.rb
  97. 1
      config/environments/production.rb
  98. 77
      config/initializers/homescreen.rb
  99. 8
      config/initializers/menus.rb
  100. 7
      config/initializers/permissions.rb
  101. Some files were not shown because too many files have changed in this diff Show More

4
.gitignore vendored

@ -70,8 +70,8 @@
/tmp/test/*
/*.rbc
/doc/app
/Gemfile.local
/Gemfile.plugins
/Gemfile.local*
/Gemfile.plugins*
/.rvmrc*
/.ruby-version
/.ruby-gemset

@ -83,8 +83,9 @@ notifications:
urls:
- "https://webhooks.gitter.im/e/435ab5d5c6944305da2f"
addons:
firefox: "38.0esr"
# Disabling coverage reporting until CodeClimate supports merging results from multiple partial tests
# addons:
# code_climate:
# repo_token:
# secure: "W/lyd8Ud18GRASuVShsIKa2MRHhxjh8WICMQ4WKr68qt0X0Tlp7Bclv4ReiEgiQeKsIoJJy5FfJfINdAT8A4sy2JbrLeISShcIU7Kqpfh6DSLNoRAuLz5P7EXMNFns1gBKCmrSzcB+9ksuTLyTCKkjUcj1NbJzGqpB4jSTecAdg="

@ -30,7 +30,7 @@ GIT
GIT
remote: https://github.com/opf/openproject-translations.git
revision: 6f7b5322d3f30487d2a009dc2e27c54b800c00d8
revision: 519ed611c49e0f014f65c838cf50bfbe05d2d183
branch: dev
specs:
openproject-translations (5.0.0.pre.alpha)

@ -41,14 +41,18 @@
.prop('disabled', checked);
}
// Hide password fields when non-internal authentication source is selected
/**
* Hide password fields when non-internal authentication source is selected.
* Also disables the fields so they are not submitted and not required to
* enter.
*/
function on_auth_source_change() {
var passwordFields = jQuery('#password_fields'),
passwordInputs = passwordFields.find('#user_password, #user_password_confirmation');
if (this.value === '') {
passwordFields.show();
passwordInputs.removeAttr('disabled');
passwordInputs.removeProp('disabled');
} else {
passwordFields.hide();
passwordInputs.prop('disabled', 'disabled');
@ -57,6 +61,6 @@
jQuery(function init(){
jQuery('#user_assign_random_password').change(on_assign_random_password_change);
jQuery('#user_auth_source_id').change(on_auth_source_change);
jQuery('#user_auth_source_id').on('change.togglePasswordFields', on_auth_source_change);
});
})();

@ -63,6 +63,7 @@
//= require search
//= require colors
//= require tooltips
//= require danger_zone_validation
//source: http://stackoverflow.com/questions/8120065/jquery-and-prototype-dont-work-together-with-array-prototype-reverse
if (typeof []._reverse == 'undefined') {

@ -26,20 +26,20 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(PathHelper) {
return {
restrict: 'A',
templateUrl: '/templates/work_packages/tabs/_attachment_user_cell.html',
scope: {
attachment: '='
},
link: function(scope, element, attributes) {
scope.attachment.links.author.fetch()
.then(function(author){
scope.authorName = author.props.name;
scope.authorId = author.props.id;
scope.userPath = PathHelper.staticUserPath(author.props.id);
(function($) {
$(function() {
// This will only work iff there is a single danger zone on the page
var dangerZoneVerification = $('.danger-zone--verification');
var expectedValue = $('.danger-zone--expected-value').text();
dangerZoneVerification.find('input').on('input', function(){
var actualValue = dangerZoneVerification.find('input').val();
if (expectedValue === actualValue) {
dangerZoneVerification.find('button').prop('disabled', false);
} else {
dangerZoneVerification.find('button').prop('disabled', true);
}
});
}
};
};
});
}(jQuery));

@ -103,10 +103,5 @@ jQuery(document).ready(function($) {
});
};
memberstab = $('#tab-members').first();
if ((memberstab !== null) && (memberstab.hasClass("selected"))) {
init_members_cb();
} else {
memberstab.click(init_members_cb);
}
init_members_cb();
});

@ -39,7 +39,9 @@ var ModalHelper = (function() {
// prototype, so that all ModalHelper instances can share them.
if (ModalHelper._done !== true) {
// one time initialization
modalDiv = jQuery('<div/>').css('hidden', true).attr('id', 'modalDiv');
modalDiv = jQuery('<div/>').css('hidden', true)
.attr('id', 'modalDiv')
.css('display', 'none');
body.append(modalDiv);
// close when body is clicked

@ -26,21 +26,29 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function() {
return {
restrict: 'A',
replace: false,
templateUrl: '/templates/work_packages/tabs/_attachment_file_size.html',
scope: {
attachment: '='
},
link: function(scope, element, attributes) {
scope.displayFileSize = "(" + formattedFileSize(scope.attachment.props.fileSize) + ")";
function formattedFileSize(fileSize) {
var size = parseFloat(fileSize);
return isNaN(size) ? "0kB" : (size / 1000).toFixed(2) + "kB";
}
(function() {
/**
* When the user chooses the default internal authentication mode
* no login field is shown as the email is taken by default.
* If another mode is chosen (e.g. LDAP) the field is shown as it
* may be required by the auth source.
*/
var toggleLogin = function() {
var newUserLogin = jQuery('#new_user_login');
if (this.value === '') {
newUserLogin.hide();
newUserLogin.find('input').prop('disabled', true);
} else {
newUserLogin.show();
newUserLogin.find('input').prop('disabled', false);
}
};
};
jQuery(function init(){
var select = jQuery('#user_auth_source_id');
select.on('change.toggleNewUserLogin', toggleLogin);
});
})();

@ -32,7 +32,7 @@
var agent = navigator.userAgent;
if (agent.match(/MSIE [789]\.0/) === null && // IE 7-9
agent.match(/Firefox\/(([1-2][0-9]|30)\.)/) === null) { // Firefox 10-30
agent.match(/Firefox\/(([1-2][0-9]|3[0-7])\.)/) === null) { // Firefox 10-37
return;
}

@ -432,6 +432,8 @@ div.issue hr
margin-bottom: 6px
#content
min-height: 250px
h3
margin: 12px 0 6px
h2 + h3

@ -38,6 +38,14 @@
float: right
text-align: right
#content .login-auth-providers.wide
width: auto
text-align: center
a.auth-provider
float: none
display: inline-block
// use id selectors to be specific enough to override general content and top-menu definitions
#content .login-auth-providers, #top-menu #nav-login-content .login-auth-providers
width: 471px

@ -57,6 +57,38 @@
</form>
```
## Forms: Bordered style - Danger zone
```
<form class="form danger-zone">
<section class="form--section">
<h3 class="form--section-title">
Delete account <em>foo.bar@openproject.org</em>
</h3>
<p>
Your account will be deleted from the system. Therefore, you will no longer be able to log in with your current credentials. If you choose to become a user of this application again, you can do so by using the means this application grants.
</p>
<p>
Of the data you created as much as possible will be deleted. Note however, that data like work packages and wiki entries can not be deleted without impeding the work of the other users. Such data is hence reassigned to an account called "Deleted user".
</p>
<p class="danger-zone--warning">
<span class="icon icon-attention2"></span>
<span>Deleting your account is an irreversible action.</span>
</p>
<p>
Enter your login <em class="danger-zone--expected-value">foo.bar@openproject.org</em> to verify the deletion.
</p>
<div class="danger-zone--verification">
<input type="text"></input>
<button class="button -highlight" disabled>
<i class="button--icon icon-delete"></i>
<span class="button--text">Delete</span>
</button>
</div>
</section>
</form>
```
## Forms: Standard layout
```
@ -178,6 +210,16 @@
The more, the better
</div>
</div>
<div class="form--field">
<label class="form--label">Telephone:</label>
<div class="form--field-container">
<div class="form--field-affix icon-context icon-phone">
</div>
<div class="form--text-field-container">
<input type="tel" class="form--text-field">
</div>
</div>
</div>
<div class="form--field -required">
<label class="form--label">Long text:</label>
<div class="form--field-container">

@ -61,6 +61,52 @@ $form--field-types: (text-field, text-area, select, check-box, radio-button, ran
&.-compressed
padding: 10px 20px 0 20px
&.danger-zone
border: 1px solid $content-form-danger-zone-bg-color
.form--section
padding-top: 0px
.form--section-title
background-color: $content-form-danger-zone-bg-color
color: $content-form-danger-zone-font-color !important
padding: 0.5rem !important
margin: 0 0 1rem 0
em
font-style: italics
p
padding-left: 0.5rem
&.danger-zone--warning
font-weight: bold
color: $content-form-danger-zone-bg-color
span.icon,
span.icon-context
vertical-align: middle
&:before
padding-left: 0px
color: $content-form-danger-zone-bg-color
.danger-zone--verification
display: flex
input
flex-basis: 50%
margin: 0 0.5rem 0 0.5rem
.button.-highlight
background: $content-form-danger-zone-bg-color
color: $content-form-danger-zone-font-color
border-color: $content-form-danger-zone-bg-color
&.icon:before,
&.icon-context:before
color: $content-form-danger-zone-font-color
padding-left: 0px
.form--separator
border: 0
border-bottom: 1px solid $content-form-separator-color
@ -244,6 +290,11 @@ fieldset.form--fieldset
text-overflow: ellipsis
overflow: hidden
&.-required
input.form--text-field:invalid
// avoids the box-shadow from Firefox at required input fields
box-shadow: none
.form--label
@include grid-content(2)
@include grid-visible-overflow
@ -535,6 +586,12 @@ input[readonly].-clickable
padding: 0 $form-padding
margin-bottom: 0.5rem
align-items: center
line-height: 1
&.icon, &.icon-context
padding: 5px
&:before
padding: 0
%form--field-element-container + &
margin-left: -1rem

@ -59,10 +59,17 @@ a
&.work_package.closed, &.work_package.closed:hover
text-decoration: line-through
&.-no-decoration
color: inherit
&:hover
text-decoration: none
a.icon, a.icon-context
color: $content-icon-link-color
font-weight: normal
&.external
color: $content-link-color
a.icon:hover, a.icon-context:hover
text-decoration: none

@ -22,7 +22,7 @@
```
<div class="notification-box -error">
<a href="#" class="notification-box--close">&times;</a>
<a href="#" title="close" class="notification-box--close icon-context icon-close2"></a>
<div class="notification-box--content">
<p>An error occured, here are the facts:</p>
<ul>
@ -39,7 +39,7 @@
```
<div class="notification-box -warning">
<a href="#" class="notification-box--close">&times;</a>
<a href="#" title="close" class="notification-box--close icon-context icon-close2"></a>
<div class="notification-box--content">
<p>This is a warning. You may ignore it, but bad things might happen.</p>
</div>
@ -50,7 +50,7 @@
```
<div class="notification-box -warning -severe">
<a href="#" class="notification-box--close">&times;</a>
<a href="#" title="close" class="notification-box--close icon-context icon-close2"></a>
<div class="notification-box--content">
<p>This is a warning with severe consequences. You should not ignore it.</p>
</div>
@ -61,7 +61,7 @@
```
<div class="notification-box -success">
<a href="#" class="notification-box--close">&times;</a>
<a href="#" title="close" class="notification-box--close icon-context icon-close2"></a>
<div class="notification-box--content">
<p>Successful update. <a href="#">A link to the past</a></p>
</div>

@ -37,8 +37,8 @@ $nm-box-padding: rem-calc(10px 35px 10px 35px)
$nm-font-color: $body-font-color
$nm-notification-width: rem-calc(550)
$nm-color-error-border: #ca3f3f
$nm-color-error-icon: #ca3f3f
$nm-color-error-border: $content-form-danger-zone-bg-color
$nm-color-error-icon: $content-form-danger-zone-bg-color
$nm-color-error-background: #fedada
$nm-color-success-border: #35c53f
@ -118,15 +118,14 @@ $nm-upload-box-padding: rem-calc(15) rem-calc(25)
.notification-box--close
position: absolute
top: rem-calc(10px)
right: rem-calc(10px)
color: #000000
background-color: rgba(0, 0, 0, 0.08)
top: rem-calc(12px)
right: rem-calc(12px)
background-color: rgba(0, 0, 0, 0)
border-radius: rem-calc(50px)
width: rem-calc(23px)
height: rem-calc(23px)
width: 1rem
height: 1rem
text-align: center
font-size: rem-calc(23px)
font-size: 1rem
line-height: rem-calc(20px)
&:hover
text-decoration: none

@ -160,6 +160,16 @@ $se2-width: 100%;
}
}
&.select2-container-multi {
.select2-search-choice {
margin: 7px 0 0 5px;
&:nth-last-of-type(1) {
margin-bottom: 7px;
}
}
}
&.ui-select-multiple {
.ui-select-search {

@ -107,6 +107,130 @@
```
## with footer
```
<div class="generic-table--container -with-footer">
<div class="generic-table--results-container" style="max-height: 340px;">
<table role="grid" class="generic-table" interactive-table>
<colgroup>
<col highlight-col>
<col highlight-col>
<col highlight-col>
<col highlight-col>
<col highlight-col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span class="sort asc">
First
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span class="sort asc">
Second
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span class="sort desc">
Third
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span class="sort desc">
Fourth
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span class="sort desc">
Fifth
</span>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lorem ipsum dolor sit amet, consectetur adipiscing elit</td>
<td>Nullam a sem et metus congue placerat.</td>
<td>Mauris ut augue viverra, consequat eros eu, maximus quam.</td>
<td>Maecenas elementum orci a varius suscipit.</td>
<td>Nunc molestie neque sit amet eros semper dapibus.</td>
</tr>
<tr>
<td>Nullam a sem et metus congue placerat.</td>
<td>Lorem ipsum dolor sit amet, consectetur adipiscing elit</td>
<td>Mauris ut augue viverra, consequat eros eu, maximus quam.</td>
<td>Maecenas elementum orci a varius suscipit.</td>
<td>Nunc molestie neque sit amet eros semper dapibus.</td>
</tr>
<tr>
<td>Nullam a sem et metus congue placerat.</td>
<td>Mauris ut augue viverra, consequat eros eu, maximus quam.</td>
<td>Lorem ipsum dolor sit amet, consectetur adipiscing elit</td>
<td>Maecenas elementum orci a varius suscipit.</td>
<td>Nunc molestie neque sit amet eros semper dapibus.</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div class="generic-table--footer-outer">
Sum1
</div>
</td>
<td>
<div class="generic-table--footer-outer">
Sum2
</div>
</td>
<td>
<div class="generic-table--footer-outer">
Sum3
</div>
</td>
<td>
<div class="generic-table--footer-outer">
Sum4
</div>
</td>
<td>
<div class="generic-table--footer-outer">
Sum5
</div>
</td>
</tr>
</tfoot>
</table>
<div class="generic-table--header-background"></div>
<div class="generic-table--footer-background"></div>
</div>
</div>
```
## with no content
```

@ -130,7 +130,7 @@ table.generic-table
tbody
tr
border-bottom: 1px solid #dddddd
border-bottom: 1px solid $light-gray
&:hover
background: #e4f7fb
@ -161,6 +161,7 @@ table.generic-table
.generic-table--footer-outer
position: absolute
bottom: 0
padding: 0 6px
line-height: $generic-table--footer-height
z-index: 1
@ -177,10 +178,12 @@ table.generic-table
&.hover
background: #f8f8f8
.generic-table--header-spacer
.generic-table--column-spacer
padding: 0 6px
visibility: hidden
height: 0px
font-size: 0px
line-height: 0px
.generic-table--sort-header
white-space: nowrap
@ -200,15 +203,16 @@ table.generic-table
width: 1em
text-align: right
overflow: visible
min-width: 1em
.generic-table--header-background
position: absolute
top: 0
width: 100%
height: $generic-table--header-height
background: white
border-bottom: 1px solid #dddddd
box-shadow: 0 5px 15px -5px #dddddd
background: $body-background
border-bottom: 1px solid $light-gray
box-shadow: 0 5px 15px -5px $light-gray
z-index: 0
.generic-table--footer-background
@ -221,7 +225,7 @@ table.generic-table
.generic-table--no-results-container
background: #fff
border: 1px solid #ddd
border: 1px solid $light-gray
border-radius: $global-radius
padding: 20px

@ -58,6 +58,67 @@ table
.hours-dec
font-size: 0.9em
&.workflow-table
margin-bottom: 16px
border-collapse: separate
overflow-x: hidden
tbody
tr:nth-of-type(2)
td
border-top: 1px solid $light-gray
td
border-bottom: 1px solid $light-gray
td.workflow-table--current-status
font-weight: bold
text-transform: uppercase
font-size: 0.875rem
.icon-context,
.icon
color: $body-font-color
.workflow-table--turned-header
&:hover
background: none
th
overflow: visible
border-right: 1px solid $light-gray
vertical-align: middle
font-size: 0.875rem
text-transform: uppercase
font-weight: bold
max-width: 50px
min-width: 50px
position: relative
span
white-space: nowrap
transform: rotate(270deg)
position: absolute
top: -40px
right: 50%
transform-origin: right center
margin-top: 8px
thead
.workflow-table--header
text-align: right
.workflow-table--check-all
font-size: 12px
font-style: italic
text-transform: none
a
color: $content-link-color
&:hover
color: $content-link-color
text-decoration: underline
tr
th:nth-of-type(1)
width: 20px
tr
&.project
td.name a

@ -0,0 +1,51 @@
# Widget Boxes
```
<form>
<section class="widget-boxes">
<div class="widget-box">
<h3 class="widget-box--header">
<span class="icon-context icon-home1"></span>
<span class="widget-box--header-title">Widget Box</span>
</h3>
<p class="widget-box--additional-info">This widget box can be used to display content belonging to one subject.</p>
<ul class="widget-box--arrow-links">
<li>
<a>Go to Link 1</a>
</li>
<li>
<a>Go to link 2</a>
</li>
</ul>
</div>
<div class="widget-box">
<h3 class="widget-box--header">
<span class="icon-context icon-home2"></span>
<span class="widget-box--header-title">Widget Box 2</span>
</h3>
<ul class="widget-box--enumeration">
<li>
Enum1
</li>
<li>
Enum2
</li>
<li>
Enum3
</li>
<li>
Enum4
</li>
</ul>
<div class="widget-box--buttons">
<a class="button -highlight">
<i class="button--icon icon-add"></i>
<span class="button--text">Add</span>
</a>
</div>
</div>
</section>
</form>
```

@ -0,0 +1,106 @@
//-- 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.
//++
@mixin widget-box--style
background: $widget-box-block-bg-color
border: 1px solid $widget-box-block-border-color
margin: 10px
$widget-box--enumeration-width: 20px
.widget-boxes--screen-header
margin-left: 10px
.widget-boxes
display: flex
flex-flow: row wrap
.icon-context:before
padding-right: 5px
.widget-box
@include widget-box--style
padding: 10px 10px 10px 20px
flex: 1
flex-basis: 32%
display: flex
flex-direction: column
min-height: 250px
word-wrap: break-words
overflow: hidden
.widget-box--header
font-weight: bold
font-size: 1.25rem
border: none
.widget-box--header-title
vertical-align: middle
.icon:before,
.icon-context:before
vertical-align: middle
.widget-box--additional-info
margin: 0
font-size: 0.9rem
font-style: italic
.widget-box--enumeration
margin-left: 1.5rem
margin-top: 0.5rem
flex-grow: 2
.widget-box--arrow-links
list-style: none
margin: 0.5rem 0 1rem 0
flex-grow: 2
li:before
@include icon-common
@extend .icon-context
content: "\e03f"
display: inline-block
font-size: 0.6rem
color: $content-icon-link-color
width: $widget-box--enumeration-width
.-widget-box--arrow-multiline
&:before
float: left
&:after
clear: both
content: ""
display: table
> div
float: left
margin-bottom: 10px
//necessary for correct alignment even with long texts
width: calc(100% - #{$widget-box--enumeration-width})

@ -42,6 +42,16 @@
color: #777777
font-style: italic
li
span
word-wrap: break-word
// important for IE
display: inline-block
max-width: 100%
vertical-align: top
p
word-wrap: break-word
.comments-number
padding: 0
float: right

@ -70,6 +70,7 @@
@import content/attributes_group
@import content/attributes_table
@import content/information_section
@import content/widget_box
@import content/work_package_details/activities_tab
@import content/work_package_details/attachments_tab
@ -106,5 +107,7 @@
@import content/select2
@import specific/homescreen
@import misc_legacy
@import jstoolbar

@ -90,10 +90,14 @@ $toggler-width: 40px
// placeholder for highlighted left-item-border
a:not(.toggler)
border-left: $main-menu-selected-hover-indicator-width solid $main-menu-bg-color
flex-basis: 100%
@extend .small-12
&.selected
+highlight-left-item-border
a:not(:only-child):first-of-type
@extend .small-10
.open .toggler
.icon-toggler:before
content: "\e0cc"

@ -61,7 +61,7 @@ A toolbar that can and should be used for actions on the current view. Initially
<option value="oob">Boo</option>
</select>
</li>
<li class="toolbar-item">
<li class="toolbar-item -icon-only">
<a href="#" class="button -highlight">
<i class="button--icon icon-add"></i>
</a>

@ -96,13 +96,19 @@
a.last, .last
margin-right: 0
.toolbar-item
&.-icon-only .button--icon
font-size: 1.1rem
position: relative
left: 1px
#repository-checkout-url
min-width: 350px
.toolbar-item
&.-icon-only
.button
text-align: center
.button--icon
font-size: 1.1rem
position: relative
left: 0
right: 1px
vertical-align: text-top
.title-container
h2

@ -234,6 +234,10 @@
.form--field-container
max-width: 400px
.work-package-table--container
table.generic-table
// HACK: This prevents a horizontal scroll bar in the work package table when there is nothing to scroll
width: calc(100% - 20px)
%flex-grow-shrink-zero
flex-grow: 0

@ -152,6 +152,9 @@ $content-form-input-hover-border: 1px solid #888888 !default
$content-form-separator-color: #DDDDDD !default
$content-form-danger-zone-bg-color: #CA3F3F !default
$content-form-danger-zone-font-color: white !default
$content-flash-msg-font-color: #111111 !default
$content-flash-error-msg-bg-color: #FF8B8B !default
$content-flash-notice-msg-bg-color: #C4EA96 !default
@ -194,3 +197,9 @@ $user-avatar-mini-border-radius: 50px !default
$user-avatar-mini-width: 20px !default
$select-element-padding: 3px, 24px, 3px, 3px
$widget-box-content-bg-color: $body-background
$widget-box-block-bg-color: $body-background
$widget-box-block-border-color: $content-default-border-color
$homescreen-footer-bg-color: $gray-light
$homescreen-footer-icon-color: #7B827B

@ -26,15 +26,38 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(I18n) {
return {
restrict: 'E',
templateUrl: '/templates/work_packages/tabs/_attachments_table.html',
scope: {
attachments: '='
},
link: function(scope) {
scope.I18n = I18n;
}
};
};
.controller-homescreen,
.controller-homescreen #content
background: $widget-box-content-bg-color
.controller-homescreen #breadcrumb
display: none
.homescreen--links
@include widget-box--style
display: flex
padding: 20px 20%
align-items: center
justify-content: center
background: $homescreen-footer-bg-color
.icon-context:before
padding-right: 0
.homescreen--links--item
flex: 1
display: block
text-align: center
color: $content-icon-link-color
&:hover,
&:hover span
text-decoration: none
color: $content-icon-link-hover-color
span
display: block
margin-bottom: 10px
font-size: 3rem
color: $homescreen-footer-icon-color

@ -113,28 +113,12 @@ class AccountController < ApplicationController
def register
return self_registration_disabled unless allow_registration?
@user = invited_user
if request.get?
session[:auth_source_registration] = nil
@user = User.new(language: Setting.default_language)
registration_through_invitation!
else
@user = User.new
@user.admin = false
@user.register
if session[:auth_source_registration]
# on-the-fly registration via omniauth or via auth source
if pending_omniauth_registration?
register_via_omniauth(@user, session, permitted_params)
else
register_and_login_via_authsource(@user, session, permitted_params)
end
else
@user.attributes = permitted_params.user
@user.login = params[:user][:login]
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
register_user_according_to_setting @user
end
self_registration!
end
end
@ -142,7 +126,7 @@ class AccountController < ApplicationController
allow = Setting.self_registration? && !OpenProject::Configuration.disable_password_login?
get = request.get? && allow
post = request.post? && (session[:auth_source_registration] || allow)
post = (request.post? || request.patch?) && (session[:auth_source_registration] || allow)
get || post
end
@ -153,17 +137,82 @@ class AccountController < ApplicationController
# Token based account activation
def activate
return redirect_to(home_url) unless Setting.self_registration? && params[:token]
token = Token.find_by(action: 'register', value: params[:token].to_s)
redirect_to(home_url) && return unless token and !token.expired?
token = Token.find_by value: params[:token].to_s
if token && token.action == 'register' && Setting.self_registration?
activate_self_registered token
else
activate_by_invite_token token
end
end
def activate_self_registered(token)
user = token.user
redirect_to(home_url) && return unless user.registered?
user.activate
if user.save
token.destroy
flash[:notice] = l(:notice_account_activated)
if not user.registered?
if user.active?
flash[:notice] = I18n.t(:notice_account_already_activated)
else
flash[:error] = I18n.t(:notice_activation_failed)
end
redirect_to home_url
else
user.activate
if user.save
token.destroy
flash[:notice] = I18n.t(:notice_account_activated)
else
flash[:error] = I18n.t(:notice_activation_failed)
end
redirect_to signin_path
end
end
def activate_by_invite_token(token)
if token.nil? || token.expired? || !token.user.invited?
flash[:error] = I18n.t(:notice_account_invalid_token)
redirect_to home_url
else
activate_invited token
end
redirect_to action: 'login'
end
def activate_invited(token)
session[:invitation_token] = token.value
user = token.user
if user.auth_source && user.auth_source.auth_method_name == 'LDAP'
activate_through_ldap user
else
activate_user user
end
end
def activate_user(user)
if Concerns::OmniauthLogin.direct_login?
direct_login user
elsif OpenProject::Configuration.disable_password_login?
flash[:notice] = I18n.t('account.omniauth_login')
redirect_to signin_path
else
redirect_to account_register_path
end
end
def activate_through_ldap(user)
session[:auth_source_registration] = {
login: user.login,
auth_source_id: user.auth_source_id
}
flash[:notice] = I18n.t('account.auth_source_login', login: user.login).html_safe
redirect_to signin_path(username: user.login)
end
# Process a password change form, used when the user is forced
@ -199,6 +248,46 @@ class AccountController < ApplicationController
private
def registration_through_invitation!
session[:auth_source_registration] = nil
if @user.nil?
@user = User.new(language: Setting.default_language)
elsif user_with_placeholder_name?(@user)
# force user to give their name
@user.firstname = nil
@user.lastname = nil
end
end
def self_registration!
if @user.nil?
@user = User.new
@user.admin = false
@user.register
end
if session[:auth_source_registration]
# on-the-fly registration via omniauth or via auth source
if pending_omniauth_registration?
register_via_omniauth(@user, session, permitted_params)
else
register_and_login_via_authsource(@user, session, permitted_params)
end
else
@user.attributes = permitted_params.user
@user.login = params[:user][:login] if params[:user][:login].present?
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
register_user_according_to_setting @user
end
end
def user_with_placeholder_name?(user)
user.firstname == user.login and user.login == user.mail
end
def direct_login(user)
if flash.empty?
ps = {}.tap do |p|
@ -233,7 +322,7 @@ class AccountController < ApplicationController
end
def password_authentication(username, password)
user = User.try_to_login(username, password)
user = User.try_to_login(username, password, session)
if user.nil?
# login failed, now try to find out why and do the appropriate thing
user = User.find_by_login(username)
@ -251,6 +340,8 @@ class AccountController < ApplicationController
else
invalid_credentials
end
elsif user and user.invited?
invited_account_not_activated(user)
else
# incorrect password
invalid_credentials
@ -346,6 +437,8 @@ class AccountController < ApplicationController
# Register a user depending on Setting.self_registration
def register_user_according_to_setting(user, opts = {}, &block)
return register_automatically(user, opts, &block) if user.invited?
case Setting.self_registration
when '1'
register_by_email_activation(user, opts, &block)
@ -433,6 +526,13 @@ class AccountController < ApplicationController
end
end
def invited_account_not_activated(user)
logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip}" \
" at #{Time.now.utc} (invited, NOT ACTIVATED)"
flash[:error] = I18n.t('account.error_inactive_activation_by_mail')
end
# Log an attempt to log in to a locked account or with invalid credentials
# and show a flash message.
def invalid_credentials(flash_now: true)
@ -464,4 +564,12 @@ class AccountController < ApplicationController
redirect_back_or_default controller: '/my', action: 'page'
end
end
def invited_user
if session.include? :invitation_token
token = Token.find_by(value: session[:invitation_token])
token.user
end
end
end

@ -426,58 +426,15 @@ class ApplicationController < ActionController::Base
params[:back_url] || request.env['HTTP_REFERER']
end
def redirect_back_or_default(default, escape = true, use_escaped = true)
escaped_back_url = if escape
URI.escape(CGI.unescape(params[:back_url].to_s))
else
params[:back_url]
end
# if we have a back_url it must not contain two consecutive dots
if escaped_back_url.present? && !escaped_back_url.match(/\.\./)
begin
uri = URI.parse(escaped_back_url)
# do not redirect user to another host (even protocol relative urls have the host set)
# whenever a host is set it must match the request's host
uri_local_to_host = uri.host.nil? || uri.host == request.host
# do not redirect user to the login or register page
uri_path_allowed = !uri.path.match(ignored_back_url_regex)
# do not redirect to another subdirectory
uri_subdir_allowed = relative_url_root.blank? || uri.path.match(/\A#{relative_url_root}/)
if uri_local_to_host && uri_path_allowed && uri_subdir_allowed
if use_escaped
redirect_to(escaped_back_url)
else
redirect_to(back_url)
end
return
end
rescue URI::InvalidURIError
# redirect to default
end
end
redirect_to default
false
end
##
# URLs that match the returned regex must be ignored when they are the back url.
def ignored_back_url_regex
%r{/(
# Ignore login since redirect to back url is result of successful login.
login |
# When signing out with a direct login provider enabled you will be left at the logout
# page with a message indicating that you were logged out. Logging in from there would
# normally cause you to be redirected to this page. As it is the logout page, however,
# this would log you right out again after a successful login.
logout |
# TODO explain reasoning for this
account/register
)}x # ignore whitespace
def redirect_back_or_default(default, use_escaped = true)
policy = RedirectPolicy.new(
params[:back_url],
hostname: request.host,
default: default,
return_escaped: use_escaped,
)
redirect_to policy.redirect_url
end
def render_400(options = {})

@ -84,9 +84,12 @@ class AuthSourcesController < ApplicationController
def destroy
@auth_source = AuthSource.find(params[:id])
unless @auth_source.users.first
if @auth_source.users.empty?
@auth_source.destroy
flash[:notice] = l(:notice_successful_delete)
flash[:notice] = t(:notice_successful_delete)
else
flash[:warning] = t(:notice_wont_delete_auth_source)
end
redirect_to action: 'index'
end

@ -43,7 +43,18 @@ module Concerns::OmniauthLogin
# Set back url to page the omniauth login link was clicked on
params[:back_url] = request.env['omniauth.origin']
user = User.find_or_initialize_by identity_url: identity_url_from_omniauth(auth_hash)
user =
if session.include? :invitation_token
tok = Token.find_by value: session[:invitation_token]
u = tok.user
u.identity_url = identity_url_from_omniauth(auth_hash)
tok.destroy
session.delete :invitation_token
u
else
User.find_or_initialize_by identity_url: identity_url_from_omniauth(auth_hash)
end
decision = OpenProject::OmniAuth::Authorization.authorized? auth_hash
if decision.approve?
@ -83,7 +94,7 @@ module Concerns::OmniauthLogin
private
def authorization_successful(user, auth_hash)
if user.new_record?
if user.new_record? || user.invited?
create_user_from_omniauth user, auth_hash
else
if user.active?
@ -144,7 +155,7 @@ module Concerns::OmniauthLogin
def fill_user_fields_from_omniauth(user, auth)
user.update_attributes omniauth_hash_to_user_attributes(auth)
user.register
user.register unless user.invited?
user
end

@ -0,0 +1,110 @@
#-- 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.
#++
module UserInvitation
EVENT_NAME = 'user_invited'
module_function
##
# Creates an invited user with the given email address.
# If no first and last is given it will default to 'OpenProject User'
# for the first name and 'To-be' for the last name.
# The default login is the email address.
#
# @param email E-Mail address the invitation is sent to.
# @param login User's login (optional)
# @param first_name The user's first name (optional)
# @param last_name The user's last name (optional)
#
# @yield [user] Allows modifying the created user before saving it.
#
# @return The invited user. If the invitation failed, calling `#registered?`
# on the returned user will yield `false`. Check for validation errors
# in that case.
def invite_new_user(email:, login: nil, first_name: nil, last_name: nil)
user = User.new login: login || email,
mail: email,
firstname: first_name || email,
lastname: last_name || '(invited)',
status: Principal::STATUSES[:invited]
yield user if block_given?
invite_user! user
end
##
# Invites the given user. An email will be sent to their email address
# containing the token necessary for the user to register.
#
# Validates and saves the given user. The invitation will fail if the user is invalid.
#
# @return The invited user or nil if the invitation failed.
def invite_user!(user)
user, token = user_invitation user
if token
OpenProject::Notifications.send(EVENT_NAME, token)
user
end
end
##
# Creates an invited user with the given email address.
# If no first and last is given it will default to 'OpenProject User'
# for the first name and 'To-be' for the last name.
# The default login is the email address.
#
# @return Returns the user and the invitation token required to register.
def user_invitation(user)
User.transaction do
user.invite
if user.valid?
token = invitation_token user
token.save!
user.save!
return [user, token]
end
end
[user, nil]
end
def token_action
'invite'
end
def invitation_token(user)
Token.find_or_initialize_by user: user, action: token_action
end
end

@ -52,7 +52,7 @@ class CopyProjectsController < ApplicationController
flash[:notice] = I18n.t('copy_project.started',
source_project_name: @project.name,
target_project_name: target_project_name)
redirect_to :back
redirect_to origin
else
from = (['admin', 'settings'].include?(params[:coming_from]) ? params[:coming_from] : 'settings')
render action: "copy_from_#{from}"
@ -74,6 +74,10 @@ class CopyProjectsController < ApplicationController
private
def origin
params[:coming_from] == 'admin' ? admin_projects_path : settings_project_path(@project.id)
end
def prepare_for_copy_project
@issue_custom_fields = WorkPackageCustomField.order("#{CustomField.table_name}.position")
@types = ::Type.all

@ -27,10 +27,13 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class WelcomeController < ApplicationController
class HomescreenController < ApplicationController
def index
@news = current_user.latest_news
@projects = current_user.latest_projects
@newest_projects = Project.visible.newest.take(3)
@newest_users = User.newest.take(3)
@news = News.latest(count: 3)
@homescreen = OpenProject::Homescreen
end
def robots

@ -0,0 +1,109 @@
#-- 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 'uri'
class InvitationsController < ApplicationController
skip_before_filter :check_if_login_required, only: [:claim]
def index
end
def create
email = params.require(:email)
user = User.create mail: email, login: email, firstname: email, lastname: email
token = invite_user user
if user.errors.empty?
first, last = email.split("@")
user.firstname = first
user.lastname = "@#{last}"
user.invite
user.save!
token.save!
puts
puts "CREATED NEW TOKEN: #{token.value}"
puts
redirect_to action: :show, id: user.id
else
flash.now[:error] = user.errors.full_messages.first
render 'index', locals: { email: email }
end
end
def show
user = User.find params.require(:id)
token = Token.find_by action: token_action, user: user
render 'show', locals: { token: token.value, email: user.mail }
end
def claim
token = Token.find_by action: token_action, value: params.require(:id)
if current_user.logged?
flash[:warning] = 'You are already registered, mate.'
redirect_to invitation_path id: token.user_id
elsif token.expired?
flash[:error] = 'The invitation has expired.'
token.destroy
redirect_to signin_path
else
session[:invitation_token] = token.value
flash[:info] = 'Create a new account or register now, pl0x!'
redirect_to signin_path
end
end
module Functions
def token_action
'invitation'
end
def invite_user(user)
token = invitation_token user
token
end
def invitation_token(user)
Token.find_or_initialize_by user: user, action: token_action
end
end
include Functions
end

@ -35,6 +35,8 @@ class MembersController < ApplicationController
before_filter :authorize
include Pagination::Controller
include PaginationHelper
paginate_model User
search_for User, :search_in_project
search_options_for User, lambda { |_| { project: @project } }
@ -45,6 +47,15 @@ class MembersController < ApplicationController
@@scripts.unshift(script)
end
def index
@roles = Role.find_all_givable
@members = index_members @project
end
def new
set_roles_and_principles!
end
def create
if params[:member]
members = new_members_from_params
@ -52,28 +63,37 @@ class MembersController < ApplicationController
end
respond_to do |format|
if members.present? && members.all?(&:valid?)
flash.now.notice = l(:notice_successful_create)
format.html do redirect_to settings_project_path(@project, tab: 'members') end
flash.notice = members_added_notice members
format.js do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page|
page.replace_html 'tab-content-members', partial: 'projects/settings/members',
locals: { members: members }
page.insert_html :top, 'tab-content-members', render_flash_messages
format.html do
redirect_to project_members_path(project_id: @project)
end
page << MembersController.tab_scripts
format.js
else
format.html do
if members.present? && params[:member]
@member = members.first
else
flash.error = l(:error_check_user_and_role)
end
set_roles_and_principles!
render 'new'
end
else
format.js do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page|
if params[:member]
page.insert_html :top, 'tab-content-members', partial: 'members/member_errors', locals: { member: members.first }
page.replace_html 'new-member-message',
partial: 'members/member_errors',
locals: { member: members.first }
else
page.insert_html :top, 'tab-content-members', partial: 'members/common_error', locals: { message: l(:error_check_user_and_role) }
page.replace_html 'new-member-message',
partial: 'members/common_error',
locals: { message: l(:error_check_user_and_role) }
end
end
end
@ -84,45 +104,31 @@ class MembersController < ApplicationController
def update
member = update_member_from_params
if member.save
flash.now.notice = l(:notice_successful_update)
flash[:notice] = l(:notice_successful_update)
else
# only possible message is about choosing at least one role
flash[:error] = member.errors.full_messages.first
end
respond_to do |format|
format.html do redirect_to controller: '/projects', action: 'settings', tab: 'members', id: @project, page: params[:page] end
format.js do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page|
if params[:membership]
@user = member.user
page.replace_html 'tab-content-memberships', partial: 'users/memberships'
else
page.replace_html 'tab-content-members', partial: 'projects/settings/members'
end
page.insert_html :top, 'tab-content-members', render_flash_messages
page << MembersController.tab_scripts
page.visual_effect(:highlight, "member-#{@member.id}") unless Member.find_by(id: @member.id).nil?
end
end
end
redirect_to project_members_path(project_id: @project,
page: params[:page],
per_page: params[:per_page])
end
def destroy
if @member.deletable?
@member.destroy
flash.now.notice = l(:notice_successful_delete)
end
respond_to do |format|
format.html do redirect_to controller: '/projects', action: 'settings', tab: 'members', id: @project end
format.js do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page|
page.replace_html 'tab-content-members', partial: 'projects/settings/members'
page.insert_html :top, 'tab-content-members', render_flash_messages
page << MembersController.tab_scripts
end
if @member.disposable?
flash.notice = I18n.t(:notice_member_deleted, user: @member.principal.name)
@member.user.destroy
else
flash.notice = I18n.t(:notice_member_removed, user: @member.principal.name)
@member.destroy
end
end
redirect_to project_members_path(project_id: @project)
end
def autocomplete_for_member
@ -140,6 +146,10 @@ class MembersController < ApplicationController
@principals = Principal.possible_members(params[:q], 100) - @project.principals
end
@email = suggest_invite_via_email? current_user,
params[:q],
(@principals | @project.principals)
respond_to do |format|
format.json
format.html do
@ -158,28 +168,83 @@ class MembersController < ApplicationController
private
def suggest_invite_via_email?(user, query, principals)
user.admin? && # only admins may add new users via email
query =~ mail_regex &&
principals.none? { |p| p.mail == query } &&
query # finally return email
end
def mail_regex
/\A\S+@\S+\.\S+\z/
end
def index_members(project)
order = User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s).join(', ')
project
.member_principals
.includes(:roles, :principal, :member_roles)
.order(order)
.page(params[:page])
.references(:users)
.per_page(per_page_param)
end
def self.tab_scripts
@@scripts.join('(); ') + '();'
end
def set_roles_and_principles!
@roles = Role.find_all_givable
# Check if there is at least one principal that can be added to the project
@principals_available = @project.possible_members('', 1)
end
def new_members_from_params
user_ids = possibly_seperated_ids_for_entity(params[:member], :user)
roles = Role.where(id: possibly_seperated_ids_for_entity(params[:member], :role))
new_member = lambda { |user_id|
Member.new(permitted_params.member).tap do |member|
member.user_id = user_id if user_id
if roles.present?
user_ids = invite_new_users possibly_seperated_ids_for_entity(params[:member], :user)
members = user_ids.map { |user_id| new_member user_id }
# most likely wrong user input, use a dummy member for error handling
if !members.present? && roles.present?
members << new_member(nil)
end
}
members = user_ids.map { |user_id|
new_member.call(user_id)
}
# most likely wrong user input, use a dummy member for error handling
if !members.present? && roles.present?
members << new_member.call(nil)
members
else
# Pick a user that exists but can't be chosen.
# We only want the missing role error message.
dummy = new_member User.anonymous.id
[dummy]
end
end
def new_member(user_id)
Member.new(permitted_params.member).tap do |member|
member.user_id = user_id if user_id
end
members
end
def invite_new_users(user_ids)
user_ids.map do |id|
if id.to_i == 0 && id.present? # we've got an email - invite that user
# only admins can invite new users
if current_user.admin?
# The invitation can pretty much only fail due to the user already
# having been invited. So look them up if it does.
user = UserInvitation.invite_new_user(email: id) ||
User.find_by_mail(id)
user.id if user
end
else
id
end
end.compact
end
def each_comma_seperated(array, &block)
@ -195,7 +260,7 @@ class MembersController < ApplicationController
def transform_array_of_comma_seperated_ids(array)
return array unless array.present?
each_comma_seperated(array) do |elem|
elem.to_s.split(',').map(&:to_i)
elem.to_s.split(',')
end
end
@ -221,4 +286,12 @@ class MembersController < ApplicationController
@member.assign_attributes(attrs)
@member
end
def members_added_notice(members)
if members.size == 1
l(:notice_member_added, name: members.first.name)
else
l(:notice_members_added, number: members.size)
end
end
end

@ -216,7 +216,6 @@ class RepositoriesController < ApplicationController
(show_error_not_found; return) unless @entry
@annotate = @repository.scm.annotate(@path, @rev)
(render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
@changeset = @repository.find_changeset_by_name(@rev)
end

@ -120,26 +120,9 @@ class UsersController < ApplicationController
@user = User.new(language: Setting.default_language, mail_notification: Setting.default_notification_option)
@user.attributes = permitted_params.user_create_as_admin(false, @user.change_password_allowed?)
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login] || @user.mail
if @user.change_password_allowed?
if params[:user][:assign_random_password]
@user.random_password!
else
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
end
end
if @user.save
# TODO: Similar to My#account
@user.pref.attributes = params[:pref] || {}
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
UserMailer.account_information(@user, @user.password).deliver if params[:send_information]
if UserInvitation.invite_user! @user
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
@ -151,8 +134,6 @@ class UsersController < ApplicationController
end
else
@auth_sources = AuthSource.all
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format|
format.html do render action: 'new' end
@ -187,8 +168,18 @@ class UsersController < ApplicationController
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
if @user.active? && params[:send_information] && !@user.password.blank? && @user.change_password_allowed?
UserMailer.account_information(@user, @user.password).deliver
if !@user.password.blank? && @user.change_password_allowed?
send_information = params[:send_information]
if @user.invited?
# setting a password for an invited user activates them implicitly
@user.activate!
send_information = true
end
if @user.active? && send_information
UserMailer.account_information(@user, @user.password).deliver
end
end
respond_to do |format|
@ -228,7 +219,12 @@ class UsersController < ApplicationController
# Was the account activated? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUSES[:registered],
User::STATUSES[:active]])
if @user.save
if params[:activate] && @user.missing_authentication_method?
flash[:error] = I18n.t(:error_status_change_failed,
errors: I18n.t(:notice_user_missing_authentication_method),
scope: :user)
elsif @user.save
flash[:notice] = I18n.t(:notice_successful_update)
if was_activated
UserMailer.account_activated(@user).deliver

@ -64,7 +64,7 @@ class WorkPackages::BulkController < ApplicationController
end
end
set_flash_from_bulk_save(@work_packages, unsaved_work_package_ids)
redirect_back_or_default({ controller: '/work_packages', action: :index, project_id: @project }, false)
redirect_back_or_default(controller: '/work_packages', action: :index, project_id: @project)
end
def destroy

@ -189,7 +189,7 @@ class WorkPackagesController < ApplicationController
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default(work_package_path(work_package), true, false)
redirect_back_or_default(work_package_path(work_package), false)
else
edit
end
@ -354,9 +354,11 @@ class WorkPackagesController < ApplicationController
changes = work_package.changesets.visible
.includes({ repository: { project: :enabled_modules } }, :user)
changes.reverse! if current_user.wants_comments_in_reverse_order?
changes
if current_user.wants_comments_in_reverse_order?
changes.reverse
else
changes.to_a
end
end
end

@ -0,0 +1,50 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module HomescreenHelper
##
# Homescreen name
def organization_name
Setting.app_title || Setting.software_name
end
##
# Homescreen organization icon
def organization_icon
content_tag :span, '', class: 'icon-context icon-enterprise'
end
##
# Returns the user avatar or a default image
def homescreen_user_avatar
avatar = avatar(User.current)
avatar.presence || content_tag(:span, '', class: 'icon-context icon-user1')
end
end

@ -52,12 +52,7 @@ module ProjectsHelper
name: 'modules',
action: :select_project_modules,
partial: 'projects/settings/modules',
label: :label_module_plural },
{
name: 'members',
action: :manage_members,
partial: 'projects/settings/members',
label: :label_member_plural
label: :label_module_plural
},
{
name: 'custom_fields',

@ -0,0 +1,39 @@
class DummyAuthSource < AuthSource
def test_connection
# the dummy connection is always available
end
def authenticate(login, password)
existing_user(login, password) || on_the_fly_user(login)
end
def auth_method_name
'LDAP'
end
private
def existing_user(login, password)
registered_login?(login) && password == 'dummy'
end
def on_the_fly_user(login)
return nil unless onthefly_register?
{
firstname: login.capitalize,
lastname: 'Dummy',
mail: 'login@DerpLAP.net',
auth_source_id: id
}
end
def registered_login?(login)
not users.where(login: login).empty? # empty? to use EXISTS query
end
# Does this auth source backend allow password changes?
def self.allow_password_changes?
false
end
end

@ -64,8 +64,16 @@ class Journal::AggregatedJournal
# 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)
raw_journals = query_aggregated_journals(journable: journable, until_version: until_version)
predecessors = {}
raw_journals.each do |journal|
journable_key = [journal.journable_type, journal.journable_id]
predecessors[journable_key] = [nil] unless predecessors[journable_key]
predecessors[journable_key] << journal
end
raw_journals.map { |journal|
journable_key = [journal.journable_type, journal.journable_id]
Journal::AggregatedJournal.new(journal, predecessor: predecessors[journable_key].shift)
}
end
@ -89,6 +97,7 @@ class Journal::AggregatedJournal
ON #{sql_on_groups_belong_condition('predecessor', table_name)}")
.where('predecessor.id IS NULL')
.order("COALESCE(addition.created_at, #{table_name}.created_at) ASC")
.order("#{version_projection} ASC")
.select("#{table_name}.journable_id,
#{table_name}.journable_type,
#{table_name}.user_id,
@ -270,8 +279,17 @@ class Journal::AggregatedJournal
:notes_version,
to: :journal
def initialize(journal)
# Initializes a new AggregatedJournal. Allows to explicitly set a predecessor, if it is already
# known. Providing a predecessor is only to improve efficiency, it is not required.
# In case the predecessor is not known, it will be lazily retrieved.
def initialize(journal, predecessor: false)
@journal = journal
# explicitly checking false to allow passing nil as "no predecessor"
# mind that we check @predecessor with defined? below, so don't assign to it in all cases!
unless predecessor == false
@predecessor = predecessor
end
end
# returns an instance of this class that is reloaded from the database

@ -44,8 +44,13 @@ class JournalNotificationMailer
# 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
work_package = aggregated.predecessor.journable
notification_receivers(work_package).each do |recipient|
job = DeliverWorkPackageNotificationJob.new(aggregated.predecessor.id,
recipient.id,
User.current.id)
Delayed::Job.enqueue job
end
end
job = EnqueueWorkPackageNotificationJob.new(journal.id, User.current.id)
@ -82,5 +87,9 @@ class JournalNotificationMailer
wp_journals = Journal::AggregatedJournal.aggregated_journals(journable: raw_journal.journable)
wp_journals.detect { |journal| journal.version == raw_journal.version }
end
def notification_receivers(work_package)
(work_package.recipients + work_package.watcher_recipients).uniq
end
end
end

@ -46,7 +46,7 @@ class Member < ActiveRecord::Base
after_destroy :unwatch_from_permission_change
def name
user.name
principal.name
end
def to_s
@ -130,6 +130,14 @@ class Member < ActiveRecord::Base
@membership
end
##
# Returns true if this user can be deleted as they have no other memberships
# and haven't been activated yet. Only applies if the member is actually a user
# as opposed to a group.
def disposable?
user && user.invited? && user.memberships.none? { |m| m.project_id != project_id }
end
protected
def destroy_if_no_roles_left!

@ -66,7 +66,7 @@ class News < ActiveRecord::Base
end
# returns latest news for projects visible by user
def self.latest(user = User.current, count = 5)
def self.latest(user: User.current, count: 5)
latest_for(user, count: count)
end

@ -36,7 +36,8 @@ class Principal < ActiveRecord::Base
builtin: 0,
active: 1,
registered: 2,
locked: 3
locked: 3,
invited: 4
}
self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
@ -55,7 +56,9 @@ class Principal < ActiveRecord::Base
scope :active, -> { where(status: STATUSES[:active]) }
scope :active_or_registered, -> { where(status: [STATUSES[:active], STATUSES[:registered]]) }
scope :active_or_registered, -> {
where(status: [STATUSES[:active], STATUSES[:registered], STATUSES[:invited]])
}
scope :active_or_registered_like, ->(query) { active_or_registered.like(query) }

@ -68,7 +68,8 @@ class Project < ActiveRecord::Base
.where("#{Principal.table_name}.type='Group' OR " +
"(#{Principal.table_name}.type='User' AND " +
"(#{Principal.table_name}.status=#{Principal::STATUSES[:active]} OR " +
"#{Principal.table_name}.status=#{Principal::STATUSES[:registered]}))")
"#{Principal.table_name}.status=#{Principal::STATUSES[:registered]} OR " +
"#{Principal.table_name}.status=#{Principal::STATUSES[:invited]}))")
}, class_name: 'Member'
has_many :users, through: :members
has_many :principals, through: :member_principals, source: :principal
@ -138,6 +139,7 @@ class Project < ActiveRecord::Base
scope :active, -> { where(status: STATUS_ACTIVE) }
scope :public_projects, -> { where(is_public: true) }
scope :visible, ->(user = User.current) { where(Project.visible_by(user)) }
scope :newest, -> { order(created_on: :desc) }
# timelines stuff
@ -290,21 +292,6 @@ class Project < ActiveRecord::Base
save
end
# returns latest created projects
# non public projects will be returned only if user is a member of those
def self.latest(user = nil, count = 5)
latest_for(user, count: count)
end
def self.latest_for(user, count: 5)
where(visible_by(user)).limit(count).newest_first
end
# table_name shouldn't be needed :(
def self.newest_first
order "#{table_name}.created_on DESC"
end
# Returns a SQL :conditions string used to find all active projects for the specified user.
#
# Examples:

@ -109,8 +109,9 @@ class Repository < ActiveRecord::Base
##
# Retrieves the :disabled_types setting from `configuration.yml
# To avoid wrong set operations for string-based configuration, force them to symbols.
def self.disabled_types
scm_config[:disabled_types] || []
(scm_config[:disabled_types] || []).map(&:to_sym)
end
def vendor

@ -161,6 +161,8 @@ class User < Principal
}
scope :admin, -> { where(admin: true) }
scope :newest, -> { order(created_on: :desc) }
def sanitize_mail_notification_setting
self.mail_notification = Setting.default_notification_option if mail_notification.blank?
true
@ -213,12 +215,12 @@ class User < Principal
register_allowance_evaluator OpenProject::PrincipalAllowanceEvaluator::Default
# Returns the user that matches provided login and password, or nil
def self.try_to_login(login, password)
def self.try_to_login(login, password, session = nil)
# Make sure no one can sign in with an empty password
return nil if password.to_s.empty?
user = find_by_login(login)
user = if user
try_authentication_for_existing_user(user, password)
try_authentication_for_existing_user(user, password, session)
else
try_authentication_and_create_user(login, password)
end
@ -231,8 +233,11 @@ class User < Principal
# Tries to authenticate a user in the database via external auth source
# or password stored in the database
def self.try_authentication_for_existing_user(user, password)
def self.try_authentication_for_existing_user(user, password, session = nil)
activate_user! user, session if session
return nil if !user.active? || OpenProject::Configuration.disable_password_login?
if user.auth_source
# user has an external authentication method
return nil unless user.auth_source.authenticate(user.login, password)
@ -245,6 +250,19 @@ class User < Principal
user
end
def self.activate_user!(user, session)
if session[:invitation_token]
token = Token.find_by_value session[:invitation_token]
invited_id = token && token.user.id
if user.id == invited_id
user.activate!
token.destroy!
session.delete :invitation_token
end
end
end
# Tries to authenticate with available sources and creates user on success
def self.try_authentication_and_create_user(login, password)
return nil if OpenProject::Configuration.disable_password_login?
@ -326,6 +344,10 @@ class User < Principal
self.status = STATUSES[:registered]
end
def invite
self.status = STATUSES[:invited]
end
def lock
self.status = STATUSES[:locked]
end
@ -338,6 +360,14 @@ class User < Principal
update_attribute(:status, STATUSES[:registered])
end
def invite!
update_attribute(:status, STATUSES[:invited])
end
def invited?
status == STATUSES[:invited]
end
def lock!
update_attribute(:status, STATUSES[:locked])
end
@ -705,6 +735,17 @@ class User < Principal
User.current.admin? ? Role.all : User.current.roles_for_project(project)
end
##
# Returns true if no authentication method has been chosen for this user yet.
# There are three possible methods currently:
#
# - username & password
# - OmniAuth
# - LDAP
def missing_authentication_method?
identity_url.nil? && passwords.empty? && auth_source.nil?
end
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
# one anonymous user per database.
def self.anonymous
@ -740,14 +781,6 @@ class User < Principal
system_user
end
def latest_news(options = {})
News.latest_for self, options
end
def latest_projects(options = {})
Project.latest_for self, options
end
protected
# Password requirement validation based on settings

@ -29,9 +29,9 @@
class WatcherNotificationMailer
class << self
def handle_watcher(watcher_id, watcher_setter_id)
unless other_jobs_queued?(Watcher.find(watcher_id).watchable)
job = DeliverWatcherNotificationJob.new(watcher_id, watcher_setter_id)
def handle_watcher(watcher, watcher_setter)
unless other_jobs_queued?(watcher.watchable)
job = DeliverWatcherNotificationJob.new(watcher.id, watcher.user.id, watcher_setter.id)
Delayed::Job.enqueue job
end
end

@ -0,0 +1,149 @@
#-- 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 'uri'
require 'cgi'
# This capsulates the validation of a requested redirect URL.
#
class RedirectPolicy
attr_reader :validated_redirect_url, :request
def initialize(requested_url, hostname:, default:, return_escaped: true)
@current_host = hostname
@return_escaped = return_escaped
@requested_url = preprocess(requested_url)
@default_url = default
end
##
# Performs all validations for the requested URL
def valid?
return false if @requested_url.nil?
[
# back_url must not contain two consecutive dots
:no_upper_levels,
# Require the path to begin with a slash
:path_has_slash,
# do not redirect user to another host
:same_host,
# do not redirect user to the login or register page
:path_not_blacklisted,
# do not redirect to another subdirectory
:matches_relative_root
].all? { |check| send(check) }
end
##
# Return a valid redirect URI.
# If the validation check on the current back URL apply
def redirect_url
if valid?
postprocess(@requested_url)
else
@default_url
end
end
private
##
# Preprocesses the requested redirect URL.
# - Escapes it when necessary
# - Tries to parse it
# - Escapes the redirect URL when requested so.
def preprocess(requested)
url = URI.escape(CGI.unescape(requested.to_s))
URI.parse(url)
rescue URI::InvalidURIError => e
Rails.logger.warn("Encountered invalid redirect URL '#{requested}': #{e.message}")
nil
end
##
# Postprocesses the validated URL
def postprocess(redirect_url)
# Remove basic auth credentials
redirect_url.userinfo = ''
if @return_escaped
redirect_url.to_s
else
URI.unescape(redirect_url.to_s)
end
end
##
# Avoid paths with references to parent paths
def no_upper_levels
!@requested_url.path.include? '../'
end
##
# Require URLs to contain a path slash.
# This will always be the case for parsed URLs unless
# +URI.parse('@foo.bar')+ or a non-root relative URL +URI.parse('foo')+
def path_has_slash
@requested_url.path =~ %r{\A/([^/]|\z)}
end
##
# do not redirect user to another host (even protocol relative urls have the host set)
# whenever a host is set it must match the request's host
def same_host
@requested_url.host.nil? || @requested_url.host == @current_host
end
##
# Avoid redirect URLs to specific locations, such as login page
def path_not_blacklisted
!@requested_url.path.match(
%r{/(
# Ignore login since redirect to back url is result of successful login.
login |
# When signing out with a direct login provider enabled you will be left at the logout
# page with a message indicating that you were logged out. Logging in from there would
# normally cause you to be redirected to this page. As it is the logout page, however,
# this would log you right out again after a successful login.
logout |
# Avoid sending users to the register form. The exact reasoning behind
# this is unclear, but grown from tradition.
account/register
)}x # ignore whitespace
)
end
##
# Requires the redirect URL to reside inside the relative root, when given.
def matches_relative_root
relative_root = OpenProject::Configuration['rails_relative_url_root']
relative_root.blank? || @requested_url.path.starts_with?(relative_root)
end
end

@ -39,12 +39,13 @@ See doc/COPYRIGHT.rdoc for more details.
auth_provider_html = call_hook :view_account_login_auth_provider
no_pwd = OpenProject::Configuration.disable_password_login?
pclass = no_pwd ? 'no-pwd' : ''
wclass = local_assigns[:wide] ? 'wide' : ''
%>
<% if auth_provider_html.strip != '' %>
<div class="login-auth-providers <%= pclass %>">
<div class="login-auth-providers <%= pclass %> <%= wclass %>">
<% unless no_pwd %>
<h3 class="login-auth-providers-title"><span>
<%= I18n.t('account.login_with_auth_provider')%>
<%= local_assigns[:omniauth_title] || I18n.t('account.login_with_auth_provider')%>
</span></h3>
<% end %>
<div class="login-auth-provider-list">

@ -33,14 +33,14 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field">
<%= styled_label_tag 'username', User.human_attribute_name(:login) %>
<div class="form--field-container">
<%= styled_text_field_tag 'username', nil, tabindex: 1 %>
<%= styled_text_field_tag 'username', params[:username], tabindex: 1 %>
</div>
</div>
<div class="form--field">
<%= styled_label_tag 'password', User.human_attribute_name(:password) %>
<div class="form--field-container">
<%= styled_password_field_tag 'password', nil, tabindex: 2 %>
<%= styled_password_field_tag 'password', nil, tabindex: 2, autofocus: params.include?(:username) %>
</div>
</div>

@ -83,4 +83,6 @@ See doc/COPYRIGHT.rdoc for more details.
</section>
<%= styled_button_tag l(:button_submit), class: '-highlight -with-icon icon-yes' %>
<%= render partial: 'auth_providers', locals: { omniauth_title: I18n.t('account.signup_with_auth_provider'), wide: true } %>
<% end %>

@ -31,7 +31,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= toolbar title: l(:label_project_new) %>
<%= labelled_tabular_form_for @copy_project, :url => { :action => "copy" } do |f| %>
<%= labelled_tabular_form_for @copy_project, url: { action: 'copy', coming_from: 'admin' } do |f| %>
<%= hidden_field_tag :coming_from, 'admin' %>
<%= render :partial => 'projects/form', :locals => { :f => f, :project => @copy_project, :renderTypes => true } %>

@ -30,7 +30,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= error_messages_for 'copy_project' %>
<%= toolbar title: l(:label_project_new) %>
<%= labelled_tabular_form_for @copy_project, :url => { :action => "copy" } do |f| %>
<%= labelled_tabular_form_for @copy_project, url: { action: 'copy', coming_from: 'settings' } do |f| %>
<section class="form--section">
<%= render :partial => "projects/form/attributes/name", :locals => { :form => f } %>
<%= render :partial => "projects/form/attributes/identifier", :locals => { :form => f,

@ -259,6 +259,15 @@ tooltip of "WHO"
<p>Include a wiki page. Example:</p>
<pre><code>{{include(Foo)}}</code></pre>
</dd>
<dt><code>child_pages</code></dt>
<dd>
<p>Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:</p>
<pre><code>
{{child_pages}} -- can be used from a wiki page only
{{child_pages(Foo)}} -- lists all children of page Foo
{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo
</code></pre>
</dd>
<dt><code>macro_list</code></dt>
<dd>
<p>Displays a list of all available macros, including description if available.</p>

@ -0,0 +1,47 @@
<h3 class="widget-box--header">
<span class="icon-context icon-settings"></span>
<span class="widget-box--header-title"><%= l(:label_administration) %></span>
</h3>
<ul class="widget-box--arrow-links">
<li>
<%= link_to l(:label_project_plural), admin_projects_path,
title: l(:label_project_plural) %>
</li>
<li>
<%= link_to l(:label_user_plural), users_path,
title: l(:label_user_plural) %>
</li>
<li>
<%= link_to l(:label_group_plural), groups_path,
title: l(:label_group_plural) %>
</li>
<li>
<%= link_to l(:label_role_and_permissions), roles_path,
title: l(:label_role_and_permissions) %>
</li>
<li>
<%= link_to l(:label_work_package_types), types_path,
title: l(:label_work_package_types) %>
</li>
<li>
<%= link_to l(:label_work_package_status), statuses_path,
title: l(:label_work_package_status) %>
</li>
<li>
<%= link_to l(:label_workflow_plural), workflows_path,
title: l(:label_workflow_plural) %>
</li>
<li>
<%= link_to l('attributes.custom_values'), custom_fields_path,
title: l('attributes.custom_values') %>
</li>
<li>
<%= link_to l(:label_settings), settings_path,
title: l(:label_settings) %>
</li>
<%= call_hook(:homescreen_administration_links) %>
</ul>

@ -0,0 +1,55 @@
<h3 class="widget-box--header">
<span class="icon-context icon-op-icon"></span>
<span class="widget-box--header-title"><%= l('homescreen.blocks.community') %></span>
</h3>
<ul class="widget-box--arrow-links">
<li>
<a href="https://www.openproject.org/open-source/release-notes/"
title="<%= l(:label_release_notes) %>">
<%= l(:label_release_notes) %>
</a>
</li>
<li>
<a href="https://community.openproject.org/projects/openproject/roadmap"
title="<%= l(:label_development_roadmap) %>">
<%= l(:label_development_roadmap) %>
</a>
</li>
<li>
<a href="https://community.openproject.org/projects/openproject/work_packages"
title="<%= l(:label_report_bug) %>">
<%= l(:label_report_bug) %>
</a>
</li>
<li>
<a href="https://www.openproject.com/enterprise-services/"
title="<%= l(:label_professional_support) %>">
<%= l(:label_professional_support) %>
</a>
</li>
<li>
<a href="https://www.openproject.org/blog/"
title="<%= l(:label_blog) %>">
<%= l(:label_blog) %>
</a>
</li>
<li>
<a href="https://www.openproject.org/open-source/openproject-plugins/"
title="<%= l(:label_plugins) %>">
<%= l(:label_plugins) %>
</a>
</li>
<li>
<a href="https://crowdin.com/projects/opf"
title="<%= l(:label_add_edit_translations) %>">
<%= l(:label_add_edit_translations) %>
</a>
</li>
<li>
<a href="https://www.openproject.org/api/"
title="<%= l(:label_api_documentation) %>">
<%= l(:label_api_documentation) %>
</a>
</li>
</ul>

@ -0,0 +1,17 @@
<h3 class="widget-box--header">
<%= homescreen_user_avatar %>
<span class="widget-box--header-title"><%= l(:label_my_account) %></span>
</h3>
<ul class="widget-box--arrow-links">
<li>
<%= link_to l(:label_profile), my_account_path, title: l(:label_profile) %>
</li>
<li>
<%= link_to l(:label_my_page), my_page_path,
title: l(:label_my_page) %>
</li>
<li>
<%= link_to l(:button_change_password), my_password_path, title: l(:button_change_password) %>
</li>
</ul>

@ -0,0 +1,20 @@
<h3 class="widget-box--header">
<span class="icon-context icon-news"></span>
<span class="widget-box--header-title"><%= l(:label_news_latest) %></span>
</h3>
<% unless @news.empty? %>
<ul class="widget-box--arrow-links">
<% @news.each do |news| %>
<li class="-widget-box--arrow-multiline">
<div>
<%= avatar(news.author, {class: 'avatar-mini'}) %>
<%= link_to_project(news.project) + ': ' %>
<%= link_to h(news.title), news_path(news) %>
<br/>
<p class="widget-box--additional-info"><%= authoring news.created_on, news.author %></p>
</div>
</li>
<% end %>
</ul>
<% end %>

@ -0,0 +1,28 @@
<h3 class="widget-box--header">
<span class="icon-context icon-unit"></span>
<span class="widget-box--header-title"><%= l(:label_project_plural) %></span>
</h3>
<% unless @newest_projects.empty? %>
<p class="widget-box--additional-info"><%= l('homescreen.additional.projects') %></p>
<ul class="widget-box--arrow-links">
<% @newest_projects.each do |project| %>
<li>
<%= link_to project, project_path(project), :title => project.short_description %>
<small>(<%= format_date(project.created_on) %>)</small>
</li>
<% end %>
</ul>
<% end %>
<div class="widget-box--blocks--buttons">
<% if User.current.allowed_to?(:add_project, nil, global: true) %>
<%= link_to new_project_path, class: 'button -alt-highlight' do %>
<i class="button--icon icon-add"></i>
<span class="button--text"><%= l(:label_project_new) %></span>
<% end %>
<% end %>
<%= link_to l(:label_project_view_all), projects_path,
class: 'button -highlight',
title: l(:label_project_view_all) %>
</div>

@ -0,0 +1,26 @@
<h3 class="widget-box--header">
<span class="icon-context icon-group"></span>
<span class="widget-box--header-title"><%= l(:label_user_plural) %></span>
</h3>
<p class="widget-box--additional-info"><%= l('homescreen.additional.users') %></p>
<% unless @newest_users.empty? %>
<ul class="widget-box--arrow-links">
<% @newest_users.each do |user| %>
<li>
<%= link_to user, user_path(user), :title => user.name %>
<small>(<%= format_date(user.created_on) %>)</small>
</li>
<% end %>
</ul>
<% end %>
<div class="widget-box--buttons">
<% if User.current.admin? %>
<%= link_to new_user_path, class: 'button -alt-highlight' do %>
<i class="button--icon icon-add"></i>
<span class="button--text"><%= l(:label_invite_user) %></span>
<% end %>
<% end %>
</div>

@ -0,0 +1,8 @@
<h3 class="widget-box--header">
<span class="icon-context icon-projects"></span>
<span class="widget-box--header-title"><%= Setting.welcome_title.presence || organization_name %></span>
</h3>
<div class="wiki">
<%= format_text(Setting.welcome_text, headings: false) %>
</div>

@ -0,0 +1,61 @@
<%#-- 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.
++#%>
<% breadcrumb_paths(nil) %>
<div class="widget-boxes--screen-header">
<h2>
<span><%= organization_icon %></span>
<%= organization_name %>
</h2>
</div>
<% if @homescreen[:blocks].any? %>
<section class="widget-boxes">
<% @homescreen[:blocks].each do |block| %>
<% if block[:if].nil? || instance_eval(&block[:if]) %>
<div class="widget-box">
<%= render partial: "homescreen/blocks/#{block[:partial]}", locals: (block[:locals] || {}) %>
</div>
<% end %>
<% end %>
</section>
<% end %>
<% if @homescreen[:links].any? %>
<section class="homescreen--links">
<% @homescreen[:links].each do |link| %>
<% title = I18n.t(link[:label], scope: 'homescreen.links') %>
<a class="homescreen--links--item" href="<%= link[:url] %>" title="<%= title %>">
<span class="<%= link[:icon] %>"></span>
<%= title %>
</a>
<% end %>
</section>
<% end %>
<%= call_hook :homescreen_after_links %>

@ -27,19 +27,28 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<% html_title t(:label_member_new) %>
<%= toolbar title: t(:label_member_new) %>
<%= labelled_tabular_form_for(:member,
:url => {:controller => '/members', :action => 'create', :project_id => project},
:method => :post,
:remote => true,
:loading => '$(\'member-add-submit\').disable();',
:complete => 'if($(\'member-add-submit\')) $(\'member-add-submit\').enable(); activateFlashError();',
:html => {:id => "members_add_form"}) do |f| %>
<div class="form--section">
<h3 class="form--section-title"><%= l(:label_member_new) %></h3>
<div id="new-member-message"></div>
<div class="grid-block medium-up-2">
<div class="form--column">
<div class="form--field">
<%= styled_label_tag :principal_search, l(:label_principal_search) %>
<%
user_id_title = I18n.t(:label_principal_search)
if current_user.admin?
user_id_title += I18n.t(:label_principal_invite_via_email)
end
%>
<%= styled_label_tag :principal_search, user_id_title %>
<%= styled_text_field_tag :principal_search, nil %>
<%= observe_field(:principal_search,
:frequency => 0.5,
@ -63,6 +72,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= f.button l(:button_add), :id => 'member-add-submit',
class: 'button -highlight -with-icon icon-yes' %>
<% end %>
<%= link_to I18n.t('button_cancel'), :back, class: 'button' %>
</div>
<% end %>

@ -29,19 +29,29 @@ See doc/COPYRIGHT.rdoc for more details.
<%= javascript_include_tag "members_select_boxes.js" %>
<% html_title I18n.t(:label_member_new) %>
<%= toolbar title: I18n.t(:label_member_new) %>
<%= labelled_tabular_form_for(:member, :url => {:controller => 'members', :action => 'create', :project_id => project},
:remote => true,
:method => :post,
:html => {:id => "members_add_form"}) do |f| %>
<div class="form--section">
<h3 class="form--section-title"><%= l(:label_member_new) %></h3>
<div id="new-member-message"></div>
<div class="grid-block medium-up-2">
<div class="form--column">
<div class="form--field -vertical">
<%= styled_label_tag :member_user_ids, l(:label_principal_search) %>
<%
user_id_title = I18n.t(:label_principal_search)
if current_user.admin?
user_id_title += I18n.t(:label_principal_invite_via_email)
end
%>
<%= styled_label_tag :member_user_ids, user_id_title %>
<%= select_tag "member[user_ids]", options_for_select([]),
:title => l(:label_principal_search),
:title => user_id_title,
:multiple => true,
:autofocus => true,
:'data-ajaxURL' => url_for(:controller => "/members", :action => "autocomplete_for_member"),
:'data-projectId' => project.id,
no_label: true,
@ -61,7 +71,8 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
</div>
</div>
<%= f.button l(:button_add), :id => 'member-add-submit',
<%= f.button l(:button_add), id: 'member-add-submit',
class: 'button -highlight -with-icon icon-yes' %>
<%= link_to I18n.t('button_cancel'), :back, class: 'button' %>
</div>
<% end %>

@ -27,16 +27,29 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<%#
Lists found users (principals) to be used for the select box.
If the user enters an email address, a 'result' in the form of
'Invite: <email>' will be returned.
#%>
{
"results":
{
"items":[
<% @principals.each_with_index do |principal, ix| %>
{
"id": <%= principal.id.to_json.html_safe %>,
"name": <%= principal.name.to_json.html_safe %>
} <%= "," unless ix == @principals.length - 1 %>
<% end %> ],
<% @principals.each_with_index do |principal, ix| %>
{
"id": <%= principal.id.to_json.html_safe %>,
"name": <%= principal.name.to_json.html_safe %>
} <%= "," unless !@email && ix == @principals.length - 1 %>
<% end %>
<% if @email %>
{
"id": "<%= @email %>",
"name": "Invite <%= @email %>"
}
<% end %>
],
"total": <%= @total ? @total : @principals.size %>,
"more": <%= @more ? @more : 0 %>
}

@ -0,0 +1,4 @@
// reload iframes parent (project members view)
// to show updated list of members and a flash message
window.parent.document.location.reload();

@ -0,0 +1,158 @@
<%#-- 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.
++#%>
<% html_title 'Members' %>
<%= toolbar title: 'Members' do %>
<% if authorize_for(:members, :new) %>
<a href="<%= new_project_member_path %>" id="add-member-button" title="Add Member" class="button -alt-highlight">
<i class="button--icon icon-add"></i>
<span class="button--text"><%= I18n.t(:button_add_member) %></span>
</a>
<% end %>
<% end %>
<%= error_messages_for 'member' %>
<div>
<% if @members.any? %>
<% authorized = authorize_for('members', 'update') %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table interactive-table role="grid" class="generic-table">
<colgroup>
<col highlight-col>
<col highlight-col>
<%= call_hook(:view_projects_settings_members_table_colgroup, :project => @project) %>
<col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= User.model_name.human %> / <%= Group.model_name.human %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= l(:label_role_plural) %>
</span>
</div>
</div>
</th>
<%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
<th></th>
</tr>
</thead>
<tbody>
<% @members.each do |member| %>
<% next if member.new_record? %>
<tr id="member-<%= member.id %>" class=" member">
<%
member_type = member.principal.class.name.downcase
classes = [
member_type,
('icon icon-group' if member_type == 'group'),
user_status_class(member.principal)
].compact.join(' ')
%>
<td class="<%= classes %>" title="<%= user_status_i18n member.principal %>">
<%= link_to_user member.principal %>
<% if member.user && member.user.invited? %>
<i title="<%= t('text_user_invited') %>" class="icon icon-mail"></i>
<% end %>
</td>
<td class="roles">
<span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
<% if authorized %>
<%= form_for(member, :url => {:controller => '/members',
:action => 'update',
:id => member,
:page => params[:page],
:per_page => params[:per_page] },
:method => :put,
:html => { :id => "member-#{member.id}-roles-form",
:class => 'hol',
:style => 'display:none' }) do |f| %>
<p><% @roles.each do |role| %>
<label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
:disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label>
<% end %></p>
<%= hidden_field_tag 'member[role_ids][]', '' %>
<p><%= submit_tag l(:button_change), :class => 'button -highlight -small' %>
<%= link_to_function l(:button_cancel),
"$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;",
class: 'button -small' %></p>
<% end %>
<% end %>
</td>
<%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
<% if authorized %>
<td class="buttons">
<%
delete_class, delete_title = if member.disposable?
['icon icon-delete', I18n.t(:title_remove_and_delete_user)]
else
['icon icon-close', I18n.t(:button_remove)]
end
%>
<%= link_to_function '', "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit', :title => l(:button_edit) %>
<%= link_to('', {:controller => '/members', :action => 'destroy', :id => member, :page => params[:page]},
:method => :delete,
data: { confirm: ((!User.current.admin? && member.include?(User.current)) ? l(:text_own_membership_delete_confirmation) : nil) },
:title => delete_title, :class => delete_class) if member.deletable? %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<div class="generic-table--header-background"></div>
</div>
</div>
<%= pagination_links_full @members, @pagination_url_options || {} %>
<% else %>
<div class="generic-table--container">
<div class="generic-table--no-results-container">
<h2 class="generic-table--no-results-title">
<i class="icon-info"></i>
<%= l(:label_nothing_display) %>
</h2>
<div class="generic-table--no-results-description">
<p class="nodata"><%= l(:label_no_data) %></p>
</div>
</div>
</div>
<% end %>
</div>

@ -0,0 +1,43 @@
<%#-- 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.
++#%>
<%= javascript_include_tag "members_form.js" %>
<%= error_messages_for 'member' %>
<div>
<% if @principals_available.any? %>
<%= render :partial => "members/member_form",
:locals => { :project => @project, :roles => @roles } %>
<% else %>
<p>
<%= I18n.t('text_no_roles_defined') %>
</p>
<%= link_to I18n.t('button_back'), :back, class: 'button' %>
<% end %>
</div>

@ -46,9 +46,12 @@ See doc/COPYRIGHT.rdoc for more details.
</li>
<% end %>
<% if Setting.welcome_on_projects_page? %>
<div class="wiki">
<%= format_text Setting.welcome_text %>
<h1><%= Setting.welcome_title %></h1>
<%= format_text(Setting.welcome_text, headings: false) %>
</div>
<% end %>
<% if User.current.logged? %>
<p style="float:right;">

@ -1,147 +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.
++#%>
<%= javascript_include_tag "members_form.js" %>
<%= error_messages_for 'member' %>
<% roles = Role.find_all_givable
# Check if there is at least one principal that can be added to the project
principals_available = @project.possible_members("", 1)
member_per_page = 20
@members = @project.member_principals.includes(:roles, :principal, :member_roles)
.order(User::USER_FORMATS_STRUCTURE[Setting.user_format].map{|attr| attr.to_s}.join(", "))
.page(params[:page])
.references(:users)
.per_page(per_page_param)
%>
<div>
<% if roles.any? && principals_available.any? %>
<%= render :partial => "members/member_form",
:locals => { :project => @project,
:members => @members,
:roles => roles } %>
<% end %>
</div>
<div>
<% if @members.any? %>
<% authorized = authorize_for('members', 'update') %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table interactive-table role="grid" class="generic-table">
<colgroup>
<col highlight-col>
<col highlight-col>
<%= call_hook(:view_projects_settings_members_table_colgroup, :project => @project) %>
<col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= User.model_name.human %> / <%= Group.model_name.human %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= l(:label_role_plural) %>
</span>
</div>
</div>
</th>
<%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
<th></th>
</tr>
</thead>
<tbody>
<% @members.each do |member| %>
<% next if member.new_record? %>
<tr id="member-<%= member.id %>" class=" member">
<td class="<%= member.principal.class.name.downcase %> <%= 'icon icon-group' if member.principal.class.name.downcase == 'group' %> <%= user_status_class member.principal%>" title="<%= user_status_i18n member.principal%>"><%= link_to_user member.principal %></td>
<td class="roles">
<span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
<% if authorized %>
<%= form_for(member, :url => {:controller => '/members',
:action => 'update',
:id => member,
:page => params[:page]},
:method => :put,
:remote => true,
:html => { :id => "member-#{member.id}-roles-form",
:class => 'hol',
:style => 'display:none' }) do |f| %>
<p><% roles.each do |role| %>
<label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
:disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label>
<% end %></p>
<%= hidden_field_tag 'member[role_ids][]', '' %>
<p><%= submit_tag l(:button_change), :class => 'button -highlight -small' %>
<%= link_to_function l(:button_cancel),
"$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;",
class: 'button -small' %></p>
<% end %>
<%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
<td class="buttons">
<%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
<%= link_to(l(:button_delete), {:controller => '/members', :action => 'destroy', :id => member, :page => params[:page]},
:method => :delete,
:remote => true,
data: { confirm: ((!User.current.admin? && member.include?(User.current)) ? l(:text_own_membership_delete_confirmation) : nil) },
:title => l(:button_delete), :class => 'icon icon-delete') if member.deletable? %>
</td>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="generic-table--header-background"></div>
</div>
</div>
<%= pagination_links_full @members, params: { tab: 'members' }.merge(@pagination_url_options || {}) %>
<% else %>
<div class="generic-table--container">
<div class="generic-table--no-results-container">
<h2 class="generic-table--no-results-title">
<i class="icon-info"></i>
<%= l(:label_nothing_display) %>
</h2>
<div class="generic-table--no-results-description">
<p class="nodata"><%= l(:label_no_data) %></p>
</div>
</div>
</div>
<% end %>
</div>

@ -52,7 +52,7 @@ dirs.each_with_index do |dir, index|
rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
%>
<span class="repository-bradcrumbs--identifier">
(<%= l('repositories.at_identifier', identifier: rev_text) unless rev_text.blank? %>)
<%= "(#{l('repositories.at_identifier', identifier: rev_text)})" if rev_text.present? %>
</span>
<% html_title(h(with_leading_slash(path))) -%>

@ -28,7 +28,7 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<div id="repository--checkout-instructions" class="notification-box -info">
<% unless embedded %>
<a href="javscript:" class="notification-box--close">&times;</a>
<a href="javscript:" title="{{ ::I18n.t('js.close_popup_title') }}" class="notification-box--close icon-context icon-close2"></a>
<% end %>
<div class="notification-box--content">
<p>

@ -54,7 +54,6 @@ See doc/COPYRIGHT.rdoc for more details.
<a id="repository--checkout-instructions-toggle" class="button -pressed" href="javascript:"
title="<%= l('repositories.checkout.show_instructions') %>">
<i class="button--icon icon-info"></i>
<span class="button--text"></span>
</a>
</li>
<% end %>
@ -63,7 +62,6 @@ See doc/COPYRIGHT.rdoc for more details.
<%= link_to stats_project_repository_path(@project),
class: 'button', title: l(:label_statistics) do %>
<i class="button--icon icon-stats1"></i>
<span class="button--text"></span>
<% end %>
</li>
<% end %>
@ -74,7 +72,6 @@ See doc/COPYRIGHT.rdoc for more details.
<%= link_to settings_project_path(@project, tab: 'repository'),
class: 'button', title: l(:label_settings) do %>
<i class="button--icon icon-settings"></i>
<span class="button--text"></span>
<% end %>
</li>
<% end %>

@ -27,6 +27,7 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<%= call_hook(:view_repositories_show_contextual, { repository: @repository, project: @project }) %>
<% html_title(l(:button_annotate)) %>
<%= render partial: 'repository_header', locals: { empty: false } %>
<div class="repository-breadcrumbs">
@ -34,25 +35,39 @@ See doc/COPYRIGHT.rdoc for more details.
locals: { path: @path, revision: @rev }.merge(kind: 'file') %>
</div>
<p><%= render partial: 'link_to_functions' %></p>
<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
<div class="autoscroll">
<table class="filecontent annotate CodeRay">
<tbody>
<% line_num = 1 %>
<% syntax_highlight(@path, to_utf8_for_repositories(@annotate.content)) do |line| %>
<% revision = @annotate.revisions[line_num-1] %>
<tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
<th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
<td class="revision">
<%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %></td>
<td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
<td class="line-code">
<pre><%= line %></pre>
</td>
</tr>
<% line_num += 1 %>
<% end %>
</tbody>
</table>
</div>
<% html_title(l(:button_annotate)) -%>
<% if @annotate.nil? || @annotate.empty? %>
<div class="generic-table--container">
<div class="generic-table--no-results-container">
<h2 class="generic-table--no-results-title">
<i class="icon-info"></i>
<%= l(:label_nothing_display) %>
</h2>
<div class="generic-table--no-results-description">
<p class="nodata"><%= l('repositories.warnings.cannot_annotate') %></p>
</div>
</div>
</div>
<% else %>
<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
<div class="autoscroll">
<table class="filecontent annotate CodeRay">
<tbody>
<% line_num = 1 %>
<% syntax_highlight(@path, to_utf8_for_repositories(@annotate.content)) do |line| %>
<% revision = @annotate.revisions[line_num-1] %>
<tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
<th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
<td class="revision">
<%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %></td>
<td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
<td class="line-code">
<pre><%= line %></pre>
</td>
</tr>
<% line_num += 1 %>
<% end %>
</tbody>
</table>
</div>
<% end %>

@ -85,7 +85,6 @@ See doc/COPYRIGHT.rdoc for more details.
key: User.current.rss_key } },
{ class: 'button', title: l('repositories.atom_revision_feed') }) do %>
<i class="button--icon icon-page-atom"></i>
<span class="button--text"></span>
<% end %>
</li>
<% end %>

@ -91,7 +91,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% if e.project != @project %>
<span class="project"><%= e.project %></span>
<% end %>
<%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), with_notes_anchor(e, @tokens) %>
<%= link_to highlight_tokens(truncate(e.event_title, escape: false, length: 255), @tokens), with_notes_anchor(e, @tokens) %>
</dt>
<dd><span class="description"><%= highlight_first([last_journal(e).try(:notes), e.event_description], @tokens) %></span>
<span class="author"><%= format_time(e.event_datetime) %></span></dd>

@ -29,10 +29,6 @@ See doc/COPYRIGHT.rdoc for more details.
<%= styled_form_tag({:action => 'edit'}) do %>
<section class="form--section">
<div class="form--field"><%= setting_text_field :app_title, :size => 30 %></div>
<div class="form--field">
<%= setting_text_area :welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit', :'ng-non-bindable' => '' %>
<%= wikitoolbar_for 'settings_welcome_text' %>
</div>
<div class="form--field">
<%= setting_text_field :attachment_max_size, :size => 6, unit: l(:"number.human.storage_units.units.kb") %>
</div>
@ -63,6 +59,16 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
<div class="form--field"><%= setting_text_field :diff_max_lines_displayed, :size => 6 %></div>
<%= call_hook(:view_settings_general_form) %>
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= l(:setting_welcome_text) %></legend>
<div class="form--field"><%= setting_text_field :welcome_title, :size => 30 %></div>
<div class="form--field">
<%= setting_text_area :welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit', :'ng-non-bindable' => '' %>
<%= wikitoolbar_for 'settings_welcome_text' %>
</div>
<div class="form--field"><%= setting_check_box :welcome_on_homescreen %></div>
<div class="form--field"><%= setting_check_box :welcome_on_projects_page %></div>
</fieldset>
</section>
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %>
<% end %>

@ -68,7 +68,7 @@ See doc/COPYRIGHT.rdoc for more details.
<tbody>
<% @user.memberships.each do |membership| %>
<% next if membership.new_record? %>
<tr id="member-<%= membership.id %>" class="class">
<tr id="member-<%= membership.id %>" class="member">
<td class="project">
<%= link_to_project membership.project %>
</td>

@ -0,0 +1,60 @@
<%#-- 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.
++#%>
<%= error_messages_for 'user' %>
<%= javascript_include_tag 'admin_users' %>
<!--[form:user]-->
<section class="form--section">
<div class="form--field"><%= f.text_field :mail, required: true, autofocus: true %></div>
<div class="form--field"><%= f.text_field :firstname, required: true %></div>
<div class="form--field"><%= f.text_field :lastname, required: true %></div>
<%= render partial: 'customizable/field',
collection: @user.custom_field_values,
as: :value,
locals: { form: f } %>
<% unless @auth_sources.empty? || OpenProject::Configuration.disable_password_login? %>
<div class="form--field">
<% sources = ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }) %>
<%= f.select :auth_source_id, sources %>
</div>
<div class="form--field" id="new_user_login" style="display: none;">
<%= f.text_field :login, :required => true, :size => 25, :disabled => true %>
</div>
<% end %>
<div class="form--field"><%= f.check_box :admin, :disabled => (@user == User.current) %></div>
<%= call_hook(:view_users_form, :user => @user, :form => f) %>
</section>
<!--[eoform:user]-->

@ -27,17 +27,16 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<%= javascript_include_tag 'new_user' %>
<% html_title l(:label_administration), l("label_user_new") %>
<%= render partial: 'toolbar', locals: { new_user: true } %>
<%= labelled_tabular_form_for @user,
:url => { :action => "create" },
:html => { :class => nil, :autocomplete => 'off' },
:as => :user do |f| %>
<%= render :partial => 'form', :locals => { :f => f } %>
<div class="form--field">
<label><%= styled_check_box_tag 'send_information', 1, true %>
<%= l(:label_send_information) %></label>
</div>
<%= render :partial => 'simple_form', :locals => { f: f, auth_sources: @auth_sources, user: @user } %>
<p>
<%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-yes' %>
<%= styled_button_tag l(:button_create_and_continue), :name => 'continue', class: '-highlight -with-icon icon-yes' %>

@ -28,57 +28,58 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table interactive-table role="grid" class="generic-table transitions-<%= name %>">
<table interactive-table role="grid" class="generic-table workflow-table transitions-<%= name %>">
<colgroup>
<col highlight-col>
<col>
<col>
<col span="<%= @statuses.length %>" highlight-col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= link_to_function(icon_wrapper('icon-context icon-yes',"#{l(:button_check_all)}/#{l(:button_uncheck_all)}"), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
:class => 'no-decoration-on-hover',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:alt => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
<%=l(:label_current_status)%>
</span>
</div>
</div>
</th>
<th></th>
<th></th>
<th colspan="<%= @statuses.length %>">
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header" style="text-align: center;">
<div class="generic-table--sort-header workflow-table--header">
<span>
<%=l(:label_new_statuses_allowed)%>
</span>
<span class="workflow-table--check-all">
(<%= check_all_links 'workflow_form_' + name %>)
</span>
</div>
</div>
</th>
</tr>
<tr>
<th></th>
<th></th>
<% for new_status in @statuses %>
<th>
<%= link_to_function(icon_wrapper('icon-context icon-yes',"#{l(:button_check_all)}/#{l(:button_uncheck_all)}"), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
<%= link_to_function(icon_wrapper('icon-context icon-yes',"#{l(:label_check_uncheck_all_in_column)}"), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
:class => 'no-decoration-on-hover',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:alt => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
:title => "#{l(:label_check_uncheck_all_in_column)}",
:alt => "#{l(:label_check_uncheck_all_in_column)}") %>
<%=h new_status.name %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<tr class='workflow-table--turned-header'>
<th rowspan="<%= @statuses.length + 1 %>">
<span>
<%=l(:label_current_status)%>
</span>
</th>
</tr>
<% for old_status in @statuses %>
<tr>
<td>
<%= link_to_function(icon_wrapper('icon-context icon-yes',"#{l(:button_check_all)}/#{l(:button_uncheck_all)}"), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
<td class="workflow-table--current-status">
<%= link_to_function(icon_wrapper('icon-context icon-yes',"#{l(:label_check_uncheck_all_in_row)}"), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
:class => 'no-decoration-on-hover',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:alt => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
:title => "#{l(:label_check_uncheck_all_in_row)}",
:alt => "#{l(:label_check_uncheck_all_in_row)}") %>
<%=h old_status.name %>
</td>
<% for new_status in @statuses -%>

@ -45,7 +45,7 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
</li>
<li class="simple-filters--controls">
<%= submit_tag l(:button_edit), :name => nil, :accesskey => accesskey(:edit), class: 'button -highlight' %>
<%= submit_tag l(:button_edit), :name => nil, :accesskey => accesskey(:edit), class: 'button -highlight' %>
</li>
</ul>
<ul class="simple-filter--trailing-labels">
@ -62,28 +62,32 @@ See doc/COPYRIGHT.rdoc for more details.
<% end %>
<%# TODO: remove the prototype stuff from the DOM -%>
<% if @type && @role && @statuses.any? %>
<%= form_tag({}, :id => 'workflow_form' ) do %>
<%= hidden_field_tag 'type_id', @type.id %>
<%= hidden_field_tag 'role_id', @role.id %>
<%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
<%= form_tag({}, :id => 'workflow_form_always' ) do %>
<%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
<% end %>
<fieldset class="form--fieldset -collapsible" style="margin-top: 0.5em;">
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_additional_workflow_transitions_for_author) %></a></legend>
<div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
<%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
</div>
</fieldset>
<%= form_tag({}, :id => 'workflow_form_author' ) do %>
<fieldset class="form--fieldset -collapsible" style="margin-top: 0.5em;">
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_additional_workflow_transitions_for_author) %></a></legend>
<div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
<%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
</div>
</fieldset>
<% end %>
<%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
<fieldset class="form--fieldset -collapsible">
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_additional_workflow_transitions_for_assignee) %></a></legend>
<div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
<%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
</div>
</fieldset>
<%= form_tag({}, :id => 'workflow_form_assignee' ) do %>
<fieldset class="form--fieldset -collapsible">
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_additional_workflow_transitions_for_assignee) %></a></legend>
<div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
<%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
</div>
</fieldset>
<% end %>
<%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %>
<% end %>
<% end %>
<% html_title(Workflow.model_name.human) -%>

@ -0,0 +1,73 @@
#-- 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 DeliverNotificationJob
include OpenProject::BeforeDelayedJob
def initialize(recipient_id, sender_id)
@recipient_id = recipient_id
@sender_id = sender_id
end
def perform
# nothing to do if recipient was deleted in the meantime
return unless recipient
mail = User.execute_as(recipient) { build_mail }
if mail
mail.deliver
end
end
private
# To be implemented by subclasses.
# Actual recipient and sender User objects are passed (always non-nil).
# Returns a Mail::Message, or nil if no message should be sent.
def render_mail(recipient:, sender:)
raise 'SubclassResponsibility'
end
def build_mail
render_mail(recipient: recipient, sender: sender)
rescue StandardError => e
Rails.logger.error "#{self.class.name}: Unexpected error rendering a mail: #{e}"
# not raising, to avoid re-schedule of DelayedJob; don't expect render errors to fix themselves
# by retrying
nil
end
def recipient
@recipient ||= User.find_by(id: @recipient_id)
end
def sender
@sender ||= User.find_by(id: @sender_id) || DeletedUser.first
end
end

@ -27,26 +27,23 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class DeliverWatcherNotificationJob
include OpenProject::BeforeDelayedJob
class DeliverWatcherNotificationJob < DeliverNotificationJob
def initialize(watcher_id, watcher_setter_id)
def initialize(watcher_id, recipient_id, watcher_setter_id)
@watcher_id = watcher_id
@watcher_setter_id = watcher_setter_id
end
def perform
return unless @watcher_id
super(recipient_id, watcher_setter_id)
end
watcher = Watcher.find(@watcher_id)
watcher_setter = User.find(@watcher_setter_id)
def render_mail(recipient:, sender:)
return nil unless watcher
return unless watcher && watcher_setter
UserMailer.work_package_watcher_added(watcher.watchable, recipient, sender)
end
mail = User.execute_as(watcher.user) {
UserMailer.work_package_watcher_added(watcher.watchable, watcher.user, watcher_setter)
}
private
mail.deliver
def watcher
@watcher ||= Watcher.find_by(id: @watcher_id)
end
end

@ -27,16 +27,15 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class DeliverWorkPackageNotificationJob
include OpenProject::BeforeDelayedJob
class DeliverWorkPackageNotificationJob < DeliverNotificationJob
def initialize(journal_id, author_id)
def initialize(journal_id, recipient_id, author_id)
@journal_id = journal_id
@author_id = author_id
super(recipient_id, author_id)
end
def perform
return unless raw_journal # abort, assuming that the underlying WP was deleted
def render_mail(recipient:, sender:)
return nil unless raw_journal # abort, assuming that the underlying WP was deleted
journal = find_aggregated_journal
@ -44,39 +43,25 @@ class DeliverWorkPackageNotificationJob
# 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
if journal.initial?
UserMailer.work_package_added(recipient, journal, sender)
else
UserMailer.work_package_updated(recipient, journal, sender)
end
end
private
def raw_journal
@raw_journal ||= Journal.find_by(id: @journal_id)
end
def find_aggregated_journal
wp_journals = Journal::AggregatedJournal.aggregated_journals(journable: work_package)
wp_journals.detect { |journal| journal.version == raw_journal.version }
end
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

@ -63,8 +63,10 @@ class EnqueueWorkPackageNotificationJob
end
def deliver_notifications_for(journal)
job = DeliverWorkPackageNotificationJob.new(journal.id, @author_id)
Delayed::Job.enqueue job
notification_receivers(work_package).each do |recipient|
job = DeliverWorkPackageNotificationJob.new(journal.id, recipient.id, @author_id)
Delayed::Job.enqueue job
end
end
def raw_journal
@ -74,4 +76,8 @@ class EnqueueWorkPackageNotificationJob
def work_package
@work_package ||= raw_journal.journable
end
def notification_receivers(work_package)
(work_package.recipients + work_package.watcher_recipients).uniq
end
end

@ -41,21 +41,23 @@ class Scm::StorageUpdaterJob
end
def perform
repository = Repository.find @id
bytes = repository.scm.count_repository!
repository.update_attributes!(
required_storage_bytes: bytes,
storage_updated_at: Time.now,
)
rescue ActiveRecord::RecordNotFound
Rails.logger.warn("StorageUpdater requested for Repository ##{@id}, which could not be found.")
end
def destroy_failed_jobs?
true
end
private
def repository
@repository ||= Repository.find @id
##
# We don't want to repeat failing jobs here,
# as they might have failed due to I/O problems and thus,
# we rather keep the old outdated value until an event
# triggers the update again.
def max_attempts
1
end
end

@ -99,6 +99,7 @@ OpenProject::Application.configure do
jstoolbar/lang/*.js
members_form.js
members_select_boxes.js
new_user.js
project/responsible_attribute.js
repository_navigation.js
select_list_move.js

@ -0,0 +1,77 @@
#-- 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 'open_project/homescreen'
OpenProject::Homescreen.manage :blocks do |blocks|
blocks.push(
{ partial: 'welcome',
if: Proc.new { Setting.welcome_on_homescreen? && !Setting.welcome_text.empty? } },
{ partial: 'projects' },
{ partial: 'users',
if: Proc.new { User.current.admin? } },
{ partial: 'my_account',
if: Proc.new { User.current.logged? } },
{ partial: 'news',
if: Proc.new { !@news.empty? } },
{ partial: 'community' },
{ partial: 'administration',
if: Proc.new { User.current.admin? } }
)
end
OpenProject::Homescreen.manage :links do |links|
links.push(
{
label: :user_guides,
icon: 'icon-context icon-rename',
url: 'https://www.openproject.org/help/user-guides/'
},
{
label: :faq,
icon: 'icon-context icon-faq',
url: 'https://www.openproject.org/help/faq/'
},
{
label: :glossary,
icon: 'icon-context icon-glossar',
url: 'https://www.openproject.org/help/user-guides/glossary/'
},
{
label: :shortcuts,
icon: 'icon-context icon-shortcuts',
url: 'https://www.openproject.org/help/user-guides/keyboard-shortcuts-access-keys/'
},
{
label: :forums,
icon: 'icon-context icon-bubble3',
url: 'https://community.openproject.org/projects/openproject/boards'
}
)
end

@ -120,7 +120,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
menu.push :types,
{ controller: '/types' },
caption: :label_type_plural,
caption: :label_work_package_types,
html: { class: 'icon2 icon-tracker' }
menu.push :statuses,
@ -267,6 +267,12 @@ Redmine::MenuManager.map :project_menu do |menu|
if: Proc.new { |p| p.project_type.try :allows_association },
html: { class: 'icon2 icon-dependency' }
menu.push :members,
{ controller: :members, action: :index },
param: :project_id,
caption: :label_member_plural,
html: { class: 'icon2 icon-group' }
menu.push :settings,
{ controller: '/projects', action: 'settings' },
caption: :label_project_settings,

@ -54,8 +54,11 @@ Redmine::AccessControl.map do |map|
require: :member
map.permission :manage_members,
{ projects: :settings,
members: [:create, :update, :destroy, :autocomplete_for_member] },
{ members: [:index, :new, :create, :update, :destroy, :autocomplete_for_member] },
require: :member
map.permission :view_members,
{ members: [:index] },
require: :member
map.permission :manage_versions,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save