Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yii2
Project
Overview
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
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Rotua Panjaitan
yii2
Commits
ca608a81
Commit
ca608a81
authored
Dec 05, 2013
by
Klimov Paul
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Mongo file Active Record updated.
parent
85a32bea
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
340 additions
and
75 deletions
+340
-75
ActiveRecord.php
extensions/mongo/ActiveRecord.php
+23
-13
ActiveRecord.php
extensions/mongo/file/ActiveRecord.php
+97
-60
Query.php
extensions/mongo/file/Query.php
+51
-0
ActiveRecord.php
tests/unit/data/ar/mongo/file/ActiveRecord.php
+17
-0
CustomerFile.php
tests/unit/data/ar/mongo/file/CustomerFile.php
+28
-0
ActiveRecordTest.php
tests/unit/extensions/mongo/file/ActiveRecordTest.php
+120
-0
QueryTest.php
tests/unit/extensions/mongo/file/QueryTest.php
+4
-2
No files found.
extensions/mongo/ActiveRecord.php
View file @
ca608a81
...
@@ -250,7 +250,7 @@ abstract class ActiveRecord extends BaseActiveRecord
...
@@ -250,7 +250,7 @@ abstract class ActiveRecord extends BaseActiveRecord
}
}
/**
/**
* @see
C
ActiveRecord::update()
* @see ActiveRecord::update()
* @throws StaleObjectException
* @throws StaleObjectException
*/
*/
protected
function
updateInternal
(
$attributes
=
null
)
protected
function
updateInternal
(
$attributes
=
null
)
...
@@ -309,24 +309,34 @@ abstract class ActiveRecord extends BaseActiveRecord
...
@@ -309,24 +309,34 @@ abstract class ActiveRecord extends BaseActiveRecord
{
{
$result
=
false
;
$result
=
false
;
if
(
$this
->
beforeDelete
())
{
if
(
$this
->
beforeDelete
())
{
// we do not check the return value of deleteAll() because it's possible
$result
=
$this
->
deleteInternal
();
// the record is already deleted in the database and thus the method will return 0
$condition
=
$this
->
getOldPrimaryKey
(
true
);
$lock
=
$this
->
optimisticLock
();
if
(
$lock
!==
null
)
{
$condition
[
$lock
]
=
$this
->
$lock
;
}
$result
=
static
::
getCollection
()
->
remove
(
$condition
);
if
(
$lock
!==
null
&&
!
$result
)
{
throw
new
StaleObjectException
(
'The object being deleted is outdated.'
);
}
$this
->
setOldAttributes
(
null
);
$this
->
afterDelete
();
$this
->
afterDelete
();
}
}
return
$result
;
return
$result
;
}
}
/**
/**
* @see ActiveRecord::delete()
* @throws StaleObjectException
*/
protected
function
deleteInternal
()
{
// we do not check the return value of deleteAll() because it's possible
// the record is already deleted in the database and thus the method will return 0
$condition
=
$this
->
getOldPrimaryKey
(
true
);
$lock
=
$this
->
optimisticLock
();
if
(
$lock
!==
null
)
{
$condition
[
$lock
]
=
$this
->
$lock
;
}
$result
=
static
::
getCollection
()
->
remove
(
$condition
);
if
(
$lock
!==
null
&&
!
$result
)
{
throw
new
StaleObjectException
(
'The object being deleted is outdated.'
);
}
$this
->
setOldAttributes
(
null
);
return
$result
;
}
/**
* Returns a value indicating whether the given active record is the same as the current one.
* Returns a value indicating whether the given active record is the same as the current one.
* The comparison is made by comparing the table names and the primary key values of the two active records.
* The comparison is made by comparing the table names and the primary key values of the two active records.
* If one of the records [[isNewRecord|is new]] they are also considered not equal.
* If one of the records [[isNewRecord|is new]] they are also considered not equal.
...
...
extensions/mongo/file/ActiveRecord.php
View file @
ca608a81
...
@@ -7,6 +7,10 @@
...
@@ -7,6 +7,10 @@
namespace
yii\mongo\file
;
namespace
yii\mongo\file
;
use
yii\base\InvalidParamException
;
use
yii\db\StaleObjectException
;
use
yii\web\UploadedFile
;
/**
/**
* ActiveRecord is the base class for classes representing Mongo GridFS files in terms of objects.
* ActiveRecord is the base class for classes representing Mongo GridFS files in terms of objects.
*
*
...
@@ -16,11 +20,6 @@ namespace yii\mongo\file;
...
@@ -16,11 +20,6 @@ namespace yii\mongo\file;
class
ActiveRecord
extends
\yii\mongo\ActiveRecord
class
ActiveRecord
extends
\yii\mongo\ActiveRecord
{
{
/**
/**
* @var \MongoGridFSFile|string
*/
public
$file
;
/**
* Creates an [[ActiveQuery]] instance.
* Creates an [[ActiveQuery]] instance.
* This method is called by [[find()]] to start a "find" command.
* This method is called by [[find()]] to start a "find" command.
* You may override this method to return a customized query (e.g. `CustomerQuery` specified
* You may override this method to return a customized query (e.g. `CustomerQuery` specified
...
@@ -54,29 +53,6 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
...
@@ -54,29 +53,6 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
}
}
/**
/**
* Creates an active record object using a row of data.
* This method is called by [[ActiveQuery]] to populate the query results
* into Active Records. It is not meant to be used to create new records.
* @param \MongoGridFSFile $row attribute values (name => value)
* @return ActiveRecord the newly created active record.
*/
public
static
function
create
(
$row
)
{
$record
=
static
::
instantiate
(
$row
);
$columns
=
array_flip
(
$record
->
attributes
());
foreach
(
$row
->
file
as
$name
=>
$value
)
{
if
(
isset
(
$columns
[
$name
]))
{
$record
->
setAttribute
(
$name
,
$value
);
}
else
{
$record
->
$name
=
$value
;
}
}
$record
->
setOldAttributes
(
$record
->
getAttributes
());
$record
->
afterFind
();
return
$record
;
}
/**
* Returns the list of all attribute names of the model.
* Returns the list of all attribute names of the model.
* This method could be overridden by child classes to define available attributes.
* This method could be overridden by child classes to define available attributes.
* Note: primary key attribute "_id" should be always present in returned array.
* Note: primary key attribute "_id" should be always present in returned array.
...
@@ -84,7 +60,16 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
...
@@ -84,7 +60,16 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
*/
*/
public
function
attributes
()
public
function
attributes
()
{
{
return
[
'id'
,
'filename'
];
return
[
'_id'
,
'filename'
,
'uploadDate'
,
'length'
,
'chunkSize'
,
'md5'
,
'file'
,
'newFileContent'
];
}
}
/**
/**
...
@@ -103,7 +88,30 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
...
@@ -103,7 +88,30 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
}
}
}
}
$collection
=
static
::
getCollection
();
$collection
=
static
::
getCollection
();
$newId
=
$collection
->
insert
(
$values
);
if
(
array_key_exists
(
'newFileContent'
,
$values
))
{
$fileContent
=
$values
[
'newFileContent'
];
unset
(
$values
[
'newFileContent'
]);
unset
(
$values
[
'file'
]);
$newId
=
$collection
->
storeBytes
(
$fileContent
,
$values
);
}
elseif
(
array_key_exists
(
'file'
,
$values
))
{
$file
=
$values
[
'file'
];
if
(
$file
instanceof
UploadedFile
)
{
$fileName
=
$file
->
tempName
;
}
elseif
(
is_string
(
$file
))
{
if
(
file_exists
(
$file
))
{
$fileName
=
$file
;
}
else
{
throw
new
InvalidParamException
(
"File '
{
$file
}
' does not exist."
);
}
}
else
{
throw
new
InvalidParamException
(
'Unsupported type of "file" attribute.'
);
}
unset
(
$values
[
'newFileContent'
]);
unset
(
$values
[
'file'
]);
$newId
=
$collection
->
storeFile
(
$fileName
,
$values
);
}
else
{
$newId
=
$collection
->
insert
(
$values
);
}
$this
->
setAttribute
(
'_id'
,
$newId
);
$this
->
setAttribute
(
'_id'
,
$newId
);
foreach
(
$values
as
$name
=>
$value
)
{
foreach
(
$values
as
$name
=>
$value
)
{
$this
->
setOldAttribute
(
$name
,
$value
);
$this
->
setOldAttribute
(
$name
,
$value
);
...
@@ -113,7 +121,7 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
...
@@ -113,7 +121,7 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
}
}
/**
/**
* @see
C
ActiveRecord::update()
* @see ActiveRecord::update()
* @throws StaleObjectException
* @throws StaleObjectException
*/
*/
protected
function
updateInternal
(
$attributes
=
null
)
protected
function
updateInternal
(
$attributes
=
null
)
...
@@ -126,20 +134,50 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
...
@@ -126,20 +134,50 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
$this
->
afterSave
(
false
);
$this
->
afterSave
(
false
);
return
0
;
return
0
;
}
}
$condition
=
$this
->
getOldPrimaryKey
(
true
);
$lock
=
$this
->
optimisticLock
();
if
(
$lock
!==
null
)
{
if
(
!
isset
(
$values
[
$lock
]))
{
$values
[
$lock
]
=
$this
->
$lock
+
1
;
}
$condition
[
$lock
]
=
$this
->
$lock
;
}
// We do not check the return value of update() because it's possible
// that it doesn't change anything and thus returns 0.
$rows
=
static
::
getCollection
()
->
update
(
$condition
,
$values
);
if
(
$lock
!==
null
&&
!
$rows
)
{
$collection
=
static
::
getCollection
();
throw
new
StaleObjectException
(
'The object being updated is outdated.'
);
if
(
array_key_exists
(
'newFileContent'
,
$values
))
{
$fileContent
=
$values
[
'newFileContent'
];
unset
(
$values
[
'newFileContent'
]);
unset
(
$values
[
'file'
]);
$values
[
'_id'
]
=
$this
->
getAttribute
(
'_id'
);
$this
->
deleteInternal
();
$collection
->
storeBytes
(
$fileContent
,
$values
);
$rows
=
1
;
}
elseif
(
array_key_exists
(
'file'
,
$values
))
{
$file
=
$values
[
'file'
];
if
(
$file
instanceof
UploadedFile
)
{
$fileName
=
$file
->
tempName
;
}
elseif
(
is_string
(
$file
))
{
if
(
file_exists
(
$file
))
{
$fileName
=
$file
;
}
else
{
throw
new
InvalidParamException
(
"File '
{
$file
}
' does not exist."
);
}
}
else
{
throw
new
InvalidParamException
(
'Unsupported type of "file" attribute.'
);
}
unset
(
$values
[
'newFileContent'
]);
unset
(
$values
[
'file'
]);
$values
[
'_id'
]
=
$this
->
getAttribute
(
'_id'
);
$this
->
deleteInternal
();
$collection
->
storeFile
(
$fileName
,
$values
);
$rows
=
1
;
}
else
{
$condition
=
$this
->
getOldPrimaryKey
(
true
);
$lock
=
$this
->
optimisticLock
();
if
(
$lock
!==
null
)
{
if
(
!
isset
(
$values
[
$lock
]))
{
$values
[
$lock
]
=
$this
->
$lock
+
1
;
}
$condition
[
$lock
]
=
$this
->
$lock
;
}
// We do not check the return value of update() because it's possible
// that it doesn't change anything and thus returns 0.
$rows
=
$collection
->
update
(
$condition
,
$values
);
if
(
$lock
!==
null
&&
!
$rows
)
{
throw
new
StaleObjectException
(
'The object being updated is outdated.'
);
}
}
}
foreach
(
$values
as
$name
=>
$value
)
{
foreach
(
$values
as
$name
=>
$value
)
{
...
@@ -149,26 +187,26 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
...
@@ -149,26 +187,26 @@ class ActiveRecord extends \yii\mongo\ActiveRecord
return
$rows
;
return
$rows
;
}
}
public
function
getContent
()
/**
* Returns the associated file content.
* @return null|string file content.
* @throws \yii\base\InvalidParamException on invalid file value.
*/
public
function
getFileContent
()
{
{
$file
=
$this
->
getAttribute
(
'file'
);
$file
=
$this
->
getAttribute
(
'file'
);
if
(
empty
(
$file
))
{
if
(
empty
(
$file
))
{
return
null
;
return
null
;
}
}
elseif
(
$file
instanceof
\MongoGridFSFile
)
{
if
(
$file
instanceof
\MongoGridFSFile
)
{
return
$file
->
getBytes
();
return
$file
->
getBytes
();
}
elseif
(
$file
instanceof
UploadedFile
)
{
return
file_get_contents
(
$file
->
tempName
);
}
elseif
(
is_string
(
$file
))
{
if
(
file_exists
(
$file
))
{
return
file_get_contents
(
$file
);
}
}
else
{
throw
new
InvalidParamException
(
'Unsupported type of "file" attribute.'
);
}
}
}
}
public
function
getFileName
()
{
$file
=
$this
->
getAttribute
(
'file'
);
if
(
empty
(
$file
))
{
return
null
;
}
if
(
$file
instanceof
\MongoGridFSFile
)
{
return
$file
->
getFilename
();
}
}
}
}
\ No newline at end of file
extensions/mongo/file/Query.php
View file @
ca608a81
...
@@ -8,6 +8,8 @@
...
@@ -8,6 +8,8 @@
namespace
yii\mongo\file
;
namespace
yii\mongo\file
;
use
Yii
;
use
Yii
;
use
yii\helpers\Json
;
use
yii\mongo\Exception
;
/**
/**
* Class Query
* Class Query
...
@@ -29,4 +31,52 @@ class Query extends \yii\mongo\Query
...
@@ -29,4 +31,52 @@ class Query extends \yii\mongo\Query
}
}
return
$db
->
getFileCollection
(
$this
->
from
);
return
$db
->
getFileCollection
(
$this
->
from
);
}
}
/**
* Fetches rows from the given Mongo cursor.
* @param \MongoCursor $cursor Mongo cursor instance to fetch data from.
* @param boolean $all whether to fetch all rows or only first one.
* @param string|callable $indexBy the column name or PHP callback,
* by which the query results should be indexed by.
* @throws Exception on failure.
* @return array|boolean result.
*/
protected
function
fetchRows
(
\MongoCursor
$cursor
,
$all
=
true
,
$indexBy
=
null
)
{
$token
=
'Querying: '
.
Json
::
encode
(
$cursor
->
info
());
Yii
::
info
(
$token
,
__METHOD__
);
try
{
Yii
::
beginProfile
(
$token
,
__METHOD__
);
$result
=
[];
if
(
$all
)
{
foreach
(
$cursor
as
$file
)
{
$row
=
$file
->
file
;
$row
[
'file'
]
=
$file
;
if
(
$indexBy
!==
null
)
{
if
(
is_string
(
$indexBy
))
{
$key
=
$row
[
$indexBy
];
}
else
{
$key
=
call_user_func
(
$indexBy
,
$row
);
}
$result
[
$key
]
=
$row
;
}
else
{
$result
[]
=
$row
;
}
}
}
else
{
if
(
$cursor
->
hasNext
())
{
$file
=
$cursor
->
getNext
();
$result
=
$file
->
file
;
$result
[
'file'
]
=
$file
;
}
else
{
$result
=
false
;
}
}
Yii
::
endProfile
(
$token
,
__METHOD__
);
return
$result
;
}
catch
(
\Exception
$e
)
{
Yii
::
endProfile
(
$token
,
__METHOD__
);
throw
new
Exception
(
$e
->
getMessage
(),
(
int
)
$e
->
getCode
(),
$e
);
}
}
}
}
\ No newline at end of file
tests/unit/data/ar/mongo/file/ActiveRecord.php
0 → 100644
View file @
ca608a81
<?php
namespace
yiiunit\data\ar\mongo\file
;
/**
* Test Mongo ActiveRecord
*/
class
ActiveRecord
extends
\yii\mongo\file\ActiveRecord
{
public
static
$db
;
public
static
function
getDb
()
{
return
self
::
$db
;
}
}
\ No newline at end of file
tests/unit/data/ar/mongo/file/CustomerFile.php
0 → 100644
View file @
ca608a81
<?php
namespace
yiiunit\data\ar\mongo\file
;
class
CustomerFile
extends
ActiveRecord
{
public
static
function
collectionName
()
{
return
'customer_fs'
;
}
public
function
attributes
()
{
return
array_merge
(
parent
::
attributes
(),
[
'tag'
,
'status'
,
]
);
}
public
static
function
activeOnly
(
$query
)
{
$query
->
andWhere
([
'status'
=>
2
]);
}
}
\ No newline at end of file
tests/unit/extensions/mongo/file/ActiveRecordTest.php
0 → 100644
View file @
ca608a81
<?php
namespace
yiiunit\extensions\mongo\file
;
use
yiiunit\extensions\mongo\MongoTestCase
;
use
yii\mongo\file\ActiveQuery
;
use
yiiunit\data\ar\mongo\file\ActiveRecord
;
use
yiiunit\data\ar\mongo\file\CustomerFile
;
/**
* @group mongo
*/
class
ActiveRecordTest
extends
MongoTestCase
{
/**
* @var array[] list of test rows.
*/
protected
$testRows
=
[];
protected
function
setUp
()
{
parent
::
setUp
();
ActiveRecord
::
$db
=
$this
->
getConnection
();
$this
->
setUpTestRows
();
}
protected
function
tearDown
()
{
$this
->
dropFileCollection
(
CustomerFile
::
collectionName
());
parent
::
tearDown
();
}
/**
* Sets up test rows.
*/
protected
function
setUpTestRows
()
{
$collection
=
$this
->
getConnection
()
->
getFileCollection
(
CustomerFile
::
collectionName
());
$rows
=
[];
for
(
$i
=
1
;
$i
<=
10
;
$i
++
)
{
$record
=
[
'tag'
=>
'tag'
.
$i
,
'status'
=>
$i
,
];
$content
=
'content'
.
$i
;
$record
[
'_id'
]
=
$collection
->
storeBytes
(
$content
,
$record
);
$record
[
'content'
]
=
$content
;
$rows
[]
=
$record
;
}
$this
->
testRows
=
$rows
;
}
// Tests :
public
function
testFind
()
{
// find one
$result
=
CustomerFile
::
find
();
$this
->
assertTrue
(
$result
instanceof
ActiveQuery
);
$customer
=
$result
->
one
();
$this
->
assertTrue
(
$customer
instanceof
CustomerFile
);
// find all
$customers
=
CustomerFile
::
find
()
->
all
();
$this
->
assertEquals
(
10
,
count
(
$customers
));
$this
->
assertTrue
(
$customers
[
0
]
instanceof
CustomerFile
);
$this
->
assertTrue
(
$customers
[
1
]
instanceof
CustomerFile
);
// find by _id
$testId
=
$this
->
testRows
[
0
][
'_id'
];
$customer
=
CustomerFile
::
find
(
$testId
);
$this
->
assertTrue
(
$customer
instanceof
CustomerFile
);
$this
->
assertEquals
(
$testId
,
$customer
->
_id
);
// find by column values
$customer
=
CustomerFile
::
find
([
'tag'
=>
'tag5'
]);
$this
->
assertTrue
(
$customer
instanceof
CustomerFile
);
$this
->
assertEquals
(
$this
->
testRows
[
4
][
'_id'
],
$customer
->
_id
);
$this
->
assertEquals
(
'tag5'
,
$customer
->
tag
);
$customer
=
CustomerFile
::
find
([
'tag'
=>
'unexisting tag'
]);
$this
->
assertNull
(
$customer
);
// find by attributes
$customer
=
CustomerFile
::
find
()
->
where
([
'status'
=>
4
])
->
one
();
$this
->
assertTrue
(
$customer
instanceof
CustomerFile
);
$this
->
assertEquals
(
4
,
$customer
->
status
);
// find count, sum, average, min, max, distinct
$this
->
assertEquals
(
10
,
CustomerFile
::
find
()
->
count
());
$this
->
assertEquals
(
1
,
CustomerFile
::
find
()
->
where
([
'status'
=>
2
])
->
count
());
$this
->
assertEquals
((
1
+
10
)
/
2
*
10
,
CustomerFile
::
find
()
->
sum
(
'status'
));
$this
->
assertEquals
((
1
+
10
)
/
2
,
CustomerFile
::
find
()
->
average
(
'status'
));
$this
->
assertEquals
(
1
,
CustomerFile
::
find
()
->
min
(
'status'
));
$this
->
assertEquals
(
10
,
CustomerFile
::
find
()
->
max
(
'status'
));
$this
->
assertEquals
(
range
(
1
,
10
),
CustomerFile
::
find
()
->
distinct
(
'status'
));
// scope
$this
->
assertEquals
(
1
,
CustomerFile
::
find
()
->
activeOnly
()
->
count
());
// asArray
$testRow
=
$this
->
testRows
[
2
];
$customer
=
CustomerFile
::
find
()
->
where
([
'_id'
=>
$testRow
[
'_id'
]])
->
asArray
()
->
one
();
$this
->
assertEquals
(
$testRow
[
'_id'
],
$customer
[
'_id'
]);
$this
->
assertEquals
(
$testRow
[
'tag'
],
$customer
[
'tag'
]);
$this
->
assertEquals
(
$testRow
[
'status'
],
$customer
[
'status'
]);
// indexBy
$customers
=
CustomerFile
::
find
()
->
indexBy
(
'tag'
)
->
all
();
$this
->
assertTrue
(
$customers
[
'tag1'
]
instanceof
CustomerFile
);
$this
->
assertTrue
(
$customers
[
'tag2'
]
instanceof
CustomerFile
);
// indexBy callable
$customers
=
CustomerFile
::
find
()
->
indexBy
(
function
(
$customer
)
{
return
$customer
->
status
.
'-'
.
$customer
->
status
;
})
->
all
();
$this
->
assertTrue
(
$customers
[
'1-1'
]
instanceof
CustomerFile
);
$this
->
assertTrue
(
$customers
[
'2-2'
]
instanceof
CustomerFile
);
}
}
\ No newline at end of file
tests/unit/extensions/mongo/file/QueryTest.php
View file @
ca608a81
...
@@ -51,7 +51,8 @@ class QueryTest extends MongoTestCase
...
@@ -51,7 +51,8 @@ class QueryTest extends MongoTestCase
$connection
=
$this
->
getConnection
();
$connection
=
$this
->
getConnection
();
$query
=
new
Query
;
$query
=
new
Query
;
$row
=
$query
->
from
(
'fs'
)
->
one
(
$connection
);
$row
=
$query
->
from
(
'fs'
)
->
one
(
$connection
);
$this
->
assertTrue
(
$row
instanceof
\MongoGridFSFile
);
$this
->
assertTrue
(
is_array
(
$row
));
$this
->
assertTrue
(
$row
[
'file'
]
instanceof
\MongoGridFSFile
);
}
}
public
function
testDirectMatch
()
public
function
testDirectMatch
()
...
@@ -64,6 +65,6 @@ class QueryTest extends MongoTestCase
...
@@ -64,6 +65,6 @@ class QueryTest extends MongoTestCase
$this
->
assertEquals
(
1
,
count
(
$rows
));
$this
->
assertEquals
(
1
,
count
(
$rows
));
/** @var $file \MongoGridFSFile */
/** @var $file \MongoGridFSFile */
$file
=
$rows
[
0
];
$file
=
$rows
[
0
];
$this
->
assertEquals
(
'name5'
,
$file
->
file
[
'filename'
]);
$this
->
assertEquals
(
'name5'
,
$file
[
'filename'
]);
}
}
}
}
\ No newline at end of file
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