Commit c76426fc authored by Douwe Maan's avatar Douwe Maan

Merge branch '27375-dashboard-activity-performance' into 'master'

Resolve "DashboardController#activity.json is slow due to SQL"

Closes #27375

See merge request gitlab-org/gitlab-ce!14985
parents 34a205b3 bf0331dc
...@@ -39,7 +39,7 @@ module NotesActions ...@@ -39,7 +39,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute @note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note) if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user) Notes::RenderService.new(current_user).execute([@note], @project)
end end
respond_to do |format| respond_to do |format|
...@@ -52,7 +52,7 @@ module NotesActions ...@@ -52,7 +52,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note) @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note) if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user) Notes::RenderService.new(current_user).execute([@note], @project)
end end
respond_to do |format| respond_to do |format|
......
...@@ -3,7 +3,7 @@ module RendersNotes ...@@ -3,7 +3,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes) preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project) preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes) preload_first_time_contribution_for_authors(noteable, notes)
Banzai::NoteRenderer.render(notes, @project, current_user) Notes::RenderService.new(current_user).execute(notes, @project)
notes notes
end end
......
...@@ -57,5 +57,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -57,5 +57,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a .to_a
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end end
end end
...@@ -32,6 +32,8 @@ class DashboardController < Dashboard::ApplicationController ...@@ -32,6 +32,8 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: @event_filter) .new(projects, offset: params[:offset].to_i, filter: @event_filter)
.to_a .to_a
Events::RenderService.new(current_user).execute(@events)
end end
def set_show_full_reference def set_show_full_reference
......
...@@ -155,6 +155,8 @@ class GroupsController < Groups::ApplicationController ...@@ -155,6 +155,8 @@ class GroupsController < Groups::ApplicationController
@events = EventCollection @events = EventCollection
.new(@projects, offset: params[:offset].to_i, filter: event_filter) .new(@projects, offset: params[:offset].to_i, filter: event_filter)
.to_a .to_a
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end end
def user_actions def user_actions
......
...@@ -300,6 +300,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -300,6 +300,8 @@ class ProjectsController < Projects::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a .to_a
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end end
def project_params def project_params
......
...@@ -108,6 +108,8 @@ class UsersController < ApplicationController ...@@ -108,6 +108,8 @@ class UsersController < ApplicationController
.references(:project) .references(:project)
.with_associations .with_associations
.limit_recent(20, params[:offset]) .limit_recent(20, params[:offset])
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end end
def load_projects def load_projects
......
...@@ -172,16 +172,6 @@ module EventsHelper ...@@ -172,16 +172,6 @@ module EventsHelper
end end
end end
def event_note(text, options = {})
text = first_line_in_markdown(text, 150, options)
sanitize(
text,
tags: %w(a img gl-emoji b pre code p span),
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
)
end
def event_commit_title(message) def event_commit_title(message)
message ||= '' message ||= ''
(message.split("\n").first || "").truncate(70) (message.split("\n").first || "").truncate(70)
......
...@@ -69,10 +69,16 @@ module MarkupHelper ...@@ -69,10 +69,16 @@ module MarkupHelper
# as Markdown. HTML tags in the parsed output are not counted toward the # as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then # +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag. # the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {}) def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
md = markdown(text, options).strip md = markdown_field(object, attribute, options)
truncate_visible(md, max_chars || md.length) if md.present? text = truncate_visible(md, max_chars || md.length) if md.present?
sanitize(
text,
tags: %w(a img gl-emoji b pre code p span),
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
)
end end
def markdown(text, context = {}) def markdown(text, context = {})
...@@ -83,15 +89,17 @@ module MarkupHelper ...@@ -83,15 +89,17 @@ module MarkupHelper
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
def markdown_field(object, field) def markdown_field(object, field, context = {})
object = object.for_display if object.respond_to?(:for_display) object = object.for_display if object.respond_to?(:for_display)
redacted_field_html = object.try(:"redacted_#{field}_html") redacted_field_html = object.try(:"redacted_#{field}_html")
return '' unless object.present? return '' unless object.present?
return redacted_field_html if redacted_field_html return redacted_field_html if redacted_field_html
html = Banzai.render_field(object, field) html = Banzai.render_field(object, field, context)
prepare_for_rendering(html, object.banzai_render_context(field)) context.reverse_merge!(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)
prepare_for_rendering(html, context)
end end
def markup(file_name, text, context = {}) def markup(file_name, text, context = {})
......
class BaseRenderer
attr_reader :current_user
def initialize(current_user = nil)
@current_user = current_user
end
end
module Events
class RenderService < BaseRenderer
def execute(events, atom_request: false)
events.map(&:note).compact.group_by(&:project).each do |project, notes|
render_notes(notes, project, atom_request)
end
end
private
def render_notes(notes, project, atom_request)
Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request))
end
def render_options(atom_request)
return {} unless atom_request
{ only_path: false, xhtml: true }
end
end
end
module Banzai module Notes
module NoteRenderer class RenderService < BaseRenderer
# Renders a collection of Note instances. # Renders a collection of Note instances.
# #
# notes - The notes to render. # notes - The notes to render.
# project - The project to use for redacting. # project - The project to use for redacting.
# user - The user viewing the notes. # user - The user viewing the notes.
# path - The request path.
# wiki - The project's wiki. # Possible options:
# git_ref - The current Git reference. # requested_path - The request path.
def self.render(notes, project, user = nil, path = nil, wiki = nil, git_ref = nil) # project_wiki - The project's wiki.
renderer = ObjectRenderer.new(project, # ref - The current Git reference.
user, # only_path - flag to turn relative paths into absolute ones.
requested_path: path, # xhtml - flag to save the html in XHTML
project_wiki: wiki, def execute(notes, project, **opts)
ref: git_ref) renderer = Banzai::ObjectRenderer.new(project, current_user, **opts)
renderer.render(notes, :note) renderer.render(notes, :note)
end end
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
.todo-body .todo-body
.todo-note .todo-note
.md .md
= event_note(todo.body, project: todo.project) = first_line_in_markdown(todo, :body, 150, project: todo.project)
- if todo.pending? - if todo.pending?
.todo-actions .todo-actions
......
%div{ xmlns: "http://www.w3.org/1999/xhtml" } %div{ xmlns: "http://www.w3.org/1999/xhtml" }
= markdown(note.note, pipeline: :atom, project: note.project, author: note.author) = markdown_field(note, :note)
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.event-body .event-body
.event-note .event-note
.md .md
= event_note(event.target.note, project: event.project) = first_line_in_markdown(event.target, :note, 150, project: event.project)
- note = event.target - note = event.target
- if note.attachment.url - if note.attachment.url
- if note.attachment.image? - if note.attachment.image?
......
---
title: Improve DashboardController#activity.json performance
merge_request: 14985
author:
type: performance
...@@ -77,7 +77,6 @@ def instrument_classes(instrumentation) ...@@ -77,7 +77,6 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Banzai::ObjectRenderer) instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
instrumentation.instrument_instance_methods(Banzai::Redactor) instrumentation.instrument_instance_methods(Banzai::Redactor)
instrumentation.instrument_methods(Banzai::NoteRenderer)
[Issuable, Mentionable, Participable].each do |klass| [Issuable, Mentionable, Participable].each do |klass|
instrumentation.instrument_instance_methods(klass) instrumentation.instrument_instance_methods(klass)
......
...@@ -3,8 +3,8 @@ module Banzai ...@@ -3,8 +3,8 @@ module Banzai
Renderer.render(text, context) Renderer.render(text, context)
end end
def self.render_field(object, field) def self.render_field(object, field, context = {})
Renderer.render_field(object, field) Renderer.render_field(object, field, context)
end end
def self.cache_collection_render(texts_and_contexts) def self.cache_collection_render(texts_and_contexts)
......
require 'uri'
module Banzai
module Filter
# HTML filter that converts relative urls into absolute ones.
class AbsoluteLinkFilter < HTML::Pipeline::Filter
def call
return doc unless context[:only_path] == false
doc.search('a.gfm').each do |el|
process_link_attr el.attribute('href')
end
doc
end
protected
def process_link_attr(html_attr)
return if html_attr.blank?
return if html_attr.value.start_with?('//')
uri = URI(html_attr.value)
html_attr.value = absolute_link_attr(uri) if uri.relative?
rescue URI::Error
# noop
end
def absolute_link_attr(uri)
URI.join(Gitlab.config.gitlab.url, uri).to_s
end
end
end
end
...@@ -311,30 +311,6 @@ module Banzai ...@@ -311,30 +311,6 @@ module Banzai
def project_refs_cache def project_refs_cache
RequestStore[:banzai_project_refs] ||= {} RequestStore[:banzai_project_refs] ||= {}
end end
def cached_call(request_store_key, cache_key, path: [])
if RequestStore.active?
cache = RequestStore[request_store_key] ||= Hash.new do |hash, key|
hash[key] = Hash.new { |h, k| h[k] = {} }
end
cache = cache.dig(*path) if path.any?
get_or_set_cache(cache, cache_key) { yield }
else
yield
end
end
def get_or_set_cache(cache, key)
if cache.key?(key)
cache[key]
else
value = yield
cache[key] = value if key.present?
value
end
end
end end
end end
end end
...@@ -8,6 +8,8 @@ module Banzai ...@@ -8,6 +8,8 @@ module Banzai
# :project (required) - Current project, ignored if reference is cross-project. # :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links. # :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter class ReferenceFilter < HTML::Pipeline::Filter
include RequestStoreReferenceCache
class << self class << self
attr_accessor :reference_type attr_accessor :reference_type
end end
......
...@@ -60,10 +60,14 @@ module Banzai ...@@ -60,10 +60,14 @@ module Banzai
self.class.references_in(text) do |match, username| self.class.references_in(text) do |match, username|
if username == 'all' && !skip_project_check? if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content) link_to_all(link_content: link_content)
elsif namespace = namespaces[username.downcase]
link_to_namespace(namespace, link_content: link_content) || match
else else
match cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
if namespace = namespaces[username.downcase]
link_to_namespace(namespace, link_content: link_content) || match
else
match
end
end
end end
end end
end end
...@@ -74,7 +78,10 @@ module Banzai ...@@ -74,7 +78,10 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the # The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects. # corresponding Namespace objects.
def namespaces def namespaces
@namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path).transform_keys(&:downcase) @namespaces ||= Namespace.eager_load(:owner, :route)
.where_full_path_in(usernames)
.index_by(&:full_path)
.transform_keys(&:downcase)
end end
# Returns all usernames referenced in the current document. # Returns all usernames referenced in the current document.
......
...@@ -37,7 +37,7 @@ module Banzai ...@@ -37,7 +37,7 @@ module Banzai
objects.each_with_index do |object, index| objects.each_with_index do |object, index|
redacted_data = redacted[index] redacted_data = redacted[index]
object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) # rubocop:disable GitlabSecurity/PublicSend object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html(save_options).html_safe) # rubocop:disable GitlabSecurity/PublicSend
object.user_visible_reference_count = redacted_data[:visible_reference_count] if object.respond_to?(:user_visible_reference_count) object.user_visible_reference_count = redacted_data[:visible_reference_count] if object.respond_to?(:user_visible_reference_count)
end end
end end
...@@ -83,5 +83,10 @@ module Banzai ...@@ -83,5 +83,10 @@ module Banzai
skip_redaction: true skip_redaction: true
) )
end end
def save_options
return {} unless base_context[:xhtml]
{ save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML }
end
end end
end end
...@@ -3,9 +3,10 @@ module Banzai ...@@ -3,9 +3,10 @@ module Banzai
class PostProcessPipeline < BasePipeline class PostProcessPipeline < BasePipeline
def self.filters def self.filters
FilterArray[ FilterArray[
Filter::RedactorFilter,
Filter::RelativeLinkFilter, Filter::RelativeLinkFilter,
Filter::IssuableStateFilter, Filter::IssuableStateFilter,
Filter::RedactorFilter Filter::AbsoluteLinkFilter
] ]
end end
......
...@@ -32,12 +32,9 @@ module Banzai ...@@ -32,12 +32,9 @@ module Banzai
# Convert a Markdown-containing field on an object into an HTML-safe String # Convert a Markdown-containing field on an object into an HTML-safe String
# of HTML. This method is analogous to calling render(object.field), but it # of HTML. This method is analogous to calling render(object.field), but it
# can cache the rendered HTML in the object, rather than Redis. # can cache the rendered HTML in the object, rather than Redis.
# def self.render_field(object, field, context = {})
# The context to use is managed by the object and cannot be changed.
# Use #render, passing it the field text, if a custom rendering is needed.
def self.render_field(object, field)
unless object.respond_to?(:cached_markdown_fields) unless object.respond_to?(:cached_markdown_fields)
return cacheless_render_field(object, field) return cacheless_render_field(object, field, context)
end end
object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field) object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
...@@ -46,9 +43,9 @@ module Banzai ...@@ -46,9 +43,9 @@ module Banzai
end end
# Same as +render_field+, but without consulting or updating the cache field # Same as +render_field+, but without consulting or updating the cache field
def self.cacheless_render_field(object, field, options = {}) def self.cacheless_render_field(object, field, context = {})
text = object.__send__(field) # rubocop:disable GitlabSecurity/PublicSend text = object.__send__(field) # rubocop:disable GitlabSecurity/PublicSend
context = object.banzai_render_context(field).merge(options) context = context.reverse_merge(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)
cacheless_render(text, context) cacheless_render(text, context)
end end
......
module Banzai
module RequestStoreReferenceCache
def cached_call(request_store_key, cache_key, path: [])
if RequestStore.active?
cache = RequestStore[request_store_key] ||= Hash.new do |hash, key|
hash[key] = Hash.new { |h, k| h[k] = {} }
end
cache = cache.dig(*path) if path.any?
get_or_set_cache(cache, cache_key) { yield }
else
yield
end
end
def get_or_set_cache(cache, key)
if cache.key?(key)
cache[key]
else
value = yield
cache[key] = value if key.present?
value
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe EventsHelper do describe EventsHelper do
describe '#event_note' do
let(:user) { build(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
it 'displays one line of plain text without alteration' do
input = 'A short, plain note'
expect(helper.event_note(input)).to match(input)
expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
end
it 'displays inline code' do
input = 'A note with `inline code`'
expected = 'A note with <code>inline code</code>'
expect(helper.event_note(input)).to match(expected)
end
it 'truncates a note with multiple paragraphs' do
input = "Paragraph 1\n\nParagraph 2"
expected = 'Paragraph 1...'
expect(helper.event_note(input)).to match(expected)
end
it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```"
expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
expect(helper.event_note(input)).to match(expected)
end
it 'truncates a single long line of text' do
text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
input = text * 4
expected = (text * 2).sub(/.{3}/, '...')
expect(helper.event_note(input)).to match(expected)
end
it 'preserves a link href when link text is truncated' do
text = 'The quick brown fox jumped over the lazy dog' # 44 chars
input = "#{text}#{text}#{text} " # 133 chars
link_url = 'http://example.com/foo/bar/baz' # 30 chars
input << link_url
expected_link_text = 'http://example...</a>'
expect(helper.event_note(input)).to match(link_url)
expect(helper.event_note(input)).to match(expected_link_text)
end
it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
"</code></pre>"
expect(helper.event_note(input)).to eq(expected)
end
it 'preserves data-src for lazy images' do
input = "![ImageTest](/uploads/test.png)"
image_url = "data-src=\"/uploads/test.png\""
expect(helper.event_note(input)).to match(image_url)
end
context 'labels formatting' do
let(:input) { 'this should be ~label_1' }
def format_event_note(project)
create(:label, title: 'label_1', project: project)
helper.event_note(input, { project: project })
end
it 'preserves style attribute for a label that can be accessed by current_user' do
project = create(:project, :public)
expect(format_event_note(project)).to match(/span class=.*style=.*/)
end
it 'does not style a label that can not be accessed by current_user' do
project = create(:project, :private)
expect(format_event_note(project)).to eq("<p>#{input}</p>")
end
end
end
describe '#event_commit_title' do describe '#event_commit_title' do
let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 } let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
subject { helper.event_commit_title(message) } subject { helper.event_commit_title(message) }
......
...@@ -67,7 +67,7 @@ describe MarkupHelper do ...@@ -67,7 +67,7 @@ describe MarkupHelper do
describe 'without redacted attribute' do describe 'without redacted attribute' do
it 'renders the markdown value' do it 'renders the markdown value' do
expect(Banzai).to receive(:render_field).with(commit, attribute).and_call_original expect(Banzai).to receive(:render_field).with(commit, attribute, {}).and_call_original
helper.markdown_field(commit, attribute) helper.markdown_field(commit, attribute)
end end
...@@ -252,38 +252,141 @@ describe MarkupHelper do ...@@ -252,38 +252,141 @@ describe MarkupHelper do
end end
describe '#first_line_in_markdown' do describe '#first_line_in_markdown' do
it 'truncates Markdown properly' do shared_examples_for 'common markdown examples' do
text = "@#{user.username}, can you look at this?\nHello world\n" let(:project_base) { build(:project, :repository) }
actual = first_line_in_markdown(text, 100, project: project)
doc = Nokogiri::HTML.parse(actual) it 'displays inline code' do
object = create_object('Text with `inline code`')
expected = 'Text with <code>inline code</code>'
# Make sure we didn't create invalid markup expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
expect(doc.errors).to be_empty end
# Leading user link it 'truncates the text with multiple paragraphs' do
expect(doc.css('a').length).to eq(1) object = create_object("Paragraph 1\n\nParagraph 2")
expect(doc.css('a')[0].attr('href')).to eq user_path(user) expected = 'Paragraph 1...'
expect(doc.css('a')[0].text).to eq "@#{user.username}"
expect(doc.content).to eq "@#{user.username}, can you look at this?..." expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
end end
it 'truncates Markdown with emoji properly' do it 'displays the first line of a code block' do
text = "foo :wink:\nbar :grinning:" object = create_object("```\nCode block\nwith two lines\n```")
actual = first_line_in_markdown(text, 100, project: project) expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
doc = Nokogiri::HTML.parse(actual) expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
end
# Make sure we didn't create invalid markup it 'truncates a single long line of text' do
# But also account for the 2 errors caused by the unknown `gl-emoji` elements text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
expect(doc.errors.length).to eq(2) object = create_object(text * 4)
expected = (text * 2).sub(/.{3}/, '...')
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected)
end
it 'preserves a link href when link text is truncated' do
text = 'The quick brown fox jumped over the lazy dog' # 44 chars
input = "#{text}#{text}#{text} " # 133 chars
link_url = 'http://example.com/foo/bar/baz' # 30 chars
input << link_url
object = create_object(input)
expected_link_text = 'http://example...</a>'
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(link_url)
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected_link_text)
end
it 'preserves code color scheme' do
object = create_object("```ruby\ndef test\n 'hello world'\nend\n```")
expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
"</code></pre>"
expect(first_line_in_markdown(object, attribute, 150, project: project)).to eq(expected)
end
it 'preserves data-src for lazy images' do
object = create_object("![ImageTest](/uploads/test.png)")
image_url = "data-src=\".*/uploads/test.png\""
expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(image_url)
end
context 'labels formatting' do
let(:label_title) { 'this should be ~label_1' }
def create_and_format_label(project)
create(:label, title: 'label_1', project: project)
object = create_object(label_title, project: project)
expect(doc.css('gl-emoji').length).to eq(2) first_line_in_markdown(object, attribute, 150, project: project)
expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink' end
expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
expect(doc.content).to eq "foo 😉\nbar 😀" it 'preserves style attribute for a label that can be accessed by current_user' do
project = create(:project, :public)
expect(create_and_format_label(project)).to match(/span class=.*style=.*/)
end
it 'does not style a label that can not be accessed by current_user' do
project = create(:project, :private)
expect(create_and_format_label(project)).to eq("<p>#{label_title}</p>")
end
end
it 'truncates Markdown properly' do
object = create_object("@#{user.username}, can you look at this?\nHello world\n")
actual = first_line_in_markdown(object, attribute, 100, project: project)
doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup
expect(doc.errors).to be_empty
# Leading user link
expect(doc.css('a').length).to eq(1)
expect(doc.css('a')[0].attr('href')).to eq user_path(user)
expect(doc.css('a')[0].text).to eq "@#{user.username}"
expect(doc.content).to eq "@#{user.username}, can you look at this?..."
end
it 'truncates Markdown with emoji properly' do
object = create_object("foo :wink:\nbar :grinning:")
actual = first_line_in_markdown(object, attribute, 100, project: project)
doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup
# But also account for the 2 errors caused by the unknown `gl-emoji` elements
expect(doc.errors.length).to eq(2)
expect(doc.css('gl-emoji').length).to eq(2)
expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
expect(doc.content).to eq "foo 😉\nbar 😀"
end
end
context 'when the asked attribute can be redacted' do
include_examples 'common markdown examples' do
let(:attribute) { :note }
def create_object(title, project: project_base)
build(:note, note: title, project: project)
end
end
end
context 'when the asked attribute can not be redacted' do
include_examples 'common markdown examples' do
let(:attribute) { :body }
def create_object(title, project: project_base)
issue = build(:issue, title: title)
build(:todo, :done, project: project_base, author: user, target: issue)
end
end
end end
end end
......
...@@ -10,7 +10,7 @@ describe Banzai::CommitRenderer do ...@@ -10,7 +10,7 @@ describe Banzai::CommitRenderer do
described_class::ATTRIBUTES.each do |attr| described_class::ATTRIBUTES.each do |attr|
expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original
expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr) expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, {})
end end
described_class.render([project.commit], project, user) described_class.render([project.commit], project, user)
......
require 'spec_helper'
describe Banzai::Filter::AbsoluteLinkFilter do
def filter(doc, context = {})
described_class.call(doc, context)
end
context 'with html links' do
context 'if only_path is false' do
let(:only_path_context) do
{ only_path: false }
end
let(:fake_url) { 'http://www.example.com' }
before do
allow(Gitlab.config.gitlab).to receive(:url).and_return(fake_url)
end
context 'has the .gfm class' do
it 'converts a relative url into absolute' do
doc = filter(link('/foo', 'gfm'), only_path_context)
expect(doc.at_css('a')['href']).to eq "#{fake_url}/foo"
end
it 'does not change the url if it already absolute' do
doc = filter(link("#{fake_url}/foo", 'gfm'), only_path_context)
expect(doc.at_css('a')['href']).to eq "#{fake_url}/foo"
end
context 'if relative_url_root is set' do
it 'joins the url without without doubling the path' do
allow(Gitlab.config.gitlab).to receive(:url).and_return("#{fake_url}/gitlab/")
doc = filter(link("/gitlab/foo", 'gfm'), only_path_context)
expect(doc.at_css('a')['href']).to eq "#{fake_url}/gitlab/foo"
end
end
end
context 'has not the .gfm class' do
it 'does not convert a relative url into absolute' do
doc = filter(link('/foo'), only_path_context)
expect(doc.at_css('a')['href']).to eq '/foo'
end
end
end
context 'if only_path is not false' do
it 'does not convert a relative url into absolute' do
expect(filter(link('/foo', 'gfm')).at_css('a')['href']).to eq '/foo'
expect(filter(link('/foo')).at_css('a')['href']).to eq '/foo'
end
end
end
def link(path, css_class = '')
%(<a class="#{css_class}" href="#{path}">example</a>)
end
end
...@@ -22,7 +22,7 @@ describe Banzai::ObjectRenderer do ...@@ -22,7 +22,7 @@ describe Banzai::ObjectRenderer do
end end
it 'retrieves field content using Banzai::Renderer.render_field' do it 'retrieves field content using Banzai::Renderer.render_field' do
expect(Banzai::Renderer).to receive(:render_field).with(object, :note).and_call_original expect(Banzai::Renderer).to receive(:render_field).with(object, :note, {}).and_call_original
renderer.render([object], :note) renderer.render([object], :note)
end end
...@@ -68,7 +68,7 @@ describe Banzai::ObjectRenderer do ...@@ -68,7 +68,7 @@ describe Banzai::ObjectRenderer do
end end
it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do
expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title).and_call_original expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title, {}).and_call_original
renderer.render([commit], :title) renderer.render([commit], :title)
end end
......
...@@ -18,7 +18,7 @@ describe Banzai::Renderer do ...@@ -18,7 +18,7 @@ describe Banzai::Renderer do
let(:commit) { create(:project, :repository).commit } let(:commit) { create(:project, :repository).commit }
it 'returns cacheless render field' do it 'returns cacheless render field' do
expect(renderer).to receive(:cacheless_render_field).with(commit, :title) expect(renderer).to receive(:cacheless_render_field).with(commit, :title, {})
renderer.render_field(commit, :title) renderer.render_field(commit, :title)
end end
......
require 'spec_helper'
describe Events::RenderService do
describe '#execute' do
let!(:note) { build(:note) }
let!(:event) { build(:event, target: note, project: note.project) }
let!(:user) { build(:user) }
context 'when the request format is atom' do
it 'renders the note inside events' do
expect(Banzai::ObjectRenderer).to receive(:new)
.with(event.project, user,
only_path: false,
xhtml: true)
.and_call_original
expect_any_instance_of(Banzai::ObjectRenderer)
.to receive(:render).with([note], :note)
described_class.new(user).execute([event], atom_request: true)
end
end
context 'when the request format is not atom' do
it 'renders the note inside events' do
expect(Banzai::ObjectRenderer).to receive(:new)
.with(event.project, user, {})
.and_call_original
expect_any_instance_of(Banzai::ObjectRenderer)
.to receive(:render).with([note], :note)
described_class.new(user).execute([event], atom_request: false)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Banzai::NoteRenderer do describe Notes::RenderService do
describe '.render' do describe '#execute' do
it 'renders a Note' do it 'renders a Note' do
note = double(:note) note = double(:note)
project = double(:project) project = double(:project)
...@@ -12,13 +12,20 @@ describe Banzai::NoteRenderer do ...@@ -12,13 +12,20 @@ describe Banzai::NoteRenderer do
.with(project, user, .with(project, user,
requested_path: 'foo', requested_path: 'foo',
project_wiki: wiki, project_wiki: wiki,
ref: 'bar') ref: 'bar',
only_path: nil,
xhtml: false)
.and_call_original .and_call_original
expect_any_instance_of(Banzai::ObjectRenderer) expect_any_instance_of(Banzai::ObjectRenderer)
.to receive(:render).with([note], :note) .to receive(:render).with([note], :note)
described_class.render([note], project, user, 'foo', wiki, 'bar') described_class.new(user).execute([note], project,
requested_path: 'foo',
project_wiki: wiki,
ref: 'bar',
only_path: nil,
xhtml: false)
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment