Commit 59f0cb28 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-08-02

# Conflicts:
#	app/models/ci/build.rb
#	app/workers/all_queues.yml
#	config/sidekiq_queues.yml
#	db/schema.rb
#	locale/gitlab.pot
#	spec/lib/gitlab/import_export/all_models.yml

[ci skip]
parents 7a610b64 9812e5dd
......@@ -4,25 +4,16 @@ entry.
## 11.1.4 (2018-07-30)
- No changes.
## 11.1.3 (2018-07-27)
### Fixed (8 changes, 1 of them is from the community)
### Fixed (4 changes, 1 of them is from the community)
- Rework some projects table indexes around repository_storage field. !20377
- Fix navigation to First and Next discussion on MR Changes tab. !20434
- Fix showing outdated discussions on Changes tab. !20445
- Fix autosave and ESC confirmation issues for MR discussions. !20569
- Fix rendering of the context lines in MR diffs page. !20642
- Don't overflow project/group dropdown results. !20704 (gfyoung)
- Fixed IDE not opening JSON files. !20798
- Disable Gitaly timeouts when creating or restoring backups. !20810
### Performance (1 change)
- Reduces the client side memory footprint on merge requests. !20744
## 11.1.3 (2018-07-27)
- Not released.
## 11.1.2 (2018-07-26)
......
......@@ -115,6 +115,7 @@ export default {
:id="list.id + '-title'"
class="form-control"
type="text"
name="issue_title"
autocomplete="off"
/>
<project-select
......
import _ from 'underscore';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
export const DEFAULT_SIZE_CLASS = 's40';
export const IDENTICON_BG_COUNT = 7;
export function getIdenticonBackgroundClass(entityId) {
const type = (entityId % IDENTICON_BG_COUNT) + 1;
return `bg${type}`;
}
export function getIdenticonTitle(entityName) {
return getFirstCharacterCapitalized(entityName) || ' ';
}
export function renderIdenticon(entity, options = {}) {
const { sizeClass = DEFAULT_SIZE_CLASS } = options;
const bgClass = getIdenticonBackgroundClass(entity.id);
const title = getIdenticonTitle(entity.name);
return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(title)}</div>`;
}
export function renderAvatar(entity, options = {}) {
if (!entity.avatar_url) {
return renderIdenticon(entity, options);
}
const { sizeClass = DEFAULT_SIZE_CLASS } = options;
return `<img src="${_.escape(entity.avatar_url)}" class="avatar ${_.escape(sizeClass)}" />`;
}
......@@ -75,6 +75,20 @@ export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
/**
* Returns the first character capitalized
*
* If falsey, returns empty string.
*
* @param {String} text
* @return {String}
*/
export function getFirstCharacterCapitalized(text) {
return text
? text.charAt(0).toUpperCase()
: '';
}
/**
* Replaces all html tags from a string with the given replacement.
*
......
......@@ -74,6 +74,9 @@ export default {
</div>
<a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span>
<span
v-if="author.status_tooltip_html"
v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">
@{{ author.username }}
</span>
......
......@@ -113,6 +113,9 @@ export default {
{{ user.name }}
</a>
<span
v-if="user.status_tooltip_html"
v-html="user.status_tooltip_html"></span>
</template>
</section>
......
<script>
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
export default {
props: {
entityId: {
......@@ -16,26 +18,11 @@ export default {
},
},
computed: {
/**
* This method is based on app/helpers/avatars_helper.rb#project_identicon
*/
identiconStyles() {
const allowedColors = [
'#FFEBEE',
'#F3E5F5',
'#E8EAF6',
'#E3F2FD',
'#E0F2F1',
'#FBE9E7',
'#EEEEEE',
];
const backgroundColor = allowedColors[this.entityId % 7];
return `background-color: ${backgroundColor}; color: #555;`;
identiconBackgroundClass() {
return getIdenticonBackgroundClass(this.entityId);
},
identiconTitle() {
return this.entityName.charAt(0).toUpperCase();
return getIdenticonTitle(this.entityName);
},
},
};
......@@ -43,8 +30,7 @@ export default {
<template>
<div
:class="sizeClass"
:style="identiconStyles"
:class="[sizeClass, identiconBackgroundClass]"
class="avatar identicon">
{{ identiconTitle }}
</div>
......
......@@ -73,7 +73,10 @@
.identicon {
text-align: center;
vertical-align: top;
color: $identicon-fg-color;
background-color: $identicon-gray;
// Sizes
&.s16 { font-size: 12px; line-height: 1.33; }
&.s24 { font-size: 13px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; }
......@@ -86,6 +89,15 @@
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; }
// Background colors
&.bg1 { background-color: $identicon-red; }
&.bg2 { background-color: $identicon-purple; }
&.bg3 { background-color: $identicon-indigo; }
&.bg4 { background-color: $identicon-blue; }
&.bg5 { background-color: $identicon-teal; }
&.bg6 { background-color: $identicon-orange; }
&.bg7 { background-color: $identicon-gray; }
}
.avatar-container {
......
......@@ -498,6 +498,18 @@ $note-icon-gutter-width: 55px;
*/
$zen-control-color: #555;
/*
* Identicon
*/
$identicon-red: #ffebee;
$identicon-purple: #f3e5f5;
$identicon-indigo: #e8eaf6;
$identicon-blue: #e3f2fd;
$identicon-teal: #e0f2f1;
$identicon-orange: #fbe9e7;
$identicon-gray: $gray-darker;
$identicon-fg-color: #555555;
/*
* Calendar
*/
......
......@@ -2,10 +2,18 @@ module MembersPresentation
extend ActiveSupport::Concern
def present_members(members)
preload_associations(members)
Gitlab::View::Presenter::Factory.new(
members,
current_user: current_user,
presenter_class: MembersPresenter
).fabricate!
end
def preload_associations(members)
ActiveRecord::Associations::Preloader.new.preload(members, :user)
ActiveRecord::Associations::Preloader.new.preload(members, :source)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
end
end
module MembershipActions
include MembersPresentation
extend ActiveSupport::Concern
def create
......@@ -20,6 +21,7 @@ module MembershipActions
.execute(member)
.present(current_user: current_user)
present_members([member])
respond_to do |format|
format.js { render 'shared/members/update', locals: { member: member } }
end
......
......@@ -41,7 +41,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
Notes::RenderService.new(current_user).execute([@note])
prepare_notes_for_rendering([@note], noteable)
end
respond_to do |format|
......@@ -56,7 +56,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Notes::RenderService.new(current_user).execute([@note])
prepare_notes_for_rendering([@note])
end
respond_to do |format|
......
......@@ -4,6 +4,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
preload_author_status(notes)
Notes::RenderService.new(current_user).execute(notes)
notes
......@@ -28,4 +29,8 @@ module RendersNotes
notes.each {|n| n.specialize_for_first_contribution!(noteable)}
end
def preload_author_status(notes)
ActiveRecord::Associations::Preloader.new.preload(notes, { author: :status })
end
end
......@@ -32,7 +32,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
@members = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user))
@members = present_members(@members)
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user))
......
......@@ -100,7 +100,8 @@ class ProfilesController < Profiles::ApplicationController
:website_url,
:organization,
:preferred_language,
:private_profile
:private_profile,
status: [:emoji, :message]
)
end
end
......@@ -22,7 +22,9 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie!
respond_to do |format|
format.html { render }
format.html do
render
end
format.diff do
send_git_diff(@project.repository, @commit.diff_refs)
end
......@@ -124,7 +126,10 @@ class Projects::CommitController < Projects::ApplicationController
end
def commit
@noteable = @commit ||= @project.commit_by(oid: params[:id])
@noteable = @commit ||= @project.commit_by(oid: params[:id]).tap do |commit|
# preload author and their status for rendering
commit&.author&.status
end
end
def define_commit_vars
......
......@@ -169,7 +169,7 @@ class Projects::IssuesController < Projects::ApplicationController
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by
@issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
@issuable = @noteable = @issue ||= @project.issues.includes(author: :status).where(iid: params[:id]).reorder(nil).take!
@note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue)
......
......@@ -8,7 +8,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
private
def merge_request
@issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
@issuable = @merge_request ||= @project.merge_requests.includes(author: :status).find_by!(iid: params[:id])
end
def merge_request_params
......
class Projects::NotesController < Projects::ApplicationController
include RendersNotes
include NotesActions
include NotesHelper
include ToggleAwardEmoji
......@@ -53,7 +54,7 @@ class Projects::NotesController < Projects::ApplicationController
private
def render_json_with_notes_serializer
Notes::RenderService.new(current_user).execute([note])
prepare_notes_for_rendering([note])
render json: note_serializer.represent(note)
end
......
......@@ -163,7 +163,11 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def pipeline
@pipeline ||= project.pipelines.find_by!(id: params[:id]).present(current_user: current_user)
@pipeline ||= project
.pipelines
.includes(user: :status)
.find_by!(id: params[:id])
.present(current_user: current_user)
end
def commit
......
......@@ -88,7 +88,7 @@ class Projects::SnippetsController < Projects::ApplicationController
protected
def snippet
@snippet ||= @project.snippets.find(params[:id])
@snippet ||= @project.snippets.inc_relations_for_view.find(params[:id])
end
alias_method :awardable, :snippet
alias_method :spammable, :snippet
......
......@@ -9,7 +9,7 @@ class Snippets::NotesController < ApplicationController
private
def note
@note ||= snippet.notes.find(params[:id])
@note ||= snippet.notes.inc_relations_for_view.find(params[:id])
end
alias_method :awardable, :note
......
......@@ -95,7 +95,7 @@ class SnippetsController < ApplicationController
protected
def snippet
@snippet ||= PersonalSnippet.find_by(id: params[:id])
@snippet ||= PersonalSnippet.inc_relations_for_view.find_by(id: params[:id])
end
alias_method :awardable, :snippet
......
......@@ -15,22 +15,12 @@ module AvatarsHelper
end
def project_identicon(project, options = {})
allowed_colors = {
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
bg_key = (project.id % 7) + 1
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
options[:class] << " bg#{bg_key}"
content_tag(:div, class: options[:class], style: style) do
content_tag(:div, class: options[:class]) do
project.name[0, 1].upcase
end
end
......
......@@ -161,6 +161,12 @@ module IssuablesHelper
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
if status = user_status(issuable.author)
author_output << "&ensp; #{status}".html_safe
end
author_output
end
output << "&ensp;".html_safe
......
......@@ -9,4 +9,8 @@ module ProfilesHelper
end
end
end
def show_user_status_field?
Feature.enabled?(:user_status_form) || cookies[:feature_user_status_form] == 'true'
end
end
......@@ -39,6 +39,24 @@ module UsersHelper
"access:#{max_project_member_access(project)}"
end
def user_status(user)
return unless user
unless user.association(:status).loaded?
exception = RuntimeError.new("Status was not preloaded")
Gitlab::Sentry.track_exception(exception, extra: { user: user.inspect })
end
return unless user.status
content_tag :span,
class: 'user-status-emoji has-tooltip',
title: user.status.message_html,
data: { html: true, placement: 'top' } do
emoji_icon user.status.emoji
end
end
private
def get_profile_tabs
......
......@@ -8,8 +8,11 @@ module Ci
include Importable
include Gitlab::Utils::StrongMemoize
<<<<<<< HEAD
prepend EE::Ci::Build
=======
>>>>>>> upstream/master
belongs_to :project, inverse_of: :builds
belongs_to :runner
belongs_to :trigger_request
......
......@@ -105,7 +105,7 @@ class Note < ActiveRecord::Base
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do
includes(:project, :author, :updated_by, :resolved_by, :award_emoji,
includes(:project, { author: :status }, :updated_by, :resolved_by, :award_emoji,
:system_note_metadata, :note_diff_file)
end
......
......@@ -50,6 +50,7 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
scope :inc_relations_for_view, -> { includes(author: :status) }
participant :author
participant :notes_with_associations
......
......@@ -143,6 +143,8 @@ class User < ActiveRecord::Base
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
has_one :status, class_name: 'UserStatus'
#
# Validations
#
......
# frozen_string_literal: true
class UserStatus < ActiveRecord::Base
include CacheMarkdownField
self.primary_key = :user_id
DEFAULT_EMOJI = 'speech_balloon'.freeze
belongs_to :user
validates :user, presence: true
validates :emoji, inclusion: { in: Gitlab::Emoji.emojis_names }
validates :message, length: { maximum: 100 }, allow_blank: true
cache_markdown_field :message, pipeline: :emoji
end
......@@ -16,6 +16,7 @@ class UserPolicy < BasePolicy
rule { ~subject_ghost & (user_is_self | admin) }.policy do
enable :destroy_user
enable :update_user
enable :update_user_status
end
rule { default }.enable :read_user_profile
......
# frozen_string_literal: true
module UserStatusTooltip
extend ActiveSupport::Concern
include ActionView::Helpers::TagHelper
include ActionView::Context
include EmojiHelper
include UsersHelper
included do
expose :user_status_if_loaded, as: :status_tooltip_html
def user_status_if_loaded
return nil unless object.association(:status).loaded?
user_status(object)
end
end
end
......@@ -2,6 +2,7 @@
class UserEntity < API::Entities::UserBasic
include RequestAwareEntity
include UserStatusTooltip
expose :path do |user|
user_path(user)
......
......@@ -35,7 +35,7 @@ module Clusters
def check_timeout
if timeouted?
begin
app.make_errored!('Installation timeouted')
app.make_errored!('Installation timed out')
ensure
remove_installation_pod
end
......
# frozen_string_literal: true
module Users
class SetStatusService
include Gitlab::Allowable
attr_reader :current_user, :target_user, :params
def initialize(current_user, params)
@current_user, @params = current_user, params.dup
@target_user = params.delete(:user) || current_user
end
def execute
return false unless can?(current_user, :update_user_status, target_user)
if params[:emoji].present? || params[:message].present?
set_status
else
remove_status
end
end
private
def set_status
params[:emoji] = UserStatus::DEFAULT_EMOJI if params[:emoji].blank?
user_status.update(params)
end
def remove_status
UserStatus.delete(target_user.id)
end
def user_status
target_user.status || target_user.build_status
end
end
end
......@@ -8,6 +8,7 @@ module Users
def initialize(current_user, params = {})
@current_user = current_user
@user = params.delete(:user)
@status_params = params.delete(:status)
@params = params.dup
end
......@@ -18,10 +19,11 @@ module Users
assign_attributes(&block)
if @user.save(validate: validate)
if @user.save(validate: validate) && update_status
notify_success(user_exists)
else
error(@user.errors.full_messages.uniq.join('. '))
messages = @user.errors.full_messages + Array(@user.status&.errors&.full_messages)
error(messages.uniq.join('. '))
end
end
......@@ -35,6 +37,12 @@ module Users
private
def update_status
return true unless @status_params
Users::SetStatusService.new(current_user, @status_params.merge(user: @user)).execute
end
def notify_success(user_exists)
notify_new_user(@user, nil) unless user_exists
......
......@@ -30,6 +30,18 @@
- if @user.avatar?
%hr
= link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
- if show_user_status_field?
%hr
.row
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0= s_("User|Current Status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface. The message can contain emoji codes, too.")
.col-lg-8
.row
= f.fields_for :status, @user.status do |status_form|
= status_form.text_field :emoji
= status_form.text_field :message, maxlength: 100
%hr
.row
.col-lg-4.profile-settings-sidebar
......
......@@ -13,6 +13,7 @@
= author_avatar(@commit, size: 24, has_tooltip: false)
%strong
= commit_author_link(@commit, avatar: true, size: 24)
= user_status(@commit.author)
- if @commit.different_committer?
%span.light= _('Committed by')
%strong
......
......@@ -11,6 +11,7 @@
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
.user-info
= link_to user.name, user_path(user), class: 'member'
= user_status(user)
%span.cgray= user.to_reference
- if user == current_user
......
......@@ -31,7 +31,9 @@
.note-header
.note-header-info
%a{ href: user_path(note.author) }
%span.note-header-author-name= sanitize(note.author.name)
%span.note-header-author-name
= sanitize(note.author.name)
= user_status(note.author)
%span.note-headline-light
= note.author.to_reference
%span.note-headline-light
......
......@@ -8,6 +8,7 @@
Authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "d-none d-sm-inline")}
= user_status(@snippet.author)
.detail-page-header-actions
- if @snippet.project_id?
......
......@@ -40,6 +40,11 @@
.cover-title
= @user.name
- if @user.status
.cover-status
= emoji_icon(@user.status.emoji)
= markdown_field(@user.status, :message)
.cover-desc.member-date
%p
%span.middle-dot-divider
......
......@@ -126,6 +126,7 @@
- create_note_diff_file
- delete_diff_files
- detect_repository_languages
<<<<<<< HEAD
# EE-specific queues
......@@ -184,3 +185,5 @@
- rebase
- repository_update_mirror
- chat_notification
=======
>>>>>>> upstream/master
---
title: Fixed list of projects not loading in group boards
merge_request: 20955
author:
type: fixed
---
title: Users can set a status message and emoji
merge_request: 20614
author: niedermyer & davamr
type: added
---
title: Fix bug setting http headers in Files API
merge_request: 20938
author:
type: fixed
......@@ -78,6 +78,7 @@
- [create_note_diff_file, 1]
- [delete_diff_files, 1]
- [detect_repository_languages, 1]
<<<<<<< HEAD
# EE-specific queues
- [ldap_group_sync, 2]
......@@ -98,3 +99,5 @@
- geo_file_download
- geo_project_sync
- geo_repository_shard_sync
=======
>>>>>>> upstream/master
# frozen_string_literal: true
class CreateUserStatuses < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :user_statuses, id: false, primary_key: :user_id do |t|
t.references :user,
foreign_key: { on_delete: :cascade },
null: false,
primary_key: true
t.integer :cached_markdown_version, limit: 4
t.string :emoji, null: false, default: 'speech_balloon'
t.string :message, limit: 100
t.string :message_html
end
end
end
......@@ -209,7 +209,10 @@ ActiveRecord::Schema.define(version: 20180726172057) do
t.boolean "pseudonymizer_enabled", default: false, null: false
t.boolean "hide_third_party_offers", default: false, null: false
t.boolean "instance_statistics_visibility_private", default: false, null: false
<<<<<<< HEAD
t.integer "custom_project_templates_group_id"
=======
>>>>>>> upstream/master
end
create_table "approvals", force: :cascade do |t|
......@@ -1947,6 +1950,7 @@ ActiveRecord::Schema.define(version: 20180726172057) do
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
<<<<<<< HEAD
create_table "plans", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
......@@ -1958,6 +1962,8 @@ ActiveRecord::Schema.define(version: 20180726172057) do
add_index "plans", ["name"], name: "index_plans_on_name", using: :btree
=======
>>>>>>> upstream/master
create_table "programming_languages", force: :cascade do |t|
t.string "name", null: false
t.string "color", null: false
......@@ -2695,6 +2701,13 @@ ActiveRecord::Schema.define(version: 20180726172057) do
add_index "user_interacted_projects", ["project_id", "user_id"], name: "index_user_interacted_projects_on_project_id_and_user_id", unique: true, using: :btree
add_index "user_interacted_projects", ["user_id"], name: "index_user_interacted_projects_on_user_id", using: :btree
create_table "user_statuses", primary_key: "user_id", force: :cascade do |t|
t.integer "cached_markdown_version"
t.string "emoji", default: "speech_balloon", null: false
t.string "message", limit: 100
t.string "message_html"
end
create_table "user_synced_attributes_metadata", force: :cascade do |t|
t.boolean "name_synced", default: false
t.boolean "email_synced", default: false
......@@ -3066,9 +3079,14 @@ ActiveRecord::Schema.define(version: 20180726172057) do
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "push_rules", "projects", name: "fk_83b29894de", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
<<<<<<< HEAD
add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade
add_foreign_key "repository_languages", "projects", on_delete: :cascade
add_foreign_key "resource_label_events", "epics", on_delete: :cascade
=======
add_foreign_key "remote_mirrors", "projects", on_delete: :cascade
add_foreign_key "repository_languages", "projects", on_delete: :cascade
>>>>>>> upstream/master
add_foreign_key "resource_label_events", "issues", on_delete: :cascade
add_foreign_key "resource_label_events", "labels", on_delete: :nullify
add_foreign_key "resource_label_events", "merge_requests", on_delete: :cascade
......@@ -3095,6 +3113,7 @@ ActiveRecord::Schema.define(version: 20180726172057) do
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade
add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade
add_foreign_key "user_statuses", "users", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users", "application_setting_terms", column: "accepted_term_id", name: "fk_789cd90b35", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
......
......@@ -445,6 +445,83 @@ GET /user
}
```
## User status
Get the status of the currently signed in user.
```
GET /user/status
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/user/status"
```
Example response:
```json
{
"emoji":"coffee",
"message":"I crave coffee :coffee:",
"message_html": "I crave coffee <gl-emoji title=\"hot beverage\" data-name=\"coffee\" data-unicode-version=\"4.0\">☕</gl-emoji>"
}
```
## Get the status of a user
Get the status of a user.
```
GET /users/:id_or_username/status
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id_or_username` | string | yes | The id or username of the user to get a status of |
```bash
curl "https://gitlab.example.com/users/janedoe/status"
```
Example response:
```json
{
"emoji":"coffee",
"message":"I crave coffee :coffee:",
"message_html": "I crave coffee <gl-emoji title=\"hot beverage\" data-name=\"coffee\" data-unicode-version=\"4.0\">☕</gl-emoji>"
}
```
## Set user status
Set the status of the current user.
```
PUT /user/status
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `emoji` | string | no | The name of the emoji to use as status, if omitted `speech_balloon` is used. Emoji name can be one of the specified names in the [Gemojione index][gemojione-index]. |
| `message` | string | no | The message to set as a status. It can also contain emoji codes. |
When both parameters `emoji` and `message` are empty, the status will be cleared.
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "emoji=coffee" --data "emoji=I crave coffee" https://gitlab.example.com/api/v4/user/status
```
Example responses
```json
{
"emoji":"coffee",
"message":"I crave coffee",
"message_html": "I crave coffee"
}
```
## List user projects
Please refer to the [List of user projects ](projects.md#list-user-projects).
......@@ -1170,3 +1247,5 @@ Example response:
```
Please note that `last_activity_at` is deprecated, please use `last_activity_on`.
[gemojione-index]: https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json
......@@ -92,18 +92,28 @@ sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) -b v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H sh -c 'if [ -x bin/compile ]; then bin/compile; fi'
```
### 7. Start application
### 7. Update gitlab-pages to the corresponding version (skip if not using pages)
```bash
cd /home/git/gitlab-pages
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
### 8. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
### 8. Check application status
### 9. Check application status
Check if GitLab and its environment are configured correctly:
......
......@@ -62,6 +62,14 @@ module API
expose :admin?, as: :is_admin
end
class UserStatus < Grape::Entity
expose :emoji
expose :message
expose :message_html do |entity|
MarkupHelper.markdown_field(entity, :message)
end
end
class Email < Grape::Entity
expose :id, :email
end
......
......@@ -3,7 +3,11 @@ module API
module HeadersHelpers
def set_http_headers(header_data)
header_data.each do |key, value|
header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value
if value.is_a?(Enumerable)
raise ArgumentError.new("Header value should be a string")
end
header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value.to_s
end
end
end
......
......@@ -127,6 +127,17 @@ module API
present user, opts
end
desc "Get the status of a user"
params do
requires :id_or_username, type: String, desc: 'The ID or username of the user'
end
get ":id_or_username/status" do
user = find_user(params[:id_or_username])
not_found!('User') unless user && can?(current_user, :read_user, user)
present user.status || {}, with: Entities::UserStatus
end
desc 'Create a user. Available only for admins.' do
success Entities::UserPublic
end
......@@ -746,6 +757,30 @@ module API
present paginate(activities), with: Entities::UserActivity
end
desc 'Set the status of the current user' do
success Entities::UserStatus
end
params do
optional :emoji, type: String, desc: "The emoji to set on the status"
optional :message, type: String, desc: "The status message to set"
end
put "status" do
forbidden! unless can?(current_user, :update_user_status, current_user)
if ::Users::SetStatusService.new(current_user, declared_params).execute
present current_user.status, with: Entities::UserStatus
else
render_validation_error!(current_user.status)
end
end
desc 'get the status of the current user' do
success Entities::UserStatus
end
get 'status' do
present current_user.status || {}, with: Entities::UserStatus
end
end
end
end
# frozen_string_literal: true
module Banzai
module Pipeline
class EmojiPipeline < BasePipeline
# These filters will only perform sanitization of the content, preventing
# XSS, and replace emoji.
def self.filters
@filters ||= FilterArray[
Filter::HtmlEntityFilter,
Filter::SanitizationFilter,
Filter::EmojiFilter
]
end
end
end
end
......@@ -3653,11 +3653,14 @@ msgstr ""
msgid "Install Runner on Kubernetes"
msgstr ""
<<<<<<< HEAD
msgid "Instance"
msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
=======
>>>>>>> upstream/master
msgid "Instance Statistics"
msgstr ""
......@@ -4487,9 +4490,12 @@ msgstr ""
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
msgstr ""
<<<<<<< HEAD
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
=======
>>>>>>> upstream/master
msgid "Notification events"
msgstr ""
......@@ -4976,6 +4982,9 @@ msgstr ""
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr ""
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface. The message can contain emoji codes, too."
msgstr ""
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
......@@ -6861,6 +6870,9 @@ msgstr ""
msgid "Users"
msgstr ""
msgid "User|Current Status"
msgstr ""
msgid "Variables"
msgstr ""
......
......@@ -78,6 +78,15 @@ describe ProfilesController, :request_store do
expect(ldap_user.name).not_to eq('John')
expect(ldap_user.location).to eq('City, Country')
end
it 'allows setting a user status' do
sign_in(user)
put :update, user: { status: { message: 'Working hard!' } }
expect(user.reload.status.message).to eq('Working hard!')
expect(response).to have_gitlab_http_status(302)
end
end
describe 'PUT update_username' do
......
......@@ -993,6 +993,29 @@ describe Projects::IssuesController do
expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion discussion_path individual_note resolvable resolved resolved_at resolved_by resolved_by_push commit_id for_commit project_id])
end
it 'renders the author status html if there is a status' do
create(:user_status, user: discussion.author)
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
note_json = json_response.first['notes'].first
expect(note_json['author']['status_tooltip_html']).to be_present
end
it 'does not cause an extra query for the status' do
control = ActiveRecord::QueryRecorder.new do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
end
create(:user_status, user: discussion.author)
second_discussion = create(:discussion_note_on_issue, noteable: issue, project: issue.project, author: create(:user))
create(:user_status, user: second_discussion.author)
expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }
.not_to exceed_query_limit(control)
end
context 'with cross-reference system note', :request_store do
let(:new_issue) { create(:issue) }
let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
......
# frozen_string_literal: true
FactoryBot.define do
factory :user_status do
user
emoji 'coffee'
message 'I crave coffee'
end
end
require 'rails_helper'
describe 'Group Boards' do
let(:group) { create(:group) }
let!(:project) { create(:project_empty_repo, group: group) }
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
before do
sign_in(user)
end
context 'Creates a an issue', :js do
before do
visit group_boards_path(group)
end
it 'Adds an issue to the backlog' do
page.within(find('.board', match: :first)) do
issue_title = 'New Issue'
find(:css, '.issue-count-badge-add-button').click
expect(find('.board-new-issue-form')).to be_visible
fill_in 'issue_title', with: issue_title
find('.dropdown-menu-toggle').click
wait_for_requests
click_link(project.name)
click_button 'Submit issue'
expect(page).to have_content(issue_title)
end
end
end
end
......@@ -9,7 +9,7 @@ describe 'Groups > Members > List members' do
let(:nested_group) { create(:group, parent: group) }
before do
gitlab_sign_in(user1)
sign_in(user1)
end
it 'show members from current group and parent', :nested_groups do
......@@ -32,6 +32,18 @@ describe 'Groups > Members > List members' do
expect(second_row).to be_blank
end
describe 'showing status of members' do
before do
group.add_developer(user2)
end
subject { visit group_group_members_path(group) }
it_behaves_like 'showing user status' do
let(:user_with_status) { user2 }
end
end
def first_row
page.all('ul.content-list > li')[0]
end
......
......@@ -55,4 +55,31 @@ describe 'User edit profile' do
expect(page).to have_link('gravatar.com')
end
end
context 'user status' do
it 'hides user status when the feature is disabled' do
stub_feature_flags(user_status_form: false)
visit(profile_path)
expect(page).not_to have_content('Current Status')
end
it 'shows the status form when the feature is enabled' do
stub_feature_flags(user_status_form: true)
visit(profile_path)
expect(page).to have_content('Current Status')
end
it 'shows the status form when the feature is enabled by setting a cookie', :js do
stub_feature_flags(user_status_form: false)
set_cookie('feature_user_status_form', 'true')
visit(profile_path)
expect(page).to have_content('Current Status')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Project > Commit > View user status' do
include RepoHelpers
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:commit_author) { create(:user, email: sample_commit.author_email) }
before do
sign_in(user)
project.add_developer(user)
end
subject { visit(project_commit_path(project, sample_commit.id)) }
describe 'status for the commit author' do
it_behaves_like 'showing user status' do
let(:user_with_status) { commit_author }
end
end
describe 'status for a comment on the commit' do
let(:note) { create(:note, :on_commit, project: project) }
it_behaves_like 'showing user status' do
let(:user_with_status) { note.author }
end
end
describe 'status for a diff note on the commit' do
let(:note) { create(:diff_note_on_commit, project: project) }
it_behaves_like 'showing user status' do
let(:user_with_status) { note.author }
end
end
end
......@@ -29,4 +29,22 @@ describe "User views issue" do
expect(page).not_to have_link('Close issue')
end
end
describe 'user status' do
subject { visit(project_issue_path(project, issue)) }
describe 'showing status of the author of the issue' do
it_behaves_like 'showing user status' do
let(:user_with_status) { issue.author }
end
end
describe 'showing status of a user who commented on an issue', :js do
let!(:note) { create(:note, noteable: issue, project: project, author: user_with_status) }
it_behaves_like 'showing user status' do
let(:user_with_status) { create(:user) }
end
end
end
end
......@@ -87,4 +87,12 @@ describe 'Projects members' do
end
end
end
describe 'showing status of members' do
it_behaves_like 'showing user status' do
let(:user_with_status) { developer }
subject { visit project_settings_members_path(project) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Project > Merge request > View user status' do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project, author: create(:user))
end
subject { visit merge_request_path(merge_request) }
describe 'the status of the merge request author' do
it_behaves_like 'showing user status' do
let(:user_with_status) { merge_request.author }
end
end
context 'for notes', :js do
describe 'the status of the author of a note on a merge request' do
let(:note) { create(:note, noteable: merge_request, project: project, author: create(:user)) }
it_behaves_like 'showing user status' do
let(:user_with_status) { note.author }
end
end
describe 'the status of the author of a diff note on a merge request' do
let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, author: create(:user)) }
it_behaves_like 'showing user status' do
let(:user_with_status) { note.author }
end
end
end
end
......@@ -63,6 +63,12 @@ describe 'Pipeline', :js do
expect(page).to have_css('#js-tab-pipeline.active')
end
it_behaves_like 'showing user status' do
let(:user_with_status) { pipeline.user }
subject { visit project_pipeline_path(project, pipeline) }
end
describe 'pipeline graph' do
context 'when pipeline has running builds' do
it 'shows a running icon and a cancel action for the running build' do
......
......@@ -141,4 +141,16 @@ describe 'Projects > Snippets > Project snippet', :js do
end
end
end
it_behaves_like 'showing user status' do
let(:file_name) { 'ruby-style-guide.md' }
let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
let(:user_with_status) { snippet.author }
subject do
visit project_snippet_path(project, snippet)
wait_for_requests
end
end
end
......@@ -16,6 +16,8 @@ describe 'Comments on personal snippets', :js do
before do
sign_in user
visit snippet_path(snippet)
wait_for_requests
end
subject { page }
......@@ -42,6 +44,15 @@ describe 'Comments on personal snippets', :js do
expect(page).to have_selector('.note-emoji-button')
end
end
it 'shows the status of a note author' do
status = create(:user_status, user: user)
visit snippet_path(snippet)
within("#note_#{snippet_notes[0].id}") do
expect(page).to show_user_status(status)
end
end
end
context 'when submitting a note' do
......
......@@ -155,4 +155,12 @@ describe 'Snippet', :js do
end
end
end
it_behaves_like 'showing user status' do
let(:file_name) { 'popen.rb' }
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
let(:user_with_status) { snippet.author }
subject { visit snippet_path(snippet) }
end
end
......@@ -53,6 +53,14 @@ describe 'User page' do
end
end
it 'shows the status if there was one' do
create(:user_status, user: user, message: "Working hard!")
visit(user_path(user))
expect(page).to have_content("Working hard!")
end
context 'signup disabled' do
it 'shows the sign in link' do
stub_application_setting(signup_enabled: false)
......
import { TEST_HOST } from 'spec/test_constants';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
import {
DEFAULT_SIZE_CLASS,
IDENTICON_BG_COUNT,
renderAvatar,
renderIdenticon,
getIdenticonBackgroundClass,
getIdenticonTitle,
} from '~/helpers/avatar_helper';
function matchAll(str) {
return new RegExp(`^${str}$`);
}
describe('avatar_helper', () => {
describe('getIdenticonBackgroundClass', () => {
it('returns identicon bg class from id', () => {
expect(getIdenticonBackgroundClass(1)).toEqual('bg2');
});
it(`wraps around if id is bigger than ${IDENTICON_BG_COUNT}`, () => {
expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT + 4)).toEqual('bg5');
expect(getIdenticonBackgroundClass((IDENTICON_BG_COUNT * 5) + 6)).toEqual('bg7');
});
});
describe('getIdenticonTitle', () => {
it('returns identicon title from name', () => {
expect(getIdenticonTitle('Lorem')).toEqual('L');
expect(getIdenticonTitle('dolar-sit-amit')).toEqual('D');
expect(getIdenticonTitle('%-with-special-chars')).toEqual('%');
});
it('returns space if name is falsey', () => {
expect(getIdenticonTitle('')).toEqual(' ');
expect(getIdenticonTitle(null)).toEqual(' ');
});
});
describe('renderIdenticon', () => {
it('renders with the first letter as title and bg based on id', () => {
const entity = {
id: IDENTICON_BG_COUNT + 3,
name: 'Xavior',
};
const options = {
sizeClass: 's32',
};
const result = renderIdenticon(entity, options);
expect(result).toHaveClass(`identicon ${options.sizeClass} bg4`);
expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
});
it('renders with defaults, if no options are given', () => {
const entity = {
id: 1,
name: 'tanuki',
};
const result = renderIdenticon(entity);
expect(result).toHaveClass(`identicon ${DEFAULT_SIZE_CLASS} bg2`);
expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
});
});
describe('renderAvatar', () => {
it('renders an image with the avatarUrl', () => {
const avatarUrl = `${TEST_HOST}/not-real-assets/test.png`;
const result = renderAvatar({
avatar_url: avatarUrl,
});
expect(result).toBeMatchedBy('img');
expect(result).toHaveAttr('src', avatarUrl);
expect(result).toHaveClass(DEFAULT_SIZE_CLASS);
});
it('renders an identicon if no avatarUrl', () => {
const entity = {
id: 1,
name: 'walrus',
};
const options = {
sizeClass: 's16',
};
const result = renderAvatar(entity, options);
expect(result).toHaveClass(`identicon ${options.sizeClass} bg2`);
expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
});
});
});
......@@ -110,4 +110,21 @@ describe('text_utility', () => {
expect(textUtils.splitCamelCase('HelloWorld')).toBe('Hello World');
});
});
describe('getFirstCharacterCapitalized', () => {
it('returns the first character captialized, if first character is alphabetic', () => {
expect(textUtils.getFirstCharacterCapitalized('loremIpsumDolar')).toEqual('L');
expect(textUtils.getFirstCharacterCapitalized('Sit amit !')).toEqual('S');
});
it('returns the first character, if first character is non-alphabetic', () => {
expect(textUtils.getFirstCharacterCapitalized(' lorem')).toEqual(' ');
expect(textUtils.getFirstCharacterCapitalized('%#!')).toEqual('%');
});
it('returns an empty string, if string is falsey', () => {
expect(textUtils.getFirstCharacterCapitalized('')).toEqual('');
expect(textUtils.getFirstCharacterCapitalized(null)).toEqual('');
});
});
});
......@@ -98,7 +98,7 @@ let longRunningTestTimeoutHandle;
beforeEach((done) => {
longRunningTestTimeoutHandle = setTimeout(() => {
done.fail('Test is running too long!');
}, 1000);
}, 2000);
done();
});
......
......@@ -25,19 +25,12 @@ describe('IdenticonComponent', () => {
vm.$destroy();
});
describe('identiconStyles', () => {
it('should return styles attribute value with `background-color` property', () => {
describe('identiconBackgroundClass', () => {
it('should return bg class based on entityId', () => {
vm.entityId = 4;
expect(vm.identiconStyles).toBeDefined();
expect(vm.identiconStyles.indexOf('background-color: #E0F2F1;') > -1).toBeTruthy();
});
it('should return styles attribute value with `color` property', () => {
vm.entityId = 4;
expect(vm.identiconStyles).toBeDefined();
expect(vm.identiconStyles.indexOf('color: #555;') > -1).toBeTruthy();
expect(vm.identiconBackgroundClass).toBeDefined();
expect(vm.identiconBackgroundClass).toBe('bg5');
});
});
......@@ -58,7 +51,7 @@ describe('IdenticonComponent', () => {
expect(vm.$el.nodeName).toBe('DIV');
expect(vm.$el.classList.contains('identicon')).toBeTruthy();
expect(vm.$el.classList.contains('s40')).toBeTruthy();
expect(vm.$el.getAttribute('style').indexOf('background-color') > -1).toBeTruthy();
expect(vm.$el.classList.contains('bg2')).toBeTruthy();
vm.$destroy();
});
......
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Pipeline::EmojiPipeline do
def parse(text)
described_class.to_html(text, {})
end
it 'replaces emoji' do
expected_result = "Hello world #{Gitlab::Emoji.gl_emoji_tag('100')}"
expect(parse('Hello world :100:')).to eq(expected_result)
end
it 'filters out HTML tags' do
expected_result = "Hello &lt;b&gt;world&lt;/b&gt; #{Gitlab::Emoji.gl_emoji_tag('100')}"
expect(parse('Hello <b>world</b> :100:')).to eq(expected_result)
end
end
......@@ -334,9 +334,12 @@ project:
- settings
- ci_cd_settings
- import_export_upload
<<<<<<< HEAD
- vulnerability_feedback
- prometheus_alerts
- software_license_policies
=======
>>>>>>> upstream/master
- repository_languages
award_emoji:
- awardable
......
......@@ -24,6 +24,7 @@ describe User do
describe 'associations' do
it { is_expected.to have_one(:namespace) }
it { is_expected.to have_one(:status) }
it { is_expected.to have_many(:snippets).dependent(:destroy) }
it { is_expected.to have_many(:members) }
it { is_expected.to have_many(:project_members) }
......
# frozen_string_literal: true
require 'spec_helper'
describe UserStatus do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to allow_value('smirk').for(:emoji) }
it { is_expected.not_to allow_value('hello world').for(:emoji) }
it { is_expected.not_to allow_value('').for(:emoji) }
it { is_expected.to validate_length_of(:message).is_at_most(100) }
it { is_expected.to allow_value('').for(:message) }
it 'is expected to be deleted when the user is deleted' do
status = create(:user_status)
expect { status.user.destroy }.to change { described_class.count }.from(1).to(0)
end
end
......@@ -35,6 +35,10 @@ describe UserPolicy do
end
end
describe "updating a user's status" do
it_behaves_like 'changing a user', :update_user_status
end
describe "destroying a user" do
it_behaves_like 'changing a user', :destroy_user
end
......
......@@ -13,6 +13,24 @@ describe API::Files do
let(:author_email) { 'user@example.org' }
let(:author_name) { 'John Doe' }
let(:helper) do
fake_class = Class.new do
include ::API::Helpers::HeadersHelpers
attr_reader :headers
def initialize
@headers = {}
end
def header(key, value)
@headers[key] = value
end
end
fake_class.new
end
before do
project.add_developer(user)
end
......@@ -21,6 +39,18 @@ describe API::Files do
"/projects/#{project.id}/repository/files/#{file_path}"
end
context 'http headers' do
it 'converts value into string' do
helper.set_http_headers(test: 1)
expect(helper.headers).to eq({ 'X-Gitlab-Test' => '1' })
end
it 'raises exception if value is an Enumerable' do
expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError)
end
end
describe "HEAD /projects/:id/repository/files/:file_path" do
shared_examples_for 'repository files' do
it 'returns file attributes in headers' do
......
......@@ -13,6 +13,27 @@ describe API::Users do
let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 }
let(:private_user) { create(:user, private_profile: true) }
shared_examples 'rendering user status' do
it 'returns the status if there was one' do
create(:user_status, user: user)
get api(path, user)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['message']).to be_present
expect(json_response['message_html']).to be_present
expect(json_response['emoji']).to be_present
end
it 'returns an empty response if there was no status' do
get api(path, user)
expect(response).to have_gitlab_http_status(:success)
expect(json_response['message']).to be_nil
expect(json_response['emoji']).to be_nil
end
end
describe 'GET /users' do
context "when unauthenticated" do
it "returns authorization error when the `username` parameter is not passed" do
......@@ -321,6 +342,20 @@ describe API::Users do
end
end
describe 'GET /users/:id_or_username/status' do
context 'when finding the user by id' do
it_behaves_like 'rendering user status' do
let(:path) { "/users/#{user.id}/status" }
end
end
context 'when finding the user by username' do
it_behaves_like 'rendering user status' do
let(:path) { "/users/#{user.username}/status" }
end
end
end
describe "POST /users" do
before do
admin
......@@ -1807,6 +1842,34 @@ describe API::Users do
end
end
describe 'GET /user/status' do
let(:path) { '/user/status' }
it_behaves_like 'rendering user status'
end
describe 'PUT /user/status' do
it 'saves the status' do
put api('/user/status', user), { emoji: 'smirk', message: 'hello world' }
expect(response).to have_gitlab_http_status(:success)
expect(json_response['emoji']).to eq('smirk')
end
it 'renders errors when the status was invalid' do
put api('/user/status', user), { emoji: 'does not exist', message: 'hello world' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['emoji']).to be_present
end
it 'deletes the status when passing empty values' do
put api('/user/status', user)
expect(response).to have_gitlab_http_status(:success)
expect(user.reload.status).to be_nil
end
end
describe 'GET /users/:user_id/impersonation_tokens' do
let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
......
......@@ -43,7 +43,7 @@ describe Clusters::Applications::CheckInstallationProgressService do
service.execute
expect(application).to be_errored
expect(application.status_reason).to match(/\btimeouted\b/)
expect(application.status_reason).to match(/\btimed out\b/)
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Users::SetStatusService do
let(:current_user) { create(:user) }
subject(:service) { described_class.new(current_user, params) }
describe '#execute' do
context 'when when params are set' do
let(:params) { { emoji: 'taurus', message: 'a random status' } }
it 'creates a status' do
service.execute
expect(current_user.status.emoji).to eq('taurus')
expect(current_user.status.message).to eq('a random status')
end
it 'updates a status if it already existed' do
create(:user_status, user: current_user)
expect { service.execute }.not_to change { UserStatus.count }
expect(current_user.status.message).to eq('a random status')
end
context 'for another user' do
let(:target_user) { create(:user) }
let(:params) do
{ emoji: 'taurus', message: 'a random status', user: target_user }
end
context 'the current user is admin' do
let(:current_user) { create(:admin) }
it 'changes the status when the current user is allowed to do that' do
expect { service.execute }.to change { target_user.status }
end
end
it 'does not update the status if the current user is not allowed' do
expect { service.execute }.not_to change { target_user.status }
end
end
end
context 'without params' do
let(:params) { {} }
it 'deletes the status' do
status = create(:user_status, user: current_user)
expect { service.execute }
.to change { current_user.reload.status }.from(status).to(nil)
end
end
end
end
......@@ -30,6 +30,27 @@ describe Users::UpdateService do
expect(result[:message]).to eq('Username has already been taken')
end
it 'updates the status if status params were given' do
update_user(user, status: { message: "On a call" })
expect(user.status.message).to eq("On a call")
end
it 'does not delete the status if no status param was passed' do
create(:user_status, user: user, message: 'Busy!')
update_user(user, name: 'New name')
expect(user.status.message).to eq('Busy!')
end
it 'includes status error messages' do
result = update_user(user, status: { emoji: "Moo!" })
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Emoji is not included in the list")
end
def update_user(user, opts)
described_class.new(user, opts.merge(user: user)).execute
end
......
# frozen_string_literal: true
RSpec::Matchers.define :show_user_status do |status|
match do |page|
expect(page).to have_selector(".user-status-emoji[title='#{status.message}']")
# The same user status might be displayed multiple times on the page
emoji_span = page.first(".user-status-emoji[title='#{status.message}']")
page.within(emoji_span) do
expect(page).to have_emoji(status.emoji)
end
end
end
# frozen_string_literal: true
shared_examples 'showing user status' do
let!(:status) { create(:user_status, user: user_with_status, emoji: 'smirk', message: 'Authoring this object') }
it 'shows the status' do
subject
expect(page).to show_user_status(status)
end
end
......@@ -17,6 +17,13 @@ describe 'projects/merge_requests/show.html.haml' do
author: user)
end
def preload_view_requirements
# This will load the status fields of the author of the note and merge request
# to avoid queries in when rendering the view being tested.
closed_merge_request.author.status
note.author.status
end
before do
assign(:project, project)
assign(:merge_request, closed_merge_request)
......@@ -26,6 +33,8 @@ describe 'projects/merge_requests/show.html.haml' do
assign(:notes, [])
assign(:pipelines, Ci::Pipeline.none)
preload_view_requirements
allow(view).to receive_messages(current_user: user,
can?: true,
current_application_settings: Gitlab::CurrentSettings.current_application_settings)
......@@ -42,6 +51,7 @@ describe 'projects/merge_requests/show.html.haml' do
it 'does not show the "Reopen" button when the source project does not exist' do
unlink_project.execute
closed_merge_request.reload
preload_view_requirements
render
......@@ -56,6 +66,7 @@ describe 'projects/merge_requests/show.html.haml' do
forked_project.destroy
# Reload merge request so MergeRequest#source_project turns to `nil`
closed_merge_request.reload
preload_view_requirements
render
......
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