summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRémy Coutable <remy@rymai.me>2018-08-01 15:21:24 +0000
committerRémy Coutable <remy@rymai.me>2018-08-01 15:21:24 +0000
commit83a0db0c551236518bdec1a7ae3a1ed1d05f5aaa (patch)
treee13ad022ea223e7bde5202a31ee81169225fec99
parentea6fc714bb0306ac8ca56b5dafe4b6777aafe5fc (diff)
parent12095251c3777c5231cab97854d5dca69d31cc5d (diff)
downloadgitlab-ce-83a0db0c551236518bdec1a7ae3a1ed1d05f5aaa.tar.gz
Merge branch 'bvl-user-status-message-35463' into 'master'
Allow users to set a status Closes #35463 See merge request gitlab-org/gitlab-ce!20614
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue3
-rw-r--r--app/controllers/concerns/members_presentation.rb8
-rw-r--r--app/controllers/concerns/membership_actions.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb4
-rw-r--r--app/controllers/concerns/renders_notes.rb5
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/commit_controller.rb9
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb2
-rw-r--r--app/controllers/projects/notes_controller.rb3
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/snippets/notes_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/profiles_helper.rb4
-rw-r--r--app/helpers/users_helper.rb18
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/user_status.rb17
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/serializers/concerns/user_status_tooltip.rb19
-rw-r--r--app/serializers/user_entity.rb1
-rw-r--r--app/services/users/set_status_service.rb39
-rw-r--r--app/services/users/update_service.rb12
-rw-r--r--app/views/profiles/show.html.haml12
-rw-r--r--app/views/projects/commit/_commit_box.html.haml1
-rw-r--r--app/views/shared/members/_member.html.haml1
-rw-r--r--app/views/shared/notes/_note.html.haml4
-rw-r--r--app/views/shared/snippets/_header.html.haml1
-rw-r--r--app/views/users/show.html.haml5
-rw-r--r--changelogs/unreleased/bvl-user-status-message-35463.yml5
-rw-r--r--db/migrate/20180713092803_create_user_statuses.rb20
-rw-r--r--db/schema.rb8
-rw-r--r--doc/api/users.md79
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/api/users.rb35
-rw-r--r--lib/banzai/pipeline/emoji_pipeline.rb17
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/profiles_controller_spec.rb9
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb23
-rw-r--r--spec/factories/user_statuses.rb9
-rw-r--r--spec/features/groups/members/list_members_spec.rb14
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb27
-rw-r--r--spec/features/projects/commit/user_views_user_status_on_commit_spec.rb40
-rw-r--r--spec/features/projects/issues/user_views_issue_spec.rb18
-rw-r--r--spec/features/projects/members/group_members_spec.rb8
-rw-r--r--spec/features/projects/merge_requests/user_views_user_status_on_merge_request_spec.rb36
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb6
-rw-r--r--spec/features/projects/snippets/show_spec.rb12
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb11
-rw-r--r--spec/features/snippets/show_spec.rb8
-rw-r--r--spec/features/users/show_spec.rb8
-rw-r--r--spec/lib/banzai/pipeline/emoji_pipeline_spec.rb21
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/models/user_status_spec.rb20
-rw-r--r--spec/policies/user_policy_spec.rb4
-rw-r--r--spec/requests/api/users_spec.rb63
-rw-r--r--spec/services/users/set_status_service_spec.rb58
-rw-r--r--spec/services/users/update_service_spec.rb21
-rw-r--r--spec/support/matchers/user_status_matcher.rb13
-rw-r--r--spec/support/shared_examples/showing_user_status_shared_examples.rb11
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb11
66 files changed, 816 insertions, 18 deletions
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index ee3580895df..a621418cf72 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 62d35f6547d..49fbce75110 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -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>
diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb
index c0622516fd3..215e0bdf3cb 100644
--- a/app/controllers/concerns/members_presentation.rb
+++ b/app/controllers/concerns/members_presentation.rb
@@ -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
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 7a6a00b8e13..409e6d4c4d2 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,4 +1,5 @@
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
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index fe9a030cdf2..5127db3f5fb 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -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|
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index 36e3d76ecfe..cf04023080a 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -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
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index ef5d5e5c742..7dc51f4c357 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -30,7 +30,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))
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 56a7b766b77..6f50cbb4a36 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -100,7 +100,8 @@ class ProfilesController < Profiles::ApplicationController
:website_url,
:organization,
:preferred_language,
- :private_profile
+ :private_profile,
+ status: [:emoji, :message]
)
end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 1d1184d46d1..44b176d304e 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -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
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 7c897b2d86c..ef8159aa553 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -165,7 +165,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)
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 8e4aeec16dc..fead81dd472 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -6,7 +6,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
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 4d4c2af2415..21e2145b73b 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,4 +1,5 @@
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
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 45cef123c34..1d5d1ee5d5a 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -161,7 +161,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
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index f742d7edf83..7c03d8ce827 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -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
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index c8b4682e6dc..217da89a1fd 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -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
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 1d6d0943674..dcf18c1f751 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -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
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8766bb43cac..678fed9c414 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -159,6 +159,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
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index e7aa92e6e5c..a6a57db3002 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -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
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 8ee4203b6f5..ceea4384f91 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -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
diff --git a/app/models/note.rb b/app/models/note.rb
index fe3507adcb3..87cf7c7b2fa 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -101,7 +101,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
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 644120453cf..390bdbf838a 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -49,6 +49,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
diff --git a/app/models/user.rb b/app/models/user.rb
index 03549872924..31b49b9bf2a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -141,6 +141,8 @@ class User < ActiveRecord::Base
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
+ has_one :status, class_name: 'UserStatus'
+
#
# Validations
#
diff --git a/app/models/user_status.rb b/app/models/user_status.rb
new file mode 100644
index 00000000000..2bbb0c59ac1
--- /dev/null
+++ b/app/models/user_status.rb
@@ -0,0 +1,17 @@
+# 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
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index b5717029354..e1efd84e510 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -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
diff --git a/app/serializers/concerns/user_status_tooltip.rb b/app/serializers/concerns/user_status_tooltip.rb
new file mode 100644
index 00000000000..aa6e67e3351
--- /dev/null
+++ b/app/serializers/concerns/user_status_tooltip.rb
@@ -0,0 +1,19 @@
+# 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
diff --git a/app/serializers/user_entity.rb b/app/serializers/user_entity.rb
index 6236d66ff4a..656900bb8af 100644
--- a/app/serializers/user_entity.rb
+++ b/app/serializers/user_entity.rb
@@ -2,6 +2,7 @@
class UserEntity < API::Entities::UserBasic
include RequestAwareEntity
+ include UserStatusTooltip
expose :path do |user|
user_path(user)
diff --git a/app/services/users/set_status_service.rb b/app/services/users/set_status_service.rb
new file mode 100644
index 00000000000..89008368d5f
--- /dev/null
+++ b/app/services/users/set_status_service.rb
@@ -0,0 +1,39 @@
+# 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
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index 6dadb5a4eac..a897e4bd56a 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -7,6 +7,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
@@ -17,10 +18,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
@@ -34,6 +36,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
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index a4835584b50..e7044f722c5 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -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
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 78522393d4b..8afbbaf378f 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -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
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 46debe1f2b9..af29c0fe59e 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -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
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index f5464058bc0..84adbd444c5 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -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
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 828ec870dc0..10bfc30492a 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -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?
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 8d9e86d02c4..7a38d290915 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -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
diff --git a/changelogs/unreleased/bvl-user-status-message-35463.yml b/changelogs/unreleased/bvl-user-status-message-35463.yml
new file mode 100644
index 00000000000..c844e7ea0e4
--- /dev/null
+++ b/changelogs/unreleased/bvl-user-status-message-35463.yml
@@ -0,0 +1,5 @@
+---
+title: Users can set a status message and emoji
+merge_request: 20614
+author: niedermyer & davamr
+type: added
diff --git a/db/migrate/20180713092803_create_user_statuses.rb b/db/migrate/20180713092803_create_user_statuses.rb
new file mode 100644
index 00000000000..cbe21b89ad9
--- /dev/null
+++ b/db/migrate/20180713092803_create_user_statuses.rb
@@ -0,0 +1,20 @@
+# 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
diff --git a/db/schema.rb b/db/schema.rb
index 905786172a2..6f3433c1003 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2069,6 +2069,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
@@ -2374,6 +2381,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
diff --git a/doc/api/users.md b/doc/api/users.md
index 07f9baf06d2..a8858468cab 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -440,6 +440,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).
@@ -1167,3 +1244,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
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4be7707d3e7..f858d9fa23d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -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
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e83887b3e9e..b0811bb4aad 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -121,6 +121,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
@@ -740,6 +751,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
diff --git a/lib/banzai/pipeline/emoji_pipeline.rb b/lib/banzai/pipeline/emoji_pipeline.rb
new file mode 100644
index 00000000000..a1b522f4303
--- /dev/null
+++ b/lib/banzai/pipeline/emoji_pipeline.rb
@@ -0,0 +1,17 @@
+# 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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a767c179dea..2d700fe8773 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4081,6 +4081,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 ""
@@ -5624,6 +5627,9 @@ msgstr ""
msgid "Users"
msgstr ""
+msgid "User|Current Status"
+msgstr ""
+
msgid "Variables"
msgstr ""
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 4530a301d4d..360c536c667 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -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
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index ff1835a34c2..5b347b1109a 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -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)}" }
diff --git a/spec/factories/user_statuses.rb b/spec/factories/user_statuses.rb
new file mode 100644
index 00000000000..9998ae9609c
--- /dev/null
+++ b/spec/factories/user_statuses.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :user_status do
+ user
+ emoji 'coffee'
+ message 'I crave coffee'
+ end
+end
diff --git a/spec/features/groups/members/list_members_spec.rb b/spec/features/groups/members/list_members_spec.rb
index 33f93fcc470..e1587a8b6a5 100644
--- a/spec/features/groups/members/list_members_spec.rb
+++ b/spec/features/groups/members/list_members_spec.rb
@@ -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
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 0b5eacbe916..96bbe6f93f1 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -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
diff --git a/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb b/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb
new file mode 100644
index 00000000000..e78b7f7ae08
--- /dev/null
+++ b/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb
@@ -0,0 +1,40 @@
+# 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
diff --git a/spec/features/projects/issues/user_views_issue_spec.rb b/spec/features/projects/issues/user_views_issue_spec.rb
index 4093876c289..117e5986f29 100644
--- a/spec/features/projects/issues/user_views_issue_spec.rb
+++ b/spec/features/projects/issues/user_views_issue_spec.rb
@@ -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
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index 41b2beb40b9..0b2cd13b8ec 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -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
diff --git a/spec/features/projects/merge_requests/user_views_user_status_on_merge_request_spec.rb b/spec/features/projects/merge_requests/user_views_user_status_on_merge_request_spec.rb
new file mode 100644
index 00000000000..78d9c6c6db1
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_user_status_on_merge_request_spec.rb
@@ -0,0 +1,36 @@
+# 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
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index ecc7cf84138..a84492ea5f1 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -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
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
index 3cc797277dd..f3dc13fb52f 100644
--- a/spec/features/projects/snippets/show_spec.rb
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -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
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index 269351e55c9..1442e011d52 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -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
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index f31457db92f..3fe0b60b18f 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -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
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 207c333c636..bc07ab48c39 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -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)
diff --git a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
new file mode 100644
index 00000000000..744df3e0b96
--- /dev/null
+++ b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
@@ -0,0 +1,21 @@
+# 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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 982d24e7eab..f5e2c977104 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -20,6 +20,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) }
diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb
new file mode 100644
index 00000000000..fcc01cdae3d
--- /dev/null
+++ b/spec/models/user_status_spec.rb
@@ -0,0 +1,20 @@
+# 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
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index a7a77abc3ee..7e0a1824200 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -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
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 6a051f865aa..d48d577afa1 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -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
@@ -310,6 +331,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
@@ -1774,6 +1809,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) }
diff --git a/spec/services/users/set_status_service_spec.rb b/spec/services/users/set_status_service_spec.rb
new file mode 100644
index 00000000000..8a8458ab9de
--- /dev/null
+++ b/spec/services/users/set_status_service_spec.rb
@@ -0,0 +1,58 @@
+# 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
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index a4b7fe4674f..529c8485202 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -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
diff --git a/spec/support/matchers/user_status_matcher.rb b/spec/support/matchers/user_status_matcher.rb
new file mode 100644
index 00000000000..3cf240d874a
--- /dev/null
+++ b/spec/support/matchers/user_status_matcher.rb
@@ -0,0 +1,13 @@
+# 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
diff --git a/spec/support/shared_examples/showing_user_status_shared_examples.rb b/spec/support/shared_examples/showing_user_status_shared_examples.rb
new file mode 100644
index 00000000000..eef769de2fc
--- /dev/null
+++ b/spec/support/shared_examples/showing_user_status_shared_examples.rb
@@ -0,0 +1,11 @@
+# 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
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index fe6ad26a6f6..fa6c4ce4ac8 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -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