Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
B
beego
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
go
beego
Commits
f6c508f1
Commit
f6c508f1
authored
Dec 21, 2015
by
fud
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'develop' of
https://github.com/astaxie/beego
into develop
parents
f9138c5a
130ce7eb
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
291 additions
and
44 deletions
+291
-44
acceptencoder.go
context/acceptencoder.go
+8
-3
types.go
orm/types.go
+261
-31
cors_test.go
plugins/cors/cors_test.go
+4
-4
staticfile_test.go
staticfile_test.go
+2
-2
tree.go
tree.go
+16
-4
No files found.
context/acceptencoder.go
View file @
f6c508f1
...
@@ -18,6 +18,7 @@ import (
...
@@ -18,6 +18,7 @@ import (
"bytes"
"bytes"
"compress/flate"
"compress/flate"
"compress/gzip"
"compress/gzip"
"compress/zlib"
"io"
"io"
"net/http"
"net/http"
"os"
"os"
...
@@ -31,9 +32,13 @@ type acceptEncoder struct {
...
@@ -31,9 +32,13 @@ type acceptEncoder struct {
}
}
var
(
var
(
noneCompressEncoder
=
acceptEncoder
{
""
,
func
(
wr
io
.
Writer
,
level
int
)
(
io
.
Writer
,
error
)
{
return
wr
,
nil
}}
noneCompressEncoder
=
acceptEncoder
{
""
,
func
(
wr
io
.
Writer
,
level
int
)
(
io
.
Writer
,
error
)
{
return
wr
,
nil
}}
gzipCompressEncoder
=
acceptEncoder
{
"gzip"
,
func
(
wr
io
.
Writer
,
level
int
)
(
io
.
Writer
,
error
)
{
return
gzip
.
NewWriterLevel
(
wr
,
level
)
}}
gzipCompressEncoder
=
acceptEncoder
{
"gzip"
,
func
(
wr
io
.
Writer
,
level
int
)
(
io
.
Writer
,
error
)
{
return
gzip
.
NewWriterLevel
(
wr
,
level
)
}}
deflateCompressEncoder
=
acceptEncoder
{
"deflate"
,
func
(
wr
io
.
Writer
,
level
int
)
(
io
.
Writer
,
error
)
{
return
flate
.
NewWriter
(
wr
,
level
)
}}
//according to the sec :http://tools.ietf.org/html/rfc2616#section-3.5 ,the deflate compress in http is zlib indeed
//deflate
//The "zlib" format defined in RFC 1950 [31] in combination with
//the "deflate" compression mechanism described in RFC 1951 [29].
deflateCompressEncoder
=
acceptEncoder
{
"deflate"
,
func
(
wr
io
.
Writer
,
level
int
)
(
io
.
Writer
,
error
)
{
return
zlib
.
NewWriterLevel
(
wr
,
level
)
}}
)
)
var
(
var
(
...
...
orm/types.go
View file @
f6c508f1
...
@@ -36,20 +36,77 @@ type Fielder interface {
...
@@ -36,20 +36,77 @@ type Fielder interface {
// Ormer define the orm interface
// Ormer define the orm interface
type
Ormer
interface
{
type
Ormer
interface
{
Read
(
interface
{},
...
string
)
error
// read data to model
ReadOrCreate
(
interface
{},
string
,
...
string
)
(
bool
,
int64
,
error
)
// for example:
// this will find User by Id field
// u = &User{Id: user.Id}
// err = Ormer.Read(u)
// this will find User by UserName field
// u = &User{UserName: "astaxie", Password: "pass"}
// err = Ormer.Read(u, "UserName")
Read
(
md
interface
{},
cols
...
string
)
error
// Try to read a row from the database, or insert one if it doesn't exist
ReadOrCreate
(
md
interface
{},
col1
string
,
cols
...
string
)
(
bool
,
int64
,
error
)
// insert model data to database
// for example:
// user := new(User)
// id, err = Ormer.Insert(user)
// user must a pointer and Insert will set user's pk field
Insert
(
interface
{})
(
int64
,
error
)
Insert
(
interface
{})
(
int64
,
error
)
InsertMulti
(
int
,
interface
{})
(
int64
,
error
)
// insert some models to database
Update
(
interface
{},
...
string
)
(
int64
,
error
)
InsertMulti
(
bulk
int
,
mds
interface
{})
(
int64
,
error
)
Delete
(
interface
{})
(
int64
,
error
)
// update model to database.
LoadRelated
(
interface
{},
string
,
...
interface
{})
(
int64
,
error
)
// cols set the columns those want to update.
QueryM2M
(
interface
{},
string
)
QueryM2Mer
// find model by Id(pk) field and update columns specified by fields, if cols is null then update all columns
QueryTable
(
interface
{})
QuerySeter
// for example:
Using
(
string
)
error
// user := User{Id: 2}
// user.Langs = append(user.Langs, "zh-CN", "en-US")
// user.Extra.Name = "beego"
// user.Extra.Data = "orm"
// num, err = Ormer.Update(&user, "Langs", "Extra")
Update
(
md
interface
{},
cols
...
string
)
(
int64
,
error
)
// delete model in database
Delete
(
md
interface
{})
(
int64
,
error
)
// load related models to md model.
// args are limit, offset int and order string.
//
// example:
// Ormer.LoadRelated(post,"Tags")
// for _,tag := range post.Tags{...}
//args[0] bool true useDefaultRelsDepth ; false depth 0
//args[0] int loadRelationDepth
//args[1] int limit default limit 1000
//args[2] int offset default offset 0
//args[3] string order for example : "-Id"
// make sure the relation is defined in model struct tags.
LoadRelated
(
md
interface
{},
name
string
,
args
...
interface
{})
(
int64
,
error
)
// create a models to models queryer
// for example:
// post := Post{Id: 4}
// m2m := Ormer.QueryM2M(&post, "Tags")
QueryM2M
(
md
interface
{},
name
string
)
QueryM2Mer
// return a QuerySeter for table operations.
// table name can be string or struct.
// e.g. QueryTable("user"), QueryTable(&user{}) or QueryTable((*User)(nil)),
QueryTable
(
ptrStructOrTableName
interface
{})
QuerySeter
// switch to another registered database driver by given name.
Using
(
name
string
)
error
// begin transaction
// for example:
// o := NewOrm()
// err := o.Begin()
// ...
// err = o.Rollback()
Begin
()
error
Begin
()
error
// commit transaction
Commit
()
error
Commit
()
error
// rollback transaction
Rollback
()
error
Rollback
()
error
Raw
(
string
,
...
interface
{})
RawSeter
// return a raw query seter for raw sql string.
// for example:
// ormer.Raw("UPDATE `user` SET `user_name` = ? WHERE `user_name` = ?", "slene", "testing").Exec()
// // update user testing's name to slene
Raw
(
query
string
,
args
...
interface
{})
RawSeter
Driver
()
Driver
Driver
()
Driver
}
}
...
@@ -61,38 +118,164 @@ type Inserter interface {
...
@@ -61,38 +118,164 @@ type Inserter interface {
// QuerySeter query seter
// QuerySeter query seter
type
QuerySeter
interface
{
type
QuerySeter
interface
{
// add condition expression to QuerySeter.
// for example:
// filter by UserName == 'slene'
// qs.Filter("UserName", "slene")
// sql : left outer join profile on t0.id1==t1.id2 where t1.age == 28
// Filter("profile__Age", 28)
// // time compare
// qs.Filter("created", time.Now())
Filter
(
string
,
...
interface
{})
QuerySeter
Filter
(
string
,
...
interface
{})
QuerySeter
// add NOT condition to querySeter.
// have the same usage as Filter
Exclude
(
string
,
...
interface
{})
QuerySeter
Exclude
(
string
,
...
interface
{})
QuerySeter
// set condition to QuerySeter.
// sql's where condition
// cond := orm.NewCondition()
// cond1 := cond.And("profile__isnull", false).AndNot("status__in", 1).Or("profile__age__gt", 2000)
// //sql-> WHERE T0.`profile_id` IS NOT NULL AND NOT T0.`Status` IN (?) OR T1.`age` > 2000
// num, err := qs.SetCond(cond1).Count()
SetCond
(
*
Condition
)
QuerySeter
SetCond
(
*
Condition
)
QuerySeter
Limit
(
interface
{},
...
interface
{})
QuerySeter
// add LIMIT value.
Offset
(
interface
{})
QuerySeter
// args[0] means offset, e.g. LIMIT num,offset.
GroupBy
(
...
string
)
QuerySeter
// if Limit <= 0 then Limit will be set to default limit ,eg 1000
OrderBy
(
...
string
)
QuerySeter
// if QuerySeter doesn't call Limit, the sql's Limit will be set to default limit, eg 1000
Distinct
()
QuerySeter
// for example:
RelatedSel
(
...
interface
{})
QuerySeter
// qs.Limit(10, 2)
// // sql-> limit 10 offset 2
Limit
(
limit
interface
{},
args
...
interface
{})
QuerySeter
// add OFFSET value
// same as Limit function's args[0]
Offset
(
offset
interface
{})
QuerySeter
// add ORDER expression.
// "column" means ASC, "-column" means DESC.
// for example:
// qs.OrderBy("-status")
OrderBy
(
exprs
...
string
)
QuerySeter
// set relation model to query together.
// it will query relation models and assign to parent model.
// for example:
// // will load all related fields use left join .
// qs.RelatedSel().One(&user)
// // will load related field only profile
// qs.RelatedSel("profile").One(&user)
// user.Profile.Age = 32
RelatedSel
(
params
...
interface
{})
QuerySeter
// return QuerySeter execution result number
// for example:
// num, err = qs.Filter("profile__age__gt", 28).Count()
Count
()
(
int64
,
error
)
Count
()
(
int64
,
error
)
// check result empty or not after QuerySeter executed
// the same as QuerySeter.Count > 0
Exist
()
bool
Exist
()
bool
Update
(
Params
)
(
int64
,
error
)
// execute update with parameters
// for example:
// num, err = qs.Filter("user_name", "slene").Update(Params{
// "Nums": ColValue(Col_Minus, 50),
// }) // user slene's Nums will minus 50
// num, err = qs.Filter("UserName", "slene").Update(Params{
// "user_name": "slene2"
// }) // user slene's name will change to slene2
Update
(
values
Params
)
(
int64
,
error
)
// delete from table
//for example:
// num ,err = qs.Filter("user_name__in", "testing1", "testing2").Delete()
// //delete two user who's name is testing1 or testing2
Delete
()
(
int64
,
error
)
Delete
()
(
int64
,
error
)
// return a insert queryer.
// it can be used in times.
// example:
// i,err := sq.PrepareInsert()
// num, err = i.Insert(&user1) // user table will add one record user1 at once
// num, err = i.Insert(&user2) // user table will add one record user2 at once
// err = i.Close() //don't forget call Close
PrepareInsert
()
(
Inserter
,
error
)
PrepareInsert
()
(
Inserter
,
error
)
All
(
interface
{},
...
string
)
(
int64
,
error
)
// query all data and map to containers.
One
(
interface
{},
...
string
)
error
// cols means the columns when querying.
Values
(
*
[]
Params
,
...
string
)
(
int64
,
error
)
// for example:
ValuesList
(
*
[]
ParamsList
,
...
string
)
(
int64
,
error
)
// var users []*User
ValuesFlat
(
*
ParamsList
,
string
)
(
int64
,
error
)
// qs.All(&users) // users[0],users[1],users[2] ...
RowsToMap
(
*
Params
,
string
,
string
)
(
int64
,
error
)
All
(
container
interface
{},
cols
...
string
)
(
int64
,
error
)
RowsToStruct
(
interface
{},
string
,
string
)
(
int64
,
error
)
// query one row data and map to containers.
// cols means the columns when querying.
// for example:
// var user User
// qs.One(&user) //user.UserName == "slene"
One
(
container
interface
{},
cols
...
string
)
error
// query all data and map to []map[string]interface.
// expres means condition expression.
// it converts data to []map[column]value.
// for example:
// var maps []Params
// qs.Values(&maps) //maps[0]["UserName"]=="slene"
Values
(
results
*
[]
Params
,
exprs
...
string
)
(
int64
,
error
)
// query all data and map to [][]interface
// it converts data to [][column_index]value
// for example:
// var list []ParamsList
// qs.ValuesList(&list) // list[0][1] == "slene"
ValuesList
(
results
*
[]
ParamsList
,
exprs
...
string
)
(
int64
,
error
)
// query all data and map to []interface.
// it's designed for one column record set, auto change to []value, not [][column]value.
// for example:
// var list ParamsList
// qs.ValuesFlat(&list, "UserName") // list[0] == "slene"
ValuesFlat
(
result
*
ParamsList
,
expr
string
)
(
int64
,
error
)
// query all rows into map[string]interface with specify key and value column name.
// keyCol = "name", valueCol = "value"
// table data
// name | value
// total | 100
// found | 200
// to map[string]interface{}{
// "total": 100,
// "found": 200,
// }
RowsToMap
(
result
*
Params
,
keyCol
,
valueCol
string
)
(
int64
,
error
)
// query all rows into struct with specify key and value column name.
// keyCol = "name", valueCol = "value"
// table data
// name | value
// total | 100
// found | 200
// to struct {
// Total int
// Found int
// }
RowsToStruct
(
ptrStruct
interface
{},
keyCol
,
valueCol
string
)
(
int64
,
error
)
}
}
// QueryM2Mer model to model query struct
// QueryM2Mer model to model query struct
// all operations are on the m2m table only, will not affect the origin model table
type
QueryM2Mer
interface
{
type
QueryM2Mer
interface
{
// add models to origin models when creating queryM2M.
// example:
// m2m := orm.QueryM2M(post,"Tag")
// m2m.Add(&Tag1{},&Tag2{})
// for _,tag := range post.Tags{}{ ... }
// param could also be any of the follow
// []*Tag{{Id:3,Name: "TestTag1"}, {Id:4,Name: "TestTag2"}}
// &Tag{Id:5,Name: "TestTag3"}
// []interface{}{&Tag{Id:6,Name: "TestTag4"}}
// insert one or more rows to m2m table
// make sure the relation is defined in post model struct tag.
Add
(
...
interface
{})
(
int64
,
error
)
Add
(
...
interface
{})
(
int64
,
error
)
// remove models following the origin model relationship
// only delete rows from m2m table
// for example:
//tag3 := &Tag{Id:5,Name: "TestTag3"}
//num, err = m2m.Remove(tag3)
Remove
(
...
interface
{})
(
int64
,
error
)
Remove
(
...
interface
{})
(
int64
,
error
)
// check model is existed in relationship of origin model
Exist
(
interface
{})
bool
Exist
(
interface
{})
bool
// clean all models in related of origin model
Clear
()
(
int64
,
error
)
Clear
()
(
int64
,
error
)
// count all related models of origin model
Count
()
(
int64
,
error
)
Count
()
(
int64
,
error
)
}
}
// RawPreparer raw query statement
// RawPreparer raw query statement
type
RawPreparer
interface
{
type
RawPreparer
interface
{
Exec
(
...
interface
{})
(
sql
.
Result
,
error
)
Exec
(
...
interface
{})
(
sql
.
Result
,
error
)
...
@@ -100,16 +283,63 @@ type RawPreparer interface {
...
@@ -100,16 +283,63 @@ type RawPreparer interface {
}
}
// RawSeter raw query seter
// RawSeter raw query seter
// create From Ormer.Raw
// for example:
// sql := fmt.Sprintf("SELECT %sid%s,%sname%s FROM %suser%s WHERE id = ?",Q,Q,Q,Q,Q,Q)
// rs := Ormer.Raw(sql, 1)
type
RawSeter
interface
{
type
RawSeter
interface
{
//execute sql and get result
Exec
()
(
sql
.
Result
,
error
)
Exec
()
(
sql
.
Result
,
error
)
QueryRow
(
...
interface
{})
error
//query data and map to container
QueryRows
(
...
interface
{})
(
int64
,
error
)
//for example:
// var name string
// var id int
// rs.QueryRow(&id,&name) // id==2 name=="slene"
QueryRow
(
containers
...
interface
{})
error
// query data rows and map to container
// var ids []int
// var names []int
// query = fmt.Sprintf("SELECT 'id','name' FROM %suser%s", Q, Q)
// num, err = dORM.Raw(query).QueryRows(&ids,&names) // ids=>{1,2},names=>{"nobody","slene"}
QueryRows
(
containers
...
interface
{})
(
int64
,
error
)
SetArgs
(
...
interface
{})
RawSeter
SetArgs
(
...
interface
{})
RawSeter
Values
(
*
[]
Params
,
...
string
)
(
int64
,
error
)
// query data to []map[string]interface
ValuesList
(
*
[]
ParamsList
,
...
string
)
(
int64
,
error
)
// see QuerySeter's Values
ValuesFlat
(
*
ParamsList
,
...
string
)
(
int64
,
error
)
Values
(
container
*
[]
Params
,
cols
...
string
)
(
int64
,
error
)
RowsToMap
(
*
Params
,
string
,
string
)
(
int64
,
error
)
// query data to [][]interface
RowsToStruct
(
interface
{},
string
,
string
)
(
int64
,
error
)
// see QuerySeter's ValuesList
ValuesList
(
container
*
[]
ParamsList
,
cols
...
string
)
(
int64
,
error
)
// query data to []interface
// see QuerySeter's ValuesFlat
ValuesFlat
(
container
*
ParamsList
,
cols
...
string
)
(
int64
,
error
)
// query all rows into map[string]interface with specify key and value column name.
// keyCol = "name", valueCol = "value"
// table data
// name | value
// total | 100
// found | 200
// to map[string]interface{}{
// "total": 100,
// "found": 200,
// }
RowsToMap
(
result
*
Params
,
keyCol
,
valueCol
string
)
(
int64
,
error
)
// query all rows into struct with specify key and value column name.
// keyCol = "name", valueCol = "value"
// table data
// name | value
// total | 100
// found | 200
// to struct {
// Total int
// Found int
// }
RowsToStruct
(
ptrStruct
interface
{},
keyCol
,
valueCol
string
)
(
int64
,
error
)
// return prepared raw statement for used in times.
// for example:
// pre, err := dORM.Raw("INSERT INTO tag (name) VALUES (?)").Prepare()
// r, err := pre.Exec("name1") // INSERT INTO tag (name) VALUES (`name1`)
Prepare
()
(
RawPreparer
,
error
)
Prepare
()
(
RawPreparer
,
error
)
}
}
...
...
plugins/cors/cors_test.go
View file @
f6c508f1
...
@@ -225,8 +225,8 @@ func Benchmark_WithoutCORS(b *testing.B) {
...
@@ -225,8 +225,8 @@ func Benchmark_WithoutCORS(b *testing.B) {
ctx
.
Output
.
SetStatus
(
500
)
ctx
.
Output
.
SetStatus
(
500
)
})
})
b
.
ResetTimer
()
b
.
ResetTimer
()
for
i
:=
0
;
i
<
100
;
i
++
{
r
,
_
:=
http
.
NewRequest
(
"PUT"
,
"/foo"
,
nil
)
r
,
_
:=
http
.
NewRequest
(
"PUT"
,
"/foo"
,
nil
)
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
handler
.
ServeHTTP
(
recorder
,
r
)
handler
.
ServeHTTP
(
recorder
,
r
)
}
}
}
}
...
@@ -246,8 +246,8 @@ func Benchmark_WithCORS(b *testing.B) {
...
@@ -246,8 +246,8 @@ func Benchmark_WithCORS(b *testing.B) {
ctx
.
Output
.
SetStatus
(
500
)
ctx
.
Output
.
SetStatus
(
500
)
})
})
b
.
ResetTimer
()
b
.
ResetTimer
()
for
i
:=
0
;
i
<
100
;
i
++
{
r
,
_
:=
http
.
NewRequest
(
"PUT"
,
"/foo"
,
nil
)
r
,
_
:=
http
.
NewRequest
(
"PUT"
,
"/foo"
,
nil
)
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
handler
.
ServeHTTP
(
recorder
,
r
)
handler
.
ServeHTTP
(
recorder
,
r
)
}
}
}
}
staticfile_test.go
View file @
f6c508f1
...
@@ -2,8 +2,8 @@ package beego
...
@@ -2,8 +2,8 @@ package beego
import
(
import
(
"bytes"
"bytes"
"compress/flate"
"compress/gzip"
"compress/gzip"
"compress/zlib"
"io"
"io"
"io/ioutil"
"io/ioutil"
"os"
"os"
...
@@ -43,7 +43,7 @@ func TestOpenStaticFileGzip_1(t *testing.T) {
...
@@ -43,7 +43,7 @@ func TestOpenStaticFileGzip_1(t *testing.T) {
func
TestOpenStaticFileDeflate_1
(
t
*
testing
.
T
)
{
func
TestOpenStaticFileDeflate_1
(
t
*
testing
.
T
)
{
file
,
_
:=
os
.
Open
(
licenseFile
)
file
,
_
:=
os
.
Open
(
licenseFile
)
var
zipBuf
bytes
.
Buffer
var
zipBuf
bytes
.
Buffer
fileWriter
,
_
:=
flate
.
NewWriter
(
&
zipBuf
,
flate
.
BestCompression
)
fileWriter
,
_
:=
zlib
.
NewWriterLevel
(
&
zipBuf
,
zlib
.
BestCompression
)
io
.
Copy
(
fileWriter
,
file
)
io
.
Copy
(
fileWriter
,
file
)
fileWriter
.
Close
()
fileWriter
.
Close
()
content
,
_
:=
ioutil
.
ReadAll
(
&
zipBuf
)
content
,
_
:=
ioutil
.
ReadAll
(
&
zipBuf
)
...
...
tree.go
View file @
f6c508f1
...
@@ -334,7 +334,7 @@ func (t *Tree) match(pattern string, wildcardValues []string, ctx *context.Conte
...
@@ -334,7 +334,7 @@ func (t *Tree) match(pattern string, wildcardValues []string, ctx *context.Conte
}
}
}
}
}
}
if
runObject
==
nil
{
if
runObject
==
nil
&&
len
(
t
.
fixrouters
)
>
0
{
// Filter the .json .xml .html extension
// Filter the .json .xml .html extension
for
_
,
str
:=
range
allowSuffixExt
{
for
_
,
str
:=
range
allowSuffixExt
{
if
strings
.
HasSuffix
(
seg
,
str
)
{
if
strings
.
HasSuffix
(
seg
,
str
)
{
...
@@ -353,11 +353,23 @@ func (t *Tree) match(pattern string, wildcardValues []string, ctx *context.Conte
...
@@ -353,11 +353,23 @@ func (t *Tree) match(pattern string, wildcardValues []string, ctx *context.Conte
runObject
=
t
.
wildcard
.
match
(
pattern
,
append
(
wildcardValues
,
seg
),
ctx
)
runObject
=
t
.
wildcard
.
match
(
pattern
,
append
(
wildcardValues
,
seg
),
ctx
)
}
}
if
runObject
==
nil
{
if
runObject
==
nil
&&
len
(
t
.
leaves
)
>
0
{
segments
:=
splitPath
(
pattern
)
wildcardValues
=
append
(
wildcardValues
,
seg
)
wildcardValues
=
append
(
wildcardValues
,
seg
)
start
,
i
:=
0
,
0
for
;
i
<
len
(
pattern
);
i
++
{
if
pattern
[
i
]
==
'/'
{
if
i
!=
0
&&
start
<
len
(
pattern
)
{
wildcardValues
=
append
(
wildcardValues
,
pattern
[
start
:
i
])
}
start
=
i
+
1
continue
}
}
if
start
>
0
{
wildcardValues
=
append
(
wildcardValues
,
pattern
[
start
:
i
])
}
for
_
,
l
:=
range
t
.
leaves
{
for
_
,
l
:=
range
t
.
leaves
{
if
ok
:=
l
.
match
(
append
(
wildcardValues
,
segments
...
)
,
ctx
);
ok
{
if
ok
:=
l
.
match
(
wildcardValues
,
ctx
);
ok
{
return
l
.
runObject
return
l
.
runObject
}
}
}
}
...
...
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