Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Boxiang Sun
gitlab-ce
Commits
bf0331dc
Commit
bf0331dc
authored
Nov 06, 2017
by
Francisco Javier López
Committed by
Douwe Maan
Nov 06, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "DashboardController#activity.json is slow due to SQL"
parent
34a205b3
Changes
34
Show whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
429 additions
and
180 deletions
+429
-180
app/controllers/concerns/notes_actions.rb
app/controllers/concerns/notes_actions.rb
+2
-2
app/controllers/concerns/renders_notes.rb
app/controllers/concerns/renders_notes.rb
+1
-1
app/controllers/dashboard/projects_controller.rb
app/controllers/dashboard/projects_controller.rb
+2
-0
app/controllers/dashboard_controller.rb
app/controllers/dashboard_controller.rb
+2
-0
app/controllers/groups_controller.rb
app/controllers/groups_controller.rb
+2
-0
app/controllers/projects_controller.rb
app/controllers/projects_controller.rb
+2
-0
app/controllers/users_controller.rb
app/controllers/users_controller.rb
+2
-0
app/helpers/events_helper.rb
app/helpers/events_helper.rb
+0
-10
app/helpers/markup_helper.rb
app/helpers/markup_helper.rb
+14
-6
app/services/base_renderer.rb
app/services/base_renderer.rb
+7
-0
app/services/events/render_service.rb
app/services/events/render_service.rb
+21
-0
app/services/notes/render_service.rb
app/services/notes/render_service.rb
+21
-0
app/views/dashboard/todos/_todo.html.haml
app/views/dashboard/todos/_todo.html.haml
+1
-1
app/views/events/_event_note.atom.haml
app/views/events/_event_note.atom.haml
+1
-1
app/views/events/event/_note.html.haml
app/views/events/event/_note.html.haml
+1
-1
changelogs/unreleased/27375-dashboard-activity-performance.yml
...elogs/unreleased/27375-dashboard-activity-performance.yml
+5
-0
config/initializers/8_metrics.rb
config/initializers/8_metrics.rb
+0
-1
lib/banzai.rb
lib/banzai.rb
+2
-2
lib/banzai/filter/absolute_link_filter.rb
lib/banzai/filter/absolute_link_filter.rb
+34
-0
lib/banzai/filter/abstract_reference_filter.rb
lib/banzai/filter/abstract_reference_filter.rb
+0
-24
lib/banzai/filter/reference_filter.rb
lib/banzai/filter/reference_filter.rb
+2
-0
lib/banzai/filter/user_reference_filter.rb
lib/banzai/filter/user_reference_filter.rb
+11
-4
lib/banzai/object_renderer.rb
lib/banzai/object_renderer.rb
+6
-1
lib/banzai/pipeline/post_process_pipeline.rb
lib/banzai/pipeline/post_process_pipeline.rb
+2
-1
lib/banzai/renderer.rb
lib/banzai/renderer.rb
+4
-7
lib/banzai/request_store_reference_cache.rb
lib/banzai/request_store_reference_cache.rb
+27
-0
spec/helpers/events_helper_spec.rb
spec/helpers/events_helper_spec.rb
+0
-90
spec/helpers/markup_helper_spec.rb
spec/helpers/markup_helper_spec.rb
+127
-24
spec/lib/banzai/commit_renderer_spec.rb
spec/lib/banzai/commit_renderer_spec.rb
+1
-1
spec/lib/banzai/filter/absolute_link_filter_spec.rb
spec/lib/banzai/filter/absolute_link_filter_spec.rb
+58
-0
spec/lib/banzai/object_renderer_spec.rb
spec/lib/banzai/object_renderer_spec.rb
+2
-2
spec/lib/banzai/renderer_spec.rb
spec/lib/banzai/renderer_spec.rb
+1
-1
spec/services/events/render_service_spec.rb
spec/services/events/render_service_spec.rb
+37
-0
spec/services/notes/render_service_spec.rb
spec/services/notes/render_service_spec.rb
+31
-0
No files found.
app/controllers/concerns/notes_actions.rb
View file @
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
|
...
...
app/controllers/concerns/renders_notes.rb
View file @
bf0331dc
...
@@ -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
...
...
app/controllers/dashboard/projects_controller.rb
View file @
bf0331dc
...
@@ -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
app/controllers/dashboard_controller.rb
View file @
bf0331dc
...
@@ -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
...
...
app/controllers/groups_controller.rb
View file @
bf0331dc
...
@@ -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
...
...
app/controllers/projects_controller.rb
View file @
bf0331dc
...
@@ -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
...
...
app/controllers/users_controller.rb
View file @
bf0331dc
...
@@ -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
...
...
app/helpers/events_helper.rb
View file @
bf0331dc
...
@@ -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
)
...
...
app/helpers/markup_helper.rb
View file @
bf0331dc
...
@@ -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
=
{})
...
...
app/services/base_renderer.rb
0 → 100644
View file @
bf0331dc
class
BaseRenderer
attr_reader
:current_user
def
initialize
(
current_user
=
nil
)
@current_user
=
current_user
end
end
app/services/events/render_service.rb
0 → 100644
View file @
bf0331dc
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
lib/banzai/note_renderer
.rb
→
app/services/notes/render_service
.rb
View file @
bf0331dc
module
Banzai
module
Notes
module
Not
eRenderer
class
RenderService
<
Bas
eRenderer
# 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
...
...
app/views/dashboard/todos/_todo.html.haml
View file @
bf0331dc
...
@@ -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
...
...
app/views/events/_event_note.atom.haml
View file @
bf0331dc
%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
)
app/views/events/event/_note.html.haml
View file @
bf0331dc
...
@@ -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?
...
...
changelogs/unreleased/27375-dashboard-activity-performance.yml
0 → 100644
View file @
bf0331dc
---
title
:
Improve DashboardController#activity.json performance
merge_request
:
14985
author
:
type
:
performance
config/initializers/8_metrics.rb
View file @
bf0331dc
...
@@ -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
)
...
...
lib/banzai.rb
View file @
bf0331dc
...
@@ -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
)
...
...
lib/banzai/filter/absolute_link_filter.rb
0 → 100644
View file @
bf0331dc
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
lib/banzai/filter/abstract_reference_filter.rb
View file @
bf0331dc
...
@@ -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
lib/banzai/filter/reference_filter.rb
View file @
bf0331dc
...
@@ -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
...
...
lib/banzai/filter/user_reference_filter.rb
View file @
bf0331dc
...
@@ -60,13 +60,17 @@ module Banzai
...
@@ -60,13 +60,17 @@ 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
]
else
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
link_to_namespace
(
namespace
,
link_content:
link_content
)
||
match
else
else
match
match
end
end
end
end
end
end
end
end
# Returns a Hash containing all Namespace objects for the username
# Returns a Hash containing all Namespace objects for the username
# references in the current document.
# references in the current document.
...
@@ -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.
...
...
lib/banzai/object_renderer.rb
View file @
bf0331dc
...
@@ -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
lib/banzai/pipeline/post_process_pipeline.rb
View file @
bf0331dc
...
@@ -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
::
Redactor
Filter
Filter
::
AbsoluteLink
Filter
]
]
end
end
...
...
lib/banzai/renderer.rb
View file @
bf0331dc
...
@@ -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
...
...
lib/banzai/request_store_reference_cache.rb
0 → 100644
View file @
bf0331dc
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
spec/helpers/events_helper_spec.rb
View file @
bf0331dc
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\n
Paragraph 2"
expected
=
'Paragraph 1...'
expect
(
helper
.
event_note
(
input
)).
to
match
(
expected
)
end
it
'displays the first line of a code block'
do
input
=
"```
\n
Code block
\n
with 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
\n
def test
\n
'hello world'
\n
end
\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
)
}
...
...
spec/helpers/markup_helper_spec.rb
View file @
bf0331dc
...
@@ -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,9 +252,92 @@ describe MarkupHelper do
...
@@ -252,9 +252,92 @@ describe MarkupHelper do
end
end
describe
'#first_line_in_markdown'
do
describe
'#first_line_in_markdown'
do
shared_examples_for
'common markdown examples'
do
let
(
:project_base
)
{
build
(
:project
,
:repository
)
}
it
'displays inline code'
do
object
=
create_object
(
'Text with `inline code`'
)
expected
=
'Text with <code>inline code</code>'
expect
(
first_line_in_markdown
(
object
,
attribute
,
100
,
project:
project
)).
to
match
(
expected
)
end
it
'truncates the text with multiple paragraphs'
do
object
=
create_object
(
"Paragraph 1
\n\n
Paragraph 2"
)
expected
=
'Paragraph 1...'
expect
(
first_line_in_markdown
(
object
,
attribute
,
100
,
project:
project
)).
to
match
(
expected
)
end
it
'displays the first line of a code block'
do
object
=
create_object
(
"```
\n
Code block
\n
with two lines
\n
```"
)
expected
=
%r{<pre.+><code><span class="line">Code block
\.\.\.
</span>
\n
</code></pre>}
expect
(
first_line_in_markdown
(
object
,
attribute
,
100
,
project:
project
)).
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
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
\n
def test
\n
'hello world'
\n
end
\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
)
first_line_in_markdown
(
object
,
attribute
,
150
,
project:
project
)
end
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
it
'truncates Markdown properly'
do
text
=
"@
#{
user
.
username
}
, can you look at this?
\n
Hello world
\n
"
object
=
create_object
(
"@
#{
user
.
username
}
, can you look at this?
\n
Hello world
\n
"
)
actual
=
first_line_in_markdown
(
text
,
100
,
project:
project
)
actual
=
first_line_in_markdown
(
object
,
attribute
,
100
,
project:
project
)
doc
=
Nokogiri
::
HTML
.
parse
(
actual
)
doc
=
Nokogiri
::
HTML
.
parse
(
actual
)
...
@@ -270,8 +353,8 @@ describe MarkupHelper do
...
@@ -270,8 +353,8 @@ describe MarkupHelper do
end
end
it
'truncates Markdown with emoji properly'
do
it
'truncates Markdown with emoji properly'
do
text
=
"foo :wink:
\n
bar :grinning:"
object
=
create_object
(
"foo :wink:
\n
bar :grinning:"
)
actual
=
first_line_in_markdown
(
text
,
100
,
project:
project
)
actual
=
first_line_in_markdown
(
object
,
attribute
,
100
,
project:
project
)
doc
=
Nokogiri
::
HTML
.
parse
(
actual
)
doc
=
Nokogiri
::
HTML
.
parse
(
actual
)
...
@@ -287,6 +370,26 @@ describe MarkupHelper do
...
@@ -287,6 +370,26 @@ describe MarkupHelper do
end
end
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
describe
'#cross_project_reference'
do
describe
'#cross_project_reference'
do
it
'shows the full MR reference'
do
it
'shows the full MR reference'
do
expect
(
helper
.
cross_project_reference
(
project
,
merge_request
)).
to
include
(
project
.
full_path
)
expect
(
helper
.
cross_project_reference
(
project
,
merge_request
)).
to
include
(
project
.
full_path
)
...
...
spec/lib/banzai/commit_renderer_spec.rb
View file @
bf0331dc
...
@@ -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
)
...
...
spec/lib/banzai/filter/absolute_link_filter_spec.rb
0 → 100644
View file @
bf0331dc
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
spec/lib/banzai/object_renderer_spec.rb
View file @
bf0331dc
...
@@ -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
...
...
spec/lib/banzai/renderer_spec.rb
View file @
bf0331dc
...
@@ -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
...
...
spec/services/events/render_service_spec.rb
0 → 100644
View file @
bf0331dc
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
spec/
lib/banzai/note_renderer
_spec.rb
→
spec/
services/notes/render_service
_spec.rb
View file @
bf0331dc
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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment