diff options
114 files changed, 1230 insertions, 516 deletions
diff --git a/CHANGELOG b/CHANGELOG index 9933f2f77d4..22a01ddfaa4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,9 @@ v 6.6.0 - Show users' group membership on users' activity page - User pages are visible without login if user is authorized to a public project - Markdown rendered headers have id derived from their name and link to their id + - Improve application to work faster with large groups (100+ members) + - Multiple emails per user + - Show last commit for file when view file source v 6.5.1 - Fix branch selectbox when create merge request from fork @@ -641,4 +644,4 @@ v 0.8.0 - stability - security fixes - increased test coverage - - email notification + - email notification
\ No newline at end of file @@ -208,6 +208,10 @@ group :development, :test do gem 'spork', '~> 1.0rc' gem 'jasmine', '2.0.0.rc5' + + gem "spring", '1.1.1' + gem "spring-commands-rspec", '1.0.1' + gem "spring-commands-spinach", '1.0.0' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 2c99063726e..91d04e2ec70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -470,6 +470,11 @@ GEM railties (>= 3) spinach (>= 0.4) spork (1.0.0rc4) + spring (1.1.1) + spring-commands-rspec (1.0.1) + spring (>= 0.9.1) + spring-commands-spinach (1.0.0) + spring (>= 0.9.1) sprockets (2.10.1) hike (~> 1.2) multi_json (~> 1.0) @@ -637,6 +642,9 @@ DEPENDENCIES slim spinach-rails spork (~> 1.0rc) + spring (= 1.1.1) + spring-commands-rspec (= 1.0.1) + spring-commands-spinach (= 1.0.0) stamp state_machine test_after_commit @@ -1,4 +1,4 @@ -Copyright (c) 2011 Dmitriy Zaporozhets +Copyright (c) 2011-2014 Dmitriy Zaporozhets Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index dcd303e08a8..90237268da0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ### Code status -* [](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) +* [](https://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) * [](https://codeclimate.com/github/gitlabhq/gitlabhq) @@ -45,13 +45,17 @@ #### Official installation methods -* [Manual installation guide for a production server](doc/install/installation.md) +* [GitLab packages (beta)](https://www.gitlab.com/downloads/) These packages contain GitLab and all its depencies (PostgreSQL, Redis, Nginx, Unicorn, etc.). They are made with [omnibus-gitlab](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md) that also contains the installation instructions. These packages currently support a reduced selection of GitLab's normal features. For instance, it is not yet possible to create/restore application backups or to use HTTPS. + +* [GitLab virtual machine images](https://www.gitlab.com/downloads/) contain an operating system and a preinstalled GitLab. They are made with [GitLab Packer](https://gitlab.com/gitlab-org/gitlab-packer/blob/master/README.md) that also contains the installation instructions. * [GitLab Chef Cookbook](https://gitlab.com/gitlab-org/cookbook-gitlab/blob/master/README.md) This cookbook can be used both for development installations and production installations. If you want to [contribute](CONTRIBUTE.md) to GitLab we suggest you follow the [development installation on a virtual machine with Vagrant](https://gitlab.com/gitlab-org/cookbook-gitlab/blob/master/doc/development.md) instructions to install all testing dependencies. +* [Manual installation guide](doc/install/installation.md) This guide to set up a production server offers detailed and complete step-by-step instructions. + #### Third party one-click installers -* [Digital Ocean 1-Click Application Install](https://www.digitalocean.com/blog_posts/host-your-git-repositories-in-55-seconds-with-gitlab) Have a new server up in 55 seconds. Digital Ocean uses SSD disks which is great for an IO intensive app such as GitLab. +* [Digital Ocean 1-Click Application Install](https://www.digitalocean.com/blog_posts/host-your-git-repositories-in-55-seconds-with-gitlab) Have a new server up in 55 seconds. Digital Ocean uses SSD disks which is great for an IO intensive app such as GitLab. We recommend selecting a droplet with [1GB of memory](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/requirements.md). * [BitNami one-click installers](http://bitnami.com/stack/gitlab) This package contains both GitLab and GitLab CI. It is available as installer, virtual machine or for cloud hosting providers (Amazon Web Services/Azure/etc.). @@ -148,7 +152,7 @@ or start each component separately * [Book](http://www.packtpub.com/gitlab-repository-management/book) written by GitLab enthusiast Jonathan M. Hethey is unofficial but it offers a good overview. -* [Gitter](https://gitter.im/gitlabhq/gitlabhq#) here you can ask questions when you need help. +* [Gitter chat room](https://gitter.im/gitlabhq/gitlabhq#) here you can ask questions when you need help. ### Getting in touch @@ -1 +1 @@ -6.6.0.pre +6.6.0.beta1
\ No newline at end of file diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 5f4a38ebbd0..fafa5cdfaa4 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -3,6 +3,7 @@ user_path: "/api/:version/users/:id.json" notes_path: "/api/:version/projects/:id/notes.json" namespaces_path: "/api/:version/namespaces.json" + project_users_path: "/api/:version/projects/:id/users.json" # Get 20 (depends on api) recent notes # and sort the ascending from oldest to newest @@ -50,6 +51,23 @@ ).done (users) -> callback(users) + # Return project users list. Filtered by query + # Only active users retrieved + projectUsers: (project_id, query, callback) -> + url = Api.buildUrl(Api.project_users_path) + url = url.replace(':id', project_id) + + $.ajax( + url: url + data: + private_token: gon.api_token + search: query + per_page: 20 + active: true + dataType: "json" + ).done (users) -> + callback(users) + # Return namespaces list. Filtered by query namespaces: (query, callback) -> url = Api.buildUrl(Api.namespaces_path) diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee new file mode 100644 index 00000000000..59a53cb52bc --- /dev/null +++ b/app/assets/javascripts/project_users_select.js.coffee @@ -0,0 +1,44 @@ +$ -> + projectUserFormatResult = (user) -> + if user.avatar_url + avatar = user.avatar_url + else if gon.gravatar_enabled + avatar = gon.gravatar_url + avatar = avatar.replace('%{hash}', md5(user.email)) + avatar = avatar.replace('%{size}', '24') + else + avatar = gon.relative_url_root + "/assets/no_avatar.png" + + "<div class='user-result'> + <div class='user-image'><img class='avatar s24' src='#{avatar}'></div> + <div class='user-name'>#{user.name}</div> + <div class='user-username'>#{user.username}</div> + </div>" + + projectUserFormatSelection = (user) -> + user.name + + $('.ajax-project-users-select').each (i, select) -> + project_id = $('body').data('project-id') + + $(select).select2 + placeholder: $(select).data('placeholder') || "Search for a user" + multiple: $(select).hasClass('multiselect') + minimumInputLength: 0 + query: (query) -> + Api.projectUsers project_id, query.term, (users) -> + data = { results: users } + query.callback(data) + + initSelection: (element, callback) -> + id = $(element).val() + if id isnt "" + Api.user(id, callback) + + + formatResult: projectUserFormatResult + formatSelection: projectUserFormatSelection + dropdownCssClass: "ajax-project-users-dropdown" + dropdownAutoWidth: true + escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results + m diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index c1fa16ca89c..ce9a505b1e3 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,7 +1,7 @@ $ -> userFormatResult = (user) -> - if user.avatar - avatar = user.avatar.url + if user.avatar_url + avatar = user.avatar_url else if gon.gravatar_enabled avatar = gon.gravatar_url avatar = avatar.replace('%{hash}', md5(user.email)) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index cc5fdf61405..d5522f00f50 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -65,6 +65,7 @@ @import "sections/wall.scss"; @import "sections/dashboard.scss"; @import "sections/stat_graph.scss"; +@import "sections/groups.scss"; /** * Code ighlight diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 4824194ec42..91618688081 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -112,6 +112,7 @@ pre.well-pre { .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { background: #29b; + color: #FFF } .breadcrumb > li + li:before { diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 931b75a3234..19e3e7a9536 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -51,3 +51,27 @@ label { .input-mn-300 { min-width: 300px; } + +.custom-form-control { + width: 150px; +} + +@media (min-width: $screen-sm-min) { + .custom-form-control { + width: 150px; + } +} + +/* Medium devices (desktops, 992px and up) */ +@media (min-width: $screen-md-min) { + .custom-form-control { + width: 170px; + } +} + +/* Large devices (large desktops, 1200px and up) */ +@media (min-width: $screen-lg-min) { + .custom-form-control { + width: 200px; + } +} diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index c506bff8a74..7ee1fec4e03 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -1,5 +1,4 @@ /** Select2 selectbox style override **/ - .select2-container, .select2-container.select2-drop-above { .select2-choice { background: #FFF; @@ -12,9 +11,13 @@ } .select2-drop-active { - border: 1px solid #BBB; + border: 1px solid #BBB !important; margin-top: 4px; + &.select2-drop-above { + margin-bottom: 8px; + } + .select2-search input { background: #fafafa; border-color: #DDD; @@ -78,3 +81,9 @@ select { .project-refs-form .select2-container { margin-right: 10px; } + +.ajax-users-dropdown, .ajax-project-users-dropdown { + .select2-search { + padding-top: 4px; + } +} diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 7f45d64fb7c..169f1268cd7 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -108,6 +108,8 @@ $pagination-active-bg: $bg_style_color; // Nav tabs .nav.nav-tabs { + margin-bottom: 15px; + li { > a { padding: 8px 20px; diff --git a/app/assets/stylesheets/sections/admin.scss b/app/assets/stylesheets/sections/admin.scss index 8ad9bc732b2..a558633d112 100644 --- a/app/assets/stylesheets/sections/admin.scss +++ b/app/assets/stylesheets/sections/admin.scss @@ -2,7 +2,7 @@ * Admin area * */ -.admin_dash { +.admin-dashboard { .data { a { h1 { @@ -14,6 +14,10 @@ } } } + + .str-truncated { + max-width: 60%; + } } .admin-filter form { diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index 1fd82c84fc9..6fc394e2e2b 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -41,7 +41,7 @@ .dash-sidebar-tabs { margin-bottom: 2px; border: none; - margin: 0; + margin: 0 !important; li { &.active { diff --git a/app/assets/stylesheets/sections/groups.scss b/app/assets/stylesheets/sections/groups.scss new file mode 100644 index 00000000000..60ec79acadb --- /dev/null +++ b/app/assets/stylesheets/sections/groups.scss @@ -0,0 +1,9 @@ +.new-group-member-holder { + margin-top: 50px; + background: #f9f9f9; + padding-top: 20px; +} + +.member-search-form { + float: left; +} diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index c8091c84891..883c9a859ef 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -229,9 +229,9 @@ header { } .title { a { - color: #BBB; + color: #FFF; &:hover { - color: #FFF; + text-decoration: underline; } } color: #fff; diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index 5afb13a86ba..a7fa900fafa 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -14,8 +14,8 @@ .issue-check { float: left; - padding: 8px 0; padding-right: 8px; + margin-bottom: 10px; min-width: 15px; } @@ -38,13 +38,21 @@ } } -input.check_all_issues { +.check-all-holder { + height: 32px; float: left; - padding: 0; - margin: 0; - margin-right: 10px; - position: relative; - top: 13px; + margin-right: 12px; + padding: 6px 10px; + border: 1px solid #ccc; + @include border-radius(4px); + + + input.check_all_issues { + padding: 0; + margin: 0; + position: relative; + top: 3px; + } } .issues_content { @@ -91,6 +99,13 @@ input.check_all_issues { .update_selected_issues { margin-left: 4px; } + + .select2-container .select2-choice { + height: 32px; + line-height: 28px; + color: #444 !important; + font-weight: 500; + } } } diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 4388da00735..6e21bf0b0a1 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -31,10 +31,10 @@ .mr_source_commit, .mr_target_commit { + margin-top: 10px; .commit { margin: 0; - padding: 0; - padding: 5px 0; + padding: 2px 0; list-style: none; &:hover { background: none; diff --git a/app/assets/stylesheets/sections/nav.scss b/app/assets/stylesheets/sections/nav.scss index 372fa9669ca..88f16a21596 100644 --- a/app/assets/stylesheets/sections/nav.scss +++ b/app/assets/stylesheets/sections/nav.scss @@ -36,8 +36,7 @@ &.active { a { color: #333; - font-weight: bolder; - + font-weight: bold; &:after { content: ''; display: block; @@ -56,7 +55,20 @@ &:hover { a { - color: $style_color; + color: $link_color; + &:after { + content: ''; + display: block; + position: relative; + bottom: 8px; + left: 50%; + width: 0; + height: 0; + border-color: transparent transparent #29b transparent; + border-style: solid; + border-width: 6px; + margin-left: -6px; + } } } @@ -73,7 +85,7 @@ a { display: block; text-align: center; - font-weight: normal; + font-weight: 500; height: 38px; line-height: 34px; color: #777; diff --git a/app/assets/stylesheets/sections/profile.scss b/app/assets/stylesheets/sections/profile.scss index 0ee46b9a2f7..7a696c21e47 100644 --- a/app/assets/stylesheets/sections/profile.scss +++ b/app/assets/stylesheets/sections/profile.scss @@ -114,3 +114,14 @@ height: 50px; } } + +.global-notifications-form .level-title { + font-size: 15px; + color: #333; + font-weight: bold; +} + +.notification-icon-holder { + width: 20px; + float: left; +} diff --git a/app/assets/stylesheets/themes/ui_color.scss b/app/assets/stylesheets/themes/ui_color.scss index 0fc72d4e0a8..edac4290e74 100644 --- a/app/assets/stylesheets/themes/ui_color.scss +++ b/app/assets/stylesheets/themes/ui_color.scss @@ -36,4 +36,8 @@ } } } + + .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { + background: #769; + } } diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7b418ec98f5..b927dd2f20a 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -63,7 +63,14 @@ class GroupsController < ApplicationController def members @project = group.projects.find(params[:project_id]) if params[:project_id] - @members = group.users_groups.order('group_access DESC') + @members = group.users_groups + + if params[:search].present? + users = group.users.search(params[:search]) + @members = @members.where(user_id: users) + end + + @members = @members.order('group_access DESC').page(params[:page]).per(50) @users_group = UsersGroup.new end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb new file mode 100644 index 00000000000..9996b67a8a4 --- /dev/null +++ b/app/controllers/profiles/emails_controller.rb @@ -0,0 +1,26 @@ +class Profiles::EmailsController < ApplicationController + layout "profile" + + def index + @primary = current_user.email + @emails = current_user.emails + end + + def create + @email = current_user.emails.new(params[:email]) + + flash[:alert] = @email.errors.full_messages.first unless @email.save + + redirect_to profile_emails_url + end + + def destroy + @email = current_user.emails.find(params[:id]) + @email.destroy + + respond_to do |format| + format.html { redirect_to profile_emails_url } + format.js { render nothing: true } + end + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ba5c52d510f..dd11948edd1 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -28,7 +28,7 @@ class Projects::IssuesController < Projects::ApplicationController @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? sort_param = params[:sort] || 'newest' @sort = sort_param.humanize unless sort_param.empty? - + @assignees = User.where(id: @project.issues.pluck(:assignee_id)) respond_to do |format| format.html diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index de31ee12b6a..d36b5b27e86 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -28,6 +28,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController assignee_id, milestone_id = params[:assignee_id], params[:milestone_id] @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero? @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? + @assignees = User.where(id: @project.merge_requests.pluck(:assignee_id)) end def show diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 18ace028b0c..a6b7ae3f127 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -11,11 +11,7 @@ class Projects::RawController < Projects::ApplicationController @blob = @repository.blob_at(@commit.id, @path) if @blob - type = if @blob.mime_type =~ /html|javascript/ - 'text/plain; charset=utf-8' - else - @blob.mime_type - end + type = get_blob_type headers['X-Content-Type-Options'] = 'nosniff' @@ -29,5 +25,17 @@ class Projects::RawController < Projects::ApplicationController not_found! end end + + private + + def get_blob_type + if @blob.mime_type =~ /html|javascript/ + 'text/plain; charset=utf-8' + elsif @blob.name =~ /(?:msi|exe|rar|r0\d|7z|7zip|zip)$/ + 'application/octet-stream' + else + @blob.mime_type + end + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1550e8b7e05..4e7d01acd2a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -162,15 +162,6 @@ module ApplicationHelper alias_method :url_to_image, :image_url - def users_select_tag(id, opts = {}) - css_class = "ajax-users-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - - hidden_field_tag(id, value, class: css_class) - end - def body_data_page path = controller.controller_path.split('/') namespace = path.first if path.second diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 663369e4584..5e5f3f77a21 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -122,17 +122,18 @@ module CommitsHelper def commit_person_link(commit, options = {}) source_name = commit.send "#{options[:source]}_name".to_sym source_email = commit.send "#{options[:source]}_email".to_sym + + user = User.find_for_commit(source_email, source_name) + person_name = user.nil? ? source_name : user.name + person_email = user.nil? ? source_email : user.email + text = if options[:avatar] - avatar = image_tag(avatar_icon(source_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "") - %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{source_name}</span>} + avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "") + %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>} else - source_name + person_name end - # Prefer email match over name match - user = User.where(email: source_email).first - user ||= User.where(name: source_name).first - options = { class: "commit-#{options[:source]}-link has_tooltip", data: { :'original-title' => sanitize(source_email) } diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index cdba6ce84dc..16981edd980 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -70,11 +70,11 @@ module IssuesHelper end def bulk_update_milestone_options - options_for_select(["None (backlog)", nil]) + options_from_collection_for_select(project_active_milestones, "id", "title", params[:milestone_id]) + options_for_select(["None (backlog)"]) + options_from_collection_for_select(project_active_milestones, "id", "title", params[:milestone_id]) end def bulk_update_assignee_options - options_for_select(["None (unassigned)", nil]) + options_from_collection_for_select(@project.team.members, "id", "name", params[:assignee_id]) + options_for_select(["None (unassigned)"]) + options_from_collection_for_select(@project.team.members, "id", "name", params[:assignee_id]) end def assignee_options object diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index ae3402b2617..b2399bb6db1 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,11 +1,11 @@ module NotificationsHelper def notification_icon(notification) if notification.disabled? - content_tag :i, nil, class: 'icon-circle cred' + content_tag :i, nil, class: 'icon-volume-off cred' elsif notification.participating? - content_tag :i, nil, class: 'icon-circle cblue' + content_tag :i, nil, class: 'icon-volume-down cblue' elsif notification.watch? - content_tag :i, nil, class: 'icon-circle cgreen' + content_tag :i, nil, class: 'icon-volume-up cgreen' else content_tag :i, nil, class: 'icon-circle-blank cblue' end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb new file mode 100644 index 00000000000..a1fe4488ae9 --- /dev/null +++ b/app/helpers/selects_helper.rb @@ -0,0 +1,20 @@ +module SelectsHelper + def users_select_tag(id, opts = {}) + css_class = "ajax-users-select " + css_class << "multiselect " if opts[:multiple] + css_class << (opts[:class] || '') + value = opts[:selected] || '' + + hidden_field_tag(id, value, class: css_class) + end + + def project_users_select_tag(id, opts = {}) + css_class = "ajax-project-users-select " + css_class << "multiselect " if opts[:multiple] + css_class << (opts[:class] || '') + value = opts[:selected] || '' + placeholder = opts[:placeholder] || 'Select user' + + hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder) + end +end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index bcd44f9476c..c91660a02b5 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -6,6 +6,12 @@ module Emails mail(to: @user.email, subject: subject("Account was created for you")) end + def new_email_email(email_id) + @email = Email.find(email_id) + @user = @email.user + mail(to: @user.email, subject: subject("Email was added to your account")) + end + def new_ssh_key_email(key_id) @key = Key.find(key_id) @user = @key.user diff --git a/app/models/email.rb b/app/models/email.rb new file mode 100644 index 00000000000..22e71e4f107 --- /dev/null +++ b/app/models/email.rb @@ -0,0 +1,33 @@ +# == Schema Information +# +# Table name: emails +# +# id :integer not null, primary key +# user_id :integer not null +# email :string not null +# created_at :datetime not null +class Email < ActiveRecord::Base + attr_accessible :email, :user_id + + # + # Relations + # + belongs_to :user + + # + # Validations + # + validates :user_id, presence: true + validates :email, presence: true, email: { strict_mode: true }, uniqueness: true + validate :unique_email, if: ->(email) { email.email_changed? } + + before_validation :cleanup_email + + def cleanup_email + self.email = self.email.downcase.strip + end + + def unique_email + self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) + end +end
\ No newline at end of file diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ca2644ec735..a3d786c2138 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -219,6 +219,14 @@ class MergeRequest < ActiveRecord::Base end end + def source_project_namespace + if source_project && source_project.namespace + source_project.namespace.path + else + "(removed)" + end + end + def source_branch_exists? return false unless self.source_project diff --git a/app/models/notification.rb b/app/models/notification.rb index ff6a18d6a51..b0f8ed6a4ec 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -9,12 +9,23 @@ class Notification attr_accessor :target - def self.notification_levels - [N_DISABLED, N_PARTICIPATING, N_WATCH] - end - - def self.project_notification_levels - [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL] + class << self + def notification_levels + [N_DISABLED, N_PARTICIPATING, N_WATCH] + end + + def options_with_labels + { + disabled: N_DISABLED, + participating: N_PARTICIPATING, + watch: N_WATCH, + global: N_GLOBAL + } + end + + def project_notification_levels + [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL] + end end def initialize(target) @@ -36,4 +47,8 @@ class Notification def global? target.notification_level == N_GLOBAL end + + def level + target.notification_level + end end diff --git a/app/models/user.rb b/app/models/user.rb index 10f21d23506..dd59f67ea3d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -78,6 +78,7 @@ class User < ActiveRecord::Base # Profile has_many :keys, dependent: :destroy + has_many :emails, dependent: :destroy # Groups has_many :users_groups, dependent: :destroy @@ -116,6 +117,7 @@ class User < ActiveRecord::Base validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :avatar_type, if: ->(user) { user.avatar_changed? } + validate :unique_email, if: ->(user) { user.email_changed? } validates :avatar, file_size: { maximum: 100.kilobytes.to_i } before_validation :generate_password, on: :create @@ -183,6 +185,13 @@ class User < ActiveRecord::Base where(conditions).first end end + + def find_for_commit(email, name) + # Prefer email match over name match + User.where(email: email).first || + User.joins(:emails).where(emails: { email: email }).first || + User.where(name: name).first + end def filter filter_name case filter_name @@ -250,6 +259,10 @@ class User < ActiveRecord::Base end end + def unique_email + self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email) + end + # Groups user has access to def authorized_groups @authorized_groups ||= begin diff --git a/app/observers/email_observer.rb b/app/observers/email_observer.rb new file mode 100644 index 00000000000..026ad8b1d9a --- /dev/null +++ b/app/observers/email_observer.rb @@ -0,0 +1,5 @@ +class EmailObserver < BaseObserver + def after_create(email) + notification.new_email(email) + end +end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index e54f88e42de..fcc03c3e4b8 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -188,8 +188,6 @@ class GitPushService end def commit_user commit - User.where(email: commit.author_email).first || - User.where(name: commit.author_name).first || - user + User.find_for_commit(commit.author_email, commit.author_name) || user end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 7c02777e914..9d7bb9639ac 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -17,6 +17,13 @@ class NotificationService end end + # Always notify user about email added to profile + def new_email(email) + if email.user + mailer.new_email_email(email.id) + end + end + # When create an issue we should send next emails: # # * issue assignee if their notification level is not Disabled diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index dd663945ea9..bbd60bc6224 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -3,136 +3,137 @@ %p.light You can manage projects, users and other GitLab data from here. %hr -.admin_dash.row - .col-sm-4 - .light-well - %h4 Projects - .data - = link_to admin_projects_path do - %h1= Project.count - %hr - = link_to 'New Project', new_project_path, class: "btn btn-new" - .col-sm-4 - .light-well - %h4 Users - .data - = link_to admin_users_path do - %h1= User.count - %hr - = link_to 'New User', new_admin_user_path, class: "btn btn-new" - .col-sm-4 - .light-well - %h4 Groups - .data - = link_to admin_groups_path do - %h1= Group.count - %hr - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" +.admin-dashboard + .row + .col-sm-4 + .light-well + %h4 Projects + .data + = link_to admin_projects_path do + %h1= Project.count + %hr + = link_to 'New Project', new_project_path, class: "btn btn-new" + .col-sm-4 + .light-well + %h4 Users + .data + = link_to admin_users_path do + %h1= User.count + %hr + = link_to 'New User', new_admin_user_path, class: "btn btn-new" + .col-sm-4 + .light-well + %h4 Groups + .data + = link_to admin_groups_path do + %h1= Group.count + %hr + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" -.row.prepend-top-10 - .col-md-4 - %h4 Latest projects - %hr - - @projects.each do |project| + .row.prepend-top-10 + .col-md-4 + %h4 Latest projects + %hr + - @projects.each do |project| + %p + = link_to project.name_with_namespace, [:admin, project], class: 'str-truncated' + %span.light.pull-right + #{time_ago_with_tooltip(project.created_at)} + + .col-md-4 + %h4 Latest users + %hr + - @users.each do |user| + %p + = link_to [:admin, user], class: 'str-truncated' do + = user.name + %span.light.pull-right + #{time_ago_with_tooltip(user.created_at)} + + .col-md-4 + %h4 Latest groups + %hr + - @groups.each do |group| + %p + = link_to [:admin, group], class: 'str-truncated' do + = group.name + %span.light.pull-right + #{time_ago_with_tooltip(group.created_at)} + + %br + .row + .col-md-4 + %h4 Stats + %hr %p - = link_to project.name_with_namespace, [:admin, project] + Forks %span.light.pull-right - #{time_ago_with_tooltip(project.created_at)} - - .col-md-4 - %h4 Latest users - %hr - - @users.each do |user| + = ForkedProjectLink.count %p - = link_to [:admin, user] do - = user.name + Issues %span.light.pull-right - #{time_ago_with_tooltip(user.created_at)} - - .col-md-4 - %h4 Latest groups - %hr - - @groups.each do |group| + = Issue.count %p - = link_to [:admin, group] do - = group.name + Merge Requests %span.light.pull-right - #{time_ago_with_tooltip(group.created_at)} - -%br -.row - .col-md-4 - %h4 Stats - %hr - %p - Forks - %span.light.pull-right - = ForkedProjectLink.count - %p - Issues - %span.light.pull-right - = Issue.count - %p - Merge Requests - %span.light.pull-right - = MergeRequest.count - %p - Notes - %span.light.pull-right - = Note.count - %p - Snippets - %span.light.pull-right - = Snippet.count - %p - SSH Keys - %span.light.pull-right - = Key.count - %p - Milestones - %span.light.pull-right - = Milestone.count - .col-md-4 - %h4 - Features - %hr - %p - Sign up - %span.light.pull-right - = boolean_to_icon gitlab_config.signup_enabled - %p - LDAP - %span.light.pull-right - = boolean_to_icon Gitlab.config.ldap.enabled - %p - Gravatar - %span.light.pull-right - = boolean_to_icon Gitlab.config.gravatar.enabled - %p - OmniAuth - %span.light.pull-right - = boolean_to_icon Gitlab.config.omniauth.enabled - .col-md-4 - %h4 Components - %hr - %p - GitLab - %span.pull-right - = Gitlab::VERSION - %p - GitLab Shell - %span.pull-right - = Gitlab::Shell.new.version - %p - GitLab API - %span.pull-right - = API::API::version - %p - Ruby - %span.pull-right - #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + = MergeRequest.count + %p + Notes + %span.light.pull-right + = Note.count + %p + Snippets + %span.light.pull-right + = Snippet.count + %p + SSH Keys + %span.light.pull-right + = Key.count + %p + Milestones + %span.light.pull-right + = Milestone.count + .col-md-4 + %h4 + Features + %hr + %p + Sign up + %span.light.pull-right + = boolean_to_icon gitlab_config.signup_enabled + %p + LDAP + %span.light.pull-right + = boolean_to_icon Gitlab.config.ldap.enabled + %p + Gravatar + %span.light.pull-right + = boolean_to_icon Gitlab.config.gravatar.enabled + %p + OmniAuth + %span.light.pull-right + = boolean_to_icon Gitlab.config.omniauth.enabled + .col-md-4 + %h4 Components + %hr + %p + GitLab + %span.pull-right + = Gitlab::VERSION + %p + GitLab Shell + %span.pull-right + = Gitlab::Shell.new.version + %p + GitLab API + %span.pull-right + = API::API::version + %p + Ruby + %span.pull-right + #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} - %p - Rails - %span.pull-right - #{Rails::VERSION::STRING} + %p + Rails + %span.pull-right + #{Rails::VERSION::STRING} diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 7a373ee586c..9a0d5967927 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,11 +1,12 @@ %h3.page-title Groups (#{@groups.total_count}) - %small - allows you to keep projects organized. - Use groups for uniting related projects. - = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" -%br + +%p.light + Group allows you to keep projects organized. + Use groups for uniting related projects. + +%hr = form_tag admin_groups_path, method: :get, class: 'form-inline' do .form-group = text_field_tag :name, params[:name], class: "form-control input-mn-300" @@ -23,24 +24,18 @@ %h4 = link_to [:admin, group] do + %i.icon-folder-close = group.name → %span.monospace - %i.icon-folder-close %strong #{group.path}/ - - .clearfix.light.append-bottom-10 - %span - %b Members: - %span.badge= group.members.size - \| - %span - %b Projects: - %span.badge= group.projects.count - .clearfix %p = truncate group.description, length: 150 + .clearfix + %p.light + #{pluralize(group.members.size, 'member')}, #{pluralize(group.projects.count, 'project')} + = paginate @groups, theme: "gitlab" diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 1fa6fdfaff1..0b3934a712d 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,12 +1,6 @@ .row .col-md-3 .admin-filter - = form_tag admin_users_path, method: :get, class: 'form-inline' do - .append-bottom-10 - .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control' - = button_tag type: 'submit', class: 'btn btn-primary' do - %i.icon-search %ul.nav.nav-pills.nav-stacked %li{class: "#{'active' unless params[:filter]}"} = link_to admin_users_path do @@ -25,6 +19,12 @@ Without projects %small.pull-right= User.without_projects.count %hr + = form_tag admin_users_path, method: :get, class: 'form-inline' do + .form-group + = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control' + = button_tag type: 'submit', class: 'btn btn-primary' do + %i.icon-search + %hr = link_to 'Reset', admin_users_path, class: "btn btn-cancel" .col-md-9 diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index dd63a232fe2..bf634d9de60 100644..100755 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -3,7 +3,8 @@ = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| .devise-errors = devise_error_messages! - = f.email_field :email, placeholder: 'Email', class: "form-control", required: true + .clearfix.append-bottom-20 + = f.email_field :email, placeholder: 'Email', class: "form-control", required: true .clearfix.append-bottom-10 = f.submit "Resend confirmation instructions", class: 'btn btn-success' %hr diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index a14ef2995c8..040821ca32a 100644..100755 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -2,7 +2,8 @@ %h3.page-title Reset password .devise-errors = devise_error_messages! - = f.email_field :email, placeholder: "Email", class: "form-control", required: true + .clearfix.append-bottom-20 + = f.email_field :email, placeholder: "Email", class: "form-control", required: true .clearfix.append-bottom-10 = f.submit "Reset password", class: "btn-primary btn" %hr diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 938f61d2093..bb87d9ecb4a 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,7 +1,7 @@ .login-box %h3.page-title Sign in - if ldap_enabled? - %ul.nav.nav-tabs.append-bottom-20 + %ul.nav.nav-tabs %li.active = link_to 'LDAP', '#tab-ldap', 'data-toggle' => 'tab' %li diff --git a/app/views/groups/_new_group_member.html.haml b/app/views/groups/_new_group_member.html.haml index 5801139a9f2..3ab9276c541 100644 --- a/app/views/groups/_new_group_member.html.haml +++ b/app/views/groups/_new_group_member.html.haml @@ -1,15 +1,8 @@ = form_for @users_group, url: group_users_groups_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| - %h4.append-bottom-20 - New member(s) for - %strong #{@group.name} - group - - %p 1. Choose users you want in the group .form-group = f.label :user_ids, "People", class: 'control-label' .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') - %p 2. Set access level for them .form-group = f.label :group_access, "Group Access", class: 'control-label' .col-sm-10= select_tag :group_access, options_for_select(UsersGroup.group_access_roles, @users_group.group_access), class: "project-access-select select2" diff --git a/app/views/groups/members.html.haml b/app/views/groups/members.html.haml index 3095a2c7b74..38069feb37b 100644 --- a/app/views/groups/members.html.haml +++ b/app/views/groups/members.html.haml @@ -6,14 +6,34 @@ %strong= link_to "here", help_permissions_path, class: "vlink" %hr -.ui-box + +.clearfix + = form_tag members_group_path(@group), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find member by name', class: 'form-control search-text-input input-mn-300' } + = submit_tag 'Search', class: 'btn' + + - if current_user.can? :manage_group, @group + .pull-right + = link_to '#', class: 'btn btn-new js-toggle-visibility-link' do + Add members + %i.icon-chevron-down + + .js-toggle-visibility-container.hide.new-group-member-holder + = render "new_group_member" + +.ui-box.prepend-top-20 .title %strong #{@group.name} group members %small - (#{@members.count}) + (#{@members.total_count}) %ul.well-list - @members.each do |member| = render 'users_groups/users_group', member: member, show_controls: true -- if current_user.can? :manage_group, @group - = render "new_group_member" += paginate @members, theme: 'gitlab' + +:coffeescript + $('form.member-search-form').on 'submit', (event) -> + event.preventDefault() + Turbolinks.visit @.action + '?' + $(@).serialize() diff --git a/app/views/help/workflow.html.haml b/app/views/help/workflow.html.haml index 2b8950cd5c2..2ac5cd3a650 100644 --- a/app/views/help/workflow.html.haml +++ b/app/views/help/workflow.html.haml @@ -1,6 +1,8 @@ = render layout: 'help/layout' do %h3.page-title Workflow + %h4 Summary + %ol.help %li %p Clone project @@ -35,3 +37,37 @@ %li %p Your team lead will review code & merge it to main branch + %h3 Authorization for Merge Requests + %p + There are two main ways to have a merge request flow with GitLab: working with protected branches in a single repository, or working with forks of an authoritative project. + + %h4 Protected branch flow + %p + With the protected branch flow everybody works within the same GitLab project. + The project maintainers get Master access and the regular developers get Developer access. + The maintainers mark the authoritative branches as 'Protected'. + The developers push feature branches to the project and create merge requests to have their feature branches reviewed and merged into one of the protected branches. + Only users with Master access can merge changes into a protected branch. + + %h5 Advantages + %ul + %li fewer projects means less clutter + %li developers need to consider only one remote repository + + %h5 Disadvantages + %ul + %li manual setup of protected branch required for each new project + + %h4 Forking workflow + %p + With the forking workflow the maintainers get Master access and the regular developers get Reporter access to the authoritative repository, which prohibits them from pushing any changes to it. + Developers create forks of the authoritative project and push their feature branches to their own forks. + To get their changes into master they need to create a merge request across forks. + + %h5 Advantages + %ul + %li in an appropriately configured GitLab group, new projects automatically get the required access restrictions for regular developers: fewer manual steps to configure authorization for new projects + + %h5 Disadvantages + %ul + %li all developers on the project need to keep their forks up to date, which requires more advanced Git skills (managing multiple remotes) diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 439cb978a76..53e0dbaef9b 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head", title: "Admin area" %body{class: "#{app_theme} admin", :'data-page' => body_data_page} + = render "layouts/broadcast" = render "layouts/head_panel", title: "Admin area" = render "layouts/flash" %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index d44cb975ea5..35d0d417502 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -4,6 +4,10 @@ %i.icon-home = nav_link(controller: :accounts) do = link_to "Account", profile_account_path + = nav_link(controller: :emails) do + = link_to profile_emails_path do + Emails + %span.count= current_user.emails.count + 1 - unless current_user.ldap_user? = nav_link(controller: :passwords) do = link_to "Password", edit_profile_password_path diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml index 7325664bc19..c43d688a2cb 100644 --- a/app/views/layouts/navless.html.haml +++ b/app/views/layouts/navless.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head", title: @title %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/broadcast" = render "layouts/head_panel", title: @title = render "layouts/flash" diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index fda81b3cc83..3c76bbb9575 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head", title: "Public Projects" %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/broadcast" - if current_user = render "layouts/head_panel", title: "Public Projects" - else diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml index 7de2347803a..5fcf9f99e75 100644 --- a/app/views/layouts/public_projects.html.haml +++ b/app/views/layouts/public_projects.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head", title: @project.name_with_namespace %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/broadcast" = render "layouts/public_head_panel", title: @project.name_with_namespace %nav.main-nav .container= render 'layouts/nav/project' diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml index 80709d650da..4aa258fea0d 100644 --- a/app/views/layouts/public_users.html.haml +++ b/app/views/layouts/public_users.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head", title: @title %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/broadcast" = render "layouts/public_head_panel", title: @title .container.navless-container .content= yield diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml index 177b3a4f8f4..97ed8ba12df 100644 --- a/app/views/layouts/search.html.haml +++ b/app/views/layouts/search.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head", title: "Search" %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/broadcast" = render "layouts/head_panel", title: "Search" = render "layouts/flash" diff --git a/app/views/layouts/user_team.html.haml b/app/views/layouts/user_team.html.haml index 191ad406c3c..ce13853ed7f 100644 --- a/app/views/layouts/user_team.html.haml +++ b/app/views/layouts/user_team.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head", title: "#{@team.name}" %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/broadcast" = render "layouts/head_panel", title: "team: #{@team.name}" = render "layouts/flash" %nav.main-nav.navbar-collapse.collapse diff --git a/app/views/notify/new_email_email.html.haml b/app/views/notify/new_email_email.html.haml new file mode 100644 index 00000000000..4a0448a573c --- /dev/null +++ b/app/views/notify/new_email_email.html.haml @@ -0,0 +1,10 @@ +%p + Hi #{@user.name}! +%p + A new email was added to your account: +%p + email: + %code= @email.email +%p + If this email was added in error, you can remove it here: + = link_to "Emails", profile_emails_url diff --git a/app/views/notify/new_email_email.text.erb b/app/views/notify/new_email_email.text.erb new file mode 100644 index 00000000000..51cba99ad0d --- /dev/null +++ b/app/views/notify/new_email_email.text.erb @@ -0,0 +1,7 @@ +Hi <%= @user.name %>! + +A new email was added to your account: + +email.................. <%= @email.email %> + +If this email was added in error, you can remove it here: <%= profile_emails_url %> diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml new file mode 100644 index 00000000000..b5f1e438ccb --- /dev/null +++ b/app/views/profiles/emails/index.html.haml @@ -0,0 +1,33 @@ +%h3.page-title + My email addresses +%p.light + Your + %b Primary Email + will be used for account notifications, avatar detection and web based operations, such as edits and merges. + %br + All email addresses will be used to identify your commits. + +%hr + +.ui-box + .title + Emails (#{@emails.count + 1}) + %ul.well-list#emails-table + %li + %strong= @primary + %span.label.label-success Primary Email + - @emails.each do |email| + %li + %strong= email.email + %span.cgray + added #{time_ago_with_tooltip(email.created_at)} + = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-small btn-remove pull-right' + +%h4 Add email address += form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f| + .form-group + = f.label :email, class: 'control-label' + .col-sm-10 + = f.text_field :email, class: 'form-control' + .form-actions + = f.submit 'Add', class: 'btn btn-create' diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml index d123b8f9407..218d51d31af 100644 --- a/app/views/profiles/notifications/_settings.html.haml +++ b/app/views/profiles/notifications/_settings.html.haml @@ -1,31 +1,17 @@ %li - .row - .col-sm-4 - %span - = notification_icon(notification) - - - if membership.kind_of? UsersGroup - = link_to membership.group.name, membership.group - - else - = link_to_project(membership.project) - .col-sm-8 - = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do - = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') - = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') - - = label_tag nil, class: 'radio-inline' do - = radio_button_tag :notification_level, Notification::N_GLOBAL, notification.global?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' - %span Use global setting - - = label_tag nil, class: 'radio-inline' do - = radio_button_tag :notification_level, Notification::N_DISABLED, notification.disabled?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' - %span Disabled - - = label_tag nil, class: 'radio-inline' do - = radio_button_tag :notification_level, Notification::N_PARTICIPATING, notification.participating?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' - %span Participating - - = label_tag nil, class: 'radio-inline' do - = radio_button_tag :notification_level, Notification::N_WATCH, notification.watch?, id: dom_id(membership, 'notification_level'), class: 'trigger-submit' - %span Watch + %span.notification-icon-holder + - if notification.global? + = notification_icon(@notification) + - else + = notification_icon(notification) + %span.str-truncated + - if membership.kind_of? UsersGroup + = link_to membership.group.name, membership.group + - else + = link_to_project(membership.project) + .pull-right + = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do + = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') + = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') + = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'trigger-submit' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 878d7f77430..efe9c032190 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -3,56 +3,49 @@ %p.light GitLab uses the email specified in your profile for notifications %hr -.alert.alert-info - %p - %i.icon-circle.cred - %strong Disabled - – You will not get any notifications via email - %p - %i.icon-circle.cblue - %strong Participating - – You will only receive notifications from related resources (e.g. from your commits or assigned issues) - %p - %i.icon-circle.cgreen - %strong Watch - – You will receive all notifications from projects in which you participate += form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications form-horizontal global-notifications-form' do + = hidden_field_tag :notification_type, 'global' -.row - .col-sm-4 - %h4 - = notification_icon(@notification) - Global setting - .col-sm-8 - = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do - = hidden_field_tag :notification_type, 'global' - - = label_tag nil, class: 'radio-inline' do + = label_tag :notification_level, 'Notification level', class: 'control-label' + .col-sm-10 + .radio + = label_tag nil, class: '' do = radio_button_tag :notification_level, Notification::N_DISABLED, @notification.disabled?, class: 'trigger-submit' - %span Disabled + .level-title + Disabled + %p You will not get any notifications via email - = label_tag nil, class: 'radio-inline' do + .radio + = label_tag nil, class: '' do = radio_button_tag :notification_level, Notification::N_PARTICIPATING, @notification.participating?, class: 'trigger-submit' - %span Participating + .level-title + Participating + %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) - = label_tag nil, class: 'radio-inline' do + .radio + = label_tag nil, class: '' do = radio_button_tag :notification_level, Notification::N_WATCH, @notification.watch?, class: 'trigger-submit' - %span Watch + .level-title + Watch + %p You will receive all notifications from projects in which you participate -%br -= link_to '#', class: 'js-toggle-visibility-link' do - %span.btn.btn-tiny - %i.icon-chevron-down - %span Advanced notifications settings -.js-toggle-visibility-container.hide +.clearfix %hr - %h4 Groups: - %ul.bordered-list - - @users_groups.each do |users_group| - - notification = Notification.new(users_group) - = render 'settings', type: 'group', membership: users_group, notification: notification + %p + You can also specify notification level per group or per project + %br + By default all projects and groups uses notification level set above +.row.all-notifications + .col-md-6 + %h4 Groups: + %ul.bordered-list + - @users_groups.each do |users_group| + - notification = Notification.new(users_group) + = render 'settings', type: 'group', membership: users_group, notification: notification - %h4 Projects: - %ul.bordered-list - - @users_projects.each do |users_project| - - notification = Notification.new(users_project) - = render 'settings', type: 'project', membership: users_project, notification: notification + .col-md-6 + %h4 Projects: + %ul.bordered-list + - @users_projects.each do |users_project| + - notification = Notification.new(users_project) + = render 'settings', type: 'project', membership: users_project, notification: notification diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index c8c0368d000..b72232ee36b 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -2,9 +2,9 @@ %hr = form_for @user, url: profile_password_path, method: :post, html: { class: 'form-horizontal '} do |f| %p.slead - Please set new password before proceed. + Please set a new password before proceeding. %br - After successful password update you will be redirected to login screen + After a successful password update you will be redirected to login screen. -if @user.errors.any? .alert.alert-danger %ul diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index b9ab27d212c..81e33743911 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-tabs.append-bottom-15 +%ul.nav.nav-tabs %li= render partial: 'shared/ref_switcher', locals: {destination: 'commits'} = nav_link(controller: [:commit, :commits]) do diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index c95e8178594..f725db57ad1 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -24,7 +24,7 @@ %i.icon-user Assign to .col-sm-10 - = f.select(:assignee_id, assignee_options(@issue), { include_blank: "Select a user" }, {class: 'select2'}) + = project_users_select_tag('issue[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @issue.assignee_id) = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' .form-group diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 61213e752f8..0b7697622b0 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-tabs.append-bottom-15 +%ul.nav.nav-tabs = nav_link(controller: :issues) do = link_to project_issues_path(@project), class: "tab" do Browse Issues @@ -17,10 +17,10 @@ %li.pull-right .pull-right - = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'inline issue-search-form' do + = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do .append-right-10.hidden-xs.hidden-sm = search_field_tag :issue_search, nil, { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do %i.icon-plus New Issue diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 7ddf470b6a0..029d7d6f0f4 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -6,12 +6,13 @@ - if can?(current_user, :modify_issue, @issue) = link_to profile_path(issue.assignee) do = image_tag(avatar_icon(issue.assignee.email), class: 'avatar avatar-inline s16 assignee') if issue.assignee - = f.select(:assignee_id, assignee_options(@issue), { include_blank: "Assign to user (none):" }, {class: 'select2'}) + + = project_users_select_tag('issue[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @issue.assignee_id) - elsif issue.assignee = link_to_member(@project, @issue.assignee) - .pull-right.hidden-sm + .pull-right.hidden-sm.hidden-xs - if issue.milestone - milestone = issue.milestone %cite.cgray Attached to milestone diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 87d30a4a163..020bfa36973 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,87 +1,86 @@ -.ui-box - .title +.append-bottom-10 + .check-all-holder = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" - .clearfix - .issues_bulk_update.hide - = form_tag bulk_update_project_issues_path(@project), method: :post do - %span Update selected issues with - = select_tag('update[status]', options_for_select(['open', 'closed']), prompt: "Status") - = select_tag('update[assignee_id]', bulk_update_assignee_options, prompt: "Assignee") - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") - = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :status, params[:status] - = button_tag "Save", class: "btn update_selected_issues btn-small btn-save" - .issues-filters - %span Filter by - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %i.icon-tags - %span.light labels: - - if params[:label_name].present? - %strong= params[:label_name] - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(label_name: nil) do - Any - - issue_label_names.each do |label_name| - %li - = link_to project_filter_path(label_name: label_name) do - %span{class: "label #{label_css_class(label_name)}"} - %i.icon-tag - = label_name - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %i.icon-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(assignee_id: nil) do - Any - = link_to project_filter_path(assignee_id: 0) do - Unassigned - - @project.team.members.sort_by(&:name).each do |user| - %li - = link_to project_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name + .issues-filters + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.icon-tags + %span.light labels: + - if params[:label_name].present? + %strong= params[:label_name] + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(label_name: nil) do + Any + - issue_label_names.each do |label_name| + %li + = link_to project_filter_path(label_name: label_name) do + %span{class: "label #{label_css_class(label_name)}"} + %i.icon-tag + = label_name + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.icon-user + %span.light assignee: + - if @assignee.present? + %strong= @assignee.name + - elsif params[:assignee_id] == "0" + Unassigned + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(assignee_id: nil) do + Any + = link_to project_filter_path(assignee_id: 0) do + Unassigned + - @assignees.sort_by(&:name).each do |user| + %li + = link_to project_filter_path(assignee_id: user.id) do + = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' + = user.name - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %i.icon-time - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(milestone_id: nil) do - Any - = link_to project_filter_path(milestone_id: 0) do - None (backlog) - - project_active_milestones.each do |milestone| - %li - = link_to project_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.icon-time + %span.light milestone: + - if @milestone.present? + %strong= @milestone.title + - elsif params[:milestone_id] == "0" + None (backlog) + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(milestone_id: nil) do + Any + = link_to project_filter_path(milestone_id: 0) do + None (backlog) + - project_active_milestones.each do |milestone| + %li + = link_to project_filter_path(milestone_id: milestone.id) do + %strong= milestone.title + %small.light= milestone.expires_at - .pull-right - = render 'shared/sort_dropdown' + .pull-right + = render 'shared/sort_dropdown' + .clearfix + .issues_bulk_update.hide + = form_tag bulk_update_project_issues_path(@project), method: :post do + = select_tag('update[status]', options_for_select(['Open', 'Closed']), prompt: "Status") + = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') + = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") + = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag :status, params[:status] + = button_tag "Update issues", class: "btn update_selected_issues btn-save" +.ui-box %ul.well-list.issues-list = render @issues - if @issues.blank? diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index b4ba127da25..9502ff95d8e 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -6,19 +6,22 @@ %li= msg .merge-request-branches - .row - .col-md-5 + .form-group + = label_tag nil, class: 'control-label' do + From + .col-sm-10 .clearfix .pull-left = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted? }) .pull-left = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2'}) - .mr_source_commit.prepend-top-10 - .col-md-2 - .merge-request-angle - %i.icon-long-arrow-right - .col-md-5 + .mr_source_commit + %br + .form-group + = label_tag nil, class: 'control-label' do + To + .col-sm-10 .clearfix .pull-left - projects = @project.forked_from_project.nil? ? [@project] : [ @project,@project.forked_from_project] @@ -26,7 +29,7 @@ .pull-left = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2'}) - .mr_target_commit.prepend-top-10 + .mr_target_commit %hr .merge-request-form-info @@ -47,7 +50,7 @@ %i.icon-user Assign to .col-sm-10 - = f.select(:assignee_id, assignee_options(@merge_request), { include_blank: "Select a user" }, {class: 'select2'}) + = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id) = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' .form-group diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index ff763bca307..980ac126742 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -10,14 +10,14 @@ %span.pull-right - if merge_request.for_fork? %span.light - = "#{merge_request.source_project_path}" - = "#{merge_request.source_branch}" + #{merge_request.source_project_namespace}: + = merge_request.source_branch %i.icon-angle-right.light - = "#{merge_request.target_branch}" + = merge_request.target_branch - else - = "#{merge_request.source_branch}" + = merge_request.source_branch %i.icon-angle-right.light - = "#{merge_request.target_branch}" + = merge_request.target_branch .merge-request-info - if merge_request.author authored by #{link_to_member(merge_request.source_project, merge_request.author)} diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 42641765c5c..0b9e4df3fd3 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -12,7 +12,7 @@ = render "projects/merge_requests/show/commits" - if @commits.present? - %ul.nav.nav-tabs.append-bottom-10 + %ul.nav.nav-tabs %li.notes-tab{data: {action: 'notes'}} = link_to project_merge_request_path(@project, @merge_request) do %i.icon-comment diff --git a/app/views/projects/merge_requests/branch_from.js.haml b/app/views/projects/merge_requests/branch_from.js.haml index ec4d7f2121b..d3147188d1c 100644 --- a/app/views/projects/merge_requests/branch_from.js.haml +++ b/app/views/projects/merge_requests/branch_from.js.haml @@ -1,5 +1,5 @@ :plain - $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project)}"); + $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project, false)}"); var mrTitle = $('#merge_request_title'); if(mrTitle.val().length == 0) { diff --git a/app/views/projects/merge_requests/branch_to.js.haml b/app/views/projects/merge_requests/branch_to.js.haml index f4e2886ee44..f7ede0ded53 100644 --- a/app/views/projects/merge_requests/branch_to.js.haml +++ b/app/views/projects/merge_requests/branch_to.js.haml @@ -1,2 +1,2 @@ :plain - $(".mr_target_commit").html("#{commit_to_html(@commit, @target_project)}"); + $(".mr_target_commit").html("#{commit_to_html(@commit, @target_project, false)}"); diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index a525a49015f..d45bec3a239 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -10,59 +10,57 @@ .col-md-3 = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project) .col-md-9 - .ui-box - .title - .mr-filters - %span Filter by - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %i.icon-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(assignee_id: nil) do - Any - = link_to project_filter_path(assignee_id: 0) do - Unassigned - - @project.team.members.sort_by(&:name).each do |user| - %li - = link_to project_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name + .mr-filters.append-bottom-10 + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.icon-user + %span.light assignee: + - if @assignee.present? + %strong= @assignee.name + - elsif params[:assignee_id] == "0" + Unassigned + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(assignee_id: nil) do + Any + = link_to project_filter_path(assignee_id: 0) do + Unassigned + - @assignees.sort_by(&:name).each do |user| + %li + = link_to project_filter_path(assignee_id: user.id) do + = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' + = user.name - .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %i.icon-time - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to project_filter_path(milestone_id: nil) do - Any - = link_to project_filter_path(milestone_id: 0) do - None (backlog) - - project_active_milestones.each do |milestone| - %li - = link_to project_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at + .dropdown.inline.prepend-left-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.icon-time + %span.light milestone: + - if @milestone.present? + %strong= @milestone.title + - elsif params[:milestone_id] == "0" + None (backlog) + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to project_filter_path(milestone_id: nil) do + Any + = link_to project_filter_path(milestone_id: 0) do + None (backlog) + - project_active_milestones.each do |milestone| + %li + = link_to project_filter_path(milestone_id: milestone.id) do + %strong= milestone.title + %small.light= milestone.expires_at - .pull-right - = render 'shared/sort_dropdown' + .pull-right + = render 'shared/sort_dropdown' + .ui-box %ul.well-list.mr-list = render @merge_requests - if @merge_requests.blank? diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 08a3fdf869a..7540c8a640f 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -34,7 +34,7 @@ %i.icon-edit Edit -.votes-holder +.votes-holder.hidden-sm.hidden-xs #votes= render 'votes/votes_block', votable: @merge_request .back-link diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 4d504380c93..31eb5765fab 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -48,7 +48,7 @@ = preserve do = markdown @milestone.description -%ul.nav.nav-tabs.append-bottom-10 +%ul.nav.nav-tabs %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index b03feded0a7..88c1cfa28e0 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,7 +3,7 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn' do + = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do %i.icon-download-alt %span Download zip %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } @@ -12,26 +12,26 @@ Select Archive Format %ul.dropdown-menu{ role: 'menu' } %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip') do + = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), rel: 'nofollow' do %i.icon-download-alt %span Download zip %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.gz') do + = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %i.icon-download-alt %span Download tar.gz %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.bz2') do + = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do %i.icon-download-alt %span Download tar.bz2 %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar') do + = link_to archive_project_repository_path(@project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.icon-download-alt %span Download tar - else %span.btn-group{class: btn_class} - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn' do + = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do %i.icon-download-alt %span zip - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.gz'), class: 'btn' do + = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do %i.icon-download-alt %span tar.gz
\ No newline at end of file diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 32a42916bd6..8a1e1d3354b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -51,7 +51,7 @@ %p %span.light Owned by - if @project.group - #{link_to @project.group.name, @project.group} Group + #{link_to @project.group.name, @project.group} group - else #{link_to @project.owner_name, @project.owner} diff --git a/app/views/projects/team_members/_group_members.html.haml b/app/views/projects/team_members/_group_members.html.haml index 68f08006854..eceec6627b9 100644 --- a/app/views/projects/team_members/_group_members.html.haml +++ b/app/views/projects/team_members/_group_members.html.haml @@ -1,10 +1,14 @@ +- group_users_count = @group.users_groups.count .ui-box .title %strong #{@group.name} - group members (#{@group.users_groups.count}) + group members (#{group_users_count}) .pull-right = link_to members_group_path(@group), class: 'btn btn-small' do %i.icon-edit %ul.well-list - - @group.users_groups.order('group_access DESC').each do |member| + - @group.users_groups.order('group_access DESC').limit(20).each do |member| = render 'users_groups/users_group', member: member, show_controls: false + - if group_users_count > 20 + %li + and #{group_users_count - 20} more. For full list visit #{link_to 'group members page', members_group_path(@group)} diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 5e5aa5170d6..0a7e51e974c 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-tabs.append-bottom-20 +%ul.nav.nav-tabs = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do = link_to 'Home', project_wiki_path(@project, :home) diff --git a/app/views/search/_project_results.html.haml b/app/views/search/_project_results.html.haml index ea324b3a9aa..f285bda5736 100644 --- a/app/views/search/_project_results.html.haml +++ b/app/views/search/_project_results.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-tabs.append-bottom-10 +%ul.nav.nav-tabs %li{class: ("active" if params[:search_code].present?)} = link_to search_path(params.merge(search_code: true)) do Repository Code diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 2e875669967..7b37b39780e 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,5 +1,5 @@ .dropdown.inline.prepend-left-10 - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} %span.light sort: - if @sort.present? = @sort diff --git a/bin/bundle b/bin/bundle new file mode 100644 index 00000000000..66e9889e8b4 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails new file mode 100644 index 00000000000..7feb6a30e69 --- /dev/null +++ b/bin/rails @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +APP_PATH = File.expand_path('../../config/application', __FILE__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100644 index 00000000000..8017a0271d2 --- /dev/null +++ b/bin/rake @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/rspec b/bin/rspec new file mode 100644 index 00000000000..41e37089ac2 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +require 'bundler/setup' +load Gem.bin_path('rspec', 'rspec') diff --git a/bin/spinach b/bin/spinach new file mode 100644 index 00000000000..a080e286cfe --- /dev/null +++ b/bin/spinach @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +require 'bundler/setup' +load Gem.bin_path('spinach', 'spinach') diff --git a/bin/spring b/bin/spring new file mode 100644 index 00000000000..253ec37c345 --- /dev/null +++ b/bin/spring @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast +# It gets overwritten when you run the `spring binstub` command + +unless defined?(Spring) + require "rubygems" + require "bundler" + + if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) + ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) + ENV["GEM_HOME"] = "" + Gem.paths = ENV + + gem "spring", match[1] + require "spring/binstub" + end +end diff --git a/config/routes.rb b/config/routes.rb index 8c66ad741f9..fdca1e62661 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -124,6 +124,7 @@ Gitlab::Application.routes.draw do end end resources :keys + resources :emails, only: [:index, :create, :destroy] resources :groups, only: [:index] do member do delete :leave diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb new file mode 100644 index 00000000000..cb78c4af11b --- /dev/null +++ b/db/migrate/20140209025651_create_emails.rb @@ -0,0 +1,13 @@ +class CreateEmails < ActiveRecord::Migration + def change + create_table :emails do |t| + t.integer :user_id, null: false + t.string :email, null: false + + t.timestamps + end + + add_index :emails, :user_id + add_index :emails, :email, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index acbb793bbe8..65f79fb7e05 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140127170938) do +ActiveRecord::Schema.define(version: 20140209025651) do create_table "broadcast_messages", force: true do |t| t.text "message", null: false @@ -33,6 +33,16 @@ ActiveRecord::Schema.define(version: 20140127170938) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree + create_table "emails", force: true do |t| + t.integer "user_id", null: false + t.string "email", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree + add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree + create_table "events", force: true do |t| t.string "target_type" t.integer "target_id" @@ -66,8 +76,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do t.integer "assignee_id" t.integer "author_id" t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "position", default: 0 t.string "branch_name" t.text "description" @@ -85,8 +95,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do create_table "keys", force: true do |t| t.integer "user_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "key" t.string "title" t.string "type" @@ -111,8 +121,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do t.integer "author_id" t.integer "assignee_id" t.string "title" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "milestone_id" t.string "state" t.string "merge_status" @@ -164,8 +174,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do t.text "note" t.string "noteable_type" t.integer "author_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "project_id" t.string "attachment" t.string "line_code" @@ -187,8 +197,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do t.string "name" t.string "path" t.text "description" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "creator_id" t.boolean "issues_enabled", default: true, null: false t.boolean "wall_enabled", default: true, null: false @@ -239,8 +249,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do t.text "content", limit: 2147483647 t.integer "author_id", null: false t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "file_name" t.datetime "expires_at" t.boolean "private", default: true, null: false @@ -262,42 +272,45 @@ ActiveRecord::Schema.define(version: 20140127170938) do t.datetime "created_at" end + add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree + add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree + create_table "tags", force: true do |t| t.string "name" end create_table "users", force: true do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", limit: 128, default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" t.string "last_sign_in_ip" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "name" - t.boolean "admin", default: false, null: false - t.integer "projects_limit", default: 10 - t.string "skype", default: "", null: false - t.string "linkedin", default: "", null: false - t.string "twitter", default: "", null: false + t.boolean "admin", default: false, null: false + t.integer "projects_limit", default: 10 + t.string "skype", default: "", null: false + t.string "linkedin", default: "", null: false + t.string "twitter", default: "", null: false t.string "authentication_token" - t.integer "theme_id", default: 1, null: false + t.integer "theme_id", default: 1, null: false t.string "bio" - t.integer "failed_attempts", default: 0 + t.integer "failed_attempts", default: 0 t.datetime "locked_at" t.string "extern_uid" t.string "provider" t.string "username" - t.boolean "can_create_group", default: true, null: false - t.boolean "can_create_team", default: true, null: false + t.boolean "can_create_group", default: true, null: false + t.boolean "can_create_team", default: true, null: false t.string "state" - t.integer "color_scheme_id", default: 1, null: false - t.integer "notification_level", default: 1, null: false + t.integer "color_scheme_id", default: 1, null: false + t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" t.string "avatar" @@ -305,14 +318,15 @@ ActiveRecord::Schema.define(version: 20140127170938) do t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.boolean "hide_no_ssh_key", default: false - t.string "website_url", default: "", null: false + t.boolean "hide_no_ssh_key", default: false + t.string "website_url", default: "", null: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree @@ -331,8 +345,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do create_table "users_projects", force: true do |t| t.integer "user_id", null: false t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "project_access", default: 0, null: false t.integer "notification_level", default: 3, null: false end @@ -344,8 +358,8 @@ ActiveRecord::Schema.define(version: 20140127170938) do create_table "web_hooks", force: true do |t| t.string "url" t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "type", default: "ProjectHook" t.integer "service_id" t.boolean "push_events", default: true, null: false diff --git a/doc/install/installation.md b/doc/install/installation.md index 8cee78b03d5..dd391eeb4b3 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -144,7 +144,7 @@ GitLab Shell is an ssh access and repository management software developed speci # 5. Database -To setup the MySQL/PostgreSQL database and dependencies please see [`doc/install/databases.md`](./databases.md). +To setup the MySQL/PostgreSQL database and dependencies please see [doc/install/databases.md](./databases.md). # 6. GitLab diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 62bb0f7b40a..28fc260b334 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -61,7 +61,7 @@ After making the release branch new commits are cherry-picked from master. When * 17th: feature freeze (stop merging new features in master) * 18th: UI freeze (stop merging changes to the user interface) * 19th: code freeze (stop merging non-essential code improvements) -* 20th: release candidate 1 (VERSION x.x.0.pre, tag and tweet about x.x.0.rc1) +* 20th: release candidate 1 (VERSION x.x.0.rc1, tag and tweet about x.x.0.rc1) * 21st: optional release candidate 2 (x.x.0.rc2, only if rc1 had problems) * 22nd: release (VERSION x.x.0, create x-x-stable branch, tag, blog and tweet) * 23nd: optional patch releases (x.x.1, x.x.2, etc., only if there are serious problems) @@ -73,4 +73,4 @@ After making the release branch new commits are cherry-picked from master. When * Mention what GitLab is on the second line: GitLab is open source software to collaborate on code. * Select and thank the the Most Valuable Person (MVP) of this release. -* Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. +* Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible.
\ No newline at end of file diff --git a/features/profile/emails.feature b/features/profile/emails.feature new file mode 100644 index 00000000000..148fc766081 --- /dev/null +++ b/features/profile/emails.feature @@ -0,0 +1,25 @@ +Feature: Profile Emails + Background: + Given I sign in as a user + And I visit profile emails page + + Scenario: I should see emails + Then I should see my emails + + Scenario: Add new email + Given I submit new email "my@email.com" + Then I should see new email "my@email.com" + And I should see my emails + + Scenario: Add duplicate email + Given I submit duplicate email @user.email + Then I should not have @user.email added + And I should see my emails + + Scenario: Remove email + Given I submit new email "my@email.com" + Then I should see new email "my@email.com" + And I should see my emails + Then I click link "Remove" for "my@email.com" + Then I should not see email "my@email.com" + And I should see my emails diff --git a/features/project/commits/commits_user_lookup.feature b/features/project/commits/commits_user_lookup.feature new file mode 100644 index 00000000000..f3864c0ab38 --- /dev/null +++ b/features/project/commits/commits_user_lookup.feature @@ -0,0 +1,14 @@ +Feature: Project Browse Commits User Lookup + Background: + Given I sign in as a user + And I own a project + And I have the user that authored the commits + And I visit my project's commits page + + Scenario: I browse commit from list + Given I click on commit link + Then I see commit info + + Scenario: I browse another commit from list + Given I click on another commit link + Then I see other commit info
\ No newline at end of file diff --git a/features/steps/group/group.rb b/features/steps/group/group.rb index bd59b7a12f6..81472d1ca35 100644 --- a/features/steps/group/group.rb +++ b/features/steps/group/group.rb @@ -29,6 +29,7 @@ class Groups < Spinach::FeatureSteps And 'I select user "Mary Jane" from list with role "Reporter"' do user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") + click_link 'Add members' within ".users-group-form" do select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "group_access" diff --git a/features/steps/profile/profile_emails.rb b/features/steps/profile/profile_emails.rb new file mode 100644 index 00000000000..99588c85991 --- /dev/null +++ b/features/steps/profile/profile_emails.rb @@ -0,0 +1,48 @@ +class ProfileEmails < Spinach::FeatureSteps + include SharedAuthentication + + Then 'I visit profile emails page' do + visit profile_emails_path + end + + Then 'I should see my emails' do + page.should have_content(@user.email) + @user.emails.each do |email| + page.should have_content(email.email) + end + end + + And 'I submit new email "my@email.com"' do + fill_in "email_email", with: "my@email.com" + click_button "Add" + end + + Then 'I should see new email "my@email.com"' do + email = @user.emails.find_by(email: "my@email.com") + email.should_not be_nil + page.should have_content("my@email.com") + end + + Then 'I should not see email "my@email.com"' do + email = @user.emails.find_by(email: "my@email.com") + email.should be_nil + page.should_not have_content("my@email.com") + end + + Then 'I click link "Remove" for "my@email.com"' do + # there should only be one remove button at this time + click_link "Remove" + # force these to reload as they have been cached + @user.emails.reload + end + + And 'I submit duplicate email @user.email' do + fill_in "email_email", with: @user.email + click_button "Add" + end + + Then 'I should not have @user.email added' do + email = @user.emails.find_by(email: @user.email) + email.should be_nil + end +end diff --git a/features/steps/profile/profile_notifications.rb b/features/steps/profile/profile_notifications.rb index 7a41687dfde..e884df3098e 100644 --- a/features/steps/profile/profile_notifications.rb +++ b/features/steps/profile/profile_notifications.rb @@ -8,6 +8,5 @@ class ProfileNotifications < Spinach::FeatureSteps step 'I should see global notifications settings' do page.should have_content "Notifications settings" - page.should have_content "Global setting" end end diff --git a/features/steps/project/project_browse_commits_user_lookup.rb b/features/steps/project/project_browse_commits_user_lookup.rb new file mode 100644 index 00000000000..328be373553 --- /dev/null +++ b/features/steps/project/project_browse_commits_user_lookup.rb @@ -0,0 +1,35 @@ +class ProjectBrowseCommitsUserLookup < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + Given 'I have the user that authored the commits' do + @user = create(:user, email: 'dmitriy.zaporozhets@gmail.com') + create(:email, { user: @user, email: 'dzaporozhets@sphereconsultinginc.com' }) + end + + Given 'I click on commit link' do + visit project_commit_path(@project, ValidCommit::ID) + end + + Given 'I click on another commit link' do + visit project_commit_path(@project, ValidCommitWithAltEmail::ID) + end + + Then 'I see commit info' do + page.should have_content ValidCommit::MESSAGE + check_author_link(ValidCommit::AUTHOR_EMAIL) + end + + Then 'I see other commit info' do + page.should have_content ValidCommitWithAltEmail::MESSAGE + check_author_link(ValidCommitWithAltEmail::AUTHOR_EMAIL) + end + + def check_author_link(email) + author_link = find('.commit-author-link') + author_link['href'].should == user_path(@user) + author_link['data-original-title'].should == email + find('.commit-author-name').text.should == @user.name + end +end diff --git a/features/support/env.rb b/features/support/env.rb index 0186002c559..7b11f5a7c6f 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -15,7 +15,7 @@ require 'spinach/capybara' require 'sidekiq/testing/inline' -%w(valid_commit big_commits select2_helper test_env).each do |f| +%w(valid_commit valid_commit_with_alt_email big_commits select2_helper test_env).each do |f| require Rails.root.join('spec', 'support', f) end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8f54d0d4d84..8557fa074d4 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -6,6 +6,12 @@ module API expose :is_admin?, as: :is_admin expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project + + expose :avatar_url do |user, options| + if user.avatar.present? + user.avatar.url + end + end end class UserSafe < Grape::Entity diff --git a/lib/api/internal.rb b/lib/api/internal.rb index ed6b50c3a6a..ebc9fef07b4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -35,7 +35,9 @@ module API user = key.user return false if user.blocked? - return false if user.ldap_user? && Gitlab::LDAP::User.blocked?(user.extern_uid) + if Gitlab.config.ldap.enabled + return false if user.ldap_user? && Gitlab::LDAP::User.blocked?(user.extern_uid) + end action = case git_cmd when *DOWNLOAD_COMMANDS diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 888aa7e77d2..bcca69ff49a 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -11,7 +11,7 @@ module API end not_found! end - + def map_public_to_visibility_level(attrs) publik = attrs.delete(:public) publik = [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(publik) @@ -308,6 +308,18 @@ module API projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%") present paginate(projects), with: Entities::Project end + + + # Get a users list + # + # Example Request: + # GET /users + get ':id/users' do + @users = User.where(id: user_project.team.users.map(&:id)) + @users = @users.search(params[:search]) if params[:search].present? + @users = paginate @users + present @users, with: Entities::User + end end end end diff --git a/script/background_jobs b/script/background_jobs index 623e26a2831..06125c11ffe 100755 --- a/script/background_jobs +++ b/script/background_jobs @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd $(dirname $0)/.. app_root=$(pwd) diff --git a/script/web b/script/web index 5464ed040aa..1ad3b5d24b9 100755 --- a/script/web +++ b/script/web @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd $(dirname $0)/.. app_root=$(pwd) diff --git a/spec/factories.rb b/spec/factories.rb index e5d05a4c2ea..37436e53b95 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -219,6 +219,19 @@ FactoryGirl.define do end end end + + factory :email do + user + email do + Faker::Internet.email('alias') + end + + factory :another_email do + email do + Faker::Internet.email('another.alias') + end + end + end factory :milestone do title diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index c1efc1fb2a0..dce28525ca4 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -8,7 +8,7 @@ describe NotificationsHelper do before { notification.stub(disabled?: true) } it "has a red icon" do - notification_icon(notification).should match('class="icon-circle cred"') + notification_icon(notification).should match('class="icon-volume-off cred"') end end @@ -16,7 +16,7 @@ describe NotificationsHelper do before { notification.stub(participating?: true) } it "has a blue icon" do - notification_icon(notification).should match('class="icon-circle cblue"') + notification_icon(notification).should match('class="icon-volume-down cblue"') end end @@ -24,7 +24,7 @@ describe NotificationsHelper do before { notification.stub(watch?: true) } it "has a green icon" do - notification_icon(notification).should match('class="icon-circle cgreen"') + notification_icon(notification).should match('class="icon-volume-up cgreen"') end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index d53dc17d977..88cae0bb756 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -90,6 +90,28 @@ describe Notify do end end + describe 'user added email' do + let(:email) { create(:email) } + + subject { Notify.new_email_email(email.id) } + + it 'is sent to the new user' do + should deliver_to email.user.email + end + + it 'has the correct subject' do + should have_subject /^gitlab \| Email was added to your account$/i + end + + it 'contains the new email address' do + should have_body_text /#{email.email}/ + end + + it 'includes a link to emails page' do + should have_body_text /#{profile_emails_path}/ + end + end + context 'for a project' do describe 'items that are assignable, the email' do let(:assignee) { create(:user, email: 'assignee@example.com') } diff --git a/spec/observers/email_observer_spec.rb b/spec/observers/email_observer_spec.rb new file mode 100644 index 00000000000..599b9a6ffba --- /dev/null +++ b/spec/observers/email_observer_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe EmailObserver do + let(:email) { create(:email) } + + before { subject.stub(notification: double('NotificationService').as_null_object) } + + subject { EmailObserver.instance } + + describe '#after_create' do + it 'trigger notification to send emails' do + subject.should_receive(:notification) + + subject.after_create(email) + end + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 8929a48973d..9b67cd432bc 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -183,6 +183,23 @@ describe Profiles::KeysController, "routing" do end end +# emails GET /emails(.:format) emails#index +# POST /keys(.:format) emails#create +# DELETE /keys/:id(.:format) keys#destroy +describe Profiles::EmailsController, "routing" do + it "to #index" do + get("/profile/emails").should route_to('profiles/emails#index') + end + + it "to #create" do + post("/profile/emails").should route_to('profiles/emails#create') + end + + it "to #destroy" do + delete("/profile/emails/1").should route_to('profiles/emails#destroy', id: '1') + end +end + # profile_avatar DELETE /profile/avatar(.:format) profiles/avatars#destroy describe Profiles::AvatarsController, "routing" do it "to #destroy" do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 09a5debe1dc..e378be04255 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -16,6 +16,19 @@ describe NotificationService do end end + describe 'Email' do + describe :new_email do + let(:email) { create(:email) } + + it { notification.new_email(email).should be_true } + + it 'should send email to email owner' do + Notify.should_receive(:new_email_email).with(email.id) + notification.new_email(email) + end + end + end + describe 'Notes' do context 'issue note' do let(:issue) { create(:issue, assignee: create(:user)) } diff --git a/spec/support/valid_commit.rb b/spec/support/valid_commit.rb index 8094b679e99..98bc59b573f 100644 --- a/spec/support/valid_commit.rb +++ b/spec/support/valid_commit.rb @@ -2,6 +2,7 @@ module ValidCommit ID = "8470d70da67355c9c009e4401746b1d5410af2e3" MESSAGE = "notes controller refactored" AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" + AUTHOR_EMAIL = "dmitriy.zaporozhets@gmail.com" FILES = [".foreman", ".gitignore", ".rails_footnotes", ".rspec", ".travis.yml", "CHANGELOG", "Gemfile", "Gemfile.lock", "LICENSE", "Procfile", "Procfile.production", "README.md", "Rakefile", "VERSION", "app", "config.ru", "config", "db", "doc", "lib", "log", "public", "resque.sh", "script", "spec", "vendor"] FILES_COUNT = 26 diff --git a/spec/support/valid_commit_with_alt_email.rb b/spec/support/valid_commit_with_alt_email.rb new file mode 100644 index 00000000000..d6e364c41f1 --- /dev/null +++ b/spec/support/valid_commit_with_alt_email.rb @@ -0,0 +1,6 @@ +module ValidCommitWithAltEmail + ID = "1e689bfba39525ead225eaf611948cfbe8ac34cf" + MESSAGE = "fixed notes logic" + AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" + AUTHOR_EMAIL = "dzaporozhets@sphereconsultinginc.com" +end
\ No newline at end of file |
