Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4f288fdc92
commit
b2cb8c48c5
2
Gemfile
2
Gemfile
|
@ -500,5 +500,3 @@ gem 'valid_email', '~> 0.1'
|
|||
# JSON
|
||||
gem 'json', '~> 2.3.0'
|
||||
gem 'json-schema', '~> 2.8.0'
|
||||
gem 'oj', '~> 3.10.6'
|
||||
gem 'multi_json', '~> 1.14.1'
|
||||
|
|
|
@ -687,7 +687,6 @@ GEM
|
|||
octokit (4.15.0)
|
||||
faraday (>= 0.9)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
oj (3.10.6)
|
||||
omniauth (1.9.0)
|
||||
hashie (>= 3.4.6, < 3.7.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
@ -1313,7 +1312,6 @@ DEPENDENCIES
|
|||
mimemagic (~> 0.3.2)
|
||||
mini_magick
|
||||
minitest (~> 5.11.0)
|
||||
multi_json (~> 1.14.1)
|
||||
nakayoshi_fork (~> 0.0.4)
|
||||
net-ldap
|
||||
net-ntp
|
||||
|
@ -1321,7 +1319,6 @@ DEPENDENCIES
|
|||
nokogiri (~> 1.10.9)
|
||||
oauth2 (~> 1.4)
|
||||
octokit (~> 4.15)
|
||||
oj (~> 3.10.6)
|
||||
omniauth (~> 1.8)
|
||||
omniauth-auth0 (~> 2.0.0)
|
||||
omniauth-authentiq (~> 0.3.3)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
const MARKDOWN_LINK_TEXT = {
|
||||
markdown: '[Link Title](page-slug)',
|
||||
|
@ -8,6 +9,9 @@ const MARKDOWN_LINK_TEXT = {
|
|||
org: '[[page-slug]]',
|
||||
};
|
||||
|
||||
const TRACKING_EVENT_NAME = 'view_wiki_page';
|
||||
const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/wiki_page_context/jsonschema/1-0-0';
|
||||
|
||||
export default class Wikis {
|
||||
constructor() {
|
||||
this.sidebarEl = document.querySelector('.js-wiki-sidebar');
|
||||
|
@ -57,6 +61,8 @@ export default class Wikis {
|
|||
window.onbeforeunload = null;
|
||||
});
|
||||
}
|
||||
|
||||
Wikis.trackPageView();
|
||||
}
|
||||
|
||||
handleWikiTitleChange(e) {
|
||||
|
@ -97,4 +103,17 @@ export default class Wikis {
|
|||
classList.remove('right-sidebar-expanded');
|
||||
}
|
||||
}
|
||||
|
||||
static trackPageView() {
|
||||
const wikiPageContent = document.querySelector('.js-wiki-page-content[data-tracking-context]');
|
||||
if (!wikiPageContent) return;
|
||||
|
||||
Tracking.event(document.body.dataset.page, TRACKING_EVENT_NAME, {
|
||||
label: TRACKING_EVENT_NAME,
|
||||
context: {
|
||||
schema: TRACKING_CONTEXT_SCHEMA,
|
||||
data: JSON.parse(wikiPageContent.dataset.trackingContext),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import sanitize from 'sanitize-html';
|
||||
|
||||
import UsersCache from './lib/utils/users_cache';
|
||||
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
|
||||
|
||||
|
@ -38,6 +40,7 @@ const populateUserInfo = user => {
|
|||
name: userData.name,
|
||||
location: userData.location,
|
||||
bio: userData.bio,
|
||||
bioHtml: sanitize(userData.bio_html),
|
||||
workInformation: userData.work_information,
|
||||
loaded: true,
|
||||
});
|
||||
|
|
|
@ -75,7 +75,7 @@ export default {
|
|||
<div class="gl-text-gray-700">
|
||||
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
|
||||
<icon name="profile" class="gl-text-gray-600 gl-flex-shrink-0" />
|
||||
<span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
|
||||
<span ref="bio" class="ml-1" v-html="user.bioHtml"></span>
|
||||
</div>
|
||||
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
|
||||
<icon name="work" class="gl-text-gray-600 gl-flex-shrink-0" />
|
||||
|
|
|
@ -128,10 +128,6 @@ label {
|
|||
display: inline;
|
||||
}
|
||||
|
||||
.wiki-content {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: $gl-text-color-tertiary;
|
||||
}
|
||||
|
|
|
@ -55,8 +55,14 @@ module Projects
|
|||
|
||||
def map_sort_values(sort)
|
||||
case sort
|
||||
when 'created_date'
|
||||
when 'created_date', 'created_desc'
|
||||
{ sort: 'created', sort_direction: 'DESC' }
|
||||
when 'created_asc'
|
||||
{ sort: 'created', sort_direction: 'ASC' }
|
||||
when 'updated_desc'
|
||||
{ sort: 'updated', sort_direction: 'DESC' }
|
||||
when 'updated_asc'
|
||||
{ sort: 'updated', sort_direction: 'ASC' }
|
||||
else
|
||||
{ sort: ::Jira::JqlBuilderService::DEFAULT_SORT, sort_direction: ::Jira::JqlBuilderService::DEFAULT_SORT_DIRECTION }
|
||||
end
|
||||
|
|
|
@ -128,4 +128,13 @@ module WikiHelper
|
|||
raise NotImplementedError, "Unknown wiki container type #{wiki.container.class.name}"
|
||||
end
|
||||
end
|
||||
|
||||
def wiki_page_tracking_context(page)
|
||||
{
|
||||
'wiki-format' => page.format,
|
||||
'wiki-title-size' => page.title.bytesize,
|
||||
'wiki-content-size' => page.raw_content.bytesize,
|
||||
'wiki-directory-nest-level' => page.path.scan('/').count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,6 +69,8 @@ class User < ApplicationRecord
|
|||
|
||||
MINIMUM_INACTIVE_DAYS = 180
|
||||
|
||||
ignore_column :bio, remove_with: '13.4', remove_after: '2020-09-22'
|
||||
|
||||
# Override Devise::Models::Trackable#update_tracked_fields!
|
||||
# to limit database writes to at most once every hour
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
|
@ -193,7 +195,6 @@ class User < ApplicationRecord
|
|||
validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email }
|
||||
validates :public_email, presence: true, uniqueness: true, devise_email: true, allow_blank: true
|
||||
validates :commit_email, devise_email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email }
|
||||
validates :bio, length: { maximum: 255 }, allow_blank: true
|
||||
validates :projects_limit,
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
|
||||
|
@ -228,7 +229,6 @@ class User < ApplicationRecord
|
|||
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
|
||||
before_validation :ensure_namespace_correct
|
||||
before_save :ensure_namespace_correct # in case validation is skipped
|
||||
before_save :ensure_bio_is_assigned_to_user_details, if: :bio_changed?
|
||||
after_validation :set_username_errors
|
||||
after_update :username_changed_hook, if: :saved_change_to_username?
|
||||
after_destroy :post_destroy_hook
|
||||
|
@ -280,6 +280,7 @@ class User < ApplicationRecord
|
|||
|
||||
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
||||
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
|
||||
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
|
||||
|
||||
accepts_nested_attributes_for :user_preference, update_only: true
|
||||
accepts_nested_attributes_for :user_detail, update_only: true
|
||||
|
@ -1272,13 +1273,6 @@ class User < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Temporary, will be removed when bio is fully migrated
|
||||
def ensure_bio_is_assigned_to_user_details
|
||||
return if Feature.disabled?(:migrate_bio_to_user_details, default_enabled: true)
|
||||
|
||||
user_detail.bio = bio.to_s[0...255] # bio can be NULL in users, but cannot be NULL in user_details
|
||||
end
|
||||
|
||||
def set_username_errors
|
||||
namespace_path_errors = self.errors.delete(:"namespace.path")
|
||||
self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
|
||||
|
|
|
@ -1,7 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserDetail < ApplicationRecord
|
||||
extend ::Gitlab::Utils::Override
|
||||
include CacheMarkdownField
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :job_title, length: { maximum: 200 }
|
||||
validates :bio, length: { maximum: 255 }, allow_blank: true
|
||||
|
||||
cache_markdown_field :bio
|
||||
|
||||
def bio_html
|
||||
read_attribute(:bio_html) || bio
|
||||
end
|
||||
|
||||
# For backward compatibility.
|
||||
# Older migrations (and their tests) reference the `User.migration_bot` where the `bio` attribute is set.
|
||||
# Here we disable writing the markdown cache when the `bio_html` column does not exists.
|
||||
override :invalidated_markdown_cache?
|
||||
def invalidated_markdown_cache?
|
||||
self.class.column_names.include?('bio_html') && super
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
= (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe
|
||||
|
||||
.gl-mt-3.gl-mb-3
|
||||
.md{ data: { qa_selector: 'wiki_page_content' } }
|
||||
.js-wiki-page-content.md{ data: { qa_selector: 'wiki_page_content', tracking_context: wiki_page_tracking_context(@page).to_json } }
|
||||
= render_wiki_content(@page)
|
||||
|
||||
= render 'shared/wikis/sidebar'
|
||||
|
|
|
@ -85,7 +85,8 @@
|
|||
- if @user.bio.present?
|
||||
.cover-desc.cgray
|
||||
%p.profile-user-bio
|
||||
= @user.bio
|
||||
= markdown(@user.bio_html)
|
||||
|
||||
|
||||
- unless profile_tabs.empty?
|
||||
.scrolling-tabs-container
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track wiki page views in Snowplow
|
||||
merge_request: 35784
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add support for Markdown in the user's bio
|
||||
merge_request: 35604
|
||||
author: Riccardo Padovani
|
||||
type: added
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Add oj gem for faster JSON
|
||||
merge_request: 35527
|
||||
author:
|
||||
type: performance
|
|
@ -1,5 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Explicitly set the JSON adapter used by MultiJson
|
||||
# Currently we want this to default to the existing json gem
|
||||
MultiJson.use(:json_gem)
|
|
@ -1,4 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Ensure Oj runs in json-gem compatibility mode by default
|
||||
Oj.default_options = { mode: :rails }
|
|
@ -22,6 +22,8 @@ en:
|
|||
grafana_enabled: "Grafana integration enabled"
|
||||
user/user_detail:
|
||||
job_title: 'Job title'
|
||||
user/user_detail:
|
||||
bio: 'Bio'
|
||||
views:
|
||||
pagination:
|
||||
previous: "Prev"
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddBioHtmlToUserDetails < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
# Note: bio_html is calculated from bio, the bio column is already constrained
|
||||
add_column :user_details, :bio_html, :text # rubocop:disable Migration/AddLimitToTextColumns
|
||||
add_column :user_details, :cached_markdown_version, :integer
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :user_details, :bio_html
|
||||
remove_column :user_details, :cached_markdown_version
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15640,7 +15640,9 @@ ALTER SEQUENCE public.user_custom_attributes_id_seq OWNED BY public.user_custom_
|
|||
CREATE TABLE public.user_details (
|
||||
user_id bigint NOT NULL,
|
||||
job_title character varying(200) DEFAULT ''::character varying NOT NULL,
|
||||
bio character varying(255) DEFAULT ''::character varying NOT NULL
|
||||
bio character varying(255) DEFAULT ''::character varying NOT NULL,
|
||||
bio_html text,
|
||||
cached_markdown_version integer
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.user_details_user_id_seq
|
||||
|
@ -23635,6 +23637,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200626060151
|
||||
20200626130220
|
||||
20200629192638
|
||||
20200630091656
|
||||
20200630110826
|
||||
20200701093859
|
||||
20200702123805
|
||||
|
|
|
@ -90,6 +90,7 @@ GET /users
|
|||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"bio_html": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
|
@ -129,6 +130,7 @@ GET /users
|
|||
"created_at": "2012-05-23T08:01:01Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"bio_html": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
|
@ -246,6 +248,7 @@ Parameters:
|
|||
"web_url": "http://localhost:3000/john_smith",
|
||||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"bio": null,
|
||||
"bio_html": null,
|
||||
"location": null,
|
||||
"public_email": "john@example.com",
|
||||
"skype": "",
|
||||
|
@ -281,6 +284,7 @@ Example Responses:
|
|||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"bio_html": null,
|
||||
"location": null,
|
||||
"public_email": "john@example.com",
|
||||
"skype": "",
|
||||
|
@ -500,6 +504,7 @@ GET /user
|
|||
"web_url": "http://localhost:3000/john_smith",
|
||||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"bio": null,
|
||||
"bio_html": null,
|
||||
"location": null,
|
||||
"public_email": "john@example.com",
|
||||
"skype": "",
|
||||
|
@ -549,6 +554,7 @@ GET /user
|
|||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"bio_html": null,
|
||||
"location": null,
|
||||
"public_email": "john@example.com",
|
||||
"skype": "",
|
||||
|
|
|
@ -14,6 +14,7 @@ tasks such as:
|
|||
To request access to Chatops on GitLab.com:
|
||||
|
||||
1. Log into <https://ops.gitlab.net/users/sign_in> **using the same username** as for GitLab.com (you may have to rename it).
|
||||
1. You could also use the "Sign in with" Google button to sign in, with your GitLab.com email address.
|
||||
1. Ask in the [#production](https://gitlab.slack.com/messages/production) channel for an existing member to add you to the `chatops` project in Ops. They can do it by running `/chatops run member add <username> gitlab-com/chatops --ops` command in that channel.
|
||||
|
||||
NOTE: **Note:** If you had to change your username for GitLab.com on the first step, make sure [to reflect this information](https://gitlab.com/gitlab-com/www-gitlab-com#adding-yourself-to-the-team-page) on [the team page](https://about.gitlab.com/company/team/).
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
---
|
||||
stage: Monitor
|
||||
group: APM
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Project operations
|
||||
|
||||
GitLab provides a variety of tools to help operate and maintain
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
stage: Monitor
|
||||
group: APM
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Monitor CI/CD Environment metrics
|
||||
|
||||
Once [configured](../../user/project/integrations/prometheus.md), GitLab will attempt to retrieve performance metrics for any
|
||||
environment which has had a successful deployment.
|
||||
|
||||
GitLab will automatically scan the Prometheus server for metrics from known servers like Kubernetes and NGINX, and attempt to identify individual environments. The supported metrics and scan process is detailed in our [Prometheus Metrics Library documentation](../../user/project/integrations/prometheus_library/index.md).
|
||||
|
||||
You can view the performance dashboard for an environment by [clicking on the monitoring button](../../ci/environments/index.md#monitoring-environments).
|
||||
|
||||
## Metrics dashboard visibility
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201924) in GitLab 13.0.
|
||||
|
||||
You can set the visibility of the metrics dashboard to **Only Project Members**
|
||||
or **Everyone With Access**. When set to **Everyone with Access**, the metrics
|
||||
dashboard is visible to authenticated and non-authenticated users.
|
|
@ -1270,14 +1270,6 @@ Prerequisites for embedding from a Grafana instance:
|
|||
1. In GitLab, paste the URL into a Markdown field and save. The chart will take a few moments to render.
|
||||
![GitLab Rendered Grafana Panel](img/rendered_grafana_embed_v12_5.png)
|
||||
|
||||
## Metrics dashboard visibility
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201924) in GitLab 13.0.
|
||||
|
||||
You can set the visibility of the metrics dashboard to **Only Project Members**
|
||||
or **Everyone With Access**. When set to **Everyone with Access**, the metrics
|
||||
dashboard is visible to authenticated and non-authenticated users.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
When troubleshooting issues with a managed Prometheus app, it is often useful to
|
||||
|
|
|
@ -5,7 +5,7 @@ module API
|
|||
class User < UserBasic
|
||||
include UsersHelper
|
||||
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
|
||||
expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title
|
||||
expose :bio, :bio_html, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title
|
||||
expose :work_information do |user|
|
||||
work_information(user)
|
||||
end
|
||||
|
|
|
@ -410,10 +410,14 @@ module API
|
|||
|
||||
def render_validation_error!(model)
|
||||
if model.errors.any?
|
||||
render_api_error!(model.errors.messages || '400 Bad Request', 400)
|
||||
render_api_error!(model_error_messages(model) || '400 Bad Request', 400)
|
||||
end
|
||||
end
|
||||
|
||||
def model_error_messages(model)
|
||||
model.errors.messages
|
||||
end
|
||||
|
||||
def render_spam_error!
|
||||
render_api_error!({ error: 'Spam detected' }, 400)
|
||||
end
|
||||
|
|
|
@ -11,6 +11,13 @@ module API
|
|||
|
||||
params :optional_index_params_ee do
|
||||
end
|
||||
|
||||
def model_error_messages(model)
|
||||
super.tap do |error_messages|
|
||||
# Remapping errors from nested associations.
|
||||
error_messages[:bio] = error_messages.delete(:"user_detail.bio") if error_messages.has_key?(:"user_detail.bio")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -117,6 +117,8 @@ module API
|
|||
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
|
||||
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
|
||||
|
||||
users = users.preload(:user_detail)
|
||||
|
||||
present paginate(users), options
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -1,140 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This is a GitLab-specific JSON interface. You should use this instead
|
||||
# of using `JSON` directly. This allows us to swap the adapter and handle
|
||||
# legacy issues.
|
||||
|
||||
module Gitlab
|
||||
module Json
|
||||
INVALID_LEGACY_TYPES = [String, TrueClass, FalseClass].freeze
|
||||
|
||||
class << self
|
||||
# Parse a string and convert it to a Ruby object
|
||||
#
|
||||
# @param string [String] the JSON string to convert to Ruby objects
|
||||
# @param opts [Hash] an options hash in the standard JSON gem format
|
||||
# @return [Boolean, String, Array, Hash]
|
||||
# @raise [JSON::ParserError] raised if parsing fails
|
||||
def parse(string, opts = {})
|
||||
# First we should ensure this really is a string, not some other
|
||||
# type which purports to be a string. This handles some legacy
|
||||
# usage of the JSON class.
|
||||
string = string.to_s unless string.is_a?(String)
|
||||
|
||||
legacy_mode = legacy_mode_enabled?(opts.delete(:legacy_mode))
|
||||
data = adapter_load(string, opts)
|
||||
def parse(string, *args, **named_args)
|
||||
legacy_mode = legacy_mode_enabled?(named_args.delete(:legacy_mode))
|
||||
data = adapter.parse(string, *args, **named_args)
|
||||
|
||||
handle_legacy_mode!(data) if legacy_mode
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
alias_method :parse!, :parse
|
||||
def parse!(string, *args, **named_args)
|
||||
legacy_mode = legacy_mode_enabled?(named_args.delete(:legacy_mode))
|
||||
data = adapter.parse!(string, *args, **named_args)
|
||||
|
||||
# Take a Ruby object and convert it to a string
|
||||
#
|
||||
# @param object [Boolean, String, Array, Hash, Object] depending on the adapter this can be a variety of types
|
||||
# @param opts [Hash] an options hash in the standard JSON gem format
|
||||
# @return [String]
|
||||
def dump(object, opts = {})
|
||||
adapter_dump(object, opts)
|
||||
handle_legacy_mode!(data) if legacy_mode
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
def dump(*args)
|
||||
adapter.dump(*args)
|
||||
end
|
||||
|
||||
# Legacy method used in our codebase that might just be an alias for `parse`.
|
||||
# Will be updated to use our `parse` method.
|
||||
def generate(*args)
|
||||
::JSON.generate(*args)
|
||||
adapter.generate(*args)
|
||||
end
|
||||
|
||||
# Generates a JSON string and formats it nicely.
|
||||
# Varies depending on adapter and will be updated to use our methods.
|
||||
def pretty_generate(*args)
|
||||
::JSON.pretty_generate(*args)
|
||||
adapter.pretty_generate(*args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Convert JSON string into Ruby through toggleable adapters.
|
||||
#
|
||||
# Must rescue adapter-specific errors and return `parser_error`, and
|
||||
# must also standardize the options hash to support each adapter as
|
||||
# they all take different options.
|
||||
#
|
||||
# @param string [String] the JSON string to convert to Ruby objects
|
||||
# @param opts [Hash] an options hash in the standard JSON gem format
|
||||
# @return [Boolean, String, Array, Hash]
|
||||
# @raise [JSON::ParserError]
|
||||
def adapter_load(string, opts = {})
|
||||
opts = standardize_opts(opts)
|
||||
|
||||
if enable_oj?
|
||||
Oj.load(string, opts)
|
||||
else
|
||||
::JSON.parse(string, opts)
|
||||
end
|
||||
rescue Oj::ParseError, Encoding::UndefinedConversionError => ex
|
||||
raise parser_error.new(ex)
|
||||
def adapter
|
||||
::JSON
|
||||
end
|
||||
|
||||
# Convert Ruby object to JSON string through toggleable adapters.
|
||||
#
|
||||
# @param object [Boolean, String, Array, Hash, Object] depending on the adapter this can be a variety of types
|
||||
# @param opts [Hash] an options hash in the standard JSON gem format
|
||||
# @return [String]
|
||||
def adapter_dump(thing, opts = {})
|
||||
opts = standardize_opts(opts)
|
||||
|
||||
if enable_oj?
|
||||
Oj.dump(thing, opts)
|
||||
else
|
||||
::JSON.dump(thing, opts)
|
||||
end
|
||||
end
|
||||
|
||||
# Take a JSON standard options hash and standardize it to work across adapters
|
||||
# An example of this is Oj taking :symbol_keys instead of :symbolize_names
|
||||
#
|
||||
# @param opts [Hash]
|
||||
# @return [Hash]
|
||||
def standardize_opts(opts = {})
|
||||
if enable_oj?
|
||||
opts[:mode] = :rails
|
||||
opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names]
|
||||
end
|
||||
|
||||
opts
|
||||
end
|
||||
|
||||
# The standard parser error we should be returning. Defined in a method
|
||||
# so we can potentially override it later.
|
||||
#
|
||||
# @return [JSON::ParserError]
|
||||
def parser_error
|
||||
::JSON::ParserError
|
||||
end
|
||||
|
||||
# @param [Nil, Boolean] an extracted :legacy_mode key from the opts hash
|
||||
# @return [Boolean]
|
||||
def legacy_mode_enabled?(arg_value)
|
||||
arg_value.nil? ? false : arg_value
|
||||
end
|
||||
|
||||
# If legacy mode is enabled, we need to raise an error depending on the values
|
||||
# provided in the string. This will be deprecated.
|
||||
#
|
||||
# @param data [Boolean, String, Array, Hash, Object]
|
||||
# @return [Boolean, String, Array, Hash, Object]
|
||||
# @raise [JSON::ParserError]
|
||||
def handle_legacy_mode!(data)
|
||||
return data unless Feature.enabled?(:json_wrapper_legacy_mode, default_enabled: true)
|
||||
|
||||
raise parser_error if INVALID_LEGACY_TYPES.any? { |type| data.is_a?(type) }
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
def enable_oj?
|
||||
Feature.enabled?(:oj_json, default_enabled: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ module Gitlab
|
|||
data.merge!(message)
|
||||
end
|
||||
|
||||
Gitlab::Json.dump(data) + "\n"
|
||||
data.to_json + "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10453,9 +10453,6 @@ msgstr ""
|
|||
msgid "Geo Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo nodes are paused using a command run on the node"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13735,9 +13732,6 @@ msgstr ""
|
|||
msgid "List available repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. \"1.1.1.1, 2.2.2.0/24\""
|
||||
msgstr ""
|
||||
|
||||
msgid "List settings"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22907,9 +22901,6 @@ msgstr ""
|
|||
msgid "The above settings apply to all projects with the selected compliance framework(s)."
|
||||
msgstr ""
|
||||
|
||||
msgid "The amount of seconds after which a request to get a secondary node status will time out."
|
||||
msgstr ""
|
||||
|
||||
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ RSpec.describe 'User edit profile' do
|
|||
fill_in 'user_twitter', with: 'testtwitter'
|
||||
fill_in 'user_website_url', with: 'testurl'
|
||||
fill_in 'user_location', with: 'Ukraine'
|
||||
fill_in 'user_bio', with: 'I <3 GitLab'
|
||||
fill_in 'user_bio', with: 'I <3 GitLab :tada:'
|
||||
fill_in 'user_job_title', with: 'Frontend Engineer'
|
||||
fill_in 'user_organization', with: 'GitLab'
|
||||
submit_settings
|
||||
|
@ -36,7 +36,8 @@ RSpec.describe 'User edit profile' do
|
|||
linkedin: 'testlinkedin',
|
||||
twitter: 'testtwitter',
|
||||
website_url: 'testurl',
|
||||
bio: 'I <3 GitLab',
|
||||
bio: 'I <3 GitLab :tada:',
|
||||
bio_html: '<p data-sourcepos="1:1-1:18" dir="auto">I <3 GitLab <gl-emoji title="party popper" data-name="tada" data-unicode-version="6.0">🎉</gl-emoji></p>',
|
||||
job_title: 'Frontend Engineer',
|
||||
organization: 'GitLab'
|
||||
)
|
||||
|
|
|
@ -76,27 +76,45 @@ RSpec.describe Projects::Integrations::Jira::IssuesFinder do
|
|||
expect(issues.map(&:key)).to eq(%w[TEST-1 TEST-2])
|
||||
end
|
||||
|
||||
context 'when sort by created_date' do
|
||||
context 'when sorting' do
|
||||
shared_examples 'maps sort values' do
|
||||
it do
|
||||
expect(::Jira::JqlBuilderService).to receive(:new)
|
||||
.with(jira_service.project_key, expected_sort_values)
|
||||
.and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'maps sort values' do
|
||||
let(:params) { { sort: 'created_date' } }
|
||||
|
||||
it 'maps sort correctly' do
|
||||
expect(::Jira::JqlBuilderService).to receive(:new)
|
||||
.with(jira_service.project_key, { sort: 'created', sort_direction: 'DESC' })
|
||||
.and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
let(:expected_sort_values) { { sort: 'created', sort_direction: 'DESC' } }
|
||||
end
|
||||
|
||||
context 'when sort by unknown_sort' do
|
||||
it_behaves_like 'maps sort values' do
|
||||
let(:params) { { sort: 'created_desc' } }
|
||||
let(:expected_sort_values) { { sort: 'created', sort_direction: 'DESC' } }
|
||||
end
|
||||
|
||||
it_behaves_like 'maps sort values' do
|
||||
let(:params) { { sort: 'created_asc' } }
|
||||
let(:expected_sort_values) { { sort: 'created', sort_direction: 'ASC' } }
|
||||
end
|
||||
|
||||
it_behaves_like 'maps sort values' do
|
||||
let(:params) { { sort: 'updated_desc' } }
|
||||
let(:expected_sort_values) { { sort: 'updated', sort_direction: 'DESC' } }
|
||||
end
|
||||
|
||||
it_behaves_like 'maps sort values' do
|
||||
let(:params) { { sort: 'updated_asc' } }
|
||||
let(:expected_sort_values) { { sort: 'updated', sort_direction: 'ASC' } }
|
||||
end
|
||||
|
||||
it_behaves_like 'maps sort values' do
|
||||
let(:params) { { sort: 'unknown_sort' } }
|
||||
|
||||
it 'maps sort to default' do
|
||||
expect(::Jira::JqlBuilderService).to receive(:new)
|
||||
.with(jira_service.project_key, { sort: 'created', sort_direction: 'DESC' })
|
||||
.and_call_original
|
||||
|
||||
subject
|
||||
let(:expected_sort_values) { { sort: 'created', sort_direction: 'DESC' } }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -83,9 +83,10 @@ describe('User Popover Component', () => {
|
|||
describe('job data', () => {
|
||||
const findWorkInformation = () => wrapper.find({ ref: 'workInformation' });
|
||||
const findBio = () => wrapper.find({ ref: 'bio' });
|
||||
const bio = 'My super interesting bio';
|
||||
|
||||
it('should show only bio if work information is not available', () => {
|
||||
const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio' };
|
||||
const user = { ...DEFAULT_PROPS.user, bio, bioHtml: bio };
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
|
@ -107,7 +108,8 @@ describe('User Popover Component', () => {
|
|||
it('should display bio and work information in separate lines', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
bio: 'My super interesting bio',
|
||||
bio,
|
||||
bioHtml: bio,
|
||||
workInformation: 'Frontend Engineer at GitLab',
|
||||
};
|
||||
|
||||
|
@ -120,12 +122,13 @@ describe('User Popover Component', () => {
|
|||
it('should not encode special characters in bio', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
bio: 'I like <html> & CSS',
|
||||
bio: 'I like CSS',
|
||||
bioHtml: 'I like <b>CSS</b>',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(findBio().text()).toBe('I like <html> & CSS');
|
||||
expect(findBio().html()).toContain('I like <b>CSS</b>');
|
||||
});
|
||||
|
||||
it('shows icon for bio', () => {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { escape } from 'lodash';
|
||||
import Wikis from '~/pages/shared/wikis/wikis';
|
||||
import Tracking from '~/tracking';
|
||||
import { setHTMLFixture } from './helpers/fixtures';
|
||||
|
||||
describe('Wikis', () => {
|
||||
|
@ -122,4 +124,32 @@ describe('Wikis', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackPageView', () => {
|
||||
const trackingPage = 'projects:wikis:show';
|
||||
const trackingContext = { foo: 'bar' };
|
||||
const showPageHtmlFixture = `
|
||||
<div class="js-wiki-page-content" data-tracking-context="${escape(
|
||||
JSON.stringify(trackingContext),
|
||||
)}"></div>
|
||||
`;
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(showPageHtmlFixture);
|
||||
document.body.dataset.page = trackingPage;
|
||||
jest.spyOn(Tracking, 'event').mockImplementation();
|
||||
|
||||
Wikis.trackPageView();
|
||||
});
|
||||
|
||||
it('sends the tracking event and context', () => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(trackingPage, 'view_wiki_page', {
|
||||
label: 'view_wiki_page',
|
||||
context: {
|
||||
schema: 'iglu:com.gitlab/wiki_page_context/jsonschema/1-0-0',
|
||||
data: trackingContext,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -104,4 +104,24 @@ RSpec.describe WikiHelper do
|
|||
expect(helper.wiki_sort_title('unknown')).to eq('Title')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#wiki_page_tracking_context' do
|
||||
let_it_be(:page) { create(:wiki_page, title: 'path/to/page 💩', content: '💩', format: :markdown) }
|
||||
|
||||
subject { helper.wiki_page_tracking_context(page) }
|
||||
|
||||
it 'returns the tracking context' do
|
||||
expect(subject).to eq(
|
||||
'wiki-format' => :markdown,
|
||||
'wiki-title-size' => 9,
|
||||
'wiki-content-size' => 4,
|
||||
'wiki-directory-nest-level' => 2
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns a nest level of zero for toplevel files' do
|
||||
expect(page).to receive(:path).and_return('page')
|
||||
expect(subject).to include('wiki-directory-nest-level' => 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
|
||||
describe '#parse!' do
|
||||
|
@ -108,7 +108,7 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
|
|||
it "sets error_message" do
|
||||
expect { subject }.not_to raise_error
|
||||
|
||||
expect(accessibility_report.error_message).to include('JSON parsing failed')
|
||||
expect(accessibility_report.error_message).to include('Pa11y parsing failed')
|
||||
expect(accessibility_report.errors_count).to eq(0)
|
||||
expect(accessibility_report.passes_count).to eq(0)
|
||||
expect(accessibility_report.scans_count).to eq(0)
|
||||
|
|
|
@ -30,7 +30,7 @@ RSpec.describe Gitlab::PhabricatorImport::Conduit::Response do
|
|||
body: 'This is no JSON')
|
||||
|
||||
expect { described_class.parse!(fake_response) }
|
||||
.to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /unexpected character/)
|
||||
.to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /unexpected token at/)
|
||||
end
|
||||
|
||||
it 'returns a parsed response for valid input' do
|
||||
|
|
|
@ -6,9 +6,38 @@ RSpec.describe UserDetail do
|
|||
it { is_expected.to belong_to(:user) }
|
||||
|
||||
describe 'validations' do
|
||||
describe 'job_title' do
|
||||
describe '#job_title' do
|
||||
it { is_expected.not_to validate_presence_of(:job_title) }
|
||||
it { is_expected.to validate_length_of(:job_title).is_at_most(200) }
|
||||
end
|
||||
|
||||
describe '#bio' do
|
||||
it { is_expected.to validate_length_of(:bio).is_at_most(255) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bio_html' do
|
||||
let(:user) { create(:user, bio: 'some **bio**') }
|
||||
|
||||
subject { user.user_detail.bio_html }
|
||||
|
||||
it 'falls back to #bio when the html representation is missing' do
|
||||
user.user_detail.update!(bio_html: nil)
|
||||
|
||||
expect(subject).to eq(user.user_detail.bio)
|
||||
end
|
||||
|
||||
it 'stores rendered html' do
|
||||
expect(subject).to include('some <strong>bio</strong>')
|
||||
end
|
||||
|
||||
it 'does not try to set the value when the column is not there' do
|
||||
without_bio_html_column = UserDetail.column_names - ['bio_html']
|
||||
|
||||
expect(described_class).to receive(:column_names).at_least(:once).and_return(without_bio_html_column)
|
||||
expect(user.user_detail).not_to receive(:bio_html=)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -58,6 +58,10 @@ RSpec.describe User do
|
|||
|
||||
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
|
||||
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }
|
||||
|
||||
it { is_expected.to delegate_method(:bio).to(:user_detail).allow_nil }
|
||||
it { is_expected.to delegate_method(:bio=).to(:user_detail).with_arguments(:args).allow_nil }
|
||||
it { is_expected.to delegate_method(:bio_html).to(:user_detail).allow_nil }
|
||||
end
|
||||
|
||||
describe 'associations' do
|
||||
|
@ -91,64 +95,28 @@ RSpec.describe User do
|
|||
it { is_expected.to have_many(:metrics_users_starred_dashboards).inverse_of(:user) }
|
||||
it { is_expected.to have_many(:reviews).inverse_of(:author) }
|
||||
|
||||
describe "#bio" do
|
||||
it 'syncs bio with `user_details.bio` on create' do
|
||||
describe "#user_detail" do
|
||||
it 'does not persist `user_detail` by default' do
|
||||
expect(create(:user).user_detail).not_to be_persisted
|
||||
end
|
||||
|
||||
it 'creates `user_detail` when `bio` is given' do
|
||||
user = create(:user, bio: 'my bio')
|
||||
|
||||
expect(user.user_detail).to be_persisted
|
||||
expect(user.user_detail.bio).to eq('my bio')
|
||||
end
|
||||
|
||||
it 'delegates `bio` to `user_detail`' do
|
||||
user = create(:user, bio: 'my bio')
|
||||
|
||||
expect(user.bio).to eq(user.user_detail.bio)
|
||||
end
|
||||
|
||||
context 'when `migrate_bio_to_user_details` feature flag is off' do
|
||||
before do
|
||||
stub_feature_flags(migrate_bio_to_user_details: false)
|
||||
end
|
||||
|
||||
it 'does not sync bio with `user_details.bio`' do
|
||||
user = create(:user, bio: 'my bio')
|
||||
|
||||
expect(user.bio).to eq('my bio')
|
||||
expect(user.user_detail.bio).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
it 'syncs bio with `user_details.bio` on update' do
|
||||
it 'creates `user_detail` when `bio` is first updated' do
|
||||
user = create(:user)
|
||||
|
||||
user.update!(bio: 'my bio')
|
||||
|
||||
expect(user.bio).to eq(user.user_detail.bio)
|
||||
end
|
||||
|
||||
context 'when `user_details` association already exists' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:user_detail, user: user)
|
||||
end
|
||||
|
||||
it 'syncs bio with `user_details.bio`' do
|
||||
user.update!(bio: 'my bio')
|
||||
|
||||
expect(user.bio).to eq(user.user_detail.bio)
|
||||
end
|
||||
|
||||
it 'falls back to "" when nil is given' do
|
||||
user.update!(bio: nil)
|
||||
|
||||
expect(user.bio).to eq(nil)
|
||||
expect(user.user_detail.bio).to eq('')
|
||||
end
|
||||
|
||||
# very unlikely scenario
|
||||
it 'truncates long bio when syncing to user_details' do
|
||||
invalid_bio = 'a' * 256
|
||||
truncated_bio = 'a' * 255
|
||||
|
||||
user.bio = invalid_bio
|
||||
user.save(validate: false)
|
||||
|
||||
expect(user.user_detail.bio).to eq(truncated_bio)
|
||||
end
|
||||
expect { user.update(bio: 'my bio') }.to change { user.user_detail.persisted? }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -337,8 +305,6 @@ RSpec.describe User do
|
|||
it { is_expected.not_to allow_value(-1).for(:projects_limit) }
|
||||
it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) }
|
||||
|
||||
it { is_expected.to validate_length_of(:bio).is_at_most(255) }
|
||||
|
||||
it_behaves_like 'an object with email-formated attributes', :email do
|
||||
subject { build(:user) }
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue