Commit 4f265c3b authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 27932-merge-request-pipelines-displays-json

* master:
  Added env external link and light web terminal spec
  Don't perform Devise trackable updates on blocked User records
  Remove orange caret icon from mr widget
  Add index to ci_trigger_requests for commit_id
  Show parent group members for nested group
  Make sure events have most properties defined
  Restore exposure of legend property for events
  Optionally make users created via the API set their password
  Removed duplicate "Visibility Level" label on New Project page
parents 487667e6 b7c5ca49
......@@ -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;
......
/* 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;
......
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: '',
},
},
};
/* 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);
/* 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');
......
......@@ -96,13 +96,6 @@
padding-right: 4px;
}
&.ci-success_with_warnings {
i {
color: $gl-warning;
}
}
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
......
......@@ -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)
......
class Explore::ApplicationController < ApplicationController
skip_before_action :authenticate_user!, :reject_blocked!
skip_before_action :authenticate_user!
layout 'explore'
end
......@@ -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)
......
class HelpController < ApplicationController
skip_before_action :authenticate_user!, :reject_blocked!
skip_before_action :authenticate_user!
layout 'help'
......
class KodingController < ApplicationController
before_action :check_integration!, :authenticate_user!, :reject_blocked!
before_action :check_integration!
layout 'koding'
def index
......
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]
......
class SearchController < ApplicationController
skip_before_action :authenticate_user!, :reject_blocked!
skip_before_action :authenticate_user!
include SearchHelper
......
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
......@@ -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
......
......@@ -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') }
......
......@@ -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
......
......@@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity
include EntityDateHelper
expose :title
expose :legend
expose :description
expose :median, as: :value do |stage|
......
......@@ -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
......
......@@ -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 }
......
......@@ -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'
......
- 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
......
......@@ -19,9 +19,9 @@
%label.label.label-danger
%strong Blocked
- if source.instance_of?(Group) && !@group
- if source.instance_of?(Group) && source != @group
&middot;
= 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
......
---
title: Optionally make users created via the API set their password
merge_request: 8957
author: Joost Rijneveld
---
title: Added external environment link to web terminal view
merge_request: 8303
author:
---
title: Fix icon colors in merge request widget mini graph
merge_request:
author:
---
title: Removed duplicate "Visibility Level" label on New Project page
merge_request:
author: Robert Marcano
---
title: Don't perform Devise trackable updates on blocked User records
merge_request: 8915
author:
---
title: Add index to ci_trigger_requests for commit_id
merge_request:
author:
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
......@@ -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"
......
......@@ -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
......
......@@ -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?
......
......@@ -13,6 +13,10 @@ module Gitlab
:code
end
def legend
"Related Merge Requests"
end
def description
"Time until first merge request"
end
......
......@@ -14,6 +14,10 @@ module Gitlab
:issue
end
def legend
"Related Issues"
end
def description
"Time before an issue gets scheduled"
end
......
......@@ -14,6 +14,10 @@ module Gitlab
:plan
end
def legend
"Related Commits"
end
def description
"Time before an issue starts implementation"
end
......
......@@ -15,6 +15,10 @@ module Gitlab
:production
end
def legend
"Related Issues"
end
def description
"From issue creation until deploy to production"
end
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
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
......@@ -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
......
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
......@@ -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 }
......
......@@ -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',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment