diff options
48 files changed, 418 insertions, 129 deletions
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index c41c57c1dcd..f161eb23795 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -97,7 +97,7 @@ $(() => { } this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([]); + cycleAnalyticsStore.setStageEvents([], stage); cycleAnalyticsStore.setActiveStage(stage); cycleAnalyticsService @@ -107,7 +107,7 @@ $(() => { }) .done((response) => { this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events); + cycleAnalyticsStore.setStageEvents(response.events, stage); }) .error(() => { this.isEmptyStage = true; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index be732971c7f..3efeb141008 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -1,4 +1,8 @@ /* eslint-disable no-param-reassign */ + +require('../lib/utils/text_utility'); +const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -34,11 +38,12 @@ }); newData.stages.forEach((item) => { - const stageName = item.title.toLowerCase(); + const stageSlug = gl.text.dasherize(item.title.toLowerCase()); item.active = false; - item.isUserAllowed = data.permissions[stageName]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; - item.component = `stage-${stageName}-component`; + item.isUserAllowed = data.permissions[stageSlug]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; + item.component = `stage-${stageSlug}-component`; + item.slug = stageSlug; }); newData.analytics = data; return newData; @@ -58,31 +63,33 @@ this.deactivateAllStages(); stage.active = true; }, - setStageEvents(events) { - this.state.events = this.decorateEvents(events); + setStageEvents(events, stage) { + this.state.events = this.decorateEvents(events, stage); }, - decorateEvents(events) { + decorateEvents(events, stage) { const newEvents = []; events.forEach((item) => { if (!item) return; - item.totalTime = item.total_time; - item.author.webUrl = item.author.web_url; - item.author.avatarUrl = item.author.avatar_url; + const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + + eventItem.totalTime = eventItem.total_time; + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; - if (item.created_at) item.createdAt = item.created_at; - if (item.short_sha) item.shortSha = item.short_sha; - if (item.commit_url) item.commitUrl = item.commit_url; + if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; + if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; + if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; - delete item.author.web_url; - delete item.author.avatar_url; - delete item.total_time; - delete item.created_at; - delete item.short_sha; - delete item.commit_url; + delete eventItem.author.web_url; + delete eventItem.author.avatar_url; + delete eventItem.total_time; + delete eventItem.created_at; + delete eventItem.short_sha; + delete eventItem.commit_url; - newEvents.push(item); + newEvents.push(eventItem); }); return newEvents; diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 new file mode 100644 index 00000000000..cfaf9835bf8 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 @@ -0,0 +1,98 @@ +module.exports = { + issue: { + created_at: '', + url: '', + iid: '', + title: '', + total_time: {}, + author: { + avatar_url: '', + id: '', + name: '', + web_url: '', + }, + }, + plan: { + title: '', + commit_url: '', + short_sha: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + code: { + title: '', + iid: '', + created_at: '', + url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + test: { + name: '', + id: '', + date: '', + url: '', + short_sha: '', + commit_url: '', + total_time: {}, + branch: { + name: '', + url: '', + }, + }, + review: { + title: '', + iid: '', + created_at: '', + url: '', + state: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + staging: { + id: '', + short_sha: '', + date: '', + url: '', + commit_url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + branch: { + name: '', + url: '', + }, + }, + production: { + title: '', + created_at: '', + url: '', + iid: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, +}; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index d9370db0cf2..326b7cb7f57 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ +require('vendor/latinise'); + (function() { (function(w) { var base; @@ -164,8 +166,14 @@ gl.text.pluralize = function(str, count) { return str + (count > 1 || count === 0 ? 's' : ''); }; - return gl.text.truncate = function(string, maxLength) { + gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; + gl.text.dasherize = function(str) { + return str.replace(/[_\s]+/g, '-'); + }; + gl.text.slugify = function(str) { + return str.trim().toLowerCase().latinise(); + }; })(window); }).call(this); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index ef99b2e92f0..75fd1394a03 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -1,14 +1,10 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ -require('vendor/latinise'); require('./breakpoints'); require('vendor/jquery.nicescroll'); ((global) => { - const dasherize = str => str.replace(/[_\s]+/g, '-'); - const slugify = str => dasherize(str.trim().toLowerCase().latinise()); - class Wikis { constructor() { this.bp = Breakpoints.get(); @@ -34,7 +30,7 @@ require('vendor/jquery.nicescroll'); if (!this.newWikiForm) return; const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - const slug = slugify(slugInput.value); + const slug = gl.text.slugify(slugInput.value); if (slug.length > 0) { const wikisPath = slugInput.getAttribute('data-wikis-path'); diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 692142c5887..1431673027f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -96,13 +96,6 @@ padding-right: 4px; } - &.ci-success_with_warnings { - - i { - color: $gl-warning; - } - } - @media (max-width: $screen-xs-max) { flex-wrap: wrap; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bb47e2a8bf7..bf6be3d516b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base before_action :authenticate_user_from_private_token! before_action :authenticate_user! before_action :validate_user_service_ticket! - before_action :reject_blocked! before_action :check_password_expiration before_action :check_2fa_requirement before_action :ldap_security_check @@ -87,22 +86,8 @@ class ApplicationController < ActionController::Base logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" end - def reject_blocked! - if current_user && current_user.blocked? - sign_out current_user - flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." - redirect_to new_user_session_path - end - end - def after_sign_in_path_for(resource) - if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked? - sign_out resource - flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it." - new_user_session_path - else - stored_location_for(:redirect) || stored_location_for(resource) || root_path - end + stored_location_for(:redirect) || stored_location_for(resource) || root_path end def after_sign_out_path_for(resource) diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb index a1ab8b99048..baf54520b9c 100644 --- a/app/controllers/explore/application_controller.rb +++ b/app/controllers/explore/application_controller.rb @@ -1,5 +1,5 @@ class Explore::ApplicationController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! layout 'explore' end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 4f273a8d4f0..0cbf3eb58a3 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -9,7 +9,7 @@ class Groups::GroupMembersController < Groups::ApplicationController @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] - @members = @group.group_members + @members = GroupMembersFinder.new(@group).execute @members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort(@sort) diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 37feff79999..87c0f8905ff 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,5 +1,5 @@ class HelpController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! layout 'help' diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb index f3759b4c0ea..6b1e64ce819 100644 --- a/app/controllers/koding_controller.rb +++ b/app/controllers/koding_controller.rb @@ -1,5 +1,5 @@ class KodingController < ApplicationController - before_action :check_integration!, :authenticate_user!, :reject_blocked! + before_action :check_integration! layout 'koding' def index diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 50ba33ed570..61686499bd3 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -1,6 +1,6 @@ class Projects::UploadsController < Projects::ApplicationController - skip_before_action :reject_blocked!, :project, - :repository, if: -> { action_name == 'show' && image_or_video? } + skip_before_action :project, :repository, + if: -> { action_name == 'show' && image_or_video? } before_action :authorize_upload_file!, only: [:create] diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 6576ebd5235..612d69cf557 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ class SearchController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked! + skip_before_action :authenticate_user! include SearchHelper diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb new file mode 100644 index 00000000000..9f2206346ce --- /dev/null +++ b/app/finders/group_members_finder.rb @@ -0,0 +1,20 @@ +class GroupMembersFinder < Projects::ApplicationController + def initialize(group) + @group = group + end + + def execute + group_members = @group.members + + return group_members unless @group.parent + + parents_members = GroupMember.non_request. + where(source_id: @group.ancestors.select(:id)). + where.not(user_id: @group.users.select(:id)) + + wheres = ["members.id IN (#{group_members.select(:id).to_sql})"] + wheres << "members.id IN (#{parents_members.select(:id).to_sql})" + + GroupMember.where(wheres.join(' OR ')) + end +end diff --git a/app/models/group.rb b/app/models/group.rb index a5b92283daa..cc6624ff4aa 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -206,7 +206,7 @@ class Group < Namespace end def members_with_parents - GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id)) + GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id)) end def users_with_parents diff --git a/app/models/member.rb b/app/models/member.rb index 26a6054e00d..d07f270b757 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -47,6 +47,7 @@ class Member < ActiveRecord::Base scope :invite, -> { where.not(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) } scope :request, -> { where.not(requested_at: nil) } + scope :non_request, -> { where(requested_at: nil) } scope :has_access, -> { active.where('access_level > 0') } diff --git a/app/models/user.rb b/app/models/user.rb index 33666b4f35b..f9245cdb82a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -167,6 +167,15 @@ class User < ActiveRecord::Base def blocked? true end + + def active_for_authentication? + false + end + + def inactive_message + "Your account has been blocked. Please contact your GitLab " \ + "administrator if you think this is an error." + end end end diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index a559d0850c4..69bf693de8d 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper expose :title + expose :legend expose :description expose :median, as: :value do |stage| diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 5405ff16bea..ad904a8708e 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -44,7 +44,7 @@ Last 90 days .stage-panel-container .panel.panel-default.stage-panel - .panel-heading + .panel-heading %nav.col-headers %ul %li.stage-header diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 1d49e9cbaf7..ef0dd0eda3c 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -16,6 +16,8 @@ .col-sm-6 .nav-controls + = link_to @environment.external_url, class: 'btn btn-default' do + = icon('external-link') = render 'projects/deployments/actions', deployment: @environment.last_deployment .terminal-container{ class: container_class } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cd685f7d0eb..41473fae4de 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -94,9 +94,8 @@ .form-group.project-visibility-level-holder = f.label :visibility_level, class: 'label-light' do Visibility Level - = link_to "(?)", help_page_path("public_access/public_access") - = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project - + = link_to icon('question-circle'), help_page_path("public_access/public_access") + = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index b11257ee0e6..73efec88bb1 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,8 +1,11 @@ +- with_label = local_assigns.fetch(:with_label, true) + .form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'control-label' do - Visibility Level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - .col-sm-10 + - if with_label + = f.label :visibility_level, class: 'control-label' do + Visibility Level + = link_to icon('question-circle'), help_page_path("public_access/public_access") + %div{ :class => ("col-sm-10" if with_label) } - if can_change_visibility_level = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) - else diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 659d4c905fc..239387fc9fa 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -19,9 +19,9 @@ %label.label.label-danger %strong Blocked - - if source.instance_of?(Group) && !@group + - if source.instance_of?(Group) && source != @group · - = link_to source.name, source, class: "member-group-link" + = link_to source.full_name, source, class: "member-group-link" .hidden-xs.cgray - if member.request? @@ -44,8 +44,9 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - if show_roles + - current_resource = @project || @group .controls.member-controls - - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) + - if show_controls && member.source == current_resource - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level diff --git a/changelogs/unreleased/1051-api-create-users-without-password.yml b/changelogs/unreleased/1051-api-create-users-without-password.yml new file mode 100644 index 00000000000..24b5a73b45c --- /dev/null +++ b/changelogs/unreleased/1051-api-create-users-without-password.yml @@ -0,0 +1,4 @@ +--- +title: Optionally make users created via the API set their password +merge_request: 8957 +author: Joost Rijneveld diff --git a/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml new file mode 100644 index 00000000000..dd4907166c4 --- /dev/null +++ b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml @@ -0,0 +1,4 @@ +--- +title: Added external environment link to web terminal view +merge_request: 8303 +author: diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml new file mode 100644 index 00000000000..703d34a5ede --- /dev/null +++ b/changelogs/unreleased/27991-success-with-warnings-caret.yml @@ -0,0 +1,4 @@ +--- +title: Fix icon colors in merge request widget mini graph +merge_request: +author: diff --git a/changelogs/unreleased/issue-newproj-layout.yml b/changelogs/unreleased/issue-newproj-layout.yml new file mode 100644 index 00000000000..d15e8b7d1e5 --- /dev/null +++ b/changelogs/unreleased/issue-newproj-layout.yml @@ -0,0 +1,4 @@ +--- +title: Removed duplicate "Visibility Level" label on New Project page +merge_request: +author: Robert Marcano diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml new file mode 100644 index 00000000000..c0c23fb6f11 --- /dev/null +++ b/changelogs/unreleased/rs-warden-blocked-users.yml @@ -0,0 +1,4 @@ +--- +title: Don't perform Devise trackable updates on blocked User records +merge_request: 8915 +author: diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml new file mode 100644 index 00000000000..bab76812a17 --- /dev/null +++ b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml @@ -0,0 +1,4 @@ +--- +title: Add index to ci_trigger_requests for commit_id +merge_request: +author: diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb new file mode 100644 index 00000000000..61e49c14fc0 --- /dev/null +++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb @@ -0,0 +1,11 @@ +class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :ci_trigger_requests, :commit_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 3fef5b82073..d71911eaf14 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: 20170206101030) do +ActiveRecord::Schema.define(version: 20170210075922) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -367,6 +367,8 @@ ActiveRecord::Schema.define(version: 20170206101030) do t.integer "commit_id" end + add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree + create_table "ci_triggers", force: :cascade do |t| t.string "token" t.integer "project_id" diff --git a/doc/api/users.md b/doc/api/users.md index fea9bdf9639..ed3469521fc 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -216,7 +216,7 @@ Parameters: ## User creation -Creates a new user. Note only administrators can create new users. +Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority). ``` POST /users @@ -225,7 +225,8 @@ POST /users Parameters: - `email` (required) - Email -- `password` (required) - Password +- `password` (optional) - Password +- `reset_password` (optional) - Send user password reset link - true or false(default) - `username` (required) - Username - `name` (required) - Name - `skype` (optional) - Skype ID diff --git a/lib/api/users.rb b/lib/api/users.rb index 4980a90f952..82ac3886ac3 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -82,7 +82,9 @@ module API end params do requires :email, type: String, desc: 'The email of the user' - requires :password, type: String, desc: 'The password of the new user' + optional :password, type: String, desc: 'The password of the new user' + optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' + at_least_one_of :password, :reset_password requires :name, type: String, desc: 'The name of the user' requires :username, type: String, desc: 'The username of the user' use :optional_attributes @@ -94,8 +96,18 @@ module API user_params = declared_params(include_missing: false) identity_attrs = user_params.slice(:provider, :extern_uid) confirm = user_params.delete(:confirm) + user = User.new(user_params.except(:extern_uid, :provider, :reset_password)) + + if user_params.delete(:reset_password) + user.attributes = { + force_random_password: true, + password_expires_at: nil, + created_by_id: current_user.id + } + user.generate_password + user.generate_reset_token + end - user = User.new(user_params.except(:extern_uid, :provider)) user.skip_confirmation! unless confirm if identity_attrs.any? diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index d1bc2055ba8..1e52b6614a1 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -13,6 +13,10 @@ module Gitlab :code end + def legend + "Related Merge Requests" + end + def description "Time until first merge request" end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index d2068fbc38f..213994988a5 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -14,6 +14,10 @@ module Gitlab :issue end + def legend + "Related Issues" + end + def description "Time before an issue gets scheduled" end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 3b4dfc6a30e..45d51d30ccc 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -14,6 +14,10 @@ module Gitlab :plan end + def legend + "Related Commits" + end + def description "Time before an issue starts implementation" end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 2a6bcc80116..9f387a02945 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -15,6 +15,10 @@ module Gitlab :production end + def legend + "Related Issues" + end + def description "From issue creation until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index fbaa3010d81..4744be834de 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -13,6 +13,10 @@ module Gitlab :review end + def legend + "Relative Merged Requests" + end + def description "Time between merge request creation and merge/close" end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 945909a4d62..3cdbe04fbaf 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -14,6 +14,10 @@ module Gitlab :staging end + def legend + "Relative Deployed Builds" + end + def description "From merge request merge until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 0079d56e0e4..e96943833bc 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -13,6 +13,10 @@ module Gitlab :test end + def legend + "Relative Builds Trigger by Commits" + end + def description "Total test time for all commits/merges" end diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index f1c8891e87a..0347e789576 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -170,68 +170,24 @@ describe Projects::UploadsController do project.team << [user, :master] end - context "when the user is blocked" do + context "when the file exists" do before do - user.block - project.team << [user, :master] - end - - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - context "when the file is an image" do - before do - allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file is not an image" do - it "redirects to the sign in page" do - go - - expect(response).to redirect_to(new_user_session_path) - end - end + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) end - context "when the file doesn't exist" do - it "redirects to the sign in page" do - go + it "responds with status 200" do + go - expect(response).to redirect_to(new_user_session_path) - end + expect(response).to have_http_status(200) end end - context "when the user isn't blocked" do - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_http_status(200) - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go + context "when the file doesn't exist" do + it "responds with status 404" do + go - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index c6f7869516e..1732b1a0081 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,6 +14,14 @@ FactoryGirl.define do admin true end + trait :blocked do + after(:build) { |user, _| user.block! } + end + + trait :external do + external true + end + trait :two_factor do two_factor_via_otp end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 2f49e89b4e4..c203e1f20c1 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -101,6 +101,22 @@ feature 'Environment', :feature do scenario 'it shows the terminal button' do expect(page).to have_terminal_button end + + context 'web terminal', :js do + before do + # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly + allow_any_instance_of(Environment).to receive(:terminals) { nil } + visit terminal_namespace_project_environment_path(project.namespace, project, environment) + end + + it 'displays a web terminal' do + expect(page).to have_selector('#terminal') + end + + it 'displays a link to the environment external url' do + expect(page).to have_link(nil, href: environment.external_url) + end + end end context 'for developer' do diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb new file mode 100644 index 00000000000..109de39b2dd --- /dev/null +++ b/spec/features/groups/members/list_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Groups members list', feature: true do + let(:user1) { create(:user, name: 'John Doe') } + let(:user2) { create(:user, name: 'Mary Jane') } + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + background do + login_as(user1) + end + + scenario 'show members from current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user2) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row.text).to include(user2.name) + end + + scenario 'show user once if member of both current group and parent' do + group.add_developer(user1) + nested_group.add_developer(user1) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row).to be_blank + end + + def first_row + page.all('ul.content-list > li')[0] + end + + def second_row + page.all('ul.content-list > li')[1] + end +end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index ab7d89306db..ae609160e18 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -32,6 +32,22 @@ feature 'Login', feature: true do end end + describe 'with a blocked account' do + it 'prevents the user from logging in' do + user = create(:user, :blocked) + + login_with(user) + + expect(page).to have_content('Your account has been blocked.') + end + + it 'does not update Devise trackable attributes' do + user = create(:user, :blocked) + + expect { login_with(user) }.not_to change { user.reload.sign_in_count } + end + end + describe 'with two-factor authentication' do def enter_code(code) fill_in 'user_otp_attempt', with: code diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb new file mode 100644 index 00000000000..b762756f9ce --- /dev/null +++ b/spec/finders/group_members_finder_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe GroupMembersFinder, '#execute' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, :access_requestable, parent: group) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:user4) { create(:user) } + + it 'returns members for top-level group' do + member1 = group.add_master(user1) + member2 = group.add_master(user2) + member3 = group.add_master(user3) + + result = described_class.new(group).execute + + expect(result.to_a).to eq([member3, member2, member1]) + end + + it 'returns members for nested group' do + group.add_master(user2) + nested_group.request_access(user4) + member1 = group.add_master(user1) + member3 = nested_group.add_master(user2) + member4 = nested_group.add_master(user3) + + result = described_class.new(nested_group).execute + + expect(result.to_a).to eq([member4, member3, member1]) + end +end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 16e2144d6a1..c720cc9f2c2 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -129,6 +129,14 @@ describe Member, models: true do it { expect(described_class.request).not_to include @accepted_request_member } end + describe '.non_request' do + it { expect(described_class.non_request).to include @master } + it { expect(described_class.non_request).to include @invited_member } + it { expect(described_class.non_request).to include @accepted_invite_member } + it { expect(described_class.non_request).not_to include @requested_member } + it { expect(described_class.non_request).to include @accepted_request_member } + end + describe '.developers' do subject { described_class.developers.to_a } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 8692f9da976..5958012672e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -190,6 +190,18 @@ describe API::Users, api: true do expect(new_user.external).to be_truthy end + it "creates user with reset password" do + post api('/users', admin), attributes_for(:user, reset_password: true).except(:password) + + expect(response).to have_http_status(201) + + user_id = json_response['id'] + new_user = User.find(user_id) + + expect(new_user).not_to eq(nil) + expect(new_user.recently_sent_password_reset?).to eq(true) + end + it "does not create user with invalid email" do post api('/users', admin), email: 'invalid email', |