Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
P
perxis-go
Manage
Activity
Members
Code
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Deploy
Package Registry
Operate
Terraform modules
Analyze
Contributor analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
perxis
perxis-go
Commits
b7e24f26
Commit
b7e24f26
authored
1 week ago
by
Semyon Krestyaninov
Browse files
Options
Downloads
Patches
Plain Diff
wip
parent
076fdd0b
No related branches found
No related tags found
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
template/builder.go
+35
-14
35 additions, 14 deletions
template/builder.go
template/builder_test.go
+157
-104
157 additions, 104 deletions
template/builder_test.go
with
192 additions
and
118 deletions
template/builder.go
+
35
−
14
View file @
b7e24f26
...
...
@@ -3,7 +3,9 @@ package template
import
(
"bytes"
"context"
"text/template"
html
"html/template"
"io"
text
"text/template"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/content"
...
...
@@ -11,32 +13,57 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/spaces"
)
type
Template
interface
{
Execute
(
w
io
.
Writer
,
data
any
)
error
}
type
Builder
struct
{
ctx
context
.
Context
cnt
*
content
.
Content
SpaceID
string
EnvID
string
CollID
string
data
map
[
string
]
interface
{}
data
map
[
string
]
any
// Для кеширования запросов
space
*
spaces
.
Space
environment
*
environments
.
Environment
collection
*
collections
.
Collection
// templateFunc парсит строку и возвращает шаблон для подстановки значений
templateFunc
func
(
data
string
)
(
Template
,
error
)
}
func
NewBuilder
(
cnt
*
content
.
Content
,
space
,
env
,
col
string
)
*
Builder
{
return
&
Builder
{
func
NewBuilder
(
cnt
*
content
.
Content
,
space
,
env
,
col
string
)
Builder
{
b
:=
Builder
{
ctx
:
context
.
Background
(),
cnt
:
cnt
,
SpaceID
:
space
,
EnvID
:
env
,
CollID
:
col
,
}
b
.
templateFunc
=
func
(
data
string
)
(
Template
,
error
)
{
return
text
.
New
(
"main_text"
)
.
Funcs
(
b
.
getFuncs
())
.
Parse
(
data
)
}
return
b
}
func
(
b
*
Builder
)
getFuncs
()
template
.
FuncMap
{
return
template
.
FuncMap
{
func
NewHTMLBuilder
(
cnt
*
content
.
Content
,
space
,
env
,
col
string
)
Builder
{
b
:=
Builder
{
ctx
:
context
.
Background
(),
cnt
:
cnt
,
SpaceID
:
space
,
EnvID
:
env
,
CollID
:
col
,
}
b
.
templateFunc
=
func
(
data
string
)
(
Template
,
error
)
{
return
html
.
New
(
"main_html"
)
.
Funcs
(
b
.
getFuncs
())
.
Parse
(
data
)
}
return
b
}
func
(
b
*
Builder
)
getFuncs
()
map
[
string
]
any
{
return
map
[
string
]
any
{
"lookup"
:
getLookup
(
b
),
"system"
:
getSystem
(
b
),
}
...
...
@@ -84,14 +111,9 @@ func (b *Builder) Context() context.Context {
return
b
.
ctx
}
func
(
b
*
Builder
)
Template
()
*
template
.
Template
{
return
template
.
New
(
"main"
)
.
Funcs
(
b
.
getFuncs
())
}
func
(
b
*
Builder
)
Execute
(
str
string
,
data
...
any
)
(
string
,
error
)
{
t
:=
b
.
Template
()
buf
:=
new
(
bytes
.
Buffer
)
t
,
err
:=
t
.
Parse
(
str
)
t
,
err
:=
b
.
templateFunc
(
str
)
if
err
!=
nil
{
return
""
,
err
}
...
...
@@ -102,14 +124,13 @@ func (b *Builder) Execute(str string, data ...any) (string, error) {
}
func
(
b
*
Builder
)
ExecuteList
(
str
[]
string
,
data
...
any
)
([]
string
,
error
)
{
t
:=
b
.
Template
()
result
:=
make
([]
string
,
len
(
str
))
buffer
:=
new
(
bytes
.
Buffer
)
for
i
,
tmpl
:=
range
str
{
if
tmpl
==
""
{
continue
}
t
,
err
:=
t
.
Parse
(
tmpl
)
t
,
err
:=
b
.
templateFunc
(
tmpl
)
if
err
!=
nil
{
return
[]
string
{},
err
}
...
...
This diff is collapsed.
Click to expand it.
template/builder_test.go
+
157
−
104
View file @
b7e24f26
...
...
@@ -19,16 +19,17 @@ import (
func
TestBuilder_Execute
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
SpaceID
string
EnvID
string
CollID
string
str
string
data
any
want
any
wantErr
bool
name
string
SpaceID
string
EnvID
string
CollID
string
str
string
data
any
want
any
htmlBuilder
bool
wantErr
bool
getCnt
func
(
)
(
cnt
*
content
.
Content
,
assertExpectations
func
(
t
*
testing
.
T
))
getCnt
func
(
t
*
testing
.
T
)
*
content
.
Content
}{
{
name
:
"error"
,
str
:
"hello {{ .a }}"
,
data
:
"world"
,
want
:
""
,
wantErr
:
true
},
{
name
:
"empty"
,
str
:
""
,
data
:
""
,
want
:
""
,
wantErr
:
false
},
...
...
@@ -36,100 +37,164 @@ func TestBuilder_Execute(t *testing.T) {
{
name
:
"#2"
,
str
:
"{{ . }}"
,
data
:
"world"
,
want
:
"world"
,
wantErr
:
false
},
{
name
:
"#3 "
,
str
:
""
,
data
:
"world"
,
want
:
""
,
wantErr
:
false
},
{
name
:
"#4 "
,
str
:
"hello"
,
data
:
"world"
,
want
:
"hello"
,
wantErr
:
false
},
{
name
:
"lookup"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"hello, {{ lookup
\"
secrets.dev.key
\"
}}"
,
data
:
""
,
want
:
"hello, Luk"
,
wantErr
:
false
,
getCnt
:
func
()
(
*
content
.
Content
,
func
(
t
*
testing
.
T
))
{
itemsSvc
:=
&
mocksitems
.
Items
{}
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{
"id"
:
"dev"
,
"key"
:
"Luk"
,
},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
},
func
(
t
*
testing
.
T
)
{
itemsSvc
.
AssertExpectations
(
t
)
}
}},
{
name
:
"lookup with slice"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"numbers {{ lookup
\"
secrets.dev.slice
\"
}}"
,
data
:
""
,
want
:
"numbers [1 2 3]"
,
wantErr
:
false
,
getCnt
:
func
()
(
*
content
.
Content
,
func
(
t
*
testing
.
T
))
{
itemsSvc
:=
&
mocksitems
.
Items
{}
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{
"id"
:
"dev"
,
"slice"
:
[]
int
{
1
,
2
,
3
},
},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
},
func
(
t
*
testing
.
T
)
{
itemsSvc
.
AssertExpectations
(
t
)
}
}},
{
name
:
"lookup with empty Data"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"numbers {{ lookup
\"
secrets.dev.slice
\"
}}"
,
data
:
""
,
want
:
"numbers <no value>"
,
wantErr
:
false
,
getCnt
:
func
()
(
*
content
.
Content
,
func
(
t
*
testing
.
T
))
{
itemsSvc
:=
&
mocksitems
.
Items
{}
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
},
func
(
t
*
testing
.
T
)
{
itemsSvc
.
AssertExpectations
(
t
)
}
}},
{
name
:
"lookup with incorrect field"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"hello {{ lookup
\"
secrets.dev.incorrect
\"
}}"
,
data
:
""
,
want
:
"hello <no value>"
,
wantErr
:
false
,
getCnt
:
func
()
(
*
content
.
Content
,
func
(
t
*
testing
.
T
))
{
itemsSvc
:=
&
mocksitems
.
Items
{}
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{
"id"
:
"dev"
,
"key"
:
"1234"
,
},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
},
func
(
t
*
testing
.
T
)
{
itemsSvc
.
AssertExpectations
(
t
)
}
}},
{
name
:
"lookup not found"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"hello {{ lookup
\"
secrets.prod.pass
\"
}}"
,
data
:
""
,
want
:
""
,
wantErr
:
true
,
getCnt
:
func
()
(
*
content
.
Content
,
func
(
t
*
testing
.
T
))
{
itemsSvc
:=
&
mocksitems
.
Items
{}
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"prod"
)
.
Return
(
nil
,
errors
.
New
(
"not found"
))
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
},
func
(
t
*
testing
.
T
)
{
itemsSvc
.
AssertExpectations
(
t
)
}
}},
{
name
:
"lookup"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"hello, {{ lookup
\"
secrets.dev.key
\"
}}"
,
data
:
""
,
want
:
"hello, Luk"
,
wantErr
:
false
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
itemsSvc
:=
mocksitems
.
NewItems
(
t
)
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{
"id"
:
"dev"
,
"key"
:
"Luk"
,
},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
}
},
},
{
name
:
"lookup with slice"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"numbers {{ lookup
\"
secrets.dev.slice
\"
}}"
,
data
:
""
,
want
:
"numbers [1 2 3]"
,
wantErr
:
false
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
itemsSvc
:=
mocksitems
.
NewItems
(
t
)
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{
"id"
:
"dev"
,
"slice"
:
[]
int
{
1
,
2
,
3
},
},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
}
},
},
{
name
:
"lookup with empty Data"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"numbers {{ lookup
\"
secrets.dev.slice
\"
}}"
,
data
:
""
,
want
:
"numbers <no value>"
,
wantErr
:
false
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
itemsSvc
:=
mocksitems
.
NewItems
(
t
)
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
}
},
},
{
name
:
"lookup with incorrect field"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"hello {{ lookup
\"
secrets.dev.incorrect
\"
}}"
,
data
:
""
,
want
:
"hello <no value>"
,
wantErr
:
false
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
itemsSvc
:=
mocksitems
.
NewItems
(
t
)
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"dev"
)
.
Return
(
&
items
.
Item
{
ID
:
"dev"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollectionID
:
"secrets"
,
Data
:
map
[
string
]
interface
{}{
"id"
:
"dev"
,
"key"
:
"1234"
,
},
},
nil
)
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
}
},
},
{
name
:
"lookup not found"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"hello {{ lookup
\"
secrets.prod.pass
\"
}}"
,
data
:
""
,
want
:
""
,
wantErr
:
true
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
itemsSvc
:=
mocksitems
.
NewItems
(
t
)
itemsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"secrets"
,
"prod"
)
.
Return
(
nil
,
errors
.
New
(
"not found"
))
.
Once
()
return
&
content
.
Content
{
Items
:
itemsSvc
}
},
},
{
name
:
"lookup without itemID"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
str
:
"hello {{ lookup
\"
secrets.pass
\"
}}"
,
data
:
""
,
want
:
""
,
wantErr
:
true
},
{
name
:
"system#1"
,
SpaceID
:
"space"
,
str
:
"hello {{ system.SpaceID }}"
,
data
:
""
,
want
:
"hello space"
,
wantErr
:
false
},
{
name
:
"system space"
,
SpaceID
:
"space"
,
getCnt
:
func
(
)
(
cnt
*
content
.
Content
,
assertExpectations
func
(
t
*
testing
.
T
))
{
spsSvc
:=
&
spsmocks
.
Spaces
{}
{
name
:
"system space"
,
SpaceID
:
"space"
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
spsSvc
:=
spsmocks
.
New
Spaces
(
t
)
spsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
)
.
Return
(
&
spaces
.
Space
{
Description
:
"description"
},
nil
)
.
Once
()
return
&
content
.
Content
{
Spaces
:
spsSvc
}
,
func
(
t
*
testing
.
T
)
{
spsSvc
.
AssertExpectations
(
t
)
}
return
&
content
.
Content
{
Spaces
:
spsSvc
}
},
str
:
"{{ system.Space.Description }}"
,
want
:
"description"
,
wantErr
:
false
},
{
name
:
"system environment"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
getCnt
:
func
(
)
(
cnt
*
content
.
Content
,
assertExpectations
func
(
t
*
testing
.
T
))
{
envsSvc
:=
&
envsmocks
.
Environments
{}
{
name
:
"system environment"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
envsSvc
:=
envsmocks
.
New
Environments
(
t
)
envsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
)
.
Return
(
&
environments
.
Environment
{
Aliases
:
[]
string
{
"master"
}},
nil
)
.
Once
()
return
&
content
.
Content
{
Environments
:
envsSvc
}
,
func
(
t
*
testing
.
T
)
{
envsSvc
.
AssertExpectations
(
t
)
}
return
&
content
.
Content
{
Environments
:
envsSvc
}
},
str
:
"{{ system.Environment.Aliases }}"
,
want
:
"[master]"
,
wantErr
:
false
},
{
name
:
"system collection"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollID
:
"col"
,
getCnt
:
func
()
(
cnt
*
content
.
Content
,
assertExpectations
func
(
t
*
testing
.
T
))
{
collsSvc
:=
&
colsmocks
.
Collections
{}
collsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"col"
)
.
Return
(
&
collections
.
Collection
{
Name
:
"cars"
},
nil
)
.
Once
()
return
&
content
.
Content
{
Collections
:
collsSvc
},
func
(
t
*
testing
.
T
)
{
collsSvc
.
AssertExpectations
(
t
)
}
},
str
:
"{{ system.Collection.Name }}"
,
want
:
"cars"
,
wantErr
:
false
},
{
name
:
"system collection"
,
SpaceID
:
"space"
,
EnvID
:
"env"
,
CollID
:
"col"
,
getCnt
:
func
(
t
*
testing
.
T
)
*
content
.
Content
{
collsSvc
:=
colsmocks
.
NewCollections
(
t
)
collsSvc
.
On
(
"Get"
,
context
.
Background
(),
"space"
,
"env"
,
"col"
)
.
Return
(
&
collections
.
Collection
{
Name
:
"cars"
},
nil
)
.
Once
()
return
&
content
.
Content
{
Collections
:
collsSvc
}
},
str
:
"{{ system.Collection.Name }}"
,
want
:
"cars"
,
wantErr
:
false
,
},
{
name
:
"system without account"
,
SpaceID
:
"space"
,
str
:
"hello {{ system.Organization.Name }}"
,
want
:
""
,
wantErr
:
true
},
{
name
:
"with html builder"
,
str
:
"{{ . }}"
,
data
:
"<script>alert(localStorage.secret)</script>"
,
want
:
"<script>alert(localStorage.secret)</script>"
,
htmlBuilder
:
true
,
wantErr
:
false
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
var
cnt
*
content
.
Content
if
tt
.
getCnt
!=
nil
{
var
checkFn
func
(
*
testing
.
T
)
cnt
,
checkFn
=
tt
.
getCnt
()
defer
checkFn
(
t
)
cnt
=
tt
.
getCnt
(
t
)
}
b
:=
&
Builder
{
ctx
:
context
.
Background
(),
cnt
:
cnt
,
SpaceID
:
tt
.
SpaceID
,
EnvID
:
tt
.
EnvID
,
CollID
:
tt
.
CollID
,
var
b
Builder
if
tt
.
htmlBuilder
{
b
=
NewHTMLBuilder
(
cnt
,
tt
.
SpaceID
,
tt
.
EnvID
,
tt
.
CollID
)
}
else
{
b
=
NewBuilder
(
cnt
,
tt
.
SpaceID
,
tt
.
EnvID
,
tt
.
CollID
)
}
got
,
err
:=
b
.
Execute
(
tt
.
str
,
tt
.
data
)
if
tt
.
wantErr
==
true
{
assert
.
Error
(
t
,
err
)
...
...
@@ -187,16 +252,11 @@ func TestBuilder_ExecuteList(t *testing.T) {
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
itemsSvc
:=
&
mocksitems
.
Items
{}
itemsSvc
:=
mocksitems
.
New
Items
(
t
)
if
tt
.
itemsCall
!=
nil
{
tt
.
itemsCall
(
itemsSvc
)
}
b
:=
&
Builder
{
ctx
:
context
.
Background
(),
cnt
:
&
content
.
Content
{
Items
:
itemsSvc
},
SpaceID
:
tt
.
SpaceID
,
EnvID
:
tt
.
EnvID
,
}
b
:=
NewBuilder
(
&
content
.
Content
{
Items
:
itemsSvc
},
tt
.
SpaceID
,
tt
.
EnvID
,
""
)
got
,
err
:=
b
.
ExecuteList
(
tt
.
str
,
tt
.
data
)
if
tt
.
wantErr
==
true
{
...
...
@@ -205,7 +265,6 @@ func TestBuilder_ExecuteList(t *testing.T) {
assert
.
NoError
(
t
,
err
)
}
assert
.
Equal
(
t
,
tt
.
want
,
got
)
itemsSvc
.
AssertExpectations
(
t
)
})
}
}
...
...
@@ -256,16 +315,11 @@ func TestBuilder_ExecuteMap(t *testing.T) {
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
itemsSvc
:=
&
mocksitems
.
Items
{}
itemsSvc
:=
mocksitems
.
New
Items
(
t
)
if
tt
.
itemsCall
!=
nil
{
tt
.
itemsCall
(
itemsSvc
)
}
b
:=
&
Builder
{
ctx
:
context
.
Background
(),
cnt
:
&
content
.
Content
{
Items
:
itemsSvc
},
SpaceID
:
tt
.
SpaceID
,
EnvID
:
tt
.
EnvID
,
}
b
:=
NewBuilder
(
&
content
.
Content
{
Items
:
itemsSvc
},
tt
.
SpaceID
,
tt
.
EnvID
,
""
)
got
,
err
:=
b
.
ExecuteMap
(
tt
.
str
,
tt
.
data
)
if
tt
.
wantErr
==
true
{
...
...
@@ -274,7 +328,6 @@ func TestBuilder_ExecuteMap(t *testing.T) {
assert
.
NoError
(
t
,
err
)
}
assert
.
Equal
(
t
,
tt
.
want
,
got
)
itemsSvc
.
AssertExpectations
(
t
)
})
}
}
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment