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/* /tmp/test/*
/*.rbc /*.rbc
/doc/app /doc/app
/Gemfile.local /Gemfile.local*
/Gemfile.plugins /Gemfile.plugins*
/.rvmrc* /.rvmrc*
/.ruby-version /.ruby-version
/.ruby-gemset /.ruby-gemset

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

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

@ -41,14 +41,18 @@
.prop('disabled', checked); .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() { function on_auth_source_change() {
var passwordFields = jQuery('#password_fields'), var passwordFields = jQuery('#password_fields'),
passwordInputs = passwordFields.find('#user_password, #user_password_confirmation'); passwordInputs = passwordFields.find('#user_password, #user_password_confirmation');
if (this.value === '') { if (this.value === '') {
passwordFields.show(); passwordFields.show();
passwordInputs.removeAttr('disabled'); passwordInputs.removeProp('disabled');
} else { } else {
passwordFields.hide(); passwordFields.hide();
passwordInputs.prop('disabled', 'disabled'); passwordInputs.prop('disabled', 'disabled');
@ -57,6 +61,6 @@
jQuery(function init(){ jQuery(function init(){
jQuery('#user_assign_random_password').change(on_assign_random_password_change); 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 search
//= require colors //= require colors
//= require tooltips //= require tooltips
//= require danger_zone_validation
//source: http://stackoverflow.com/questions/8120065/jquery-and-prototype-dont-work-together-with-array-prototype-reverse //source: http://stackoverflow.com/questions/8120065/jquery-and-prototype-dont-work-together-with-array-prototype-reverse
if (typeof []._reverse == 'undefined') { if (typeof []._reverse == 'undefined') {

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

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

@ -39,7 +39,9 @@ var ModalHelper = (function() {
// prototype, so that all ModalHelper instances can share them. // prototype, so that all ModalHelper instances can share them.
if (ModalHelper._done !== true) { if (ModalHelper._done !== true) {
// one time initialization // 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); body.append(modalDiv);
// close when body is clicked // close when body is clicked

@ -26,21 +26,29 @@
// See doc/COPYRIGHT.rdoc for more details. // 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) { (function() {
var size = parseFloat(fileSize); /**
return isNaN(size) ? "0kB" : (size / 1000).toFixed(2) + "kB"; * 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; var agent = navigator.userAgent;
if (agent.match(/MSIE [789]\.0/) === null && // IE 7-9 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; return;
} }

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

@ -38,6 +38,14 @@
float: right float: right
text-align: 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 // 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 #content .login-auth-providers, #top-menu #nav-login-content .login-auth-providers
width: 471px width: 471px

@ -57,6 +57,38 @@
</form> </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 ## Forms: Standard layout
``` ```
@ -178,6 +210,16 @@
The more, the better The more, the better
</div> </div>
</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"> <div class="form--field -required">
<label class="form--label">Long text:</label> <label class="form--label">Long text:</label>
<div class="form--field-container"> <div class="form--field-container">

@ -61,6 +61,52 @@ $form--field-types: (text-field, text-area, select, check-box, radio-button, ran
&.-compressed &.-compressed
padding: 10px 20px 0 20px 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 .form--separator
border: 0 border: 0
border-bottom: 1px solid $content-form-separator-color border-bottom: 1px solid $content-form-separator-color
@ -244,6 +290,11 @@ fieldset.form--fieldset
text-overflow: ellipsis text-overflow: ellipsis
overflow: hidden overflow: hidden
&.-required
input.form--text-field:invalid
// avoids the box-shadow from Firefox at required input fields
box-shadow: none
.form--label .form--label
@include grid-content(2) @include grid-content(2)
@include grid-visible-overflow @include grid-visible-overflow
@ -535,6 +586,12 @@ input[readonly].-clickable
padding: 0 $form-padding padding: 0 $form-padding
margin-bottom: 0.5rem margin-bottom: 0.5rem
align-items: center align-items: center
line-height: 1
&.icon, &.icon-context
padding: 5px
&:before
padding: 0
%form--field-element-container + & %form--field-element-container + &
margin-left: -1rem margin-left: -1rem

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

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

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

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

@ -58,6 +58,67 @@ table
.hours-dec .hours-dec
font-size: 0.9em 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 tr
&.project &.project
td.name a 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 color: #777777
font-style: italic 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 .comments-number
padding: 0 padding: 0
float: right float: right

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

@ -90,10 +90,14 @@ $toggler-width: 40px
// placeholder for highlighted left-item-border // placeholder for highlighted left-item-border
a:not(.toggler) a:not(.toggler)
border-left: $main-menu-selected-hover-indicator-width solid $main-menu-bg-color border-left: $main-menu-selected-hover-indicator-width solid $main-menu-bg-color
flex-basis: 100% @extend .small-12
&.selected &.selected
+highlight-left-item-border +highlight-left-item-border
a:not(:only-child):first-of-type
@extend .small-10
.open .toggler .open .toggler
.icon-toggler:before .icon-toggler:before
content: "\e0cc" 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> <option value="oob">Boo</option>
</select> </select>
</li> </li>
<li class="toolbar-item"> <li class="toolbar-item -icon-only">
<a href="#" class="button -highlight"> <a href="#" class="button -highlight">
<i class="button--icon icon-add"></i> <i class="button--icon icon-add"></i>
</a> </a>

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

@ -234,6 +234,10 @@
.form--field-container .form--field-container
max-width: 400px 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-shrink-zero
flex-grow: 0 flex-grow: 0

@ -152,6 +152,9 @@ $content-form-input-hover-border: 1px solid #888888 !default
$content-form-separator-color: #DDDDDD !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-msg-font-color: #111111 !default
$content-flash-error-msg-bg-color: #FF8B8B !default $content-flash-error-msg-bg-color: #FF8B8B !default
$content-flash-notice-msg-bg-color: #C4EA96 !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 $user-avatar-mini-width: 20px !default
$select-element-padding: 3px, 24px, 3px, 3px $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. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function(I18n) { .controller-homescreen,
return { .controller-homescreen #content
restrict: 'E', background: $widget-box-content-bg-color
templateUrl: '/templates/work_packages/tabs/_attachments_table.html',
scope: { .controller-homescreen #breadcrumb
attachments: '=' display: none
},
link: function(scope) {
scope.I18n = I18n; .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 def register
return self_registration_disabled unless allow_registration? return self_registration_disabled unless allow_registration?
@user = invited_user
if request.get? if request.get?
session[:auth_source_registration] = nil registration_through_invitation!
@user = User.new(language: Setting.default_language)
else else
@user = User.new self_registration!
@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
end end
end end
@ -142,7 +126,7 @@ class AccountController < ApplicationController
allow = Setting.self_registration? && !OpenProject::Configuration.disable_password_login? allow = Setting.self_registration? && !OpenProject::Configuration.disable_password_login?
get = request.get? && allow get = request.get? && allow
post = request.post? && (session[:auth_source_registration] || allow) post = (request.post? || request.patch?) && (session[:auth_source_registration] || allow)
get || post get || post
end end
@ -153,17 +137,82 @@ class AccountController < ApplicationController
# Token based account activation # Token based account activation
def activate def activate
return redirect_to(home_url) unless Setting.self_registration? && params[:token] token = Token.find_by value: params[:token].to_s
token = Token.find_by(action: 'register', value: params[:token].to_s)
redirect_to(home_url) && return unless token and !token.expired? 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 user = token.user
redirect_to(home_url) && return unless user.registered?
user.activate if not user.registered?
if user.save if user.active?
token.destroy flash[:notice] = I18n.t(:notice_account_already_activated)
flash[:notice] = l(:notice_account_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 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 end
# Process a password change form, used when the user is forced # Process a password change form, used when the user is forced
@ -199,6 +248,46 @@ class AccountController < ApplicationController
private 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) def direct_login(user)
if flash.empty? if flash.empty?
ps = {}.tap do |p| ps = {}.tap do |p|
@ -233,7 +322,7 @@ class AccountController < ApplicationController
end end
def password_authentication(username, password) def password_authentication(username, password)
user = User.try_to_login(username, password) user = User.try_to_login(username, password, session)
if user.nil? if user.nil?
# login failed, now try to find out why and do the appropriate thing # login failed, now try to find out why and do the appropriate thing
user = User.find_by_login(username) user = User.find_by_login(username)
@ -251,6 +340,8 @@ class AccountController < ApplicationController
else else
invalid_credentials invalid_credentials
end end
elsif user and user.invited?
invited_account_not_activated(user)
else else
# incorrect password # incorrect password
invalid_credentials invalid_credentials
@ -346,6 +437,8 @@ class AccountController < ApplicationController
# Register a user depending on Setting.self_registration # Register a user depending on Setting.self_registration
def register_user_according_to_setting(user, opts = {}, &block) def register_user_according_to_setting(user, opts = {}, &block)
return register_automatically(user, opts, &block) if user.invited?
case Setting.self_registration case Setting.self_registration
when '1' when '1'
register_by_email_activation(user, opts, &block) register_by_email_activation(user, opts, &block)
@ -433,6 +526,13 @@ class AccountController < ApplicationController
end end
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 # Log an attempt to log in to a locked account or with invalid credentials
# and show a flash message. # and show a flash message.
def invalid_credentials(flash_now: true) def invalid_credentials(flash_now: true)
@ -464,4 +564,12 @@ class AccountController < ApplicationController
redirect_back_or_default controller: '/my', action: 'page' redirect_back_or_default controller: '/my', action: 'page'
end end
end end
def invited_user
if session.include? :invitation_token
token = Token.find_by(value: session[:invitation_token])
token.user
end
end
end end

@ -426,58 +426,15 @@ class ApplicationController < ActionController::Base
params[:back_url] || request.env['HTTP_REFERER'] params[:back_url] || request.env['HTTP_REFERER']
end end
def redirect_back_or_default(default, escape = true, use_escaped = true) def redirect_back_or_default(default, use_escaped = true)
escaped_back_url = if escape policy = RedirectPolicy.new(
URI.escape(CGI.unescape(params[:back_url].to_s)) params[:back_url],
else hostname: request.host,
params[:back_url] default: default,
end return_escaped: use_escaped,
)
# if we have a back_url it must not contain two consecutive dots
if escaped_back_url.present? && !escaped_back_url.match(/\.\./) redirect_to policy.redirect_url
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
end end
def render_400(options = {}) def render_400(options = {})

@ -84,9 +84,12 @@ class AuthSourcesController < ApplicationController
def destroy def destroy
@auth_source = AuthSource.find(params[:id]) @auth_source = AuthSource.find(params[:id])
unless @auth_source.users.first if @auth_source.users.empty?
@auth_source.destroy @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 end
redirect_to action: 'index' redirect_to action: 'index'
end end

@ -43,7 +43,18 @@ module Concerns::OmniauthLogin
# Set back url to page the omniauth login link was clicked on # Set back url to page the omniauth login link was clicked on
params[:back_url] = request.env['omniauth.origin'] 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 decision = OpenProject::OmniAuth::Authorization.authorized? auth_hash
if decision.approve? if decision.approve?
@ -83,7 +94,7 @@ module Concerns::OmniauthLogin
private private
def authorization_successful(user, auth_hash) def authorization_successful(user, auth_hash)
if user.new_record? if user.new_record? || user.invited?
create_user_from_omniauth user, auth_hash create_user_from_omniauth user, auth_hash
else else
if user.active? if user.active?
@ -144,7 +155,7 @@ module Concerns::OmniauthLogin
def fill_user_fields_from_omniauth(user, auth) def fill_user_fields_from_omniauth(user, auth)
user.update_attributes omniauth_hash_to_user_attributes(auth) user.update_attributes omniauth_hash_to_user_attributes(auth)
user.register user.register unless user.invited?
user user
end 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', flash[:notice] = I18n.t('copy_project.started',
source_project_name: @project.name, source_project_name: @project.name,
target_project_name: target_project_name) target_project_name: target_project_name)
redirect_to :back redirect_to origin
else else
from = (['admin', 'settings'].include?(params[:coming_from]) ? params[:coming_from] : 'settings') from = (['admin', 'settings'].include?(params[:coming_from]) ? params[:coming_from] : 'settings')
render action: "copy_from_#{from}" render action: "copy_from_#{from}"
@ -74,6 +74,10 @@ class CopyProjectsController < ApplicationController
private private
def origin
params[:coming_from] == 'admin' ? admin_projects_path : settings_project_path(@project.id)
end
def prepare_for_copy_project def prepare_for_copy_project
@issue_custom_fields = WorkPackageCustomField.order("#{CustomField.table_name}.position") @issue_custom_fields = WorkPackageCustomField.order("#{CustomField.table_name}.position")
@types = ::Type.all @types = ::Type.all

@ -27,10 +27,13 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class WelcomeController < ApplicationController class HomescreenController < ApplicationController
def index def index
@news = current_user.latest_news @newest_projects = Project.visible.newest.take(3)
@projects = current_user.latest_projects @newest_users = User.newest.take(3)
@news = News.latest(count: 3)
@homescreen = OpenProject::Homescreen
end end
def robots 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 before_filter :authorize
include Pagination::Controller include Pagination::Controller
include PaginationHelper
paginate_model User paginate_model User
search_for User, :search_in_project search_for User, :search_in_project
search_options_for User, lambda { |_| { project: @project } } search_options_for User, lambda { |_| { project: @project } }
@ -45,6 +47,15 @@ class MembersController < ApplicationController
@@scripts.unshift(script) @@scripts.unshift(script)
end end
def index
@roles = Role.find_all_givable
@members = index_members @project
end
def new
set_roles_and_principles!
end
def create def create
if params[:member] if params[:member]
members = new_members_from_params members = new_members_from_params
@ -52,28 +63,37 @@ class MembersController < ApplicationController
end end
respond_to do |format| respond_to do |format|
if members.present? && members.all?(&:valid?) if members.present? && members.all?(&:valid?)
flash.now.notice = l(:notice_successful_create) flash.notice = members_added_notice members
format.html do redirect_to settings_project_path(@project, tab: 'members') end
format.js do format.html do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project } redirect_to project_members_path(project_id: @project)
render(:update) do |page| end
page.replace_html 'tab-content-members', partial: 'projects/settings/members',
locals: { members: members }
page.insert_html :top, 'tab-content-members', render_flash_messages
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 end
set_roles_and_principles!
render 'new'
end end
else
format.js do format.js do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project } @pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page| render(:update) do |page|
if params[:member] 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 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 end
end end
@ -84,45 +104,31 @@ class MembersController < ApplicationController
def update def update
member = update_member_from_params member = update_member_from_params
if member.save 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 end
respond_to do |format| redirect_to project_members_path(project_id: @project,
format.html do redirect_to controller: '/projects', action: 'settings', tab: 'members', id: @project, page: params[:page] end page: params[:page],
format.js do per_page: params[:per_page])
@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
end end
def destroy def destroy
if @member.deletable? if @member.deletable?
@member.destroy if @member.disposable?
flash.now.notice = l(:notice_successful_delete) flash.notice = I18n.t(:notice_member_deleted, user: @member.principal.name)
end
respond_to do |format| @member.user.destroy
format.html do redirect_to controller: '/projects', action: 'settings', tab: 'members', id: @project end else
format.js do flash.notice = I18n.t(:notice_member_removed, user: @member.principal.name)
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page| @member.destroy
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
end end
end end
redirect_to project_members_path(project_id: @project)
end end
def autocomplete_for_member def autocomplete_for_member
@ -140,6 +146,10 @@ class MembersController < ApplicationController
@principals = Principal.possible_members(params[:q], 100) - @project.principals @principals = Principal.possible_members(params[:q], 100) - @project.principals
end end
@email = suggest_invite_via_email? current_user,
params[:q],
(@principals | @project.principals)
respond_to do |format| respond_to do |format|
format.json format.json
format.html do format.html do
@ -158,28 +168,83 @@ class MembersController < ApplicationController
private 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 def self.tab_scripts
@@scripts.join('(); ') + '();' @@scripts.join('(); ') + '();'
end 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 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)) roles = Role.where(id: possibly_seperated_ids_for_entity(params[:member], :role))
new_member = lambda { |user_id| if roles.present?
Member.new(permitted_params.member).tap do |member| user_ids = invite_new_users possibly_seperated_ids_for_entity(params[:member], :user)
member.user_id = user_id if user_id 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 end
}
members
members = user_ids.map { |user_id| else
new_member.call(user_id) # Pick a user that exists but can't be chosen.
} # We only want the missing role error message.
# most likely wrong user input, use a dummy member for error handling dummy = new_member User.anonymous.id
if !members.present? && roles.present?
members << new_member.call(nil) [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 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 end
def each_comma_seperated(array, &block) def each_comma_seperated(array, &block)
@ -195,7 +260,7 @@ class MembersController < ApplicationController
def transform_array_of_comma_seperated_ids(array) def transform_array_of_comma_seperated_ids(array)
return array unless array.present? return array unless array.present?
each_comma_seperated(array) do |elem| each_comma_seperated(array) do |elem|
elem.to_s.split(',').map(&:to_i) elem.to_s.split(',')
end end
end end
@ -221,4 +286,12 @@ class MembersController < ApplicationController
@member.assign_attributes(attrs) @member.assign_attributes(attrs)
@member @member
end 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 end

@ -216,7 +216,6 @@ class RepositoriesController < ApplicationController
(show_error_not_found; return) unless @entry (show_error_not_found; return) unless @entry
@annotate = @repository.scm.annotate(@path, @rev) @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) @changeset = @repository.find_changeset_by_name(@rev)
end end

@ -120,26 +120,9 @@ class UsersController < ApplicationController
@user = User.new(language: Setting.default_language, mail_notification: Setting.default_notification_option) @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.attributes = permitted_params.user_create_as_admin(false, @user.change_password_allowed?)
@user.admin = params[:user][:admin] || false @user.admin = params[:user][:admin] || false
@user.login = params[:user][:login] || @user.mail
if @user.change_password_allowed? if UserInvitation.invite_user! @user
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]
respond_to do |format| respond_to do |format|
format.html do format.html do
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
@ -151,8 +134,6 @@ class UsersController < ApplicationController
end end
else else
@auth_sources = AuthSource.all @auth_sources = AuthSource.all
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format| respond_to do |format|
format.html do render action: 'new' end 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] : []) @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? if !@user.password.blank? && @user.change_password_allowed?
UserMailer.account_information(@user, @user.password).deliver 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 end
respond_to do |format| respond_to do |format|
@ -228,7 +219,12 @@ class UsersController < ApplicationController
# Was the account activated? (do it before User#save clears the change) # Was the account activated? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUSES[:registered], was_activated = (@user.status_change == [User::STATUSES[:registered],
User::STATUSES[:active]]) 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) flash[:notice] = I18n.t(:notice_successful_update)
if was_activated if was_activated
UserMailer.account_activated(@user).deliver UserMailer.account_activated(@user).deliver

@ -64,7 +64,7 @@ class WorkPackages::BulkController < ApplicationController
end end
end end
set_flash_from_bulk_save(@work_packages, unsaved_work_package_ids) 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 end
def destroy def destroy

@ -189,7 +189,7 @@ class WorkPackagesController < ApplicationController
flash[:notice] = l(:notice_successful_update) 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 else
edit edit
end end
@ -354,9 +354,11 @@ class WorkPackagesController < ApplicationController
changes = work_package.changesets.visible changes = work_package.changesets.visible
.includes({ repository: { project: :enabled_modules } }, :user) .includes({ repository: { project: :enabled_modules } }, :user)
changes.reverse! if current_user.wants_comments_in_reverse_order? if current_user.wants_comments_in_reverse_order?
changes.reverse
changes else
changes.to_a
end
end 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', name: 'modules',
action: :select_project_modules, action: :select_project_modules,
partial: 'projects/settings/modules', partial: 'projects/settings/modules',
label: :label_module_plural }, label: :label_module_plural
{
name: 'members',
action: :manage_members,
partial: 'projects/settings/members',
label: :label_member_plural
}, },
{ {
name: 'custom_fields', 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 # 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. # to see the aggregated journals as if no versions were known after the specified version.
def aggregated_journals(journable: nil, until_version: nil) def aggregated_journals(journable: nil, until_version: nil)
query_aggregated_journals(journable: journable, until_version: until_version).map { |journal| raw_journals = query_aggregated_journals(journable: journable, until_version: until_version)
Journal::AggregatedJournal.new(journal) 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 end
@ -89,6 +97,7 @@ class Journal::AggregatedJournal
ON #{sql_on_groups_belong_condition('predecessor', table_name)}") ON #{sql_on_groups_belong_condition('predecessor', table_name)}")
.where('predecessor.id IS NULL') .where('predecessor.id IS NULL')
.order("COALESCE(addition.created_at, #{table_name}.created_at) ASC") .order("COALESCE(addition.created_at, #{table_name}.created_at) ASC")
.order("#{version_projection} ASC")
.select("#{table_name}.journable_id, .select("#{table_name}.journable_id,
#{table_name}.journable_type, #{table_name}.journable_type,
#{table_name}.user_id, #{table_name}.user_id,
@ -270,8 +279,17 @@ class Journal::AggregatedJournal
:notes_version, :notes_version,
to: :journal 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 @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 end
# returns an instance of this class that is reloaded from the database # 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 # 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) if Journal::AggregatedJournal.hides_notifications?(aggregated, aggregated.predecessor)
job = DeliverWorkPackageNotificationJob.new(aggregated.predecessor.id, User.current.id) work_package = aggregated.predecessor.journable
Delayed::Job.enqueue job notification_receivers(work_package).each do |recipient|
job = DeliverWorkPackageNotificationJob.new(aggregated.predecessor.id,
recipient.id,
User.current.id)
Delayed::Job.enqueue job
end
end end
job = EnqueueWorkPackageNotificationJob.new(journal.id, User.current.id) 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 = Journal::AggregatedJournal.aggregated_journals(journable: raw_journal.journable)
wp_journals.detect { |journal| journal.version == raw_journal.version } wp_journals.detect { |journal| journal.version == raw_journal.version }
end end
def notification_receivers(work_package)
(work_package.recipients + work_package.watcher_recipients).uniq
end
end end
end end

@ -46,7 +46,7 @@ class Member < ActiveRecord::Base
after_destroy :unwatch_from_permission_change after_destroy :unwatch_from_permission_change
def name def name
user.name principal.name
end end
def to_s def to_s
@ -130,6 +130,14 @@ class Member < ActiveRecord::Base
@membership @membership
end 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 protected
def destroy_if_no_roles_left! def destroy_if_no_roles_left!

@ -66,7 +66,7 @@ class News < ActiveRecord::Base
end end
# returns latest news for projects visible by user # 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) latest_for(user, count: count)
end end

@ -36,7 +36,8 @@ class Principal < ActiveRecord::Base
builtin: 0, builtin: 0,
active: 1, active: 1,
registered: 2, registered: 2,
locked: 3 locked: 3,
invited: 4
} }
self.table_name = "#{table_name_prefix}users#{table_name_suffix}" 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, -> { 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) } 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 " + .where("#{Principal.table_name}.type='Group' OR " +
"(#{Principal.table_name}.type='User' AND " + "(#{Principal.table_name}.type='User' AND " +
"(#{Principal.table_name}.status=#{Principal::STATUSES[:active]} OR " + "(#{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' }, class_name: 'Member'
has_many :users, through: :members has_many :users, through: :members
has_many :principals, through: :member_principals, source: :principal has_many :principals, through: :member_principals, source: :principal
@ -138,6 +139,7 @@ class Project < ActiveRecord::Base
scope :active, -> { where(status: STATUS_ACTIVE) } scope :active, -> { where(status: STATUS_ACTIVE) }
scope :public_projects, -> { where(is_public: true) } scope :public_projects, -> { where(is_public: true) }
scope :visible, ->(user = User.current) { where(Project.visible_by(user)) } scope :visible, ->(user = User.current) { where(Project.visible_by(user)) }
scope :newest, -> { order(created_on: :desc) }
# timelines stuff # timelines stuff
@ -290,21 +292,6 @@ class Project < ActiveRecord::Base
save save
end 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. # Returns a SQL :conditions string used to find all active projects for the specified user.
# #
# Examples: # Examples:

@ -109,8 +109,9 @@ class Repository < ActiveRecord::Base
## ##
# Retrieves the :disabled_types setting from `configuration.yml # 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 def self.disabled_types
scm_config[:disabled_types] || [] (scm_config[:disabled_types] || []).map(&:to_sym)
end end
def vendor def vendor

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

@ -29,9 +29,9 @@
class WatcherNotificationMailer class WatcherNotificationMailer
class << self class << self
def handle_watcher(watcher_id, watcher_setter_id) def handle_watcher(watcher, watcher_setter)
unless other_jobs_queued?(Watcher.find(watcher_id).watchable) unless other_jobs_queued?(watcher.watchable)
job = DeliverWatcherNotificationJob.new(watcher_id, watcher_setter_id) job = DeliverWatcherNotificationJob.new(watcher.id, watcher.user.id, watcher_setter.id)
Delayed::Job.enqueue job Delayed::Job.enqueue job
end end
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 auth_provider_html = call_hook :view_account_login_auth_provider
no_pwd = OpenProject::Configuration.disable_password_login? no_pwd = OpenProject::Configuration.disable_password_login?
pclass = no_pwd ? 'no-pwd' : '' pclass = no_pwd ? 'no-pwd' : ''
wclass = local_assigns[:wide] ? 'wide' : ''
%> %>
<% if auth_provider_html.strip != '' %> <% if auth_provider_html.strip != '' %>
<div class="login-auth-providers <%= pclass %>"> <div class="login-auth-providers <%= pclass %> <%= wclass %>">
<% unless no_pwd %> <% unless no_pwd %>
<h3 class="login-auth-providers-title"><span> <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> </span></h3>
<% end %> <% end %>
<div class="login-auth-provider-list"> <div class="login-auth-provider-list">

@ -33,14 +33,14 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field"> <div class="form--field">
<%= styled_label_tag 'username', User.human_attribute_name(:login) %> <%= styled_label_tag 'username', User.human_attribute_name(:login) %>
<div class="form--field-container"> <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> </div>
<div class="form--field"> <div class="form--field">
<%= styled_label_tag 'password', User.human_attribute_name(:password) %> <%= styled_label_tag 'password', User.human_attribute_name(:password) %>
<div class="form--field-container"> <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>
</div> </div>

@ -83,4 +83,6 @@ See doc/COPYRIGHT.rdoc for more details.
</section> </section>
<%= styled_button_tag l(:button_submit), class: '-highlight -with-icon icon-yes' %> <%= 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 %> <% end %>

@ -31,7 +31,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= toolbar title: l(:label_project_new) %> <%= 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' %> <%= hidden_field_tag :coming_from, 'admin' %>
<%= render :partial => 'projects/form', :locals => { :f => f, :project => @copy_project, :renderTypes => true } %> <%= 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' %> <%= error_messages_for 'copy_project' %>
<%= toolbar title: l(:label_project_new) %> <%= 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"> <section class="form--section">
<%= render :partial => "projects/form/attributes/name", :locals => { :form => f } %> <%= render :partial => "projects/form/attributes/name", :locals => { :form => f } %>
<%= render :partial => "projects/form/attributes/identifier", :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> <p>Include a wiki page. Example:</p>
<pre><code>{{include(Foo)}}</code></pre> <pre><code>{{include(Foo)}}</code></pre>
</dd> </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> <dt><code>macro_list</code></dt>
<dd> <dd>
<p>Displays a list of all available macros, including description if available.</p> <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, <%= labelled_tabular_form_for(:member,
:url => {:controller => '/members', :action => 'create', :project_id => project}, :url => {:controller => '/members', :action => 'create', :project_id => project},
:method => :post, :method => :post,
:remote => true,
:loading => '$(\'member-add-submit\').disable();', :loading => '$(\'member-add-submit\').disable();',
:complete => 'if($(\'member-add-submit\')) $(\'member-add-submit\').enable(); activateFlashError();', :complete => 'if($(\'member-add-submit\')) $(\'member-add-submit\').enable(); activateFlashError();',
:html => {:id => "members_add_form"}) do |f| %> :html => {:id => "members_add_form"}) do |f| %>
<div class="form--section"> <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="grid-block medium-up-2">
<div class="form--column"> <div class="form--column">
<div class="form--field"> <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 %> <%= styled_text_field_tag :principal_search, nil %>
<%= observe_field(:principal_search, <%= observe_field(:principal_search,
:frequency => 0.5, :frequency => 0.5,
@ -63,6 +72,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= 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' %> class: 'button -highlight -with-icon icon-yes' %>
<% end %> <% end %>
<%= link_to I18n.t('button_cancel'), :back, class: 'button' %>
</div> </div>
<% end %> <% end %>

@ -29,19 +29,29 @@ See doc/COPYRIGHT.rdoc for more details.
<%= javascript_include_tag "members_select_boxes.js" %> <%= 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}, <%= labelled_tabular_form_for(:member, :url => {:controller => 'members', :action => 'create', :project_id => project},
:remote => true,
:method => :post, :method => :post,
:html => {:id => "members_add_form"}) do |f| %> :html => {:id => "members_add_form"}) do |f| %>
<div class="form--section"> <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="grid-block medium-up-2">
<div class="form--column"> <div class="form--column">
<div class="form--field -vertical"> <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([]), <%= select_tag "member[user_ids]", options_for_select([]),
:title => l(:label_principal_search), :title => user_id_title,
:multiple => true, :multiple => true,
:autofocus => true,
:'data-ajaxURL' => url_for(:controller => "/members", :action => "autocomplete_for_member"), :'data-ajaxURL' => url_for(:controller => "/members", :action => "autocomplete_for_member"),
:'data-projectId' => project.id, :'data-projectId' => project.id,
no_label: true, no_label: true,
@ -61,7 +71,8 @@ See doc/COPYRIGHT.rdoc for more details.
</div> </div>
</div> </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' %> class: 'button -highlight -with-icon icon-yes' %>
<%= link_to I18n.t('button_cancel'), :back, class: 'button' %>
</div> </div>
<% end %> <% 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": "results":
{ {
"items":[ "items":[
<% @principals.each_with_index do |principal, ix| %> <% @principals.each_with_index do |principal, ix| %>
{ {
"id": <%= principal.id.to_json.html_safe %>, "id": <%= principal.id.to_json.html_safe %>,
"name": <%= principal.name.to_json.html_safe %> "name": <%= principal.name.to_json.html_safe %>
} <%= "," unless ix == @principals.length - 1 %> } <%= "," unless !@email && ix == @principals.length - 1 %>
<% end %> ], <% end %>
<% if @email %>
{
"id": "<%= @email %>",
"name": "Invite <%= @email %>"
}
<% end %>
],
"total": <%= @total ? @total : @principals.size %>, "total": <%= @total ? @total : @principals.size %>,
"more": <%= @more ? @more : 0 %> "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> </li>
<% end %> <% end %>
<% if Setting.welcome_on_projects_page? %>
<div class="wiki"> <div class="wiki">
<%= format_text Setting.welcome_text %> <h1><%= Setting.welcome_title %></h1>
<%= format_text(Setting.welcome_text, headings: false) %>
</div> </div>
<% end %>
<% if User.current.logged? %> <% if User.current.logged? %>
<p style="float:right;"> <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) rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
%> %>
<span class="repository-bradcrumbs--identifier"> <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> </span>
<% html_title(h(with_leading_slash(path))) -%> <% 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"> <div id="repository--checkout-instructions" class="notification-box -info">
<% unless embedded %> <% 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 %> <% end %>
<div class="notification-box--content"> <div class="notification-box--content">
<p> <p>

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

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

@ -91,7 +91,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% if e.project != @project %> <% if e.project != @project %>
<span class="project"><%= e.project %></span> <span class="project"><%= e.project %></span>
<% end %> <% 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> </dt>
<dd><span class="description"><%= highlight_first([last_journal(e).try(:notes), e.event_description], @tokens) %></span> <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> <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 %> <%= styled_form_tag({:action => 'edit'}) do %>
<section class="form--section"> <section class="form--section">
<div class="form--field"><%= setting_text_field :app_title, :size => 30 %></div> <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"> <div class="form--field">
<%= setting_text_field :attachment_max_size, :size => 6, unit: l(:"number.human.storage_units.units.kb") %> <%= setting_text_field :attachment_max_size, :size => 6, unit: l(:"number.human.storage_units.units.kb") %>
</div> </div>
@ -63,6 +59,16 @@ See doc/COPYRIGHT.rdoc for more details.
</div> </div>
<div class="form--field"><%= setting_text_field :diff_max_lines_displayed, :size => 6 %></div> <div class="form--field"><%= setting_text_field :diff_max_lines_displayed, :size => 6 %></div>
<%= call_hook(:view_settings_general_form) %> <%= 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> </section>
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %> <%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %>
<% end %> <% end %>

@ -68,7 +68,7 @@ See doc/COPYRIGHT.rdoc for more details.
<tbody> <tbody>
<% @user.memberships.each do |membership| %> <% @user.memberships.each do |membership| %>
<% next if membership.new_record? %> <% next if membership.new_record? %>
<tr id="member-<%= membership.id %>" class="class"> <tr id="member-<%= membership.id %>" class="member">
<td class="project"> <td class="project">
<%= link_to_project membership.project %> <%= link_to_project membership.project %>
</td> </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") %> <% html_title l(:label_administration), l("label_user_new") %>
<%= render partial: 'toolbar', locals: { new_user: true } %> <%= render partial: 'toolbar', locals: { new_user: true } %>
<%= labelled_tabular_form_for @user, <%= labelled_tabular_form_for @user,
:url => { :action => "create" }, :url => { :action => "create" },
:html => { :class => nil, :autocomplete => 'off' }, :html => { :class => nil, :autocomplete => 'off' },
:as => :user do |f| %> :as => :user do |f| %>
<%= render :partial => 'form', :locals => { :f => f } %> <%= render :partial => 'simple_form', :locals => { f: f, auth_sources: @auth_sources, user: @user } %>
<div class="form--field">
<label><%= styled_check_box_tag 'send_information', 1, true %>
<%= l(:label_send_information) %></label>
</div>
<p> <p>
<%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-yes' %> <%= 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' %> <%= 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--container">
<div class="generic-table--results-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> <colgroup>
<col highlight-col> <col>
<col>
<col span="<%= @statuses.length %>" highlight-col> <col span="<%= @statuses.length %>" highlight-col>
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<th> <th></th>
<div class="generic-table--sort-header-outer"> <th></th>
<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 colspan="<%= @statuses.length %>"> <th colspan="<%= @statuses.length %>">
<div class="generic-table--sort-header-outer"> <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> <span>
<%=l(:label_new_statuses_allowed)%> <%=l(:label_new_statuses_allowed)%>
</span> </span>
<span class="workflow-table--check-all">
(<%= check_all_links 'workflow_form_' + name %>)
</span>
</div> </div>
</div> </div>
</th> </th>
</tr> </tr>
<tr> <tr>
<th></th>
<th></th> <th></th>
<% for new_status in @statuses %> <% for new_status in @statuses %>
<th> <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', :class => 'no-decoration-on-hover',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :title => "#{l(:label_check_uncheck_all_in_column)}",
:alt => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> :alt => "#{l(:label_check_uncheck_all_in_column)}") %>
<%=h new_status.name %> <%=h new_status.name %>
</th> </th>
<% end %> <% end %>
</tr> </tr>
</thead> </thead>
<tbody> <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 %> <% for old_status in @statuses %>
<tr> <tr>
<td> <td class="workflow-table--current-status">
<%= 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}')", <%= 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', :class => 'no-decoration-on-hover',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :title => "#{l(:label_check_uncheck_all_in_row)}",
:alt => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> :alt => "#{l(:label_check_uncheck_all_in_row)}") %>
<%=h old_status.name %> <%=h old_status.name %>
</td> </td>
<% for new_status in @statuses -%> <% for new_status in @statuses -%>

@ -45,7 +45,7 @@ See doc/COPYRIGHT.rdoc for more details.
</div> </div>
</li> </li>
<li class="simple-filters--controls"> <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> </li>
</ul> </ul>
<ul class="simple-filter--trailing-labels"> <ul class="simple-filter--trailing-labels">
@ -62,28 +62,32 @@ See doc/COPYRIGHT.rdoc for more details.
<% end %> <% end %>
<%# TODO: remove the prototype stuff from the DOM -%> <%# TODO: remove the prototype stuff from the DOM -%>
<% if @type && @role && @statuses.any? %> <% if @type && @role && @statuses.any? %>
<%= form_tag({}, :id => 'workflow_form' ) do %> <%= form_tag({}, :id => 'workflow_form_always' ) do %>
<%= hidden_field_tag 'type_id', @type.id %> <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
<%= hidden_field_tag 'role_id', @role.id %> <% end %>
<%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
<fieldset class="form--fieldset -collapsible" style="margin-top: 0.5em;"> <%= form_tag({}, :id => 'workflow_form_author' ) do %>
<legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_additional_workflow_transitions_for_author) %></a></legend> <fieldset class="form--fieldset -collapsible" style="margin-top: 0.5em;">
<div id="author_workflows" style="margin: 0.5em 0 0.5em 0;"> <legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_additional_workflow_transitions_for_author) %></a></legend>
<%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %> <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
</div> <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
</fieldset> </div>
</fieldset>
<% end %>
<%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %> <%= 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> <%= form_tag({}, :id => 'workflow_form_assignee' ) do %>
<div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;"> <fieldset class="form--fieldset -collapsible">
<%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %> <legend class="form--fieldset-legend" onclick="toggleFieldset(this);"><a href="javascript:"><%= l(:label_additional_workflow_transitions_for_assignee) %></a></legend>
</div> <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
</fieldset> <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
</div>
</fieldset>
<% end %>
<%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %> <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %> <%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-yes' %>
<% end %>
<% end %> <% end %>
<% html_title(Workflow.model_name.human) -%> <% 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. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class DeliverWatcherNotificationJob class DeliverWatcherNotificationJob < DeliverNotificationJob
include OpenProject::BeforeDelayedJob
def initialize(watcher_id, watcher_setter_id) def initialize(watcher_id, recipient_id, watcher_setter_id)
@watcher_id = watcher_id @watcher_id = watcher_id
@watcher_setter_id = watcher_setter_id
end
def perform super(recipient_id, watcher_setter_id)
return unless @watcher_id end
watcher = Watcher.find(@watcher_id) def render_mail(recipient:, sender:)
watcher_setter = User.find(@watcher_setter_id) 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) { private
UserMailer.work_package_watcher_added(watcher.watchable, watcher.user, watcher_setter)
}
mail.deliver def watcher
@watcher ||= Watcher.find_by(id: @watcher_id)
end end
end end

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

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

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

@ -99,6 +99,7 @@ OpenProject::Application.configure do
jstoolbar/lang/*.js jstoolbar/lang/*.js
members_form.js members_form.js
members_select_boxes.js members_select_boxes.js
new_user.js
project/responsible_attribute.js project/responsible_attribute.js
repository_navigation.js repository_navigation.js
select_list_move.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, menu.push :types,
{ controller: '/types' }, { controller: '/types' },
caption: :label_type_plural, caption: :label_work_package_types,
html: { class: 'icon2 icon-tracker' } html: { class: 'icon2 icon-tracker' }
menu.push :statuses, menu.push :statuses,
@ -267,6 +267,12 @@ Redmine::MenuManager.map :project_menu do |menu|
if: Proc.new { |p| p.project_type.try :allows_association }, if: Proc.new { |p| p.project_type.try :allows_association },
html: { class: 'icon2 icon-dependency' } 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, menu.push :settings,
{ controller: '/projects', action: 'settings' }, { controller: '/projects', action: 'settings' },
caption: :label_project_settings, caption: :label_project_settings,

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

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

Loading…
Cancel
Save